Files
welshman/docs/app/app.md
T
hodlbod fe5c11b00f
tests / tests (push) Failing after 5m4s
rename client, update docs/skills
2026-06-18 19:31:14 +00:00

174 lines
6.1 KiB
Markdown

# 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`).