# Plugin Architecture Every feature in `@welshman/app` is a **plugin** — a class constructed with a single `IApp` argument and resolved lazily via `app.use(...)`. All the data-bearing plugins are built on a small set of base classes defined in `plugins/base.ts`. Understanding these three bases and the `Projection` type is enough to read (and extend) the entire library. ```typescript const profiles = app.use(Profiles) // new Profiles(app), memoized per app ``` ## `Projection` Almost every accessor in the library returns a `Projection` — a value you can read either synchronously or reactively. ```typescript type Projection = { get: () => T // synchronous "hot" snapshot $: Readable // a Svelte readable for subscriptions / $-syntax } ``` ```typescript const display = app.use(Profiles).display(pubkey) display.get() // string, right now display.$ // Readable, for `$display` in a component ``` Helpers: ```typescript // Wrap a Readable into a Projection (default getter is hot-path aware) projection($: Readable, get?): Projection // Derive one Projection from another, preserving both access modes projectFrom(src: Projection, read: ($: S) => U): Projection ``` The default `get` is `getter($)` from `@welshman/store`, which automatically switches between `svelte.get` and a live subscription based on how often it is called — so `.get()` is safe in hot code paths. ## The three base classes | Base class | Source of truth | Loads from network? | Used for | |---|---|---|---| | `MapPlugin` | Its own `Map` | No | Local, non-event data (e.g. relay stats) | | `LoadableMapPlugin` | Its own `Map` | Yes (HTTP) | Data fetched over HTTP (relay NIP-11 info, NIP-05 handles, zappers) | | `DerivedPlugin` | The `repository` | Yes (events) | Anything derived from nostr events (profiles, lists, …) | `DerivedPlugin` is the dominant pattern: it is a live view over the app's event repository, so cached events appear immediately and new ones stream in automatically. ### `MapPlugin` A reactive, keyed in-memory collection that owns its own `Map`. ```typescript class MapPlugin { index: Projection> // the whole Map all: Projection // values one: (key?: string, ...args: any[]) => Readable> get(key: string): Maybe // sync read project(key: string, read: (item: Maybe) => U): Projection set(key: string, value: T): void delete(key: string): void clear(): void onItem(subscriber: (key: string, value: Maybe) => void): Unsubscriber } ``` `set`/`delete`/`clear` fire `onItem` subscribers — handy for persisting the collection to storage. ### `LoadableMapPlugin` A `MapPlugin` that lazily fetches items. Subclasses implement `fetch`; the base adds caching and backoff. ```typescript abstract class LoadableMapPlugin extends MapPlugin { abstract fetch(key: string, ...args: any[]): Promise load(key: string, ...args: any[]): Promise> // cached + deduped + backoff forceLoad(key: string, ...args: any[]): Promise> // bypass the cache } ``` Subscribing to `one(key)` triggers a lazy `load`. Caching, in-flight de-duplication, and exponential backoff come from `makeLoadItem` in `@welshman/store` (default staleness window: one hour). ### `DerivedPlugin` A keyed collection derived from repository events. There is no duplicated map — the repository is the single source of truth. ```typescript type DerivedPluginOptions = { filters: Filter[] eventToItem: (event: TrustedEvent) => MaybeAsync> getKey: (item: T) => string loadOptions?: MakeLoadItemOptions } abstract class DerivedPlugin { index: Projection> all: Projection one: (key?: string, ...args: any[]) => Readable> load(key: string, ...args: any[]): Promise> forceLoad(key: string, ...args: any[]): Promise> get(key: string): Maybe project(key: string, read: (item: Maybe) => U): Projection abstract fetch(key: string, ...args: any[]): Promise } ``` Internally it builds `index` from `app.use(Stores).itemsByKey({filters, eventToItem, getKey})`, a live readable derived over the repository. `eventToItem` may be async — useful when a list has encrypted entries that must be decrypted first. ## Lifecycle of a `DerivedPlugin` read 1. **Read (cached):** `get(key)` (sync) or `one(key)` (reactive) returns whatever already matches in the repository — instantly. 2. **Lazy load:** subscribing to `one(key)` (or calling `load(key)`) triggers `fetch(key)`. Caching skips recently-loaded keys; in-flight calls for the same key collapse; failures back off exponentially. 3. **Decode:** inbound events flow through `eventToItem`. Async decoders resolve and update the index when ready. 4. **Derive:** convenience accessors (`display(...)`, `urls(...)`, …) are `project(key, read)` calls returning a `Projection`. `forceLoad` bypasses the cache and resolves to the freshly-read item. ## The `Stores` plugin `app.use(Stores)` is the repository/tracker-bound factory that `DerivedPlugin` builds on. It mostly forwards to `@welshman/store`, injecting the app's `repository` and `tracker`: - `itemsByKey(opts)` — the live keyed collection used by `DerivedPlugin` - `events(opts)` / `eventsById(opts)` / `makeEvent(opts)` — derived event stores - `eventsByIdByUrl(opts)` / `eventsByIdForUrl(opts)` — relay-scoped views (inject the tracker) - `isDeleted(event)` — reactive deletion status You rarely call `Stores` directly — the higher-level data plugins are usually what you want — but it is the seam to use when you need a custom repository-derived store wired to the app. ## Writing your own plugin A plugin is any class with the shape `new (app: IApp) => T`. Extend one of the base classes for a data collection, or write a plain class for behavior: ```typescript import {DerivedPlugin, Network, type IApp} from "@welshman/app" import {SOME_KIND, readSomething} from "@welshman/util" export class Somethings extends DerivedPlugin> { constructor(app: IApp) { super(app, { filters: [{kinds: [SOME_KIND]}], eventToItem: event => readSomething(event), getKey: item => item.event.pubkey, }) } fetch = (pubkey: string, relayHints: string[] = []) => this.app.use(Network).loadUsingOutbox(pubkey, {kinds: [SOME_KIND]}, relayHints) } // usage const things = app.use(Somethings) const thing$ = things.one(pubkey) // lazily loads via the outbox model ```