The App
An App is an application instance. It owns every piece of per-identity state and is the entry point to all features, which are reached through app.use(...).
Creating an app
createApp(options?)
Returns an App wired with the default policies, unless policies is passed.
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?)
The constructor installs no policies unless they are passed, which suits tests and apps that install their own.
import {App} from "@welshman/app"
const app = new App() // no policies installedAppOptions
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
repository?: Repository // where events live; a MemoryRepository by default
wrapManager?: WrapManager // which wraps were unwrapped, over the same repository
}A SqliteRepository passed as repository keeps events across sessions and loads none of them at startup. Each collection plugin still reads every stored event of its kinds into memory the first time it's used, and keeps them there. The wraps it keeps come back too, and the app unwraps each one it doesn't know was unwrapped, so pass a wrapManager restored from wherever you persist its dump() alongside it.
AppConfig
App-level configuration. All fields are optional; the three relay getters return string[] and feed the Router.
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 depend on the IApp interface rather than the App class:
interface IApp {
user?: User
config: AppConfig
use: <T>(Ctor: Plugin<T>) => T
onCleanup: (unsubscriber: Unsubscriber) => void
netContext: NetContext // {pool, repository, getAdapter} for the net layer
pool: Pool // connection pool
repository: Repository // the single source of truth for events
wrapManager: WrapManager // NIP-59 gift-wrap bookkeeping
}Resolving features: use
type Plugin<T> = new (app: IApp) => T
use: <T>(Ctor: Plugin<T>) => TThe first call with a plugin class constructs new Ctor(app) and caches it. Later calls return the same instance.
const profiles = app.use(Profiles)
const sameInstance = app.use(Profiles) // identical referencePlugins reach their own dependencies the same way (this.app.use(Network)), so there is no constructor wiring.
Teardown: cleanup
app.cleanup()cleanup() runs every registered teardown in reverse registration order, then clears the pool. A repository or wrap manager the app created for itself is cleared too; one passed in belongs to the caller and is left as it is. The order matters for teardowns that restore something they replaced: appPolicyCacheDecrypt and appPolicyLogSignerMethods both wrap user.signer, and each unwraps in the opposite order it wrapped.
A plugin that owns a resource the app can't see, such as a queue, a timer, or an outside subscription, registers its own teardown with onCleanup:
class MyPlugin {
queue = new TaskQueue({...})
constructor(readonly app: IApp) {
app.onCleanup(() => this.queue.clear())
}
}Policies
A policy is a side effect. It runs once at construction and returns an Unsubscriber that cleanup() calls. Policies own every subscription, so the data plugins stay free of them.
type AppPolicy = (app: IApp) => UnsubscriberDefault policies
createApp installs defaultAppPolicies:
| Policy | What it does |
|---|---|
appPolicyIngest | Verifies events received on any socket and passes them to app.ingestEvent, which skips DVM and ephemeral kinds, records each one's relay on the repository's tracker, and writes it to the repository. That populates every store-backed collection. A custom ingest policy can call app.ingestEvent after its own check. |
appPolicyRelayStats | Pipes socket activity into the RelayStats store. |
appPolicyNotify | Pipes NOTIFY messages into the Notify store. |
appPolicyWraps | Enqueues existing and newly-arriving gift-wrap events for unwrapping. |
appPolicyCacheDecrypt | Wraps the user's signer so NIP-04/NIP-44 decryption consults the Plaintext cache before doing real work. No-op when there is no user. |
appPolicyLogSignerMethods | Wraps the user's signer to log every call to the Logger. No-op when there is no user. |
appPolicyAuthUnlessBlocked | Answers NIP-42 AUTH challenges, except for relays in the user's blocked-relay list. |
Auth policy builders
makeAppPolicyAuth(
shouldAuth: (socket: Socket, app: IApp) => boolean,
recheck?: (app: IApp) => Readable<unknown>, // re-asks shouldAuth when this changes
): AppPolicy
appPolicyAuthNever // never answer AUTH
appPolicyAuthAlways // always answer AUTH
appPolicyAuthUnlessBlocked // answer unless the relay is blocked by the userAuth policies are no-ops when there is no signed-in user.
Customizing policies
A policies array replaces the defaults:
import type {AppPolicy} from "@welshman/app"
import {App, defaultAppPolicies} from "@welshman/app"
const myPolicy: AppPolicy = app =>
app.pool.subscribe(socket => {
// ...install a side effect, return an Unsubscriber
return () => {}
})
const app = new App({
user,
policies: [...defaultAppPolicies, myPolicy],
})Logging
The Logger plugin keeps an in-memory log of the most recent 1000 entries. log(source, {...}) appends an entry, and the messages projection reads them back.
type LogMessage = {
source: string
id: string // defaults to a random id
at: number // defaults to Date.now()
[key: string]: unknown
}import {Logger} from "@welshman/app"
const logger = app.use(Logger)
logger.log("my-feature", {status: "ok"})
logger.messages.$.subscribe(messages => {
for (const msg of messages) {
if (msg.source === "signer" && msg.status === "failure") {
console.error("signing failed", msg.method, msg.error)
}
}
})appPolicyLogSignerMethods logs a "signer" message for every signer call, with method and a status of "pending", then "success" or "failure" (with error).