import { type Equivalence, Schema, Stream } from 'effect'; type SubscriptionBrand = { readonly __subscription: never; }; type DependenciesSchema = Schema.Schema & { readonly fields: Schema.Struct.Fields; }; /** * The entry shape produced by helpers like `Subscription.persistent` and * `Port.subscription` before branding. Pass values of this shape into * `Subscription.make` as entry values. */ export type EntryWithoutKeepAlive = { readonly dependenciesSchema: DependenciesSchema; readonly modelToDependencies: (model: Model) => Dependencies; readonly keepAliveEquivalence?: never; readonly dependenciesToStream: (dependencies: Dependencies) => Stream.Stream; }; type EntryWithKeepAlive = { readonly dependenciesSchema: DependenciesSchema; readonly modelToDependencies: (model: Model) => Dependencies; readonly keepAliveEquivalence: Equivalence.Equivalence; readonly dependenciesToStream: (dependencies: Dependencies, readDependencies: () => Dependencies) => Stream.Stream; }; type Entry = EntryWithoutKeepAlive | EntryWithKeepAlive; /** * 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 `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. */ export type Subscription = Entry & SubscriptionBrand; /** A record of named Subscriptions keyed by dependency field name. */ export type Subscriptions = Readonly>>; /** * Callbacks for a subscription entry without `keepAliveEquivalence`. Dependencies * are inferred from the field map passed to `entry`. */ type EntryCallbacksWithoutKeepAlive = { readonly modelToDependencies: (model: Model) => Dependencies; readonly keepAliveEquivalence?: never; readonly dependenciesToStream: (dependencies: Dependencies) => Stream.Stream; }; /** * Callbacks for a subscription entry with `keepAliveEquivalence`. Dependencies * are inferred from the field map passed to `entry`. */ type EntryCallbacksWithKeepAlive = { readonly modelToDependencies: (model: Model) => Dependencies; readonly keepAliveEquivalence: Equivalence.Equivalence; readonly dependenciesToStream: (dependencies: Dependencies, readDependencies: () => Dependencies) => Stream.Stream; }; /** * Builds a single subscription entry from a field map and callbacks. * * The field map is the same shape you would pass to `S.Struct`. Reading the * schema as a positional argument (rather than a property on the entry * literal) lets TypeScript fully resolve the `Dependencies` type before * contextually typing `modelToDependencies` and `dependenciesToStream`, so * destructuring patterns like `({ maybeMapHostId })` are inferred correctly * even when the field schemas use transforms (e.g. `S.Option`). * * Two overloads, one per `keepAliveEquivalence` presence: * * - Without `keepAliveEquivalence`, `dependenciesToStream` takes a single * `dependencies` argument. * - With `keepAliveEquivalence`, `dependenciesToStream` also receives a * `readDependencies` thunk for accessing the latest value while the Stream * stays running across Model changes the equivalence accepts as equal. */ export type EntryBuilder = , Services> | EntryCallbacksWithKeepAlive, Services>>(fields: Fields, callbacks: Callbacks) => Callbacks extends { readonly keepAliveEquivalence: Equivalence.Equivalence; } ? EntryWithKeepAlive, Services> : EntryWithoutKeepAlive, Services>; /** * 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. * * @example * ```ts * Subscription.make()(entry => ({ * tick: entry( * { isRunning: S.Boolean }, * { * modelToDependencies: model => ({ isRunning: model.isRunning }), * dependenciesToStream: ({ isRunning }) => * Stream.when(..., Effect.sync(() => isRunning)), * }, * ), * })) * ``` */ export declare const make: () => >>>(build: (entry: EntryBuilder) => Entries) => { readonly [K in keyof Entries]: Entries[K] & SubscriptionBrand; }; /** * Combines multiple Subscriptions records into one. Throws on duplicate * keys so a misconfigured aggregate fails loudly at startup rather than * silently overriding. */ export declare const aggregate: () => (...records: ReadonlyArray>) => Subscriptions; /** * 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. */ export declare const persistent: (stream: Stream.Stream) => EntryWithoutKeepAlive, Services>; type ChildModelOf = ChildSubscriptions[keyof ChildSubscriptions] extends Subscription ? ChildModel : never; type ChildMessageOf = ChildSubscriptions[keyof ChildSubscriptions] extends Subscription ? ChildMessage : never; /** * 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. */ export declare const lift: >>>(subscriptions: Subscriptions) => (config: { readonly toChildModel: (parentModel: ParentModel) => ChildModelOf; readonly toParentMessage: (message: ChildMessageOf) => ParentMessage; }) => { readonly [K in keyof Subscriptions]: Subscriptions[K] extends Subscription ? Subscription : never; }; export {}; //# sourceMappingURL=subscription.d.ts.map