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
+91
View File
@@ -0,0 +1,91 @@
import { untrack, update } from "../signals/primitives.js";
import { createMachineCore } from "../machine.js";
//#region src/core/actors/create-machine-actor.ts
/**
* Creates a message-driven actor from a declarative definition.
*
* The actor owns a reactive snapshot signal (state + context), an optional
* runner, and dispatches incoming messages to per-state handlers. `'destroyed'`
* is always the implicit terminal state — `destroy()` transitions there
* unconditionally and all subsequent `send()` calls are no-ops.
*
* When a state declares `onSettled`, the framework calls `runner.whenSettled()`
* after the handler returns. The runner owns the generation-token logic — if
* new tasks are scheduled before the current batch settles, the callback is
* automatically superseded.
*
* @example
* const actor = createMachineActor({
* runner: () => new SerialRunner(),
* initial: 'idle',
* context: {},
* states: {
* idle: {
* on: {
* load: (msg, { transition, runner }) => {
* segments.forEach(s => runner.schedule(new Task(...)));
* transition('loading');
* }
* }
* },
* loading: {
* onSettled: 'idle',
* on: {
* load: (msg, { runner }) => {
* runner.abortAll();
* segments.forEach(s => runner.schedule(new Task(...)));
* }
* }
* }
* }
* });
*/
function createMachineActor(def) {
const runner = def.runner?.();
const { snapshotSignal, getState, transition } = createMachineCore({
value: def.initial,
context: def.context
});
const getContext = () => untrack(() => snapshotSignal.get().context);
const setContext = (context) => {
update(snapshotSignal, { context });
};
return {
get snapshot() {
return snapshotSignal;
},
send(message) {
const state = getState();
if (state === "destroyed") return;
const handler = def.states[state]?.on?.[message.type];
if (!handler) return;
handler(message, {
context: getContext(),
getContext,
transition: (to) => transition(to),
setContext,
...runner ? { runner } : {}
});
const newState = getState();
if (newState !== "destroyed") {
const newStateDef = def.states[newState];
if (newStateDef?.onSettled && runner) {
const targetState = newStateDef.onSettled;
runner.whenSettled(() => {
if (getState() !== newState) return;
transition(targetState);
});
}
}
},
destroy() {
if (getState() === "destroyed") return;
runner?.destroy();
transition("destroyed");
}
};
}
//#endregion
export { createMachineActor };
//# sourceMappingURL=create-machine-actor.js.map
File diff suppressed because one or more lines are too long
+46
View File
@@ -0,0 +1,46 @@
import { untrack, update } from "../signals/primitives.js";
import { createMachineCore } from "../machine.js";
//#region src/core/actors/create-transition-actor.ts
/**
* Creates a reducer-shaped actor from an initial context and a reducer function.
*
* The reducer receives the current context and a message and returns the next
* context. Returning the same reference (by identity) skips the signal update —
* so early-returning `context` unchanged is both the no-op and the optimization.
*
* Side effects (e.g. DOM mutations) may be performed inside the reducer.
* They run synchronously before the signal is updated.
*
* @example
* const actor = createTransitionActor(
* { count: 0 },
* (context, message: { type: 'increment' }) => ({ count: context.count + 1 })
* );
*/
function createTransitionActor(initialContext, reducer) {
const { snapshotSignal, getState, transition } = createMachineCore({
value: "active",
context: initialContext
});
const getContext = () => untrack(() => snapshotSignal.get().context);
const setContext = (context) => update(snapshotSignal, { context });
return {
get snapshot() {
return snapshotSignal;
},
send(message) {
if (getState() === "destroyed") return;
const context = getContext();
const newContext = reducer(context, message);
if (newContext !== context) setContext(newContext);
},
destroy() {
if (getState() === "destroyed") return;
transition("destroyed");
}
};
}
//#endregion
export { createTransitionActor };
//# sourceMappingURL=create-transition-actor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"create-transition-actor.js","names":[],"sources":["../../../../src/core/actors/create-transition-actor.ts"],"sourcesContent":["import type { Machine } from '../machine';\nimport { createMachineCore } from '../machine';\nimport { untrack, update } from '../signals/primitives';\nimport type { ActorSnapshot } from './actor';\n\n// =============================================================================\n// Definition types\n// =============================================================================\n\n/**\n * A reducer-shaped actor: `(context, message) => context`.\n *\n * No finite states — the snapshot carries `value: 'active' | 'destroyed'`\n * as a universal lifecycle marker rather than domain state. The interesting\n * state is entirely in the context, which is reactive via `snapshot`.\n *\n * Use this when the actor has context that needs to be reactive but no\n * meaningful state machine (e.g., a message-driven model with DOM side\n * effects). For actors that need per-state behavior, use `createMachineActor`.\n */\nexport interface TransitionActor<Context extends object, Message extends { type: string }>\n extends Machine<ActorSnapshot<'active' | 'destroyed', Context>> {\n send(message: Message): void;\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a reducer-shaped actor from an initial context and a reducer function.\n *\n * The reducer receives the current context and a message and returns the next\n * context. Returning the same reference (by identity) skips the signal update —\n * so early-returning `context` unchanged is both the no-op and the optimization.\n *\n * Side effects (e.g. DOM mutations) may be performed inside the reducer.\n * They run synchronously before the signal is updated.\n *\n * @example\n * const actor = createTransitionActor(\n * { count: 0 },\n * (context, message: { type: 'increment' }) => ({ count: context.count + 1 })\n * );\n */\nexport function createTransitionActor<Context extends object, Message extends { type: string }>(\n initialContext: Context,\n reducer: (context: Context, message: Message) => Context\n): TransitionActor<Context, Message> {\n const { snapshotSignal, getState, transition } = createMachineCore<\n 'active' | 'destroyed',\n ActorSnapshot<'active' | 'destroyed', Context>\n >({ value: 'active', context: initialContext });\n\n const getContext = (): Context => untrack(() => snapshotSignal.get().context);\n const setContext = (context: Context): void => update(snapshotSignal, { context });\n\n return {\n get snapshot() {\n return snapshotSignal;\n },\n\n send(message: Message): void {\n if (getState() === 'destroyed') return;\n const context = getContext();\n const newContext = reducer(context, message);\n if (newContext !== context) setContext(newContext);\n },\n\n destroy(): void {\n if (getState() === 'destroyed') return;\n transition('destroyed');\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,sBACd,gBACA,SACmC;CACnC,MAAM,EAAE,gBAAgB,UAAU,eAAe,kBAG/C;EAAE,OAAO;EAAU,SAAS;CAAe,CAAC;CAE9C,MAAM,mBAA4B,cAAc,eAAe,IAAI,CAAC,CAAC,OAAO;CAC5E,MAAM,cAAc,YAA2B,OAAO,gBAAgB,EAAE,QAAQ,CAAC;CAEjF,OAAO;EACL,IAAI,WAAW;GACb,OAAO;EACT;EAEA,KAAK,SAAwB;GAC3B,IAAI,SAAS,MAAM,aAAa;GAChC,MAAM,UAAU,WAAW;GAC3B,MAAM,aAAa,QAAQ,SAAS,OAAO;GAC3C,IAAI,eAAe,SAAS,WAAW,UAAU;EACnD;EAEA,UAAgB;GACd,IAAI,SAAS,MAAM,aAAa;GAChC,WAAW,WAAW;EACxB;CACF;AACF"}
+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
+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"}
+26
View File
@@ -0,0 +1,26 @@
import { signal, untrack, update } from "./signals/primitives.js";
//#region src/core/machine.ts
/**
* Provisions the shared mechanics for all machine-like primitives: a snapshot
* signal, an untracked state reader, and a transition function.
*
* Internal — consumed by `createMachineActor` and `createMachineReactor`. Not part of the
* public API.
*/
function createMachineCore(initialSnapshot) {
const snapshotSignal = signal(initialSnapshot);
const getState = () => untrack(() => snapshotSignal.get().value);
const transition = (to) => update(snapshotSignal, (current) => ({
...current,
value: to
}));
return {
snapshotSignal,
getState,
transition
};
}
//#endregion
export { createMachineCore };
//# sourceMappingURL=machine.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"machine.js","names":[],"sources":["../../../src/core/machine.ts"],"sourcesContent":["import type { ReadonlySignal } from './signals/primitives';\nimport { signal, untrack, update } from './signals/primitives';\n\n// =============================================================================\n// Shared snapshot type\n// =============================================================================\n\n/**\n * Base snapshot for all machine-like primitives (Actors and Reactors).\n * Carries only the finite state value. Actors extend this with `context`.\n */\nexport interface MachineSnapshot<State extends string> {\n value: State;\n}\n\n// =============================================================================\n// Shared interface\n// =============================================================================\n\n/**\n * Shared interface for all machine-like primitives.\n * Both Actors (message-driven) and Reactors (signal-driven) implement this.\n */\nexport interface Machine<Snapshot extends MachineSnapshot<string>> {\n readonly snapshot: ReadonlySignal<Snapshot>;\n destroy(): void;\n}\n\n// =============================================================================\n// Shared core factory\n// =============================================================================\n\n/**\n * Provisions the shared mechanics for all machine-like primitives: a snapshot\n * signal, an untracked state reader, and a transition function.\n *\n * Internal — consumed by `createMachineActor` and `createMachineReactor`. Not part of the\n * public API.\n */\nexport function createMachineCore<FullState extends string, Snapshot extends MachineSnapshot<FullState>>(\n initialSnapshot: Snapshot\n) {\n const snapshotSignal = signal(initialSnapshot);\n const getState = (): FullState => untrack(() => snapshotSignal.get().value);\n const transition = (to: FullState): void => update(snapshotSignal, (current) => ({ ...current, value: to }));\n return { snapshotSignal, getState, transition };\n}\n"],"mappings":";;;;;;;;;AAuCA,SAAgB,kBACd,iBACA;CACA,MAAM,iBAAiB,OAAO,eAAe;CAC7C,MAAM,iBAA4B,cAAc,eAAe,IAAI,CAAC,CAAC,KAAK;CAC1E,MAAM,cAAc,OAAwB,OAAO,iBAAiB,aAAa;EAAE,GAAG;EAAS,OAAO;CAAG,EAAE;CAC3G,OAAO;EAAE;EAAgB;EAAU;CAAW;AAChD"}
+83
View File
@@ -0,0 +1,83 @@
import { untrack } from "../signals/primitives.js";
import { effect } from "../signals/effect.js";
import { createMachineCore } from "../machine.js";
//#region src/core/reactors/create-machine-reactor.ts
const toArray = (x) => x === void 0 ? [] : Array.isArray(x) ? x : [x];
/**
* Creates a reactive Reactor from a declarative definition.
*
* A Reactor is driven by subscriptions to external signals rather than
* imperative messages. Each state holds an array of effect functions —
* every element becomes one independent `effect()` call gated on that state,
* with its own dependency tracking and cleanup lifecycle.
*
* `'destroying'` and `'destroyed'` are always implicit terminal states.
* `destroy()` transitions through both in sequence: `'destroying'` first (for
* potential async teardown in a future extension), then immediately `'destroyed'`
* for the synchronous base case. Active effect cleanups fire via disposal.
*
* @example
* const reactor = createMachineReactor({
* initial: 'waiting',
* monitor: () => srcSignal.get() ? 'active' : 'waiting',
* states: {
* active: {
* // entry: runs once on state entry; fn body is automatically untracked.
* entry: () => listen(el, 'play', handler),
* // effects: re-runs whenever tracked signals change.
* effects: () => { currentTimeSignal.get(); return cleanup; },
* },
* waiting: {},
* }
* });
*/
function createMachineReactor(def) {
const { snapshotSignal, getState, transition } = createMachineCore({ value: def.initial });
const effectDisposals = [];
const wrapResult = (result) => {
if (!result) return void 0;
if (typeof result === "function") return result;
return () => result.abort();
};
const untracked = (baseCall) => () => untrack(baseCall);
const isTerminal = (snapshot) => snapshot.value === "destroying" || snapshot.value === "destroyed";
const descriptors = [...toArray(def.monitor).map((fn) => ({
fn: () => {
const target = fn();
if (target !== getState()) transition(target);
},
shouldSkip: isTerminal
})), ...Object.entries(def.states).flatMap(([state, stateDef]) => {
const isNotState = (snapshot) => snapshot.value !== state;
return [...toArray(stateDef.entry).map((fn) => ({
fn,
shouldSkip: isNotState,
toFnCall: untracked
})), ...toArray(stateDef.effects).map((fn) => ({
fn,
shouldSkip: isNotState
}))];
})];
const toEffect = ({ fn, shouldSkip, toFnCall = (baseCall) => baseCall }) => effect(() => {
if (shouldSkip(snapshotSignal.get())) return;
const baseCall = () => fn();
return wrapResult(toFnCall(baseCall)());
});
effectDisposals.push(...descriptors.map(toEffect));
return {
get snapshot() {
return snapshotSignal;
},
destroy() {
const state = getState();
if (state === "destroying" || state === "destroyed") return;
transition("destroying");
transition("destroyed");
for (const dispose of effectDisposals) dispose();
}
};
}
//#endregion
export { createMachineReactor };
//# sourceMappingURL=create-machine-reactor.js.map
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
import { Signal } from "signal-polyfill";
//#region src/core/signals/effect.ts
const pending = /* @__PURE__ */ new Set();
const watcher = new Signal.subtle.Watcher(() => {
queueMicrotask(runPending);
});
function runPending() {
for (const c of watcher.getPending()) pending.add(c);
watcher.watch();
for (const c of pending) {
pending.delete(c);
c.get();
}
}
/**
* Run a side effect whenever its signal dependencies change.
*
* Executes immediately (synchronous initial run), then re-runs on the next
* microtask after any dependency changes. If the callback returns a function,
* it is called before each re-run and when the effect is stopped — the same
* cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.
*
* Returns a cleanup function that stops the effect.
*/
function effect(fn) {
let cleanup;
const c = new Signal.Computed(() => {
if (typeof cleanup === "function") cleanup();
cleanup = fn();
});
watcher.watch(c);
c.get();
return () => {
watcher.unwatch(c);
if (typeof cleanup === "function") cleanup();
};
}
//#endregion
export { effect };
//# sourceMappingURL=effect.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"effect.js","names":[],"sources":["../../../../src/core/signals/effect.ts"],"sourcesContent":["import { Signal } from 'signal-polyfill';\n\n// Computeds waiting to re-run after their dependencies changed.\nconst pending = new Set<Signal.Computed<void>>();\n\nconst watcher = new Signal.subtle.Watcher(() => {\n queueMicrotask(runPending);\n});\n\nfunction runPending() {\n for (const c of watcher.getPending()) {\n pending.add(c as Signal.Computed<void>);\n }\n watcher.watch(); // re-arm before running effects, in case they write signals\n for (const c of pending) {\n pending.delete(c);\n c.get(); // re-run the effect body\n }\n}\n\n/**\n * Run a side effect whenever its signal dependencies change.\n *\n * Executes immediately (synchronous initial run), then re-runs on the next\n * microtask after any dependency changes. If the callback returns a function,\n * it is called before each re-run and when the effect is stopped — the same\n * cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.\n *\n * Returns a cleanup function that stops the effect.\n */\nexport function effect(fn: () => (() => void) | void): () => void {\n let cleanup: (() => void) | void;\n const c = new Signal.Computed(() => {\n if (typeof cleanup === 'function') cleanup();\n cleanup = fn();\n });\n watcher.watch(c);\n c.get(); // initial run\n return () => {\n watcher.unwatch(c);\n if (typeof cleanup === 'function') cleanup();\n };\n}\n"],"mappings":";;AAGA,MAAM,0BAAU,IAAI,IAA2B;AAE/C,MAAM,UAAU,IAAI,OAAO,OAAO,cAAc;CAC9C,eAAe,UAAU;AAC3B,CAAC;AAED,SAAS,aAAa;CACpB,KAAK,MAAM,KAAK,QAAQ,WAAW,GACjC,QAAQ,IAAI,CAA0B;CAExC,QAAQ,MAAM;CACd,KAAK,MAAM,KAAK,SAAS;EACvB,QAAQ,OAAO,CAAC;EAChB,EAAE,IAAI;CACR;AACF;;;;;;;;;;;AAYA,SAAgB,OAAO,IAA2C;CAChE,IAAI;CACJ,MAAM,IAAI,IAAI,OAAO,eAAe;EAClC,IAAI,OAAO,YAAY,YAAY,QAAQ;EAC3C,UAAU,GAAG;CACf,CAAC;CACD,QAAQ,MAAM,CAAC;CACf,EAAE,IAAI;CACN,aAAa;EACX,QAAQ,QAAQ,CAAC;EACjB,IAAI,OAAO,YAAY,YAAY,QAAQ;CAC7C;AACF"}
+54
View File
@@ -0,0 +1,54 @@
import { Signal } from "signal-polyfill";
//#region src/core/signals/primitives.ts
/** Read a signal value without tracking it as a dependency. */
const untrack = Signal.subtle.untrack;
/**
* Read a signal's current value without tracking it as a dependency. Sugar
* for `untrack(() => signal.get())` to reduce boilerplate at single-read
* sites. Structurally typed to accept any signal-like (Signal, Computed,
* ReadonlySignal).
*
* Accepts an optional `transform` to project the value in the same call;
* the default is the identity function so the single-arg form returns `T`
* unchanged.
*
* @example
* const value = peek(someSignal);
* const id = peek(presentationSignal, (p) => p?.id);
*/
function peek(source, transform = (v) => v) {
return untrack(() => transform(source.get()));
}
/** Create a writable reactive value. */
function signal(initialValue, options) {
return new Signal.State(initialValue, options);
}
/** Create a computed reactive value. */
function computed(fn, options) {
return new Signal.Computed(fn, options);
}
function update(signal, updater) {
const current = untrack(() => signal.get());
if (typeof updater === "function") signal.set(updater(current));
else signal.set({
...current,
...updater
});
}
/**
* Read every signal in a map and return a plain object snapshot. Each read
* tracks in the surrounding Computed/Effect — equivalent to calling `.get()`
* on a single `Signal<S>` over the merged shape.
*
* Convenience for behaviors that pass whole state/context snapshots to pure
* helpers; prefer per-field reads when only a few fields are needed.
*/
function snapshot(map) {
const out = {};
for (const key in map) out[key] = map[key].get();
return out;
}
//#endregion
export { computed, peek, signal, snapshot, untrack, update };
//# sourceMappingURL=primitives.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"primitives.js","names":["SignalNS"],"sources":["../../../../src/core/signals/primitives.ts"],"sourcesContent":["import { Signal as SignalNS } from 'signal-polyfill';\n\n/** Read a signal value without tracking it as a dependency. */\nexport const untrack: <T>(fn: () => T) => T = SignalNS.subtle.untrack;\n\n/**\n * Read a signal's current value without tracking it as a dependency. Sugar\n * for `untrack(() => signal.get())` to reduce boilerplate at single-read\n * sites. Structurally typed to accept any signal-like (Signal, Computed,\n * ReadonlySignal).\n *\n * Accepts an optional `transform` to project the value in the same call;\n * the default is the identity function so the single-arg form returns `T`\n * unchanged.\n *\n * @example\n * const value = peek(someSignal);\n * const id = peek(presentationSignal, (p) => p?.id);\n */\nexport function peek<T, R = T>(source: { get(): T }, transform: (value: T) => R = (v: T) => v as unknown as R): R {\n return untrack(() => transform(source.get()));\n}\n\n/** A writable reactive value (read + write). */\nexport type Signal<T> = SignalNS.State<T>;\n\n/** A derived reactive value that re-evaluates when its dependencies change (read-only). */\nexport type Computed<T> = SignalNS.Computed<T>;\n\n/** A read-only view of a reactive value. */\nexport type ReadonlySignal<T> = Omit<SignalNS.State<T>, 'set'>;\n\nexport interface SignalOptions<T> {\n equals?: (t: T, t2: T) => boolean;\n}\n\n/** Create a writable reactive value. */\nexport function signal<T>(initialValue: T, options?: SignalOptions<T>): Signal<T> {\n return new SignalNS.State(initialValue, options as SignalNS.Options<T>);\n}\n\n/** Create a computed reactive value. */\nexport function computed<T>(fn: () => T, options?: SignalOptions<T>): Computed<T> {\n return new SignalNS.Computed(fn, options as SignalNS.Options<T>);\n}\n\n/**\n * Update a writable signal. Two forms:\n *\n * - **Updater function** `(current) => next`. Works for any signal type,\n * including `Signal<T | undefined>` — handle undefined in the updater.\n * - **Partial object** to merge into the current state. Requires\n * `T extends object`.\n *\n * @example\n * update(state, { playbackRate: 2 });\n * update(state, (s) => ({ ...s, count: s.count + 1 }));\n * update(maybeUndefinedSignal, (current) => current ?? defaultValue);\n */\nexport function update<T>(signal: Signal<T>, updater: (current: T) => T): void;\nexport function update<T extends object>(signal: Signal<T>, updater: Partial<T>): void;\nexport function update<T>(signal: Signal<T>, updater: ((current: T) => T) | object): void {\n const current = untrack(() => signal.get());\n if (typeof updater === 'function') {\n signal.set((updater as (current: T) => T)(current));\n } else {\n // Partial<T> form — `T extends object` enforced by the public overload.\n signal.set({ ...(current as object), ...updater } as T);\n }\n}\n\n/**\n * Equality comparator for objects with an optional `id` field. Designed for\n * use as a `computed` `equals` option when reacting to identity changes\n * (Ham-shaped objects, JSON-API-shaped resources) while filtering internal\n * updates that preserve the id.\n *\n * Handles undefined inputs symmetrically: both undefined → equal; one\n * undefined → different.\n *\n * @example\n * const presentationById = computed(() => state.presentation.get(), {\n * equals: equalsById,\n * });\n */\nexport function equalsById<T extends { id?: string }>(a: T | undefined, b: T | undefined): boolean {\n return a?.id === b?.id;\n}\n\n/**\n * Read every signal in a map and return a plain object snapshot. Each read\n * tracks in the surrounding Computed/Effect — equivalent to calling `.get()`\n * on a single `Signal<S>` over the merged shape.\n *\n * Convenience for behaviors that pass whole state/context snapshots to pure\n * helpers; prefer per-field reads when only a few fields are needed.\n */\nexport function snapshot<M extends Record<string, ReadonlySignal<unknown>>>(\n map: M\n): { [K in keyof M]: M[K] extends { get(): infer V } ? V : never } {\n const out = {} as { [K in keyof M]: M[K] extends { get(): infer V } ? V : never };\n for (const key in map) {\n out[key] = map[key]!.get() as never;\n }\n return out;\n}\n"],"mappings":";;;AAGA,MAAa,UAAiCA,OAAS,OAAO;;;;;;;;;;;;;;;AAgB9D,SAAgB,KAAe,QAAsB,aAA8B,MAAS,GAAsB;CAChH,OAAO,cAAc,UAAU,OAAO,IAAI,CAAC,CAAC;AAC9C;;AAgBA,SAAgB,OAAU,cAAiB,SAAuC;CAChF,OAAO,IAAIA,OAAS,MAAM,cAAc,OAA8B;AACxE;;AAGA,SAAgB,SAAY,IAAa,SAAyC;CAChF,OAAO,IAAIA,OAAS,SAAS,IAAI,OAA8B;AACjE;AAiBA,SAAgB,OAAU,QAAmB,SAA6C;CACxF,MAAM,UAAU,cAAc,OAAO,IAAI,CAAC;CAC1C,IAAI,OAAO,YAAY,YACrB,OAAO,IAAK,QAA8B,OAAO,CAAC;MAGlD,OAAO,IAAI;EAAE,GAAI;EAAoB,GAAG;CAAQ,CAAM;AAE1D;;;;;;;;;AA4BA,SAAgB,SACd,KACiE;CACjE,MAAM,MAAM,CAAC;CACb,KAAK,MAAM,OAAO,KAChB,IAAI,OAAO,IAAI,IAAI,CAAE,IAAI;CAE3B,OAAO;AACT"}
+191
View File
@@ -0,0 +1,191 @@
import { anyAbortSignal } from "@videojs/utils/events";
import { generateId } from "@videojs/utils/string";
//#region src/core/tasks/task.ts
/**
* Generic reusable task that wraps an async run function.
*
* Owns its own AbortController so it can always be aborted independently.
* Optionally composes an external AbortSignal so that a parent's cancellation
* propagates into the task's work without requiring the caller to track the
* task separately.
*
* Ordering guarantee: `value` is written before `status` transitions to `'done'`;
* `error` is written before `status` transitions to `'error'`. Any reader
* observing `status === 'done'` is guaranteed `value` is already present.
*/
var Task = class {
id;
#runFn;
#abortController = new AbortController();
#signal;
#status = "pending";
#value = void 0;
#error = void 0;
constructor(runFn, config) {
this.#runFn = runFn;
const rawId = config?.id;
this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
}
get status() {
return this.#status;
}
get value() {
return this.#value;
}
get error() {
return this.#error;
}
async run() {
this.#status = "running";
try {
const result = await this.#runFn(this.#signal);
this.#value = result;
this.#status = "done";
return result;
} catch (e) {
this.#error = e;
this.#status = "error";
throw e;
}
}
abort() {
this.#abortController.abort();
}
};
/**
* Runs tasks concurrently, deduplicated by task id.
*
* If a task with a given id is already in flight, subsequent schedule() calls
* for that id are silently ignored until the first completes. Tasks are stored
* so abortAll() can cancel any in-flight work (e.g. on engine cleanup).
*/
var ConcurrentRunner = class {
#pending = /* @__PURE__ */ new Map();
#settled = Promise.resolve();
#resolveSettled = null;
#destroyed = false;
schedule(task) {
if (this.#destroyed) return Promise.resolve();
const existing = this.#pending.get(task.id);
if (existing) return existing.promise;
if (this.#pending.size === 0) this.#settled = new Promise((resolve) => {
this.#resolveSettled = resolve;
});
const promise = task.run();
promise.catch(() => {});
const cleanup = () => {
this.#pending.delete(task.id);
if (this.#pending.size === 0) {
this.#resolveSettled?.();
this.#resolveSettled = null;
}
};
promise.then(cleanup, cleanup);
this.#pending.set(task.id, {
task,
promise
});
return promise;
}
/**
* Registers a callback to fire when all currently in-flight tasks settle.
* If the runner is already idle, the callback is never called. If abortAll()
* is called before the batch settles, the callback is superseded and silently
* dropped — no stale callbacks, no generation token required by the caller.
*/
whenSettled(callback) {
if (this.#pending.size === 0) return;
const captured = this.#settled;
captured.then(() => {
if (this.#settled !== captured) return;
callback();
}, () => {});
}
abortAll() {
for (const { task } of this.#pending.values()) task.abort();
this.#pending.clear();
this.#resolveSettled?.();
this.#resolveSettled = null;
this.#settled = Promise.resolve();
}
destroy() {
this.#destroyed = true;
this.abortAll();
}
};
/**
* Runs tasks one at a time in submission order.
*
* Each schedule() call returns a Promise that resolves or rejects with the
* task's result when it is eventually executed. Tasks wait in queue until the
* prior task completes.
*
* Serialization is achieved by chaining each task's run() onto the tail of a
* shared promise chain — no explicit queue or drain loop needed.
*
* abortAll() aborts all pending (not yet started) tasks and the currently
* in-flight task. Pending tasks still run briefly but receive an aborted
* signal and are expected to exit early.
*/
var SerialRunner = class {
#chain = Promise.resolve();
#pending = /* @__PURE__ */ new Set();
#current = null;
#destroyed = false;
schedule(task) {
if (this.#destroyed) return Promise.resolve();
const t = task;
this.#pending.add(t);
const result = this.#chain.then(() => {
this.#pending.delete(t);
this.#current = t;
return task.run();
}).finally(() => {
this.#current = null;
});
this.#chain = result.then(() => {}, () => {});
return result;
}
/**
* A promise that resolves when all currently-scheduled tasks have settled.
* Use the reference as a generation token: capture it after scheduling a
* batch, then check identity in the resolution callback to detect whether
* a subsequent abortAll() + new batch has superseded this one.
*/
get settled() {
return this.#chain;
}
/**
* Registers a callback to fire when all currently-pending tasks settle.
* If the runner is already idle (no pending or running tasks), the callback
* is never called. If new tasks are scheduled before the current batch
* settles, the callback is superseded and silently dropped — no stale
* callbacks, no generation token required by the caller.
*/
whenSettled(callback) {
if (this.#pending.size === 0 && this.#current === null) return;
const currentChain = this.#chain;
currentChain.then(() => {
if (this.#chain !== currentChain) return;
callback();
}, () => {});
}
/** Aborts and clears queued tasks without touching the in-flight task. */
abortPending() {
for (const task of this.#pending) task.abort();
this.#pending.clear();
}
abortAll() {
this.abortPending();
this.#current?.abort();
}
destroy() {
this.#destroyed = true;
this.abortAll();
}
};
//#endregion
export { ConcurrentRunner, SerialRunner, Task };
//# sourceMappingURL=task.js.map
File diff suppressed because one or more lines are too long