Skip to main content
On this pageFunctions

Subscription

Functions

aggregate

functionsource
/**
 * Combines multiple Subscriptions records into one. Throws on duplicate
 * keys so a misconfigured aggregate fails loudly at startup rather than
 * silently overriding.
 */
<Model, Message, Services = never>(): (records: readonly Array<Readonly<Record<string, Subscription<Model, Message, any, Services>>>>) => Subscriptions<Model, Message, Services>

animationFrame

functionsource
/**
 * Build a Subscription that emits a Message on every
 * `requestAnimationFrame` tick, with the inter-frame delta in milliseconds.
 */
<Model, Message>(config: AnimationFrameConfig<Model, Message>): {
  dependenciesSchema: Struct<{
    isActive: Boolean
  }>
  dependenciesToStream: (__namedParameters: {
    isActive: boolean
  }) => Stream<Message, never, never>
  modelToDependencies: (model: Model) => {
    isActive: boolean
  }
}

lift

functionsource
/**
 * Lifts a record of child Subscriptions into a parent's Model and Message
 * context, applying a Model accessor and a Message wrapper uniformly to
 * every entry. Per-entry dependency types, schemas, and `keepAliveEquivalence`
 * settings are preserved; each lifted entry's variant (with or without
 * `readDependencies`) matches its source entry's.
 */
<Subscriptions extends Readonly<Record<string, Subscription<any, any, any, any>>>>(subscriptions: Subscriptions): (config: {
  toChildModel: (parentModel: ParentModel) => ChildModelOf<Subscriptions>
  toParentMessage: (message: ChildMessageOf<Subscriptions>) => ParentMessage
}) => {
  readonly [K in string | number | symbol]: Subscriptions[K] extends Subscription<any, any, Dependencies, Services>
    ? Subscription<ParentModel, ParentMessage, Dependencies, Services>
    : never
}

make

functionsource
/**
 * Declares a Subscriptions record. The Model, Message, and optional Services
 * generics are provided up front; the entries record follows, built from
 * calls to the `entry` builder passed into the inner function.
 * 
 * Reach for `Subscription.aggregate` to combine multiple records, and
 * `Subscription.lift` to translate a child Submodel's record into a parent
 * context.
 */
<Model, Message, Services = never>(): (build: (entry: EntryBuilder<Model, Message, Services>) => Entries) => {
  readonly [K in string | number | symbol]: Entries[K] & SubscriptionBrand
}

persistent

functionsource
/**
 * Wraps a Stream as a Subscription entry whose lifecycle is independent of
 * the Model. The Stream runs for the lifetime of the Subscriptions record;
 * no Model change tears it down or restarts it. Use for any Stream whose
 * work doesn't depend on Model state, such as system theme listeners,
 * viewport width observers, or route-independent timers.
 * 
 * Returns an entry shape, not a branded Subscription. Pass it into `make`
 * as an entry value.
 */
<Message, Services = never>(stream: Stream<Message, never, Services>): EntryWithoutKeepAlive<unknown, Message, Record<string, never>, Services>

Types

AnimationFrameConfig

typesource
/**
 * Configuration for the `animationFrame` Subscription helper.
 * 
 * `isActive(model)` controls whether the request-animation-frame loop is
 * scheduled at all. When it returns `false` (e.g. the game is paused, the
 * scene is static, or the canvas is offscreen), no rAF callbacks fire and
 * no Messages are emitted. The Subscription system automatically restarts
 * the loop when `isActive` flips back to `true`.
 */
type AnimationFrameConfig = Readonly<{
  isActive: (model: Model) => boolean
  toMessage: (deltaTime: number) => Message
}>

Subscription

typesource
/**
 * A single subscription entry produced by `Subscription.make`,
 * `Subscription.lift`, or `Subscription.aggregate`. The brand field is
 * `never`, so application code cannot manually construct a `Subscription`
 * value: it must go through one of those constructors (or a helper like
 * `Subscription.persistent` that returns an entry shape, then through
 * `make`).
 * 
 * Two variants by `keepAliveEquivalence` presence:
 * 
 * - Without `keepAliveEquivalence` (the common case), every Model change recomputes
 *   the dependencies. Equivalent dependencies leave the Stream alone; any
 *   change tears it down and restarts. `dependenciesToStream` takes a single
 *   argument: the latest dependencies.
 * - With `keepAliveEquivalence` (an escape hatch), Model changes that the
 *   equivalence treats as equal leave the Stream running, but the running
 *   Stream can still read the latest dependencies via the second
 *   `readDependencies` argument. Use this when the Stream needs mid-flight
 *   access to data that changes often but shouldn't trigger restarts
 *   (Foldkit UI's `Ui.DragAndDrop.autoScroll` reading the latest pointer
 *   `clientY` each rAF tick is the canonical example).
 * 
 * `dependenciesSchema` must be a `Schema.Struct` so every dependency is
 * explicitly named at the schema level.
 */
type Subscription = Entry<Model, Message, Dependencies, Services> & SubscriptionBrand

Subscriptions

typesource
/** A record of named Subscriptions keyed by dependency field name. */
type Subscriptions = Readonly<Record<string, Subscription<Model, Message, any, Services>>>

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson