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:
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
import { Machine, MachineSnapshot } from "../machine.js";
|
||||
//#region src/core/actors/actor.d.ts
|
||||
/**
|
||||
* Complete actor snapshot: finite state + non-finite context.
|
||||
* Extends `MachineSnapshot` with context — the non-finite data managed by the actor.
|
||||
*/
|
||||
interface ActorSnapshot<State extends string, Context extends object> extends MachineSnapshot<State> {
|
||||
context: Context;
|
||||
}
|
||||
/** Generic actor interface: owns its snapshot as a reactive signal. */
|
||||
interface SignalActor<State extends string, Context extends object> extends Machine<ActorSnapshot<State, Context>> {}
|
||||
/**
|
||||
* A message-driven actor with no reactive snapshot.
|
||||
*
|
||||
* Use for actors that coordinate async work but have no state that external
|
||||
* consumers need to observe. Analogous to XState's `fromCallback`.
|
||||
*/
|
||||
interface CallbackActor<Message extends {
|
||||
type: string;
|
||||
}> {
|
||||
send(message: Message): void;
|
||||
destroy(): void;
|
||||
}
|
||||
//#endregion
|
||||
export { ActorSnapshot, CallbackActor, SignalActor };
|
||||
//# sourceMappingURL=actor.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"actor.d.ts","names":[],"sources":["../../../../src/core/actors/actor.ts"],"mappings":";;;;;;UAeiB,cAAc,sBAAsB,gCAAgC,gBAAgB;EACnG,SAAS;;;UAIM,YAAY,sBAAsB,gCACzC,QAAQ,cAAc,OAAO;;;;;;;UAQtB,cAAc;EAAkB;;EAC/C,KAAK,SAAS;EACd"}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { TaskLike } from "../tasks/task.js";
|
||||
import { SignalActor } from "./actor.js";
|
||||
//#region src/core/actors/create-machine-actor.d.ts
|
||||
/**
|
||||
* Minimal interface for any runner that can be used with createMachineActor.
|
||||
*/
|
||||
interface RunnerLike {
|
||||
schedule<Value = void, Err = unknown>(task: TaskLike<Value, Err>): Promise<Value>;
|
||||
abortAll(): void;
|
||||
destroy(): void;
|
||||
whenSettled(callback: () => void): void;
|
||||
}
|
||||
/**
|
||||
* Context passed to message handlers.
|
||||
* `runner` is present and typed as the exact runner instance only when the
|
||||
* definition includes a runner factory.
|
||||
*/
|
||||
type HandlerContext<UserState extends string, Context extends object, RunnerFactory extends (() => RunnerLike) | undefined> = {
|
||||
transition: (to: UserState) => void;
|
||||
/** Context snapshot captured at dispatch time. Stale after any `setContext` call. */
|
||||
context: Context;
|
||||
/**
|
||||
* Live untracked read of the current context. Use in async task closures that
|
||||
* execute after the handler returns — e.g. `getCtx: getContext` passed to tasks
|
||||
* scheduled on the runner, so each task reads the context committed by the
|
||||
* previous task rather than the stale snapshot from dispatch time.
|
||||
*/
|
||||
getContext: () => Context;
|
||||
setContext: (next: Context) => void;
|
||||
} & (RunnerFactory extends (() => infer R) ? {
|
||||
runner: R;
|
||||
} : object);
|
||||
/**
|
||||
* Definition for a single user-defined state.
|
||||
*/
|
||||
type ActorStateDefinition<UserState extends string, Context extends object, Message extends {
|
||||
type: string;
|
||||
}, RunnerFactory extends (() => RunnerLike) | undefined> = {
|
||||
/**
|
||||
* When the actor's runner settles while in this state, automatically
|
||||
* transition to this state. The framework owns the generation-token logic —
|
||||
* re-registering after each `runner.schedule()` call so that
|
||||
* `abortAll()` + reschedule correctly supersedes stale callbacks.
|
||||
*/
|
||||
onSettled?: UserState;
|
||||
/** Message handlers active in this state. Messages with no handler are silently dropped. */
|
||||
on?: { [M in Message as M['type']]?: (message: Extract<Message, {
|
||||
type: M['type'];
|
||||
}>, ctx: HandlerContext<UserState, Context, RunnerFactory>) => void; };
|
||||
};
|
||||
/**
|
||||
* Full actor definition passed to `createMachineActor`.
|
||||
*
|
||||
* `UserState` is the set of domain-meaningful states. `'destroyed'` is always
|
||||
* added by the framework as the implicit terminal state — do not include it here.
|
||||
*/
|
||||
type ActorDefinition<UserState extends string, Context extends object, Message extends {
|
||||
type: string;
|
||||
}, RunnerFactory extends (() => RunnerLike) | undefined = undefined> = {
|
||||
/**
|
||||
* Runner factory — called once at `createMachineActor()` time.
|
||||
* The runner lives for the full actor lifetime and is destroyed with it.
|
||||
*
|
||||
* @example
|
||||
* runner: () => new SerialRunner()
|
||||
*/
|
||||
runner?: RunnerFactory;
|
||||
/** Initial state. */
|
||||
initial: UserState;
|
||||
/** Initial context. */
|
||||
context: Context;
|
||||
/**
|
||||
* Per-state definitions. States with no definition silently drop all messages.
|
||||
* All user-defined states must appear as keys in the `UserState` union.
|
||||
*/
|
||||
states: Partial<Record<UserState, ActorStateDefinition<UserState, Context, Message, RunnerFactory>>>;
|
||||
};
|
||||
/** Live actor instance returned by `createMachineActor`. */
|
||||
interface MessageActor<State extends string, Context extends object, Message extends {
|
||||
type: string;
|
||||
}> extends SignalActor<State, Context> {
|
||||
send(message: Message): void;
|
||||
}
|
||||
/**
|
||||
* 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(...)));
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
declare function createMachineActor<UserState extends string, Context extends object, Message extends {
|
||||
type: string;
|
||||
}, RunnerFactory extends (() => RunnerLike) | undefined = undefined>(def: ActorDefinition<UserState, Context, Message, RunnerFactory>): MessageActor<UserState | 'destroyed', Context, Message>;
|
||||
//#endregion
|
||||
export { ActorDefinition, ActorStateDefinition, HandlerContext, MessageActor, RunnerLike, createMachineActor };
|
||||
//# sourceMappingURL=create-machine-actor.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"create-machine-actor.d.ts","names":[],"sources":["../../../../src/core/actors/create-machine-actor.ts"],"mappings":";;;;;;UAYiB;EACf,SAAS,cAAc,eAAe,MAAM,SAAS,OAAO,OAAO,QAAQ;EAC3E;EACA;EACA,YAAY;;;;;;;KAYF,eACV,0BACA,wBACA,6BAA6B;EAE7B,aAAa,IAAI;;EAEjB,SAAS;;;;;;;EAOT,kBAAkB;EAClB,aAAa,MAAM;KAChB,mCAAkC;EAAM,QAAQ;;;;;KAKzC,qBACV,0BACA,wBACA;EAAkB;GAClB,6BAA6B;;;;;;;EAQ7B,YAAY;;EAEZ,QACG,KAAK,WAAW,cACf,SAAS,QAAQ;IAAW,MAAM;MAClC,KAAK,eAAe,WAAW,SAAS;;;;;;;;KAWlC,gBACV,0BACA,wBACA;EAAkB;GAClB,6BAA6B;;;;;;;;EAS7B,SAAS;;EAET,SAAS;;EAET,SAAS;;;;;EAKT,QAAQ,QAAQ,OAAO,WAAW,qBAAqB,WAAW,SAAS,SAAS;;;UAQrE,aAAa,sBAAsB,wBAAwB;EAAkB;WACpF,YAAY,OAAO;EAC3B,KAAK,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8CA,mBACd,0BACA,wBACA;EAAkB;GAClB,6BAA6B,qCAE7B,KAAK,gBAAgB,WAAW,SAAS,SAAS,iBACjD,aAAa,yBAAyB,SAAS"}
|
||||
+91
@@ -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,41 @@
|
||||
import { ActorSnapshot } from "./actor.js";
|
||||
import { Machine } from "../machine.js";
|
||||
//#region src/core/actors/create-transition-actor.d.ts
|
||||
/**
|
||||
* A reducer-shaped actor: `(context, message) => context`.
|
||||
*
|
||||
* No finite states — the snapshot carries `value: 'active' | 'destroyed'`
|
||||
* as a universal lifecycle marker rather than domain state. The interesting
|
||||
* state is entirely in the context, which is reactive via `snapshot`.
|
||||
*
|
||||
* Use this when the actor has context that needs to be reactive but no
|
||||
* meaningful state machine (e.g., a message-driven model with DOM side
|
||||
* effects). For actors that need per-state behavior, use `createMachineActor`.
|
||||
*/
|
||||
interface TransitionActor<Context extends object, Message extends {
|
||||
type: string;
|
||||
}> extends Machine<ActorSnapshot<'active' | 'destroyed', Context>> {
|
||||
send(message: Message): void;
|
||||
}
|
||||
/**
|
||||
* 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 })
|
||||
* );
|
||||
*/
|
||||
declare function createTransitionActor<Context extends object, Message extends {
|
||||
type: string;
|
||||
}>(initialContext: Context, reducer: (context: Context, message: Message) => Context): TransitionActor<Context, Message>;
|
||||
//#endregion
|
||||
export { TransitionActor, createTransitionActor };
|
||||
//# sourceMappingURL=create-transition-actor.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"create-transition-actor.d.ts","names":[],"sources":["../../../../src/core/actors/create-transition-actor.ts"],"mappings":";;;;;;;;;;;;;;UAoBiB,gBAAgB,wBAAwB;EAAkB;WACjE,QAAQ,sCAAsC;EACtD,KAAK,SAAS;;;;;;;;;;;;;;;;;;iBAuBA,sBAAsB,wBAAwB;EAAkB;GAC9E,gBAAgB,SAChB,UAAU,SAAS,SAAS,SAAS,YAAY,UAChD,gBAAgB,SAAS"}
|
||||
+46
@@ -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"}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
import { ReadonlySignal, Signal } from "../signals/primitives.js";
|
||||
//#region src/core/composition/create-composition.d.ts
|
||||
/**
|
||||
* Cleanup returned by a behavior. Behaviors may return:
|
||||
* - `void` / `undefined` — no cleanup needed
|
||||
* - A function — called on destroy (may return a Promise)
|
||||
* - An object with `destroy()` — called on destroy (may return a Promise)
|
||||
*/
|
||||
type BehaviorCleanup = void | (() => void | Promise<void>) | {
|
||||
destroy(): void | Promise<void>;
|
||||
};
|
||||
/**
|
||||
* A signal map keyed by the fields of `S`. Each field is a writable signal.
|
||||
*
|
||||
* Optional fields on `S` map to required signal slots whose value type
|
||||
* includes `undefined`, ensuring every key has a signal even when the
|
||||
* underlying value is absent.
|
||||
*
|
||||
* Used in two roles:
|
||||
* - Engine-side **construction**: `Composition<S, C>` exposes its public
|
||||
* surface as `StateSignals<S>` (everything writable) so external code
|
||||
* can read or write any slot.
|
||||
* - Behavior **input convenience**: a behavior that writes to every slot
|
||||
* can type its setup state param as `StateSignals<{ ... }>` rather than
|
||||
* spelling out per-slot `Signal<T>` types.
|
||||
*
|
||||
* Behaviors that mix read-only and writable slots type the setup param
|
||||
* directly as a slot map (`{ x: Signal<T>; y: ReadonlySignal<U> }`)
|
||||
* instead of going through `StateSignals<>`.
|
||||
*/
|
||||
type StateSignals<S extends object> = { [K in keyof S]-?: Signal<S[K]>; };
|
||||
/**
|
||||
* A signal map keyed by the fields of `C`. Each field is a writable signal
|
||||
* for a platform object or actor reference. Same dual role as
|
||||
* `StateSignals<S>` — see its docblock.
|
||||
*/
|
||||
type ContextSignals<C extends object> = { [K in keyof C]-?: Signal<C[K]>; };
|
||||
/**
|
||||
* Slot-map shape — a record where each value is at least a `ReadonlySignal`.
|
||||
* `Signal<T>` is structurally a subtype of `ReadonlySignal<T>` (it adds
|
||||
* `.set()`), so a writable slot satisfies this bound too.
|
||||
*
|
||||
* This is the bound used for behavior `state` / `context` slot maps. It
|
||||
* lets a single behavior declare a *heterogeneous* slot map where some
|
||||
* slots are `Signal<T>` (writable) and others are `ReadonlySignal<T>`
|
||||
* (read-only) — making read/write intent explicit at the call site and
|
||||
* giving body-level enforcement (TS rejects `.set()` on a read-only slot).
|
||||
*/
|
||||
type AnySlotMap = Record<PropertyKey, ReadonlySignal<unknown>>;
|
||||
/**
|
||||
* The deps object passed to each behavior by the composition.
|
||||
*
|
||||
* - `state` — slot map for state fields (reactive data). Per-slot read/
|
||||
* write intent expressed via `Signal<T>` vs `ReadonlySignal<T>`.
|
||||
* - `context` — slot map for platform objects and actor references.
|
||||
* - `config` — static configuration, passed once at composition creation.
|
||||
*/
|
||||
interface BehaviorDeps<StateMap extends AnySlotMap, ContextMap extends AnySlotMap, Cfg extends object> {
|
||||
state: StateMap;
|
||||
context: ContextMap;
|
||||
config: Cfg;
|
||||
}
|
||||
/**
|
||||
* A behavior announces the state and context keys it needs alongside a
|
||||
* `setup` function that receives deps (state, context, config) and
|
||||
* returns an optional cleanup handle.
|
||||
*
|
||||
* The `stateKeys` / `contextKeys` declarations are the runtime expression
|
||||
* of the behavior's contract — the caller (e.g. `createComposition`) uses
|
||||
* them to know which signals to provide. The setup parameter type
|
||||
* declares the *slot map* (per-slot `Signal<T>` vs `ReadonlySignal<T>`);
|
||||
* together they form a complete contract.
|
||||
*
|
||||
* Manual `Behavior<>` literals (e.g. engine wrappers that forward keys
|
||||
* from a wrapped behavior, or pass-through behaviors like `shareSignals`)
|
||||
* opt out of exhaustiveness — the type alias is permissive (subset).
|
||||
* Source behaviors should use `defineBehavior` to get exhaustiveness
|
||||
* enforcement at the call site.
|
||||
*/
|
||||
interface Behavior<StateMap extends AnySlotMap = Empty, ContextMap extends AnySlotMap = Empty, Cfg extends object = Empty> {
|
||||
/** State keys this behavior reads/writes. Subset of `keyof StateMap`. */
|
||||
stateKeys: readonly (keyof StateMap)[];
|
||||
/** Context keys this behavior reads/writes. Subset of `keyof ContextMap`. */
|
||||
contextKeys: readonly (keyof ContextMap)[];
|
||||
setup: (deps: BehaviorDeps<StateMap, ContextMap, Cfg>) => BehaviorCleanup;
|
||||
}
|
||||
/** A behavior with an unconstrained setup — used as a generic bound. */
|
||||
type AnyBehavior = {
|
||||
stateKeys: readonly PropertyKey[];
|
||||
contextKeys: readonly PropertyKey[];
|
||||
setup: (deps: any) => BehaviorCleanup;
|
||||
};
|
||||
/** Extract the deps type from a behavior's setup function. */
|
||||
type DepsOf<B> = B extends {
|
||||
setup: (deps: infer D, ...args: any[]) => any;
|
||||
} ? D : never;
|
||||
/**
|
||||
* Empty-object fallback used when a behavior omits state, context, or config.
|
||||
*
|
||||
* Using `{}` rather than `object` is deliberate — `object & {x: T}` collapses
|
||||
* to `{x: never}` under TS's union-to-intersection conversion in some inference
|
||||
* contexts (likely a TS quirk around the `object` upper bound), whereas
|
||||
* `{} & {x: T}` simplifies cleanly to `{x: T}`.
|
||||
*/
|
||||
type Empty = {};
|
||||
/**
|
||||
* Unwrap a signal map back to its state/context shape.
|
||||
*
|
||||
* Inferring through `{ get(): infer V }` rather than `Signal<infer V>`
|
||||
* sidesteps `Signal`'s nominal/invariance behaviour — the conditional
|
||||
* matches structurally on the read side, and `V` is inferred covariantly.
|
||||
*/
|
||||
type UnwrapSignals<M> = M extends object ? { [K in keyof M]: M[K] extends {
|
||||
get(): infer V;
|
||||
} ? V : never; } : Empty;
|
||||
/** Infer the state shape a behavior requires from its deps parameter. */
|
||||
type InferBehaviorState<F> = DepsOf<F> extends {
|
||||
state: infer M;
|
||||
} ? UnwrapSignals<M> : Empty;
|
||||
/** Infer the context shape a behavior requires from its deps parameter. */
|
||||
type InferBehaviorContext<F> = DepsOf<F> extends {
|
||||
context: infer M;
|
||||
} ? UnwrapSignals<M> : Empty;
|
||||
/** Infer the config shape a behavior requires from its deps parameter. */
|
||||
type InferBehaviorConfig<F> = DepsOf<F> extends {
|
||||
config: infer C extends object;
|
||||
} ? C : Empty;
|
||||
/**
|
||||
* Recursively intersect a per-behavior projection across the tuple.
|
||||
*
|
||||
* Iterating over the tuple directly avoids `UnionToIntersection`'s
|
||||
* function-contravariance trick, which produces unstable intersections
|
||||
* (collapsing concrete fields to `never` or unrelated types) when one of the
|
||||
* union members is the empty `{}` fallback.
|
||||
*/
|
||||
type IntersectBehaviors<Behaviors extends readonly AnyBehavior[], Project extends object> = Behaviors extends readonly [infer First extends AnyBehavior, ...infer Rest extends readonly AnyBehavior[]] ? Apply<Project, First> & IntersectBehaviors<Rest, Project> : Empty;
|
||||
/**
|
||||
* Apply a projection (one of the marker types below) to a single behavior.
|
||||
* Encoded as a discriminated dispatch so the recursion above can stay generic
|
||||
* and we don't have to write three near-identical recursive types.
|
||||
*/
|
||||
type Apply<Project extends object, F> = Project extends {
|
||||
kind: 'state';
|
||||
} ? InferBehaviorState<F> : Project extends {
|
||||
kind: 'context';
|
||||
} ? InferBehaviorContext<F> : Project extends {
|
||||
kind: 'config';
|
||||
} ? InferBehaviorConfig<F> : never;
|
||||
type StateProjection = {
|
||||
kind: 'state';
|
||||
};
|
||||
type ContextProjection = {
|
||||
kind: 'context';
|
||||
};
|
||||
type ConfigProjection = {
|
||||
kind: 'config';
|
||||
};
|
||||
/** Resolve the combined state shape from an array of behaviors (intersection of all requirements). */
|
||||
type ResolveBehaviorState<Behaviors extends readonly AnyBehavior[]> = IntersectBehaviors<Behaviors, StateProjection> extends (infer R extends object) ? R : Empty;
|
||||
/** Resolve the combined context shape from an array of behaviors (intersection of all requirements). */
|
||||
type ResolveBehaviorContext<Behaviors extends readonly AnyBehavior[]> = IntersectBehaviors<Behaviors, ContextProjection> extends (infer R extends object) ? R : Empty;
|
||||
/** Resolve the combined config shape from an array of behaviors (intersection of all requirements). */
|
||||
type ResolveBehaviorConfig<Behaviors extends readonly AnyBehavior[]> = IntersectBehaviors<Behaviors, ConfigProjection> extends (infer R extends object) ? R : Empty;
|
||||
/**
|
||||
* True if any property in `T` collapsed to `undefined` or `never` — indicating
|
||||
* a type conflict from intersecting incompatible behavior requirements.
|
||||
*
|
||||
* - Required conflicts: `{ v: number } & { v: string }` → `{ v: never }` — caught via `[never] extends [undefined]`
|
||||
* - Optional conflicts: `{ v?: number } & { v?: string }` → `{ v?: undefined }` — caught directly
|
||||
*/
|
||||
type HasConflict<T extends object> = true extends { [K in keyof T]: [T[K]] extends [undefined] ? true : never; }[keyof T] ? true : false;
|
||||
/**
|
||||
* Validate that a behavior composition has no type conflicts.
|
||||
* Returns the behaviors tuple if valid, or an error message type if conflicts are detected.
|
||||
*
|
||||
* State, context, and config are all checked the same way — by intersecting
|
||||
* each behavior's requirement and looking for collapsed fields. The
|
||||
* intersection-based check applies the same rule to context as to state, so
|
||||
* two behaviors that disagree on a context field's type (e.g. `Surface` vs
|
||||
* `VideoSurface`) surface a conflict at compose time. The prior subtype-based
|
||||
* approach for owners is gone — the unified rule is simpler and catches the
|
||||
* cases where two behaviors silently agreed on a wider supertype.
|
||||
*/
|
||||
type ValidateComposition<Behaviors extends readonly AnyBehavior[]> = HasConflict<ResolveBehaviorState<Behaviors>> extends true ? 'Error: behaviors have conflicting state types' : HasConflict<ResolveBehaviorContext<Behaviors>> extends true ? 'Error: behaviors have conflicting context types' : HasConflict<ResolveBehaviorConfig<Behaviors>> extends true ? 'Error: behaviors have conflicting config types' : [...Behaviors];
|
||||
/**
|
||||
* A composition of behaviors with shared state and context signal maps.
|
||||
*/
|
||||
interface Composition<S extends object, C extends object> {
|
||||
state: StateSignals<S>;
|
||||
context: ContextSignals<C>;
|
||||
destroy(): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* Options for `createComposition`.
|
||||
*
|
||||
* Composition derives the state and context signal maps from each
|
||||
* behavior's declared `stateKeys` / `contextKeys`; `initialState` and
|
||||
* `initialContext` seed those signals at creation time. Any unseeded
|
||||
* signal starts as `undefined`.
|
||||
*/
|
||||
interface CompositionOptions<S extends object, C extends object, Cfg extends object> {
|
||||
/** Static configuration passed to every behavior. */
|
||||
config?: Cfg;
|
||||
/** Initial values for state signals — any subset of `keyof S`. */
|
||||
initialState?: Partial<S>;
|
||||
/** Initial values for context signals — any subset of `keyof C`. */
|
||||
initialContext?: Partial<C>;
|
||||
}
|
||||
declare function createComposition<const Behaviors extends readonly AnyBehavior[]>(behaviors: ValidateComposition<Behaviors>, options?: CompositionOptions<ResolveBehaviorState<Behaviors>, ResolveBehaviorContext<Behaviors>, ResolveBehaviorConfig<Behaviors>>): Composition<ResolveBehaviorState<Behaviors>, ResolveBehaviorContext<Behaviors>>;
|
||||
/**
|
||||
* Compose-time exhaustiveness check.
|
||||
*
|
||||
* Adds a phantom error tag to the parameter shape when `Keys` does not
|
||||
* cover every key in `Slot`. The user's value won't satisfy the phantom
|
||||
* field requirement, so TS surfaces the failure at the call site with a
|
||||
* descriptive message. When exhaustive, the tag is `Empty` and adds no
|
||||
* constraint.
|
||||
*/
|
||||
type ExhaustiveKeys<Keys extends readonly PropertyKey[], Slot extends object, Name extends string> = [keyof Slot] extends [Keys[number]] ? Empty : { [K in `Error: ${Name}Keys must list every key in the typed slice`]: Exclude<keyof Slot, Keys[number]>; };
|
||||
/**
|
||||
* Typed factory for behaviors that enforces single-behavior key/param
|
||||
* consistency: declared `stateKeys` must equal `keyof S` (where `S` is
|
||||
* inferred from the setup's `state` parameter type), and same for
|
||||
* `contextKeys` / `C`.
|
||||
*
|
||||
* The `const` modifier on `SK` / `CK` captures literal tuples so e.g.
|
||||
* `stateKeys: ['preload']` infers as `readonly ['preload']`, no `as
|
||||
* const` needed at the call site.
|
||||
*
|
||||
* Cross-behavior consistency at `createComposition` is unchanged — the
|
||||
* existing `IntersectBehaviors` machinery still runs over each
|
||||
* behavior's setup param type.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export const syncPreload = defineBehavior({
|
||||
* stateKeys: ['preload'],
|
||||
* contextKeys: ['mediaElement'],
|
||||
* setup: ({ state, context }: {
|
||||
* state: StateSignals<{ preload?: 'auto' | 'metadata' | 'none' }>;
|
||||
* context: ContextSignals<{ mediaElement?: HTMLMediaElement | undefined }>;
|
||||
* }) => { ... },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
/**
|
||||
* Deps shape for a behavior whose deps slot is empty (no keys). When a
|
||||
* slot is empty, the corresponding deps field is optional — callers
|
||||
* (typically tests) can omit it, and it defaults to `{}` at runtime via
|
||||
* `createComposition`.
|
||||
*
|
||||
* When a slot has at least one key, the behavior reads `state.foo` /
|
||||
* `context.bar` / `config.baz` and we need the field to be required so
|
||||
* the access is type-safe.
|
||||
*/
|
||||
type RequireIfNonEmpty<Key extends string, T extends object> = keyof T extends never ? { [K in Key]?: T; } : { [K in Key]: T; };
|
||||
type DepsForCfg<StateMap extends AnySlotMap, ContextMap extends AnySlotMap, Cfg extends object> = RequireIfNonEmpty<'state', StateMap> & RequireIfNonEmpty<'context', ContextMap> & RequireIfNonEmpty<'config', Cfg>;
|
||||
declare function defineBehavior<StateMap extends AnySlotMap = Empty, ContextMap extends AnySlotMap = Empty, Cfg extends object = Empty, const SK extends readonly (keyof StateMap)[] = readonly [], const CK extends readonly (keyof ContextMap)[] = readonly [], R extends BehaviorCleanup = BehaviorCleanup>(behavior: {
|
||||
stateKeys: SK;
|
||||
contextKeys: CK;
|
||||
setup: (deps: {
|
||||
state: StateMap;
|
||||
context: ContextMap;
|
||||
config: Cfg;
|
||||
}) => R;
|
||||
} & ExhaustiveKeys<SK, StateMap, 'state'> & ExhaustiveKeys<CK, ContextMap, 'context'>): {
|
||||
stateKeys: SK;
|
||||
contextKeys: CK;
|
||||
setup: (deps: DepsForCfg<StateMap, ContextMap, Cfg>) => R;
|
||||
};
|
||||
//#endregion
|
||||
export { AnySlotMap, Behavior, BehaviorCleanup, BehaviorDeps, Composition, CompositionOptions, ContextSignals, InferBehaviorConfig, InferBehaviorContext, InferBehaviorState, ResolveBehaviorConfig, ResolveBehaviorContext, ResolveBehaviorState, StateSignals, createComposition, defineBehavior };
|
||||
//# sourceMappingURL=create-composition.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"create-composition.d.ts","names":[],"sources":["../../../../src/core/composition/create-composition.ts"],"mappings":";;;;;;;;KAQY,uCAAuC;EAAmB,kBAAkB;;;;;;;;;;;;;;;;;;;;;KAqB5E,aAAa,uBAAuB,WAAW,MAAM,OAAO,EAAE;;;;;;KAO9D,eAAe,uBAAuB,WAAW,MAAM,OAAO,EAAE;;;;;;;;;;;;KAahE,aAAa,OAAO,aAAa;;;;;;;;;UAU5B,aAAa,iBAAiB,YAAY,mBAAmB,YAAY;EACxF,OAAO;EACP,SAAS;EACT,QAAQ;;;;;;;;;;;;;;;;;;;UAoBO,SACf,iBAAiB,aAAa,OAC9B,mBAAmB,aAAa,OAChC,qBAAqB;;EAGrB,2BAA2B;;EAE3B,6BAA6B;EAC7B,QAAQ,MAAM,aAAa,UAAU,YAAY,SAAS;;;KAQvD;EACH,oBAAoB;EACpB,sBAAsB;EACtB,QAAQ,cAAc;;;KAInB,OAAO,KAAK;EAAY,QAAQ,YAAY,MAAM;IAAwB;;;;;;;;;KAW1E;;;;;;;;KASA,cAAc,KAAK,sBAAsB,WAAW,IAAI,EAAE;EAAa,aAAa;IAAM,eAAc;;KAGjG,mBAAmB,KAAK,OAAO;EAAa,aAAa;IAAM,cAAc,KAAK;;KAGlF,qBAAqB,KAAK,OAAO;EAAa,eAAe;IAAM,cAAc,KAAK;;KAGtF,oBAAoB,KAAK,OAAO;EAAa,cAAc;IAAqB,IAAI;;;;;;;;;KAU3F,mBAAmB,2BAA2B,eAAe,0BAA0B,kCACpF,cAAc,sBACX,sBAAsB,iBAE7B,MAAM,SAAS,SAAS,mBAAmB,MAAM,WACjD;;;;;;KAOC,MAAM,wBAAwB,KAAK;EAAkB;IACtD,mBAAmB,KACnB;EAAkB;IAChB,qBAAqB,KACrB;EAAkB;IAChB,oBAAoB;KAGvB;EAAoB;;KACpB;EAAsB;;KACtB;EAAqB;;;KAGd,qBAAqB,2BAA2B,iBAC1D,mBAAmB,WAAW,gCAA+B,oBAAmB,IAAI;;KAG1E,uBAAuB,2BAA2B,iBAC5D,mBAAmB,WAAW,kCAAiC,oBAAmB,IAAI;;KAG5E,sBAAsB,2BAA2B,iBAC3D,mBAAmB,WAAW,iCAAgC,oBAAmB,IAAI;;;;;;;;KASlF,YAAY,oCACd,WAAW,KAAK,EAAE,gDACb;;;;;;;;;;;;;KAoBH,oBAAoB,2BAA2B,iBAClD,YAAY,qBAAqB,6EAE7B,YAAY,uBAAuB,+EAEjC,YAAY,sBAAsB,kFAE5B;;;;UASG,YAAY,kBAAkB;EAC7C,OAAO,aAAa;EACpB,SAAS,eAAe;EACxB,WAAW;;;;;;;;;;UAWI,mBAAmB,kBAAkB,kBAAkB;;EAEtE,SAAS;;EAET,eAAe,QAAQ;;EAEvB,iBAAiB,QAAQ;;iBA2DX,wBAAwB,2BAA2B,eACjE,WAAW,oBAAoB,YAC/B,UAAU,mBACR,qBAAqB,YACrB,uBAAuB,YACvB,sBAAsB,cAEvB,YAAY,qBAAqB,YAAY,uBAAuB;;;;;;;;;;KA8DlE,eAAe,sBAAsB,eAAe,qBAAqB,8BACtE,eACG,gBACP,WACG,eAAe,oDAAoD,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAsCzF,kBAAkB,oBAAoB,0BAA0B,qBAC9D,KAAK,OAAO,UACZ,KAAK,MAAM;KAEb,WAAW,iBAAiB,YAAY,mBAAmB,YAAY,sBAAsB,2BAEhG,YAEA,6BAA6B,cAC7B,4BAA4B;iBAEd,eACd,iBAAiB,aAAa,OAC9B,mBAAmB,aAAa,OAChC,qBAAqB,aACf,2BAA2B,iCAC3B,2BAA2B,6BACjC,UAAU,kBAAkB,iBAE5B;EACE,WAAW;EACX,aAAa;EACb,QAAQ;IAAQ,OAAO;IAAU,SAAS;IAAY,QAAQ;QAAU;IACtE,eAAe,IAAI,qBACrB,eAAe,IAAI;EAErB,WAAW;EACX,aAAa;EACb,QAAQ,MAAM,WAAW,UAAU,YAAY,SAAS"}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { signal } from "../signals/primitives.js";
|
||||
//#region src/core/composition/create-composition.ts
|
||||
/**
|
||||
* Create a composition from a set of behaviors.
|
||||
*
|
||||
* Composition unions the behaviors' declared `stateKeys` / `contextKeys`
|
||||
* to know which signals to create. Each signal is seeded from
|
||||
* `initialState` / `initialContext` when supplied, defaulting to
|
||||
* `undefined`. Behaviors are responsible for writing their own slots
|
||||
* once their preconditions are met.
|
||||
*
|
||||
* Cross-behavior type conflicts (e.g. two behaviors disagreeing on a
|
||||
* field's type) surface as a compose-time type error via
|
||||
* `ValidateComposition`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const composition = createComposition([resolvePresentation, switchVideoTrack], {
|
||||
* config: { parsePresentation: parseMultivariantPlaylist, initialBandwidth: 2_000_000 },
|
||||
* initialState: { bandwidthState: { fastEstimate: 0, ... } },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
/**
|
||||
* Create a typed signal map for a given set of keys, seeded from an
|
||||
* optional partial initial value.
|
||||
*
|
||||
* Pipeline: `Set` dedupes the iterable (insertion order preserved, so
|
||||
* first occurrence wins) → `Object.fromEntries` materializes one
|
||||
* `signal()` per unique key, seeded from `initial[key]` or `undefined`.
|
||||
*
|
||||
* Per-key value types live in TypeScript only — at runtime every signal
|
||||
* is `Signal<unknown>`. The boundary cast at the return narrows the wide
|
||||
* `Record<PropertyKey, Signal<unknown>>` shape to the caller's expected
|
||||
* per-key types from `S`.
|
||||
*
|
||||
* Used by `createComposition` to derive engine state/context maps from
|
||||
* the union of behaviors' declared `stateKeys` / `contextKeys`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* interface State { count?: number; label?: string }
|
||||
* const state = buildSignalMap<State>(['count', 'label'], { count: 5 });
|
||||
* state.count.get(); // 5
|
||||
* state.label.get(); // undefined
|
||||
* ```
|
||||
*/
|
||||
function buildSignalMap(keys, initial) {
|
||||
const init = initial;
|
||||
const uniqueKeys = new Set(keys);
|
||||
return Object.fromEntries([...uniqueKeys].map((key) => [key, signal(init[key])]));
|
||||
}
|
||||
function createComposition(behaviors, options) {
|
||||
const validBehaviors = behaviors;
|
||||
const state = buildSignalMap(validBehaviors.flatMap((b) => b.stateKeys), options?.initialState ?? {});
|
||||
const context = buildSignalMap(validBehaviors.flatMap((b) => b.contextKeys), options?.initialContext ?? {});
|
||||
const deps = {
|
||||
state,
|
||||
context,
|
||||
config: options?.config ?? {}
|
||||
};
|
||||
const cleanups = validBehaviors.map((behavior) => behavior.setup(deps));
|
||||
return {
|
||||
state,
|
||||
context,
|
||||
async destroy() {
|
||||
const results = [];
|
||||
for (const cleanup of cleanups) {
|
||||
if (cleanup == null) continue;
|
||||
if (typeof cleanup === "function") results.push(cleanup());
|
||||
else if ("destroy" in cleanup) results.push(cleanup.destroy());
|
||||
}
|
||||
await Promise.all(results);
|
||||
for (const sig of Object.values(state)) sig.set(void 0);
|
||||
for (const sig of Object.values(context)) sig.set(void 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
function defineBehavior(behavior) {
|
||||
return behavior;
|
||||
}
|
||||
//#endregion
|
||||
export { buildSignalMap, createComposition, defineBehavior };
|
||||
|
||||
//# sourceMappingURL=create-composition.js.map
|
||||
File diff suppressed because one or more lines are too long
+46
@@ -0,0 +1,46 @@
|
||||
import { Behavior, ContextSignals, StateSignals } from "./create-composition.js";
|
||||
//#region src/core/composition/share-signals.d.ts
|
||||
/**
|
||||
* Config consumed by the `shareSignals` behavior.
|
||||
*
|
||||
* The callback fires once during composition setup with the composition's
|
||||
* state and context signal refs. Capture them to drive the composition
|
||||
* externally (writes) or observe its state (reads).
|
||||
*
|
||||
* The callback runs while other behaviors are still in their setup phase —
|
||||
* for the typical "capture refs, use later" pattern this is fine (signal
|
||||
* refs are stable identities), but reading inside the callback may yield
|
||||
* only initial-seed values rather than what later behaviors write.
|
||||
*/
|
||||
interface ShareSignalsConfig<S extends object, C extends object> {
|
||||
onSignalsReady?: (signals: {
|
||||
state: StateSignals<S>;
|
||||
context: ContextSignals<C>;
|
||||
}) => void;
|
||||
}
|
||||
/**
|
||||
* Behavior factory that hands the composition's signal refs to a
|
||||
* consumer-supplied callback (`config.onSignalsReady`) at setup time.
|
||||
*
|
||||
* Generic over `S` and `C` — the caller instantiates with their own
|
||||
* state/context types, and the callback's parameter shape is fully
|
||||
* type-driven from those. Suitable for both reads and writes (per-slot
|
||||
* intent can be expressed by typing captured refs as `Signal<T>` or
|
||||
* `ReadonlySignal<T>` at the call site).
|
||||
*
|
||||
* By default declares no keys; the composition's state/context maps come from
|
||||
* other behaviors. Pass `inputStateKeys` / `inputContextKeys` to *materialize*
|
||||
* consumer-input slots that no other behavior produces — a slot the consumer
|
||||
* writes (e.g. `userAudioTrackSelection`) but only a rule reads. shareSignals
|
||||
* is the consumer boundary, so it's the natural place to bring those slots into
|
||||
* existence; readers then treat them as optional.
|
||||
*
|
||||
* Uses a `Behavior<>` literal (not `defineBehavior`) so its (possibly empty,
|
||||
* possibly partial) key arrays don't trip the exhaustiveness check — the
|
||||
* setup-param state/context shapes describe what the callback receives (the
|
||||
* full `S` / `C`), not the subset this behavior materializes.
|
||||
*/
|
||||
declare function makeShareSignals<S extends object, C extends object>(inputStateKeys?: readonly (keyof S)[], inputContextKeys?: readonly (keyof C)[]): Behavior<StateSignals<S>, ContextSignals<C>, ShareSignalsConfig<S, C>>;
|
||||
//#endregion
|
||||
export { ShareSignalsConfig, makeShareSignals };
|
||||
//# sourceMappingURL=share-signals.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"share-signals.d.ts","names":[],"sources":["../../../../src/core/composition/share-signals.ts"],"mappings":";;;;;;;;;;;;;;UAciB,mBAAmB,kBAAkB;EACpD,kBAAkB;IAAW,OAAO,aAAa;IAAI,SAAS,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;iBAyB/D,iBAAiB,kBAAkB,kBACjD,iCAAgC,MAChC,mCAAkC,OACjC,SAAS,aAAa,IAAI,eAAe,IAAI,mBAAmB,GAAG"}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
//#region src/core/composition/share-signals.ts
|
||||
/**
|
||||
* Behavior factory that hands the composition's signal refs to a
|
||||
* consumer-supplied callback (`config.onSignalsReady`) at setup time.
|
||||
*
|
||||
* Generic over `S` and `C` — the caller instantiates with their own
|
||||
* state/context types, and the callback's parameter shape is fully
|
||||
* type-driven from those. Suitable for both reads and writes (per-slot
|
||||
* intent can be expressed by typing captured refs as `Signal<T>` or
|
||||
* `ReadonlySignal<T>` at the call site).
|
||||
*
|
||||
* By default declares no keys; the composition's state/context maps come from
|
||||
* other behaviors. Pass `inputStateKeys` / `inputContextKeys` to *materialize*
|
||||
* consumer-input slots that no other behavior produces — a slot the consumer
|
||||
* writes (e.g. `userAudioTrackSelection`) but only a rule reads. shareSignals
|
||||
* is the consumer boundary, so it's the natural place to bring those slots into
|
||||
* existence; readers then treat them as optional.
|
||||
*
|
||||
* Uses a `Behavior<>` literal (not `defineBehavior`) so its (possibly empty,
|
||||
* possibly partial) key arrays don't trip the exhaustiveness check — the
|
||||
* setup-param state/context shapes describe what the callback receives (the
|
||||
* full `S` / `C`), not the subset this behavior materializes.
|
||||
*/
|
||||
function makeShareSignals(inputStateKeys = [], inputContextKeys = []) {
|
||||
return {
|
||||
stateKeys: inputStateKeys,
|
||||
contextKeys: inputContextKeys,
|
||||
setup: ({ state, context, config }) => {
|
||||
config.onSignalsReady?.({
|
||||
state,
|
||||
context
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { makeShareSignals };
|
||||
|
||||
//# sourceMappingURL=share-signals.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"share-signals.js","names":[],"sources":["../../../../src/core/composition/share-signals.ts"],"sourcesContent":["import type { Behavior, ContextSignals, StateSignals } from './create-composition';\n\n/**\n * Config consumed by the `shareSignals` behavior.\n *\n * The callback fires once during composition setup with the composition's\n * state and context signal refs. Capture them to drive the composition\n * externally (writes) or observe its state (reads).\n *\n * The callback runs while other behaviors are still in their setup phase —\n * for the typical \"capture refs, use later\" pattern this is fine (signal\n * refs are stable identities), but reading inside the callback may yield\n * only initial-seed values rather than what later behaviors write.\n */\nexport interface ShareSignalsConfig<S extends object, C extends object> {\n onSignalsReady?: (signals: { state: StateSignals<S>; context: ContextSignals<C> }) => void;\n}\n\n/**\n * Behavior factory that hands the composition's signal refs to a\n * consumer-supplied callback (`config.onSignalsReady`) at setup time.\n *\n * Generic over `S` and `C` — the caller instantiates with their own\n * state/context types, and the callback's parameter shape is fully\n * type-driven from those. Suitable for both reads and writes (per-slot\n * intent can be expressed by typing captured refs as `Signal<T>` or\n * `ReadonlySignal<T>` at the call site).\n *\n * By default declares no keys; the composition's state/context maps come from\n * other behaviors. Pass `inputStateKeys` / `inputContextKeys` to *materialize*\n * consumer-input slots that no other behavior produces — a slot the consumer\n * writes (e.g. `userAudioTrackSelection`) but only a rule reads. shareSignals\n * is the consumer boundary, so it's the natural place to bring those slots into\n * existence; readers then treat them as optional.\n *\n * Uses a `Behavior<>` literal (not `defineBehavior`) so its (possibly empty,\n * possibly partial) key arrays don't trip the exhaustiveness check — the\n * setup-param state/context shapes describe what the callback receives (the\n * full `S` / `C`), not the subset this behavior materializes.\n */\nexport function makeShareSignals<S extends object, C extends object>(\n inputStateKeys: readonly (keyof S)[] = [],\n inputContextKeys: readonly (keyof C)[] = []\n): Behavior<StateSignals<S>, ContextSignals<C>, ShareSignalsConfig<S, C>> {\n return {\n stateKeys: inputStateKeys,\n contextKeys: inputContextKeys,\n setup: ({ state, context, config }) => {\n config.onSignalsReady?.({ state, context });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,iBACd,iBAAuC,CAAC,GACxC,mBAAyC,CAAC,GAC8B;CACxE,OAAO;EACL,WAAW;EACX,aAAa;EACb,QAAQ,EAAE,OAAO,SAAS,aAAa;GACrC,OAAO,iBAAiB;IAAE;IAAO;GAAQ,CAAC;EAC5C;CACF;AACF"}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
import { ReadonlySignal } from "./signals/primitives.js";
|
||||
import "../index.js";
|
||||
//#region src/core/machine.d.ts
|
||||
/**
|
||||
* Base snapshot for all machine-like primitives (Actors and Reactors).
|
||||
* Carries only the finite state value. Actors extend this with `context`.
|
||||
*/
|
||||
interface MachineSnapshot<State extends string> {
|
||||
value: State;
|
||||
}
|
||||
/**
|
||||
* Shared interface for all machine-like primitives.
|
||||
* Both Actors (message-driven) and Reactors (signal-driven) implement this.
|
||||
*/
|
||||
interface Machine<Snapshot extends MachineSnapshot<string>> {
|
||||
readonly snapshot: ReadonlySignal<Snapshot>;
|
||||
destroy(): void;
|
||||
}
|
||||
//#endregion
|
||||
export { Machine, MachineSnapshot };
|
||||
//# sourceMappingURL=machine.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"machine.d.ts","names":[],"sources":["../../../src/core/machine.ts"],"mappings":";;;;;;;UAWiB,gBAAgB;EAC/B,OAAO;;;;;;UAWQ,QAAQ,iBAAiB;WAC/B,UAAU,eAAe;EAClC"}
|
||||
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,93 @@
|
||||
import { Machine, MachineSnapshot } from "../machine.js";
|
||||
//#region src/core/reactors/create-machine-reactor.d.ts
|
||||
/**
|
||||
* A reactive state-deriving function used in the `monitor` field.
|
||||
*
|
||||
* Returns the target state the reactor should be in. Any signals read inside
|
||||
* the fn body create reactive dependencies — the framework re-evaluates it when
|
||||
* those signals change and automatically calls `transition()` when the returned
|
||||
* state differs from the current one.
|
||||
*/
|
||||
type ReactorDeriveFn<State extends string> = () => State;
|
||||
/**
|
||||
* An effect function used in reactor `entry` and `effects` blocks.
|
||||
*
|
||||
* May return a cleanup function that runs before each re-evaluation and on
|
||||
* state exit (including destroy).
|
||||
*/
|
||||
type ReactorEffectFn = () => (() => void) | {
|
||||
abort(): void;
|
||||
} | void;
|
||||
/**
|
||||
* Per-state effect grouping for a single reactor state.
|
||||
*
|
||||
* - `entry` effects run once on state entry. The fn body is automatically
|
||||
* untracked — no `untrack()` calls are needed inside. Use this for
|
||||
* one-time setup: reading current values, attaching event listeners, etc.
|
||||
* - `effects` run on state entry and re-run whenever a signal read inside
|
||||
* the fn body changes. Use `untrack()` for reads you do not want to track.
|
||||
* Use this for work that must stay in sync with reactive state.
|
||||
*
|
||||
* Both are optional; pass `{}` for states with no effects.
|
||||
*/
|
||||
type ReactorStateDefinition = {
|
||||
entry?: ReactorEffectFn | ReactorEffectFn[];
|
||||
effects?: ReactorEffectFn | ReactorEffectFn[];
|
||||
};
|
||||
/**
|
||||
* Full reactor definition passed to `createMachineReactor`.
|
||||
*
|
||||
* `State` is the set of domain-meaningful states. `'destroying'` and
|
||||
* `'destroyed'` are always added by the framework as implicit terminal states —
|
||||
* do not include them here.
|
||||
*/
|
||||
type ReactorDefinition<State extends string> = {
|
||||
/** Initial state. */
|
||||
initial: State;
|
||||
/**
|
||||
* Reactive state derivation. Registered before per-state effects — the
|
||||
* ordering guarantee ensures transitions fired here take effect before
|
||||
* per-state effects re-evaluate in the same flush.
|
||||
*/
|
||||
monitor?: ReactorDeriveFn<State> | ReactorDeriveFn<State>[];
|
||||
/**
|
||||
* Per-state effect groupings. Every valid state must be declared — pass `{}`
|
||||
* for states with no effects. `entry` and `effects` each become independent
|
||||
* `effect()` calls gated on that state, with their own cleanup lifecycles.
|
||||
*/
|
||||
states: Record<State, ReactorStateDefinition>;
|
||||
};
|
||||
/** Live reactor instance returned by `createMachineReactor`. */
|
||||
type Reactor<State extends string> = Machine<MachineSnapshot<State>>;
|
||||
/**
|
||||
* 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: {},
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
declare function createMachineReactor<State extends string>(def: ReactorDefinition<State>): Reactor<State | 'destroying' | 'destroyed'>;
|
||||
//#endregion
|
||||
export { Reactor, ReactorDefinition, ReactorDeriveFn, ReactorEffectFn, ReactorStateDefinition, createMachineReactor };
|
||||
//# sourceMappingURL=create-machine-reactor.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"create-machine-reactor.d.ts","names":[],"sources":["../../../../src/core/reactors/create-machine-reactor.ts"],"mappings":";;;;;;;;;;KAiBY,gBAAgB,8BAA8B;;;;;;;KAQ9C;EAAyC;;;;;;;;;;;;;;KAczC;EACV,QAAQ,kBAAkB;EAC1B,UAAU,kBAAkB;;;;;;;;;KAUlB,kBAAkB;;EAE5B,SAAS;;;;;;EAMT,UAAU,gBAAgB,SAAS,gBAAgB;;;;;;EAMnD,QAAQ,OAAO,OAAO;;;KAQZ,QAAQ,wBAAwB,QAAQ,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwCpD,qBAAqB,sBACnC,KAAK,kBAAkB,SACtB,QAAQ"}
|
||||
@@ -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
+15
@@ -0,0 +1,15 @@
|
||||
//#region src/core/signals/effect.d.ts
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
declare function effect(fn: () => (() => void) | void): () => void;
|
||||
//#endregion
|
||||
export { effect };
|
||||
//# sourceMappingURL=effect.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"effect.d.ts","names":[],"sources":["../../../../src/core/signals/effect.ts"],"mappings":";;;;;;;;;;;iBA8BgB,OAAO"}
|
||||
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
|
||||
Vendored
+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"}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { Signal } from "signal-polyfill";
|
||||
//#region src/core/signals/primitives.d.ts
|
||||
/** Read a signal value without tracking it as a dependency. */
|
||||
declare const untrack: <T>(fn: () => T) => T;
|
||||
/** A writable reactive value (read + write). */
|
||||
type Signal$1<T> = Signal.State<T>;
|
||||
/** A derived reactive value that re-evaluates when its dependencies change (read-only). */
|
||||
type Computed<T> = Signal.Computed<T>;
|
||||
/** A read-only view of a reactive value. */
|
||||
type ReadonlySignal<T> = Omit<Signal.State<T>, 'set'>;
|
||||
interface SignalOptions<T> {
|
||||
equals?: (t: T, t2: T) => boolean;
|
||||
}
|
||||
/** Create a writable reactive value. */
|
||||
declare function signal<T>(initialValue: T, options?: SignalOptions<T>): Signal$1<T>;
|
||||
/** Create a computed reactive value. */
|
||||
declare function computed<T>(fn: () => T, options?: SignalOptions<T>): Computed<T>;
|
||||
/**
|
||||
* Update a writable signal. Two forms:
|
||||
*
|
||||
* - **Updater function** `(current) => next`. Works for any signal type,
|
||||
* including `Signal<T | undefined>` — handle undefined in the updater.
|
||||
* - **Partial object** to merge into the current state. Requires
|
||||
* `T extends object`.
|
||||
*
|
||||
* @example
|
||||
* update(state, { playbackRate: 2 });
|
||||
* update(state, (s) => ({ ...s, count: s.count + 1 }));
|
||||
* update(maybeUndefinedSignal, (current) => current ?? defaultValue);
|
||||
*/
|
||||
declare function update<T>(signal: Signal$1<T>, updater: (current: T) => T): void;
|
||||
declare function update<T extends object>(signal: Signal$1<T>, updater: Partial<T>): void;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
declare function snapshot<M extends Record<string, ReadonlySignal<unknown>>>(map: M): { [K in keyof M]: M[K] extends {
|
||||
get(): infer V;
|
||||
} ? V : never; };
|
||||
//#endregion
|
||||
export { Computed, ReadonlySignal, Signal$1 as Signal, SignalOptions, computed, signal, snapshot, untrack, update };
|
||||
//# sourceMappingURL=primitives.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"primitives.d.ts","names":[],"sources":["../../../../src/core/signals/primitives.ts"],"mappings":";;;cAGa,UAAU,GAAG,UAAU,MAAM;;KAqB9B,SAAO,KAAK,OAAS,MAAM;;KAG3B,SAAS,KAAK,OAAS,SAAS;;KAGhC,eAAe,KAAK,KAAK,OAAS,MAAM;UAEnC,cAAc;EAC7B,UAAU,GAAG,GAAG,IAAI;;;iBAIN,OAAO,GAAG,cAAc,GAAG,UAAU,cAAc,KAAK,SAAO;;iBAK/D,SAAS,GAAG,UAAU,GAAG,UAAU,cAAc,KAAK,SAAS;;;;;;;;;;;;;;iBAiB/D,OAAO,GAAG,QAAQ,SAAO,IAAI,UAAU,SAAS,MAAM;iBACtD,OAAO,kBAAkB,QAAQ,SAAO,IAAI,SAAS,QAAQ;;;;;;;;;iBAqC7D,SAAS,UAAU,eAAe,0BAChD,KAAK,OACD,WAAW,IAAI,EAAE;EAAa,aAAa;IAAM"}
|
||||
Vendored
+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
+115
@@ -0,0 +1,115 @@
|
||||
//#region src/core/tasks/task.d.ts
|
||||
/** Recursively marks all properties as readonly. */
|
||||
type DeepReadonly<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T;
|
||||
type TaskStatus = 'pending' | 'running' | 'done' | 'error';
|
||||
/**
|
||||
* Configuration for a Task.
|
||||
*/
|
||||
interface TaskConfig {
|
||||
/**
|
||||
* Identifier for this task.
|
||||
* - string: used as-is
|
||||
* - () => string: called once at construction time
|
||||
* - undefined: a unique ID is generated via generateId()
|
||||
*/
|
||||
id?: string | (() => string);
|
||||
/**
|
||||
* Optional external AbortSignal to compose with the task's internal one.
|
||||
* The task's work is aborted when either the internal controller (via abort())
|
||||
* or this external signal fires — whichever comes first.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/**
|
||||
* Minimal contract for a schedulable unit of async work.
|
||||
*/
|
||||
interface TaskLike<TValue = void, TError = unknown> {
|
||||
readonly id: string;
|
||||
readonly status: TaskStatus;
|
||||
readonly value: DeepReadonly<TValue> | undefined;
|
||||
readonly error: DeepReadonly<TError> | undefined;
|
||||
run(): Promise<TValue>;
|
||||
abort(): void;
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
declare class Task<TValue = void, TError = unknown> implements TaskLike<TValue, TError> {
|
||||
#private;
|
||||
readonly id: string;
|
||||
constructor(runFn: (signal: AbortSignal) => Promise<TValue>, config?: TaskConfig);
|
||||
get status(): TaskStatus;
|
||||
get value(): DeepReadonly<TValue> | undefined;
|
||||
get error(): DeepReadonly<TError> | undefined;
|
||||
run(): Promise<TValue>;
|
||||
abort(): void;
|
||||
}
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
declare class ConcurrentRunner {
|
||||
#private;
|
||||
schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue>;
|
||||
/**
|
||||
* 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: () => void): void;
|
||||
abortAll(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
declare class SerialRunner {
|
||||
#private;
|
||||
schedule<TValue = void, TError = unknown>(task: TaskLike<TValue, TError>): Promise<TValue>;
|
||||
/**
|
||||
* 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(): Promise<void>;
|
||||
/**
|
||||
* 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: () => void): void;
|
||||
/** Aborts and clears queued tasks without touching the in-flight task. */
|
||||
abortPending(): void;
|
||||
abortAll(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
//#endregion
|
||||
export { ConcurrentRunner, DeepReadonly, SerialRunner, Task, TaskConfig, TaskLike, TaskStatus };
|
||||
//# sourceMappingURL=task.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"task.d.ts","names":[],"sources":["../../../../src/core/tasks/task.ts"],"mappings":";;KAQY,aAAa,KAAK,iBAAiB,OAC3C,cAAc,aAAa,MAC3B,+BACc,WAAW,IAAI,aAAa,EAAE,SAC1C;KAMM;;;;UAKK;;;;;;;EAOf;;;;;;EAOA,SAAS;;;;;UAMM,SAAS,eAAe;WAC9B;WACA,QAAQ;WACR,OAAO,aAAa;WACpB,OAAO,aAAa;EAC7B,OAAO,QAAQ;EACf;;;;;;;;;;;;;;cAeW,KAAK,eAAe,6BAA6B,SAAS,QAAQ;;WACpE;EAST,YAAY,QAAQ,QAAQ,gBAAgB,QAAQ,SAAS,SAAS;MASlE,UAAU;MAIV,SAAS,aAAa;MAItB,SAAS,aAAa;EAIpB,OAAO,QAAQ;EAcrB;;;;;;;;;cAgBW;;EAMX,SAAS,eAAe,kBAAkB,MAAM,SAAS,QAAQ,UAAU,QAAQ;;;;;;;EAkCnF,YAAY;EAYZ;EAWA;;;;;;;;;;;;;;;;cAwBW;;EAMX,SAAS,eAAe,kBAAkB,MAAM,SAAS,QAAQ,UAAU,QAAQ;;;;;;;MA8B/E,WAAW;;;;;;;;EAWf,YAAY;;EAaZ;EAKA;EAKA"}
|
||||
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