Skip to content

Derivations ​

Live stores over a repository, which update as the repository reports changes.

Reads from a repository are async, so each derivation seeds itself with a query when it is first subscribed and then follows the repository's "update" events. Until the query answers, it holds what it held last. An update that arrives while the query is in flight wins over the query's answer, so a stale result never undoes it.

Event Stores ​

Each of these is a Projection: subscribe to it or to its .$, and get() it for a snapshot. get() returns the current value while the store is subscribed, and the last value it held otherwise. The ones typed AsyncProjection add load(), which reads the repository fresh.

typescript
type AsyncProjection<T> = Projection<T> & {load: () => Promise<T>}

// Derive map of events by ID
deriveEventsById(options: {
  repository: Repository
  filters: Filter[]
  includeDeleted?: boolean
}): AsyncProjection<Map<string, TrustedEvent>>

// The matching events as an array
deriveEvents(options: {repository: Repository, filters: Filter[], includeDeleted?: boolean}): AsyncProjection<TrustedEvent[]>

// Convert events by ID to array
deriveEventsArray(eventsById: AsyncProjection<Map<string, TrustedEvent>>): AsyncProjection<TrustedEvent[]>

// Sort events ascending by created_at
deriveEventsAsc(eventsById: AsyncProjection<Map<string, TrustedEvent>>): AsyncProjection<TrustedEvent[]>

// Sort events descending by created_at
deriveEventsDesc(eventsById: AsyncProjection<Map<string, TrustedEvent>>): AsyncProjection<TrustedEvent[]>

// Derive single event by ID or address
makeDeriveEvent(options: {repository: Repository, includeDeleted?: boolean, onDerive?: (filters: Filter[], ...args: any[]) => void}): (idOrAddress: string, ...args: any[]) => AsyncProjection<TrustedEvent | undefined>

// Track if event is deleted, synchronously through `repository.isDeleted`
deriveIsDeleted(repository: Repository, event: TrustedEvent): Projection<boolean>

getEventsById, getEventsByIdByUrl and getEventsByIdForUrl are the async one-shot reads behind load(). projectAsync(src, read) derives an AsyncProjection from another, whose load() reads src fresh.

Changing filters ​

A new event store starts empty and fills when its query answers, so a store that builds one each time its filters change goes blank on every change. observeEventsById calls back only once its read has answered, then on each change, and returns a function that stops it, so a store built on it keeps showing what it had until the new read answers.

typescript
observeEventsById(options: {
  repository: Repository
  filters: Filter[]
  includeDeleted?: boolean
  onEvents: (eventsById: Map<string, TrustedEvent>) => void
}): Unsubscriber

// The selected notes, which stay on screen while a new selection is read
const notes = derived(selectedIds, ($ids, set) =>
  observeEventsById({repository, filters: [{ids: $ids}], onEvents: set}),
)

Relay-scoped Stores ​

These key events by the relays the repository's Tracker has seen them on, so the same event or address stays distinct per relay.

typescript
// Every relay at once
deriveEventsByIdByUrl(options: EventsByIdOptions): AsyncProjection<Map<string, Map<string, TrustedEvent>>>

// One relay, queried with `relay: url` so a limit counts only that relay's events
deriveEventsByIdForUrl(options: EventsByIdOptions & {url: string}): AsyncProjection<Map<string, TrustedEvent>>

The per-relay store also picks up an event the repository already held once the tracker sees it on that relay.

Indexed Collections ​

typescript
// Create indexed map of items from stored events
deriveItemsByKey<T>(options: {
  repository: Repository
  filters: Filter[]
  eventToItem: (event: TrustedEvent) => MaybeAsync<Maybe<T>>
  getKey: (item: T) => string
  includeDeleted?: boolean
}): ItemCollection<T>

// The same, keyed once per relay each event was seen on; `undefined` excludes the item on that relay
deriveItemsByKeyByUrl<T>(options: {
  repository: Repository
  filters: Filter[]
  eventToItem: (event: TrustedEvent) => MaybeAsync<Maybe<T>>
  getKey: (item: T, url: string) => string | undefined
  includeDeleted?: boolean
  revalidateOn?: Readable<unknown>
}): ItemCollection<T>

// A Projection over the index, plus a read that waits out the seeding query and any
// outstanding parses. An indexed item comes back synchronously.
type ItemCollection<T> = Projection<Map<string, T>> & {
  read: (key: string) => MaybeAsync<Maybe<T>>
}

// Convert itemsByKey map to array
deriveItems<T>(itemsByKeyStore: Readable<Map<string, T>>): Readable<T[]>

// Create function to derive single item by key
makeDeriveItem<T>(
  itemsByKeyStore: Readable<Map<string, T>>,
  onDerive?: (key: string, ...args: any[]) => void
): (key: string, ...args: any[]) => Readable<T | undefined>

// Create cached loader with a staleness window in seconds and exponential backoff
makeLoadItem<T>(
  loadItem: (key: string, ...args: any[]) => Promise<unknown>,
  getItem: (key: string, ...args: any[]) => MaybeAsync<T | undefined>,
  options?: {getFetched?, setFetched?, getSource?, maxSize?, timeout?}
): (key: string, ...args: any[]) => Promise<T | undefined>

// Create loader that always fetches fresh data
makeForceLoadItem<T>(
  loadItem: (key: string, ...args: any[]) => Promise<unknown>,
  getItem: (key: string, ...args: any[]) => MaybeAsync<T | undefined>
): (key: string, ...args: any[]) => Promise<T | undefined>

read(key) waits for the repository's pendingWrites() first, so an item whose event was just written, such as one a fetch has just stored, is found. On a collection nothing is subscribed to, it subscribes long enough to seed it, so a cold read still finds a stored event.

Example ​

typescript
import {parseJson} from "@welshman/lib"
import {PROFILE} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {MemoryRepository, deriveEvents, deriveItemsByKey, deriveItems} from "@welshman/store"

const repository = new MemoryRepository()

type Profile = {event: TrustedEvent; name?: string; about?: string}

// Reactive store of text notes
const notes = deriveEvents({repository, filters: [{kinds: [1], limit: 100}]})

// Reactive store of profiles indexed by pubkey
const profilesByPubkey = deriveItemsByKey<Profile>({
  repository,
  filters: [{kinds: [PROFILE]}],
  eventToItem: event => ({event, ...parseJson(event.content)}),
  getKey: profile => profile.event.pubkey,
})
const profiles = deriveItems(profilesByPubkey)

// Each emits its starting value, then what the repository's query and updates bring
notes.subscribe($notes => {
  console.log(`Found ${$notes.length} text notes`)
})

profiles.subscribe($profiles => {
  console.log(`Found ${$profiles.length} profiles`)
})