mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
build(spf): build26 from 45504a2b
This commit is contained in:
+273
@@ -0,0 +1,273 @@
|
||||
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<void>) | {
|
||||
destroy(): void | Promise<void>;
|
||||
};
|
||||
/**
|
||||
* 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<S, C>` exposes its public
|
||||
* surface as `StateSignals<S>` (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<T>` types.
|
||||
*
|
||||
* Behaviors that mix read-only and writable slots type the setup param
|
||||
* directly as a slot map (`{ x: Signal<T>; y: ReadonlySignal<U> }`)
|
||||
* instead of going through `StateSignals<>`.
|
||||
*/
|
||||
type StateSignals<S extends object> = { [K in keyof S]-?: Signal<S[K]>; };
|
||||
/**
|
||||
* 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<S>` — see its docblock.
|
||||
*/
|
||||
type ContextSignals<C extends object> = { [K in keyof C]-?: Signal<C[K]>; };
|
||||
/**
|
||||
* Slot-map shape — a record where each value is at least a `ReadonlySignal`.
|
||||
* `Signal<T>` is structurally a subtype of `ReadonlySignal<T>` (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<T>` (writable) and others are `ReadonlySignal<T>`
|
||||
* (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<PropertyKey, ReadonlySignal<unknown>>;
|
||||
/**
|
||||
* 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<T>` vs `ReadonlySignal<T>`.
|
||||
* - `context` — slot map for platform objects and actor references.
|
||||
* - `config` — static configuration, passed once at composition creation.
|
||||
*/
|
||||
interface BehaviorDeps<StateMap extends AnySlotMap, ContextMap extends AnySlotMap, Cfg extends object> {
|
||||
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<T>` vs `ReadonlySignal<T>`);
|
||||
* 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<StateMap extends AnySlotMap = Empty, ContextMap extends AnySlotMap = Empty, Cfg extends object = Empty> {
|
||||
/** 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<StateMap, ContextMap, Cfg>) => 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> = 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<infer V>`
|
||||
* sidesteps `Signal`'s nominal/invariance behaviour — the conditional
|
||||
* matches structurally on the read side, and `V` is inferred covariantly.
|
||||
*/
|
||||
type UnwrapSignals<M> = 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<F> = DepsOf<F> extends {
|
||||
state: infer M;
|
||||
} ? UnwrapSignals<M> : Empty;
|
||||
/** Infer the context shape a behavior requires from its deps parameter. */
|
||||
type InferBehaviorContext<F> = DepsOf<F> extends {
|
||||
context: infer M;
|
||||
} ? UnwrapSignals<M> : Empty;
|
||||
/** Infer the config shape a behavior requires from its deps parameter. */
|
||||
type InferBehaviorConfig<F> = DepsOf<F> 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 AnyBehavior[], Project extends object> = Behaviors extends readonly [infer First extends AnyBehavior, ...infer Rest extends readonly AnyBehavior[]] ? Apply<Project, First> & IntersectBehaviors<Rest, Project> : 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 object, F> = Project extends {
|
||||
kind: 'state';
|
||||
} ? InferBehaviorState<F> : Project extends {
|
||||
kind: 'context';
|
||||
} ? InferBehaviorContext<F> : Project extends {
|
||||
kind: 'config';
|
||||
} ? InferBehaviorConfig<F> : 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<Behaviors extends readonly AnyBehavior[]> = IntersectBehaviors<Behaviors, StateProjection> extends (infer R extends object) ? R : Empty;
|
||||
/** Resolve the combined context shape from an array of behaviors (intersection of all requirements). */
|
||||
type ResolveBehaviorContext<Behaviors extends readonly AnyBehavior[]> = IntersectBehaviors<Behaviors, ContextProjection> extends (infer R extends object) ? R : Empty;
|
||||
/** Resolve the combined config shape from an array of behaviors (intersection of all requirements). */
|
||||
type ResolveBehaviorConfig<Behaviors extends readonly AnyBehavior[]> = IntersectBehaviors<Behaviors, ConfigProjection> 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<T extends object> = 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<Behaviors extends readonly AnyBehavior[]> = HasConflict<ResolveBehaviorState<Behaviors>> extends true ? 'Error: behaviors have conflicting state types' : HasConflict<ResolveBehaviorContext<Behaviors>> extends true ? 'Error: behaviors have conflicting context types' : HasConflict<ResolveBehaviorConfig<Behaviors>> extends true ? 'Error: behaviors have conflicting config types' : [...Behaviors];
|
||||
/**
|
||||
* A composition of behaviors with shared state and context signal maps.
|
||||
*/
|
||||
interface Composition<S extends object, C extends object> {
|
||||
state: StateSignals<S>;
|
||||
context: ContextSignals<C>;
|
||||
destroy(): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* 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<S extends object, C extends object, Cfg extends object> {
|
||||
/** Static configuration passed to every behavior. */
|
||||
config?: Cfg;
|
||||
/** Initial values for state signals — any subset of `keyof S`. */
|
||||
initialState?: Partial<S>;
|
||||
/** Initial values for context signals — any subset of `keyof C`. */
|
||||
initialContext?: Partial<C>;
|
||||
}
|
||||
declare function createComposition<const Behaviors extends readonly AnyBehavior[]>(behaviors: ValidateComposition<Behaviors>, options?: CompositionOptions<ResolveBehaviorState<Behaviors>, ResolveBehaviorContext<Behaviors>, ResolveBehaviorConfig<Behaviors>>): Composition<ResolveBehaviorState<Behaviors>, ResolveBehaviorContext<Behaviors>>;
|
||||
/**
|
||||
* 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<Keys extends readonly PropertyKey[], Slot extends object, Name extends string> = [keyof Slot] extends [Keys[number]] ? Empty : { [K in `Error: ${Name}Keys must list every key in the typed slice`]: Exclude<keyof Slot, Keys[number]>; };
|
||||
/**
|
||||
* 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<Key extends string, T extends object> = keyof T extends never ? { [K in Key]?: T; } : { [K in Key]: T; };
|
||||
type DepsForCfg<StateMap extends AnySlotMap, ContextMap extends AnySlotMap, Cfg extends object> = RequireIfNonEmpty<'state', StateMap> & RequireIfNonEmpty<'context', ContextMap> & RequireIfNonEmpty<'config', Cfg>;
|
||||
declare function defineBehavior<StateMap extends AnySlotMap = Empty, ContextMap extends AnySlotMap = Empty, Cfg extends object = Empty, const SK extends readonly (keyof StateMap)[] = readonly [], const CK extends readonly (keyof ContextMap)[] = readonly [], R extends BehaviorCleanup = BehaviorCleanup>(behavior: {
|
||||
stateKeys: SK;
|
||||
contextKeys: CK;
|
||||
setup: (deps: {
|
||||
state: StateMap;
|
||||
context: ContextMap;
|
||||
config: Cfg;
|
||||
}) => R;
|
||||
} & ExhaustiveKeys<SK, StateMap, 'state'> & ExhaustiveKeys<CK, ContextMap, 'context'>): {
|
||||
stateKeys: SK;
|
||||
contextKeys: CK;
|
||||
setup: (deps: DepsForCfg<StateMap, ContextMap, Cfg>) => 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
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"create-composition.d.ts","names":[],"sources":["../../../../src/core/composition/create-composition.ts"],"mappings":";;;;;;;;KAQY,uCAAuC;EAAmB,kBAAkB;;;;;;;;;;;;;;;;;;;;;KAqB5E,aAAa,uBAAuB,WAAW,MAAM,OAAO,EAAE;;;;;;KAO9D,eAAe,uBAAuB,WAAW,MAAM,OAAO,EAAE;;;;;;;;;;;;KAahE,aAAa,OAAO,aAAa;;;;;;;;;UAU5B,aAAa,iBAAiB,YAAY,mBAAmB,YAAY;EACxF,OAAO;EACP,SAAS;EACT,QAAQ;;;;;;;;;;;;;;;;;;;UAoBO,SACf,iBAAiB,aAAa,OAC9B,mBAAmB,aAAa,OAChC,qBAAqB;;EAGrB,2BAA2B;;EAE3B,6BAA6B;EAC7B,QAAQ,MAAM,aAAa,UAAU,YAAY,SAAS;;;KAQvD;EACH,oBAAoB;EACpB,sBAAsB;EACtB,QAAQ,cAAc;;;KAInB,OAAO,KAAK;EAAY,QAAQ,YAAY,MAAM;IAAwB;;;;;;;;;KAW1E;;;;;;;;KASA,cAAc,KAAK,sBAAsB,WAAW,IAAI,EAAE;EAAa,aAAa;IAAM,eAAc;;KAGjG,mBAAmB,KAAK,OAAO;EAAa,aAAa;IAAM,cAAc,KAAK;;KAGlF,qBAAqB,KAAK,OAAO;EAAa,eAAe;IAAM,cAAc,KAAK;;KAGtF,oBAAoB,KAAK,OAAO;EAAa,cAAc;IAAqB,IAAI;;;;;;;;;KAU3F,mBAAmB,2BAA2B,eAAe,0BAA0B,kCACpF,cAAc,sBACX,sBAAsB,iBAE7B,MAAM,SAAS,SAAS,mBAAmB,MAAM,WACjD;;;;;;KAOC,MAAM,wBAAwB,KAAK;EAAkB;IACtD,mBAAmB,KACnB;EAAkB;IAChB,qBAAqB,KACrB;EAAkB;IAChB,oBAAoB;KAGvB;EAAoB;;KACpB;EAAsB;;KACtB;EAAqB;;;KAGd,qBAAqB,2BAA2B,iBAC1D,mBAAmB,WAAW,gCAA+B,oBAAmB,IAAI;;KAG1E,uBAAuB,2BAA2B,iBAC5D,mBAAmB,WAAW,kCAAiC,oBAAmB,IAAI;;KAG5E,sBAAsB,2BAA2B,iBAC3D,mBAAmB,WAAW,iCAAgC,oBAAmB,IAAI;;;;;;;;KASlF,YAAY,oCACd,WAAW,KAAK,EAAE,gDACb;;;;;;;;;;;;;KAoBH,oBAAoB,2BAA2B,iBAClD,YAAY,qBAAqB,6EAE7B,YAAY,uBAAuB,+EAEjC,YAAY,sBAAsB,kFAE5B;;;;UASG,YAAY,kBAAkB;EAC7C,OAAO,aAAa;EACpB,SAAS,eAAe;EACxB,WAAW;;;;;;;;;;UAWI,mBAAmB,kBAAkB,kBAAkB;;EAEtE,SAAS;;EAET,eAAe,QAAQ;;EAEvB,iBAAiB,QAAQ;;iBA2DX,wBAAwB,2BAA2B,eACjE,WAAW,oBAAoB,YAC/B,UAAU,mBACR,qBAAqB,YACrB,uBAAuB,YACvB,sBAAsB,cAEvB,YAAY,qBAAqB,YAAY,uBAAuB;;;;;;;;;;KA8DlE,eAAe,sBAAsB,eAAe,qBAAqB,8BACtE,eACG,gBACP,WACG,eAAe,oDAAoD,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAsCzF,kBAAkB,oBAAoB,0BAA0B,qBAC9D,KAAK,OAAO,UACZ,KAAK,MAAM;KAEb,WAAW,iBAAiB,YAAY,mBAAmB,YAAY,sBAAsB,2BAEhG,YAEA,6BAA6B,cAC7B,4BAA4B;iBAEd,eACd,iBAAiB,aAAa,OAC9B,mBAAmB,aAAa,OAChC,qBAAqB,aACf,2BAA2B,iCAC3B,2BAA2B,6BACjC,UAAU,kBAAkB,iBAE5B;EACE,WAAW;EACX,aAAa;EACb,QAAQ;IAAQ,OAAO;IAAU,SAAS;IAAY,QAAQ;QAAU;IACtE,eAAe,IAAI,qBACrB,eAAe,IAAI;EAErB,WAAW;EACX,aAAa;EACb,QAAQ,MAAM,WAAW,UAAU,YAAY,SAAS"}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { signal } from "../signals/primitives.js";
|
||||
//#region src/core/composition/create-composition.ts
|
||||
/**
|
||||
* Create a composition from a set of behaviors.
|
||||
*
|
||||
* Composition unions the behaviors' declared `stateKeys` / `contextKeys`
|
||||
* to know which signals to create. Each signal is seeded from
|
||||
* `initialState` / `initialContext` when supplied, defaulting to
|
||||
* `undefined`. Behaviors are responsible for writing their own slots
|
||||
* once their preconditions are met.
|
||||
*
|
||||
* Cross-behavior type conflicts (e.g. two behaviors disagreeing on a
|
||||
* field's type) surface as a compose-time type error via
|
||||
* `ValidateComposition`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const composition = createComposition([resolvePresentation, switchVideoTrack], {
|
||||
* config: { parsePresentation: parseMultivariantPlaylist, initialBandwidth: 2_000_000 },
|
||||
* initialState: { bandwidthState: { fastEstimate: 0, ... } },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
/**
|
||||
* Create a typed signal map for a given set of keys, seeded from an
|
||||
* optional partial initial value.
|
||||
*
|
||||
* Pipeline: `Set` dedupes the iterable (insertion order preserved, so
|
||||
* first occurrence wins) → `Object.fromEntries` materializes one
|
||||
* `signal()` per unique key, seeded from `initial[key]` or `undefined`.
|
||||
*
|
||||
* Per-key value types live in TypeScript only — at runtime every signal
|
||||
* is `Signal<unknown>`. The boundary cast at the return narrows the wide
|
||||
* `Record<PropertyKey, Signal<unknown>>` shape to the caller's expected
|
||||
* per-key types from `S`.
|
||||
*
|
||||
* Used by `createComposition` to derive engine state/context maps from
|
||||
* the union of behaviors' declared `stateKeys` / `contextKeys`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* interface State { count?: number; label?: string }
|
||||
* const state = buildSignalMap<State>(['count', 'label'], { count: 5 });
|
||||
* state.count.get(); // 5
|
||||
* state.label.get(); // undefined
|
||||
* ```
|
||||
*/
|
||||
function buildSignalMap(keys, initial) {
|
||||
const init = initial;
|
||||
const uniqueKeys = new Set(keys);
|
||||
return Object.fromEntries([...uniqueKeys].map((key) => [key, signal(init[key])]));
|
||||
}
|
||||
function createComposition(behaviors, options) {
|
||||
const validBehaviors = behaviors;
|
||||
const state = buildSignalMap(validBehaviors.flatMap((b) => b.stateKeys), options?.initialState ?? {});
|
||||
const context = buildSignalMap(validBehaviors.flatMap((b) => b.contextKeys), options?.initialContext ?? {});
|
||||
const deps = {
|
||||
state,
|
||||
context,
|
||||
config: options?.config ?? {}
|
||||
};
|
||||
const cleanups = validBehaviors.map((behavior) => behavior.setup(deps));
|
||||
return {
|
||||
state,
|
||||
context,
|
||||
async destroy() {
|
||||
const results = [];
|
||||
for (const cleanup of cleanups) {
|
||||
if (cleanup == null) continue;
|
||||
if (typeof cleanup === "function") results.push(cleanup());
|
||||
else if ("destroy" in cleanup) results.push(cleanup.destroy());
|
||||
}
|
||||
await Promise.all(results);
|
||||
for (const sig of Object.values(state)) sig.set(void 0);
|
||||
for (const sig of Object.values(context)) sig.set(void 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
function defineBehavior(behavior) {
|
||||
return behavior;
|
||||
}
|
||||
//#endregion
|
||||
export { buildSignalMap, createComposition, defineBehavior };
|
||||
|
||||
//# sourceMappingURL=create-composition.js.map
|
||||
File diff suppressed because one or more lines are too long
+46
@@ -0,0 +1,46 @@
|
||||
import { Behavior, ContextSignals, StateSignals } from "./create-composition.js";
|
||||
//#region src/core/composition/share-signals.d.ts
|
||||
/**
|
||||
* Config consumed by the `shareSignals` behavior.
|
||||
*
|
||||
* The callback fires once during composition setup with the composition's
|
||||
* state and context signal refs. Capture them to drive the composition
|
||||
* externally (writes) or observe its state (reads).
|
||||
*
|
||||
* The callback runs while other behaviors are still in their setup phase —
|
||||
* for the typical "capture refs, use later" pattern this is fine (signal
|
||||
* refs are stable identities), but reading inside the callback may yield
|
||||
* only initial-seed values rather than what later behaviors write.
|
||||
*/
|
||||
interface ShareSignalsConfig<S extends object, C extends object> {
|
||||
onSignalsReady?: (signals: {
|
||||
state: StateSignals<S>;
|
||||
context: ContextSignals<C>;
|
||||
}) => void;
|
||||
}
|
||||
/**
|
||||
* Behavior factory that hands the composition's signal refs to a
|
||||
* consumer-supplied callback (`config.onSignalsReady`) at setup time.
|
||||
*
|
||||
* Generic over `S` and `C` — the caller instantiates with their own
|
||||
* state/context types, and the callback's parameter shape is fully
|
||||
* type-driven from those. Suitable for both reads and writes (per-slot
|
||||
* intent can be expressed by typing captured refs as `Signal<T>` or
|
||||
* `ReadonlySignal<T>` at the call site).
|
||||
*
|
||||
* By default declares no keys; the composition's state/context maps come from
|
||||
* other behaviors. Pass `inputStateKeys` / `inputContextKeys` to *materialize*
|
||||
* consumer-input slots that no other behavior produces — a slot the consumer
|
||||
* writes (e.g. `userAudioTrackSelection`) but only a rule reads. shareSignals
|
||||
* is the consumer boundary, so it's the natural place to bring those slots into
|
||||
* existence; readers then treat them as optional.
|
||||
*
|
||||
* Uses a `Behavior<>` literal (not `defineBehavior`) so its (possibly empty,
|
||||
* possibly partial) key arrays don't trip the exhaustiveness check — the
|
||||
* setup-param state/context shapes describe what the callback receives (the
|
||||
* full `S` / `C`), not the subset this behavior materializes.
|
||||
*/
|
||||
declare function makeShareSignals<S extends object, C extends object>(inputStateKeys?: readonly (keyof S)[], inputContextKeys?: readonly (keyof C)[]): Behavior<StateSignals<S>, ContextSignals<C>, ShareSignalsConfig<S, C>>;
|
||||
//#endregion
|
||||
export { ShareSignalsConfig, makeShareSignals };
|
||||
//# sourceMappingURL=share-signals.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"share-signals.d.ts","names":[],"sources":["../../../../src/core/composition/share-signals.ts"],"mappings":";;;;;;;;;;;;;;UAciB,mBAAmB,kBAAkB;EACpD,kBAAkB;IAAW,OAAO,aAAa;IAAI,SAAS,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;iBAyB/D,iBAAiB,kBAAkB,kBACjD,iCAAgC,MAChC,mCAAkC,OACjC,SAAS,aAAa,IAAI,eAAe,IAAI,mBAAmB,GAAG"}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
//#region src/core/composition/share-signals.ts
|
||||
/**
|
||||
* Behavior factory that hands the composition's signal refs to a
|
||||
* consumer-supplied callback (`config.onSignalsReady`) at setup time.
|
||||
*
|
||||
* Generic over `S` and `C` — the caller instantiates with their own
|
||||
* state/context types, and the callback's parameter shape is fully
|
||||
* type-driven from those. Suitable for both reads and writes (per-slot
|
||||
* intent can be expressed by typing captured refs as `Signal<T>` or
|
||||
* `ReadonlySignal<T>` at the call site).
|
||||
*
|
||||
* By default declares no keys; the composition's state/context maps come from
|
||||
* other behaviors. Pass `inputStateKeys` / `inputContextKeys` to *materialize*
|
||||
* consumer-input slots that no other behavior produces — a slot the consumer
|
||||
* writes (e.g. `userAudioTrackSelection`) but only a rule reads. shareSignals
|
||||
* is the consumer boundary, so it's the natural place to bring those slots into
|
||||
* existence; readers then treat them as optional.
|
||||
*
|
||||
* Uses a `Behavior<>` literal (not `defineBehavior`) so its (possibly empty,
|
||||
* possibly partial) key arrays don't trip the exhaustiveness check — the
|
||||
* setup-param state/context shapes describe what the callback receives (the
|
||||
* full `S` / `C`), not the subset this behavior materializes.
|
||||
*/
|
||||
function makeShareSignals(inputStateKeys = [], inputContextKeys = []) {
|
||||
return {
|
||||
stateKeys: inputStateKeys,
|
||||
contextKeys: inputContextKeys,
|
||||
setup: ({ state, context, config }) => {
|
||||
config.onSignalsReady?.({
|
||||
state,
|
||||
context
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { makeShareSignals };
|
||||
|
||||
//# sourceMappingURL=share-signals.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"share-signals.js","names":[],"sources":["../../../../src/core/composition/share-signals.ts"],"sourcesContent":["import type { Behavior, ContextSignals, StateSignals } from './create-composition';\n\n/**\n * Config consumed by the `shareSignals` behavior.\n *\n * The callback fires once during composition setup with the composition's\n * state and context signal refs. Capture them to drive the composition\n * externally (writes) or observe its state (reads).\n *\n * The callback runs while other behaviors are still in their setup phase —\n * for the typical \"capture refs, use later\" pattern this is fine (signal\n * refs are stable identities), but reading inside the callback may yield\n * only initial-seed values rather than what later behaviors write.\n */\nexport interface ShareSignalsConfig<S extends object, C extends object> {\n onSignalsReady?: (signals: { state: StateSignals<S>; context: ContextSignals<C> }) => void;\n}\n\n/**\n * Behavior factory that hands the composition's signal refs to a\n * consumer-supplied callback (`config.onSignalsReady`) at setup time.\n *\n * Generic over `S` and `C` — the caller instantiates with their own\n * state/context types, and the callback's parameter shape is fully\n * type-driven from those. Suitable for both reads and writes (per-slot\n * intent can be expressed by typing captured refs as `Signal<T>` or\n * `ReadonlySignal<T>` at the call site).\n *\n * By default declares no keys; the composition's state/context maps come from\n * other behaviors. Pass `inputStateKeys` / `inputContextKeys` to *materialize*\n * consumer-input slots that no other behavior produces — a slot the consumer\n * writes (e.g. `userAudioTrackSelection`) but only a rule reads. shareSignals\n * is the consumer boundary, so it's the natural place to bring those slots into\n * existence; readers then treat them as optional.\n *\n * Uses a `Behavior<>` literal (not `defineBehavior`) so its (possibly empty,\n * possibly partial) key arrays don't trip the exhaustiveness check — the\n * setup-param state/context shapes describe what the callback receives (the\n * full `S` / `C`), not the subset this behavior materializes.\n */\nexport function makeShareSignals<S extends object, C extends object>(\n inputStateKeys: readonly (keyof S)[] = [],\n inputContextKeys: readonly (keyof C)[] = []\n): Behavior<StateSignals<S>, ContextSignals<C>, ShareSignalsConfig<S, C>> {\n return {\n stateKeys: inputStateKeys,\n contextKeys: inputContextKeys,\n setup: ({ state, context, config }) => {\n config.onSignalsReady?.({ state, context });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,iBACd,iBAAuC,CAAC,GACxC,mBAAyC,CAAC,GAC8B;CACxE,OAAO;EACL,WAAW;EACX,aAAa;EACb,QAAQ,EAAE,OAAO,SAAS,aAAa;GACrC,OAAO,iBAAiB;IAAE;IAAO;GAAQ,CAAC;EAC5C;CACF;AACF"}
|
||||
Reference in New Issue
Block a user