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:
@@ -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
@@ -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"}
|
||||
@@ -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
@@ -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"}
|
||||
Vendored
+26
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
@@ -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
Vendored
+41
@@ -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
@@ -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
@@ -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
@@ -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"}
|
||||
Vendored
+191
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user