build(spf): build26 from 45504a2b

This commit is contained in:
publish
2026-08-05 18:57:39 +02:00
commit 8920135306
424 changed files with 17907 additions and 0 deletions
+273
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"create-composition.d.ts","names":[],"sources":["../../../../src/core/composition/create-composition.ts"],"mappings":"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"}
+85
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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"}