import { ReadonlySignal, Signal } from "../signals/primitives.js"; //#region src/core/composition/create-composition.d.ts /** * Cleanup returned by a behavior. Behaviors may return: * - `void` / `undefined` — no cleanup needed * - A function — called on destroy (may return a Promise) * - An object with `destroy()` — called on destroy (may return a Promise) */ type BehaviorCleanup = void | (() => void | Promise) | { destroy(): void | Promise; }; /** * A signal map keyed by the fields of `S`. Each field is a writable signal. * * Optional fields on `S` map to required signal slots whose value type * includes `undefined`, ensuring every key has a signal even when the * underlying value is absent. * * Used in two roles: * - Engine-side **construction**: `Composition` exposes its public * surface as `StateSignals` (everything writable) so external code * can read or write any slot. * - Behavior **input convenience**: a behavior that writes to every slot * can type its setup state param as `StateSignals<{ ... }>` rather than * spelling out per-slot `Signal` types. * * Behaviors that mix read-only and writable slots type the setup param * directly as a slot map (`{ x: Signal; y: ReadonlySignal }`) * instead of going through `StateSignals<>`. */ type StateSignals = { [K in keyof S]-?: Signal; }; /** * A signal map keyed by the fields of `C`. Each field is a writable signal * for a platform object or actor reference. Same dual role as * `StateSignals` — see its docblock. */ type ContextSignals = { [K in keyof C]-?: Signal; }; /** * Slot-map shape — a record where each value is at least a `ReadonlySignal`. * `Signal` is structurally a subtype of `ReadonlySignal` (it adds * `.set()`), so a writable slot satisfies this bound too. * * This is the bound used for behavior `state` / `context` slot maps. It * lets a single behavior declare a *heterogeneous* slot map where some * slots are `Signal` (writable) and others are `ReadonlySignal` * (read-only) — making read/write intent explicit at the call site and * giving body-level enforcement (TS rejects `.set()` on a read-only slot). */ type AnySlotMap = Record>; /** * The deps object passed to each behavior by the composition. * * - `state` — slot map for state fields (reactive data). Per-slot read/ * write intent expressed via `Signal` vs `ReadonlySignal`. * - `context` — slot map for platform objects and actor references. * - `config` — static configuration, passed once at composition creation. */ interface BehaviorDeps { state: StateMap; context: ContextMap; config: Cfg; } /** * A behavior announces the state and context keys it needs alongside a * `setup` function that receives deps (state, context, config) and * returns an optional cleanup handle. * * The `stateKeys` / `contextKeys` declarations are the runtime expression * of the behavior's contract — the caller (e.g. `createComposition`) uses * them to know which signals to provide. The setup parameter type * declares the *slot map* (per-slot `Signal` vs `ReadonlySignal`); * together they form a complete contract. * * Manual `Behavior<>` literals (e.g. engine wrappers that forward keys * from a wrapped behavior, or pass-through behaviors like `shareSignals`) * opt out of exhaustiveness — the type alias is permissive (subset). * Source behaviors should use `defineBehavior` to get exhaustiveness * enforcement at the call site. */ interface Behavior { /** State keys this behavior reads/writes. Subset of `keyof StateMap`. */ stateKeys: readonly (keyof StateMap)[]; /** Context keys this behavior reads/writes. Subset of `keyof ContextMap`. */ contextKeys: readonly (keyof ContextMap)[]; setup: (deps: BehaviorDeps) => BehaviorCleanup; } /** A behavior with an unconstrained setup — used as a generic bound. */ type AnyBehavior = { stateKeys: readonly PropertyKey[]; contextKeys: readonly PropertyKey[]; setup: (deps: any) => BehaviorCleanup; }; /** Extract the deps type from a behavior's setup function. */ type DepsOf = B extends { setup: (deps: infer D, ...args: any[]) => any; } ? D : never; /** * Empty-object fallback used when a behavior omits state, context, or config. * * Using `{}` rather than `object` is deliberate — `object & {x: T}` collapses * to `{x: never}` under TS's union-to-intersection conversion in some inference * contexts (likely a TS quirk around the `object` upper bound), whereas * `{} & {x: T}` simplifies cleanly to `{x: T}`. */ type Empty = {}; /** * Unwrap a signal map back to its state/context shape. * * Inferring through `{ get(): infer V }` rather than `Signal` * sidesteps `Signal`'s nominal/invariance behaviour — the conditional * matches structurally on the read side, and `V` is inferred covariantly. */ type UnwrapSignals = M extends object ? { [K in keyof M]: M[K] extends { get(): infer V; } ? V : never; } : Empty; /** Infer the state shape a behavior requires from its deps parameter. */ type InferBehaviorState = DepsOf extends { state: infer M; } ? UnwrapSignals : Empty; /** Infer the context shape a behavior requires from its deps parameter. */ type InferBehaviorContext = DepsOf extends { context: infer M; } ? UnwrapSignals : Empty; /** Infer the config shape a behavior requires from its deps parameter. */ type InferBehaviorConfig = DepsOf extends { config: infer C extends object; } ? C : Empty; /** * Recursively intersect a per-behavior projection across the tuple. * * Iterating over the tuple directly avoids `UnionToIntersection`'s * function-contravariance trick, which produces unstable intersections * (collapsing concrete fields to `never` or unrelated types) when one of the * union members is the empty `{}` fallback. */ type IntersectBehaviors = Behaviors extends readonly [infer First extends AnyBehavior, ...infer Rest extends readonly AnyBehavior[]] ? Apply & IntersectBehaviors : Empty; /** * Apply a projection (one of the marker types below) to a single behavior. * Encoded as a discriminated dispatch so the recursion above can stay generic * and we don't have to write three near-identical recursive types. */ type Apply = Project extends { kind: 'state'; } ? InferBehaviorState : Project extends { kind: 'context'; } ? InferBehaviorContext : Project extends { kind: 'config'; } ? InferBehaviorConfig : never; type StateProjection = { kind: 'state'; }; type ContextProjection = { kind: 'context'; }; type ConfigProjection = { kind: 'config'; }; /** Resolve the combined state shape from an array of behaviors (intersection of all requirements). */ type ResolveBehaviorState = IntersectBehaviors extends (infer R extends object) ? R : Empty; /** Resolve the combined context shape from an array of behaviors (intersection of all requirements). */ type ResolveBehaviorContext = IntersectBehaviors extends (infer R extends object) ? R : Empty; /** Resolve the combined config shape from an array of behaviors (intersection of all requirements). */ type ResolveBehaviorConfig = IntersectBehaviors extends (infer R extends object) ? R : Empty; /** * True if any property in `T` collapsed to `undefined` or `never` — indicating * a type conflict from intersecting incompatible behavior requirements. * * - Required conflicts: `{ v: number } & { v: string }` → `{ v: never }` — caught via `[never] extends [undefined]` * - Optional conflicts: `{ v?: number } & { v?: string }` → `{ v?: undefined }` — caught directly */ type HasConflict = true extends { [K in keyof T]: [T[K]] extends [undefined] ? true : never; }[keyof T] ? true : false; /** * Validate that a behavior composition has no type conflicts. * Returns the behaviors tuple if valid, or an error message type if conflicts are detected. * * State, context, and config are all checked the same way — by intersecting * each behavior's requirement and looking for collapsed fields. The * intersection-based check applies the same rule to context as to state, so * two behaviors that disagree on a context field's type (e.g. `Surface` vs * `VideoSurface`) surface a conflict at compose time. The prior subtype-based * approach for owners is gone — the unified rule is simpler and catches the * cases where two behaviors silently agreed on a wider supertype. */ type ValidateComposition = HasConflict> extends true ? 'Error: behaviors have conflicting state types' : HasConflict> extends true ? 'Error: behaviors have conflicting context types' : HasConflict> extends true ? 'Error: behaviors have conflicting config types' : [...Behaviors]; /** * A composition of behaviors with shared state and context signal maps. */ interface Composition { state: StateSignals; context: ContextSignals; destroy(): Promise; } /** * Options for `createComposition`. * * Composition derives the state and context signal maps from each * behavior's declared `stateKeys` / `contextKeys`; `initialState` and * `initialContext` seed those signals at creation time. Any unseeded * signal starts as `undefined`. */ interface CompositionOptions { /** Static configuration passed to every behavior. */ config?: Cfg; /** Initial values for state signals — any subset of `keyof S`. */ initialState?: Partial; /** Initial values for context signals — any subset of `keyof C`. */ initialContext?: Partial; } declare function createComposition(behaviors: ValidateComposition, options?: CompositionOptions, ResolveBehaviorContext, ResolveBehaviorConfig>): Composition, ResolveBehaviorContext>; /** * Compose-time exhaustiveness check. * * Adds a phantom error tag to the parameter shape when `Keys` does not * cover every key in `Slot`. The user's value won't satisfy the phantom * field requirement, so TS surfaces the failure at the call site with a * descriptive message. When exhaustive, the tag is `Empty` and adds no * constraint. */ type ExhaustiveKeys = [keyof Slot] extends [Keys[number]] ? Empty : { [K in `Error: ${Name}Keys must list every key in the typed slice`]: Exclude; }; /** * Typed factory for behaviors that enforces single-behavior key/param * consistency: declared `stateKeys` must equal `keyof S` (where `S` is * inferred from the setup's `state` parameter type), and same for * `contextKeys` / `C`. * * The `const` modifier on `SK` / `CK` captures literal tuples so e.g. * `stateKeys: ['preload']` infers as `readonly ['preload']`, no `as * const` needed at the call site. * * Cross-behavior consistency at `createComposition` is unchanged — the * existing `IntersectBehaviors` machinery still runs over each * behavior's setup param type. * * @example * ```ts * export const syncPreload = defineBehavior({ * stateKeys: ['preload'], * contextKeys: ['mediaElement'], * setup: ({ state, context }: { * state: StateSignals<{ preload?: 'auto' | 'metadata' | 'none' }>; * context: ContextSignals<{ mediaElement?: HTMLMediaElement | undefined }>; * }) => { ... }, * }); * ``` */ /** * Deps shape for a behavior whose deps slot is empty (no keys). When a * slot is empty, the corresponding deps field is optional — callers * (typically tests) can omit it, and it defaults to `{}` at runtime via * `createComposition`. * * When a slot has at least one key, the behavior reads `state.foo` / * `context.bar` / `config.baz` and we need the field to be required so * the access is type-safe. */ type RequireIfNonEmpty = keyof T extends never ? { [K in Key]?: T; } : { [K in Key]: T; }; type DepsForCfg = RequireIfNonEmpty<'state', StateMap> & RequireIfNonEmpty<'context', ContextMap> & RequireIfNonEmpty<'config', Cfg>; declare function defineBehavior(behavior: { stateKeys: SK; contextKeys: CK; setup: (deps: { state: StateMap; context: ContextMap; config: Cfg; }) => R; } & ExhaustiveKeys & ExhaustiveKeys): { stateKeys: SK; contextKeys: CK; setup: (deps: DepsForCfg) => R; }; //#endregion export { AnySlotMap, Behavior, BehaviorCleanup, BehaviorDeps, Composition, CompositionOptions, ContextSignals, InferBehaviorConfig, InferBehaviorContext, InferBehaviorState, ResolveBehaviorConfig, ResolveBehaviorContext, ResolveBehaviorState, StateSignals, createComposition, defineBehavior }; //# sourceMappingURL=create-composition.d.ts.map