This commit is contained in:
+173
@@ -0,0 +1,173 @@
|
||||
# The App
|
||||
|
||||
An `App` is an application instance. It owns every piece of per-identity state and is the entry point to all features. You will usually create one with `createApp` and access everything else through `app.use(...)`.
|
||||
|
||||
## Creating an app
|
||||
|
||||
### `createApp(options?)`
|
||||
|
||||
The batteries-included factory. It returns an `App` wired with the [default policies](#policies) (event ingestion, relay-stats collection, gift-wrap unwrapping, and NIP-42 auth) unless you pass your own `policies`.
|
||||
|
||||
```typescript
|
||||
import {createApp} from "@welshman/app"
|
||||
|
||||
const app = createApp({
|
||||
user, // optional signed-in User
|
||||
config: {
|
||||
dufflepudUrl: "https://dufflepud.example",
|
||||
getDefaultRelays: () => ["wss://relay.example"],
|
||||
getIndexerRelays: () => ["wss://purplepag.es"],
|
||||
getSearchRelays: () => ["wss://relay.nostr.band"],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### `new App(options?)`
|
||||
|
||||
Use the constructor directly when you want a bare app with **no** side effects (for example in tests, or when you install policies yourself).
|
||||
|
||||
```typescript
|
||||
import {App} from "@welshman/app"
|
||||
|
||||
const app = new App() // no policies installed
|
||||
```
|
||||
|
||||
## `AppOptions`
|
||||
|
||||
```typescript
|
||||
type AppOptions = {
|
||||
user?: User // the signed-in identity (at most one)
|
||||
config?: AppConfig
|
||||
getAdapter?: AdapterFactory // net-layer adapter factory
|
||||
policies?: AppPolicy[] // side effects to install at construction
|
||||
}
|
||||
```
|
||||
|
||||
## `AppConfig`
|
||||
|
||||
App-level configuration. All fields are optional; the three relay getters return `string[]` and feed the [Router](./routing).
|
||||
|
||||
```typescript
|
||||
type AppConfig = {
|
||||
dufflepudUrl?: string // optional dufflepud service (batches NIP-05 / zapper lookups)
|
||||
getDefaultRelays?: () => string[]
|
||||
getIndexerRelays?: () => string[] // relays used to discover relay lists / profiles
|
||||
getSearchRelays?: () => string[] // NIP-50 search relays
|
||||
}
|
||||
```
|
||||
|
||||
## `IApp`
|
||||
|
||||
Plugins and policies never depend on the concrete `App` class — they take the `IApp` contract:
|
||||
|
||||
```typescript
|
||||
interface IApp {
|
||||
user?: User
|
||||
config: AppConfig
|
||||
use: <T>(Ctor: new (app: IApp) => T) => T
|
||||
netContext: NetContext // {pool, repository, getAdapter} for the net layer
|
||||
pool: Pool // connection pool
|
||||
tracker: Tracker // tracks which relays have seen each event
|
||||
repository: Repository // the local event store / single source of truth
|
||||
wrapManager: WrapManager // NIP-59 gift-wrap bookkeeping
|
||||
}
|
||||
```
|
||||
|
||||
Every primitive (`pool`, `tracker`, `repository`, `wrapManager`) is constructed fresh per instance, so data never bleeds across identities or sessions.
|
||||
|
||||
## Resolving features: `use`
|
||||
|
||||
```typescript
|
||||
use: <T>(Ctor: new (app: IApp) => T) => T
|
||||
```
|
||||
|
||||
`use` is a per-app singleton resolver. The first time you pass a plugin class, the app constructs `new Ctor(this)` and caches it; subsequent calls return the same instance.
|
||||
|
||||
```typescript
|
||||
const profiles = app.use(Profiles)
|
||||
const sameInstance = app.use(Profiles) // identical reference
|
||||
```
|
||||
|
||||
This is dependency resolution by demand. Plugins reach their own dependencies the same way (`this.app.use(Network)`, `this.app.use(Router)`), which means dependency cycles resolve lazily and there is no constructor wiring to maintain.
|
||||
|
||||
## Teardown: `cleanup`
|
||||
|
||||
```typescript
|
||||
app.cleanup()
|
||||
```
|
||||
|
||||
`cleanup()` runs every policy's unsubscribe function, then clears the `pool`, `tracker`, `repository`, and `wrapManager`. Call it when you discard an app (e.g. switching identities) to release connections and free memory.
|
||||
|
||||
## Policies
|
||||
|
||||
A **policy** is the unit of side effects. It runs once at construction and returns an `Unsubscriber` that `cleanup()` will later call. Keeping side effects in policies leaves the data plugins pure and centralizes teardown.
|
||||
|
||||
```typescript
|
||||
type AppPolicy = (app: IApp) => Unsubscriber
|
||||
```
|
||||
|
||||
### Default policies
|
||||
|
||||
`createApp` installs `defaultAppPolicies`:
|
||||
|
||||
| Policy | What it does |
|
||||
|---|---|
|
||||
| `appPolicyIngest` | Subscribes to the pool; verifies inbound relay events (skipping DVM/ephemeral kinds) and writes them to the `repository` and `tracker`. This is how every repository-backed store gets populated. |
|
||||
| `appPolicyRelayStats` | Pipes socket activity into the [`RelayStats`](./routing#relay-quality) store. |
|
||||
| `appPolicyWraps` | Enqueues existing and newly-arriving gift-wrap events for unwrapping. |
|
||||
| `appPolicyAuthUnlessBlocked` | Answers NIP-42 AUTH challenges, except for relays in the user's blocked-relay list. |
|
||||
|
||||
### Auth policy builders
|
||||
|
||||
```typescript
|
||||
makeAppPolicyAuth(shouldAuth: (socket: Socket, app: IApp) => boolean): AppPolicy
|
||||
|
||||
appPolicyAuthNever // never answer AUTH
|
||||
appPolicyAuthAlways // always answer AUTH
|
||||
appPolicyAuthUnlessBlocked // answer unless the relay is blocked by the user
|
||||
```
|
||||
|
||||
Auth policies are no-ops when there is no signed-in user.
|
||||
|
||||
### Customizing policies
|
||||
|
||||
Pass your own `policies` array to opt out of, or extend, the defaults:
|
||||
|
||||
```typescript
|
||||
import {App, defaultAppPolicies, makeAppPolicyLogger} from "@welshman/app"
|
||||
|
||||
const app = new App({
|
||||
user,
|
||||
policies: [
|
||||
...defaultAppPolicies,
|
||||
makeAppPolicyLogger(msg => console.log(msg)), // see Logging
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
`@welshman/app` can make a user's signer observable. `User.fromSigner`/`User.fromSession` wrap the underlying signer in a `LoggingSigner`, which emits a structured `LogMessage` for every signer operation (pending → success/failure).
|
||||
|
||||
```typescript
|
||||
type LogMessage =
|
||||
| {type: "signer"; id: string; method: string; status: "pending" | "success" | "failure"; error?: unknown; at: number}
|
||||
| {type: string; at: number; [key: string]: unknown}
|
||||
```
|
||||
|
||||
Forward those messages by installing `makeAppPolicyLogger`:
|
||||
|
||||
```typescript
|
||||
import {makeAppPolicyLogger} from "@welshman/app"
|
||||
|
||||
const app = new App({
|
||||
user,
|
||||
policies: [...defaultAppPolicies, makeAppPolicyLogger(msg => {
|
||||
if (msg.type === "signer" && msg.status === "failure") {
|
||||
console.error("signing failed", msg.method, msg.error)
|
||||
}
|
||||
})],
|
||||
})
|
||||
```
|
||||
|
||||
The logger policy is a no-op unless the user's signer is a `LoggingSigner` (which it is when the user was created via `User.fromSigner`/`User.fromSession`).
|
||||
Reference in New Issue
Block a user