build(store): build26 from 45504a2b

This commit is contained in:
publish
2026-08-05 18:57:36 +02:00
commit 3f9bdddf36
102 changed files with 2265 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
//#region src/core/abort-controller-registry.d.ts
type SignalKey = PropertyKey;
declare class AbortControllerRegistry {
#private;
/** The attach-scoped signal. Aborts on detach or reattach. */
get base(): AbortSignal;
/** Clears all keyed signals, leaving base intact. */
clear(): void;
/** Resets base and clears all keyed signals. */
reset(): void;
/** Creates a new signal for the key, superseding any previous signal. */
supersede(key: SignalKey): AbortSignal;
}
//#endregion
export { AbortControllerRegistry, SignalKey };
//# sourceMappingURL=abort-controller-registry.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"abort-controller-registry.d.ts","names":[],"sources":["../../../src/core/abort-controller-registry.ts"],"mappings":";KAEY,YAAY;cAEX;;;MAKP,QAAQ;;EAKZ;;EAQA;;EAOA,UAAU,KAAK,YAAY"}
+32
View File
@@ -0,0 +1,32 @@
import { anyAbortSignal } from "@videojs/utils/events";
//#region src/core/abort-controller-registry.ts
var AbortControllerRegistry = class {
#base = new AbortController();
#keys = /* @__PURE__ */ new Map();
/** The attach-scoped signal. Aborts on detach or reattach. */
get base() {
return this.#base.signal;
}
/** Clears all keyed signals, leaving base intact. */
clear() {
for (const controller of this.#keys.values()) controller.abort();
this.#keys.clear();
}
/** Resets base and clears all keyed signals. */
reset() {
this.clear();
this.#base.abort();
this.#base = new AbortController();
}
/** Creates a new signal for the key, superseding any previous signal. */
supersede(key) {
this.#keys.get(key)?.abort();
const controller = new AbortController();
this.#keys.set(key, controller);
return anyAbortSignal([this.#base.signal, controller.signal]);
}
};
//#endregion
export { AbortControllerRegistry };
//# sourceMappingURL=abort-controller-registry.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"abort-controller-registry.js","names":["#base","#keys"],"sources":["../../../src/core/abort-controller-registry.ts"],"sourcesContent":["import { anyAbortSignal } from '@videojs/utils/events';\n\nexport type SignalKey = PropertyKey;\n\nexport class AbortControllerRegistry {\n #base = new AbortController();\n #keys = new Map<SignalKey, AbortController>();\n\n /** The attach-scoped signal. Aborts on detach or reattach. */\n get base(): AbortSignal {\n return this.#base.signal;\n }\n\n /** Clears all keyed signals, leaving base intact. */\n clear(): void {\n for (const controller of this.#keys.values()) {\n controller.abort();\n }\n this.#keys.clear();\n }\n\n /** Resets base and clears all keyed signals. */\n reset(): void {\n this.clear();\n this.#base.abort();\n this.#base = new AbortController();\n }\n\n /** Creates a new signal for the key, superseding any previous signal. */\n supersede(key: SignalKey): AbortSignal {\n this.#keys.get(key)?.abort();\n const controller = new AbortController();\n this.#keys.set(key, controller);\n return anyAbortSignal([this.#base.signal, controller.signal]);\n }\n}\n"],"mappings":";;AAIA,IAAa,0BAAb,MAAqC;CACnC,QAAQ,IAAI,gBAAgB;CAC5B,wBAAQ,IAAI,IAAgC;;CAG5C,IAAI,OAAoB;EACtB,OAAO,KAAKA,MAAM;CACpB;;CAGA,QAAc;EACZ,KAAK,MAAM,cAAc,KAAKC,MAAM,OAAO,GACzC,WAAW,MAAM;EAEnB,KAAKA,MAAM,MAAM;CACnB;;CAGA,QAAc;EACZ,KAAK,MAAM;EACX,KAAKD,MAAM,MAAM;EACjB,KAAKA,QAAQ,IAAI,gBAAgB;CACnC;;CAGA,UAAU,KAA6B;EACrC,KAAKC,MAAM,IAAI,GAAG,CAAC,EAAE,MAAM;EAC3B,MAAM,aAAa,IAAI,gBAAgB;EACvC,KAAKA,MAAM,IAAI,KAAK,UAAU;EAC9B,OAAO,eAAe,CAAC,KAAKD,MAAM,QAAQ,WAAW,MAAM,CAAC;CAC9D;AACF"}
+12
View File
@@ -0,0 +1,12 @@
import { Slice, UnionSliceState } from "./slice.js";
//#region src/core/combine.d.ts
/**
* Combines multiple slices into a single slice.
*
* @param slices - The slices to combine.
* @returns A new slice that represents the combination of the input slices.
*/
declare function combine<Target, const Slices extends Slice<Target, any>[]>(...slices: Slices): Slice<Target, UnionSliceState<Slices>>;
//#endregion
export { combine };
//# sourceMappingURL=combine.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"combine.d.ts","names":[],"sources":["../../../src/core/combine.ts"],"mappings":";;;;;;;;iBAQgB,QAAQ,cAAc,eAAe,MAAM,mBACtD,QAAQ,SACV,MAAM,QAAQ,gBAAgB"}
+33
View File
@@ -0,0 +1,33 @@
//#region src/core/combine.ts
/**
* Combines multiple slices into a single slice.
*
* @param slices - The slices to combine.
* @returns A new slice that represents the combination of the input slices.
*/
function combine(...slices) {
return {
state: (ctx) => {
const states = slices.map((slice) => slice.state(ctx));
{
const seen = /* @__PURE__ */ new Set();
for (const state of states) for (const key of Object.keys(state)) {
if (seen.has(key)) console.warn(`[vjs-store] combine(): duplicate state key "${key}" — later slice overwrites earlier one`);
seen.add(key);
}
}
return Object.assign({}, ...states);
},
attach: (ctx) => {
for (const slice of slices) try {
slice.attach?.(ctx);
} catch (err) {
ctx.reportError(err);
}
}
};
}
//#endregion
export { combine };
//# sourceMappingURL=combine.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"combine.js","names":[],"sources":["../../../src/core/combine.ts"],"sourcesContent":["import type { AttachContext, InferSliceState, Slice, StateContext, UnionSliceState } from './slice';\n\n/**\n * Combines multiple slices into a single slice.\n *\n * @param slices - The slices to combine.\n * @returns A new slice that represents the combination of the input slices.\n */\nexport function combine<Target, const Slices extends Slice<Target, any>[]>(\n ...slices: Slices\n): Slice<Target, UnionSliceState<Slices>> {\n return {\n state: (ctx: StateContext<Target>) => {\n const states = slices.map((slice) => slice.state(ctx));\n\n if (__DEV__) {\n const seen = new Set<string>();\n for (const state of states) {\n for (const key of Object.keys(state as object)) {\n if (seen.has(key)) {\n console.warn(`[vjs-store] combine(): duplicate state key \"${key}\" — later slice overwrites earlier one`);\n }\n seen.add(key);\n }\n }\n }\n\n return Object.assign({}, ...states) as UnionSliceState<Slices>;\n },\n\n attach: (ctx: AttachContext<Target, UnionSliceState<Slices>>) => {\n for (const slice of slices) {\n try {\n slice.attach?.(ctx as AttachContext<Target, InferSliceState<typeof slice>>);\n } catch (err) {\n ctx.reportError(err);\n }\n }\n },\n };\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,QACd,GAAG,QACqC;CACxC,OAAO;EACL,QAAQ,QAA8B;GACpC,MAAM,SAAS,OAAO,KAAK,UAAU,MAAM,MAAM,GAAG,CAAC;GAExC;IACX,MAAM,uBAAO,IAAI,IAAY;IAC7B,KAAK,MAAM,SAAS,QAClB,KAAK,MAAM,OAAO,OAAO,KAAK,KAAe,GAAG;KAC9C,IAAI,KAAK,IAAI,GAAG,GACd,QAAQ,KAAK,+CAA+C,IAAI,uCAAuC;KAEzG,KAAK,IAAI,GAAG;IACd;GAEJ;GAEA,OAAO,OAAO,OAAO,CAAC,GAAG,GAAG,MAAM;EACpC;EAEA,SAAS,QAAwD;GAC/D,KAAK,MAAM,SAAS,QAClB,IAAI;IACF,MAAM,SAAS,GAA2D;GAC5E,SAAS,KAAK;IACZ,IAAI,YAAY,GAAG;GACrB;EAEJ;CACF;AACF"}
+23
View File
@@ -0,0 +1,23 @@
import { Store } from "./store.js";
//#region src/core/config.d.ts
interface StoreCallbacks<Target, State> {
onSetup?: (ctx: StoreSetupContext<Target, State>) => void;
onAttach?: (ctx: StoreAttachContext<Target, State>) => void;
onError?: (ctx: StoreErrorContext<Target, State>) => void;
}
interface StoreSetupContext<Target, State> {
store: Store<Target, State>;
signal: AbortSignal;
}
interface StoreAttachContext<Target, State> {
store: Store<Target, State>;
target: Target;
signal: AbortSignal;
}
interface StoreErrorContext<Target, State> {
store: Store<Target, State>;
error: unknown;
}
//#endregion
export { StoreAttachContext, StoreCallbacks, StoreErrorContext, StoreSetupContext };
//# sourceMappingURL=config.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"config.d.ts","names":[],"sources":["../../../src/core/config.ts"],"mappings":";;UAEiB,eAAe,QAAQ;EACtC,WAAW,KAAK,kBAAkB,QAAQ;EAC1C,YAAY,KAAK,mBAAmB,QAAQ;EAC5C,WAAW,KAAK,kBAAkB,QAAQ;;UAG3B,kBAAkB,QAAQ;EACzC,OAAO,MAAM,QAAQ;EACrB,QAAQ;;UAGO,mBAAmB,QAAQ;EAC1C,OAAO,MAAM,QAAQ;EACrB,QAAQ;EACR,QAAQ;;UAGO,kBAAkB,QAAQ;EACzC,OAAO,MAAM,QAAQ;EACrB"}
+21
View File
@@ -0,0 +1,21 @@
//#region src/core/errors.d.ts
type StoreErrorCode =
/** Store was destroyed. */
'DESTROYED' |
/** No target is attached to the store. */
'NO_TARGET';
interface StoreErrorOptions {
cause?: unknown;
message?: string;
}
declare class StoreError extends Error {
readonly code: StoreErrorCode;
cause?: unknown;
constructor(code: StoreErrorCode, options?: StoreErrorOptions);
}
declare function isStoreError(error: unknown): error is StoreError;
declare function throwNoTargetError(): never;
declare function throwDestroyedError(): never;
//#endregion
export { StoreError, StoreErrorCode, StoreErrorOptions, isStoreError, throwDestroyedError, throwNoTargetError };
//# sourceMappingURL=errors.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"errors.d.ts","names":[],"sources":["../../../src/core/errors.ts"],"mappings":";KAAY;;;;;UAMK;EACf;EACA;;cAGW,mBAAmB;WACrB,MAAM;EACf;EAEA,YAAY,MAAM,gBAAgB,UAAU;;iBAQ9B,aAAa,iBAAiB,SAAS;iBAIvC;iBAIA"}
+24
View File
@@ -0,0 +1,24 @@
//#region src/core/errors.ts
var StoreError = class extends Error {
code;
cause;
constructor(code, options) {
super(options?.message ?? code);
this.name = "StoreError";
this.code = code;
this.cause = options?.cause;
}
};
function isStoreError(error) {
return error instanceof StoreError;
}
function throwNoTargetError() {
throw new StoreError("NO_TARGET");
}
function throwDestroyedError() {
throw new StoreError("DESTROYED");
}
//#endregion
export { StoreError, isStoreError, throwDestroyedError, throwNoTargetError };
//# sourceMappingURL=errors.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"errors.js","names":[],"sources":["../../../src/core/errors.ts"],"sourcesContent":["export type StoreErrorCode =\n /** Store was destroyed. */\n | 'DESTROYED'\n /** No target is attached to the store. */\n | 'NO_TARGET';\n\nexport interface StoreErrorOptions {\n cause?: unknown;\n message?: string;\n}\n\nexport class StoreError extends Error {\n readonly code: StoreErrorCode;\n cause?: unknown;\n\n constructor(code: StoreErrorCode, options?: StoreErrorOptions) {\n super(options?.message ?? code);\n this.name = 'StoreError';\n this.code = code;\n this.cause = options?.cause;\n }\n}\n\nexport function isStoreError(error: unknown): error is StoreError {\n return error instanceof StoreError;\n}\n\nexport function throwNoTargetError(): never {\n throw new StoreError('NO_TARGET');\n}\n\nexport function throwDestroyedError(): never {\n throw new StoreError('DESTROYED');\n}\n"],"mappings":";AAWA,IAAa,aAAb,cAAgC,MAAM;CACpC;CACA;CAEA,YAAY,MAAsB,SAA6B;EAC7D,MAAM,SAAS,WAAW,IAAI;EAC9B,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ,SAAS;CACxB;AACF;AAEA,SAAgB,aAAa,OAAqC;CAChE,OAAO,iBAAiB;AAC1B;AAEA,SAAgB,qBAA4B;CAC1C,MAAM,IAAI,WAAW,WAAW;AAClC;AAEA,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,WAAW,WAAW;AAClC"}
+22
View File
@@ -0,0 +1,22 @@
import { AnySlice, InferSliceState } from "./slice.js";
import { Selector } from "./shallow-equal.js";
//#region src/core/selector.d.ts
/**
* Create a type-safe selector for a slice's state.
*
* The selector returns the slice's state, or `undefined` if the slice
* is not configured in the store.
*
* @example
* ```ts
* const selectPlayback = createSelector(playbackSlice);
* selectPlayback(store.state); // { paused, play, pause, ... } | undefined
* selectPlayback.displayName; // 'playback' (from slice name)
* ```
*
* @param slice - The slice to create a selector for.
*/
declare function createSelector<S extends AnySlice>(slice: S): Selector<object, InferSliceState<S> | undefined>;
//#endregion
export { createSelector };
//# sourceMappingURL=selector.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"selector.d.ts","names":[],"sources":["../../../src/core/selector.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBA4BgB,eAAe,UAAU,UAAU,OAAO,IAAI,iBAAiB,gBAAgB"}
+39
View File
@@ -0,0 +1,39 @@
import { AbortControllerRegistry } from "./abort-controller-registry.js";
import { throwNoTargetError } from "./errors.js";
import { pick } from "@videojs/utils/object";
//#region src/core/selector.ts
const stateContext = {
target: throwNoTargetError,
signals: new AbortControllerRegistry(),
get: throwNoTargetError,
set: throwNoTargetError
};
/**
* Create a type-safe selector for a slice's state.
*
* The selector returns the slice's state, or `undefined` if the slice
* is not configured in the store.
*
* @example
* ```ts
* const selectPlayback = createSelector(playbackSlice);
* selectPlayback(store.state); // { paused, play, pause, ... } | undefined
* selectPlayback.displayName; // 'playback' (from slice name)
* ```
*
* @param slice - The slice to create a selector for.
*/
function createSelector(slice) {
const initialState = slice.state(stateContext);
const keys = Object.keys(initialState);
const firstKey = keys[0];
if (!firstKey) return Object.assign(() => void 0, { displayName: slice.name });
return Object.assign((state) => {
if (!(firstKey in state)) return void 0;
return pick(state, keys);
}, { displayName: slice.name });
}
//#endregion
export { createSelector };
//# sourceMappingURL=selector.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"selector.js","names":[],"sources":["../../../src/core/selector.ts"],"sourcesContent":["import { pick } from '@videojs/utils/object';\nimport { AbortControllerRegistry } from './abort-controller-registry';\nimport { throwNoTargetError } from './errors';\nimport type { Selector } from './shallow-equal';\nimport type { AnySlice, InferSliceState, StateContext } from './slice';\n\nconst stateContext: StateContext<unknown> = {\n target: throwNoTargetError,\n signals: new AbortControllerRegistry(),\n get: throwNoTargetError,\n set: throwNoTargetError,\n};\n\n/**\n * Create a type-safe selector for a slice's state.\n *\n * The selector returns the slice's state, or `undefined` if the slice\n * is not configured in the store.\n *\n * @example\n * ```ts\n * const selectPlayback = createSelector(playbackSlice);\n * selectPlayback(store.state); // { paused, play, pause, ... } | undefined\n * selectPlayback.displayName; // 'playback' (from slice name)\n * ```\n *\n * @param slice - The slice to create a selector for.\n */\nexport function createSelector<S extends AnySlice>(slice: S): Selector<object, InferSliceState<S> | undefined> {\n const initialState = slice.state(stateContext);\n const keys = Object.keys(initialState as object);\n\n const firstKey = keys[0];\n\n if (!firstKey) {\n return Object.assign(() => undefined, { displayName: slice.name });\n }\n\n return Object.assign(\n (state: object) => {\n // WARN: Could be the source of a bug if two slices have overlapping state keys\n if (!(firstKey in state)) return undefined;\n return pick(state as Record<string, unknown>, keys) as InferSliceState<S>;\n },\n { displayName: slice.name }\n );\n}\n"],"mappings":";;;;AAMA,MAAM,eAAsC;CAC1C,QAAQ;CACR,SAAS,IAAI,wBAAwB;CACrC,KAAK;CACL,KAAK;AACP;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAmC,OAA4D;CAC7G,MAAM,eAAe,MAAM,MAAM,YAAY;CAC7C,MAAM,OAAO,OAAO,KAAK,YAAsB;CAE/C,MAAM,WAAW,KAAK;CAEtB,IAAI,CAAC,UACH,OAAO,OAAO,aAAa,KAAA,GAAW,EAAE,aAAa,MAAM,KAAK,CAAC;CAGnE,OAAO,OAAO,QACX,UAAkB;EAEjB,IAAI,EAAE,YAAY,QAAQ,OAAO,KAAA;EACjC,OAAO,KAAK,OAAkC,IAAI;CACpD,GACA,EAAE,aAAa,MAAM,KAAK,CAC5B;AACF"}
+10
View File
@@ -0,0 +1,10 @@
import { shallowEqual } from "@videojs/utils/object";
//#region src/core/shallow-equal.d.ts
interface Selector<State, Result> {
(state: State): Result;
displayName?: string | undefined;
}
type Comparator<T> = (a: T, b: T) => boolean;
//#endregion
export { Comparator, Selector, shallowEqual };
//# sourceMappingURL=shallow-equal.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"shallow-equal.d.ts","names":[],"sources":["../../../src/core/shallow-equal.ts"],"mappings":";;UAEiB,SAAS,OAAO;GAC9B,OAAO,QAAQ;EAChB;;KAGU,WAAW,MAAM,GAAG,GAAG,GAAG"}
+2
View File
@@ -0,0 +1,2 @@
import { shallowEqual } from "@videojs/utils/object";
export { shallowEqual };
+52
View File
@@ -0,0 +1,52 @@
import { AbortControllerRegistry } from "./abort-controller-registry.js";
import { UnknownState } from "./state.js";
import { Simplify, UnionToIntersection } from "@videojs/utils/types";
//#region src/core/slice.d.ts
type Attach<Target, State> = (ctx: AttachContext<Target, State>) => void;
interface AttachStore {
readonly state: UnknownState;
subscribe: (callback: () => void) => () => void;
}
interface AttachContext<Target, State> {
target: Target;
signal: AbortSignal;
store: AttachStore;
get: () => Readonly<State>;
set: (partial: Partial<State>) => void;
reportError: (error: unknown) => void;
}
interface StateContext<Target> {
/** Returns the current target. Throws if not attached. */
target: () => Target;
/**
* Cancellation signals for async operations.
*
* - `signals.base` — Aborts on detach or reattach. Use for cleanup.
* - `signals.supersede(key)` — Returns a signal that aborts when the same key
* is superseded or when base aborts. Use for operations that should cancel
* previous in-flight work (e.g., seek superseding seek).
* - `signals.clear()` — Aborts all keyed signals. Use when starting fresh
* (e.g., loading a new source cancels pending seeks).
*/
signals: AbortControllerRegistry;
/** Read current slice state. Safe to use inside action closures (not during `state()` init). */
get: () => Readonly<Record<string, unknown>>;
/** Patch the slice state. Safe to use inside action closures (not during `state()` init). */
set: (partial: Record<string, unknown>) => void;
}
interface SliceConfig<Target, State> {
/** Debug label. Used as `displayName` on selectors created from this slice. */
name?: string;
state: (ctx: StateContext<Target>) => State;
attach?: (ctx: AttachContext<Target, State>) => void;
}
type Slice<Target, State> = SliceConfig<Target, State>;
type AnySlice<Target = any> = Slice<Target, any>;
type SliceFactory<Target> = <State>(config: SliceConfig<Target, State>) => Slice<Target, State>;
declare function defineSlice<Target>(): SliceFactory<Target>;
type InferSliceTarget<S> = S extends Slice<infer Target, any> ? Target : never;
type InferSliceState<S> = S extends Slice<any, infer State> ? State : never;
type UnionSliceState<Slices extends AnySlice[]> = Simplify<UnionToIntersection<InferSliceState<Slices[number]>>>;
//#endregion
export { AnySlice, Attach, AttachContext, AttachStore, InferSliceState, InferSliceTarget, Slice, SliceConfig, SliceFactory, StateContext, UnionSliceState, defineSlice };
//# sourceMappingURL=slice.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"slice.d.ts","names":[],"sources":["../../../src/core/slice.ts"],"mappings":";;;;KAQY,OAAO,QAAQ,UAAU,KAAK,cAAc,QAAQ;UAE/C;WACN,OAAO;EAChB,YAAY;;UAGG,cAAc,QAAQ;EACrC,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,WAAW,SAAS;EACpB,MAAM,SAAS,QAAQ;EACvB,cAAc;;UAOC,aAAa;;EAE5B,cAAc;;;;;;;;;;;EAWd,SAAS;;EAET,WAAW,SAAS;;EAEpB,MAAM,SAAS;;UAOA,YAAY,QAAQ;;EAEnC;EACA,QAAQ,KAAK,aAAa,YAAY;EACtC,UAAU,KAAK,cAAc,QAAQ;;KAG3B,MAAM,QAAQ,SAAS,YAAY,QAAQ;KAE3C,SAAS,gBAAgB,MAAM;KAM/B,aAAa,WAAW,OAAO,QAAQ,YAAY,QAAQ,WAAW,MAAM,QAAQ;iBAEhF,YAAY,WAAW,aAAa;KAQxC,iBAAiB,KAAK,UAAU,YAAY,eAAe;KAE3D,gBAAgB,KAAK,UAAU,iBAAiB,SAAS;KAEzD,gBAAgB,eAAe,cAAc,SAAS,oBAAoB,gBAAgB"}
+8
View File
@@ -0,0 +1,8 @@
//#region src/core/slice.ts
function defineSlice() {
return (config) => config;
}
//#endregion
export { defineSlice };
//# sourceMappingURL=slice.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"slice.js","names":[],"sources":["../../../src/core/slice.ts"],"sourcesContent":["import type { Simplify, UnionToIntersection } from '@videojs/utils/types';\nimport type { AbortControllerRegistry } from './abort-controller-registry';\nimport type { UnknownState } from './state';\n\n// ----------------------------------------\n// Attach\n// ----------------------------------------\n\nexport type Attach<Target, State> = (ctx: AttachContext<Target, State>) => void;\n\nexport interface AttachStore {\n readonly state: UnknownState;\n subscribe: (callback: () => void) => () => void;\n}\n\nexport interface AttachContext<Target, State> {\n target: Target;\n signal: AbortSignal;\n store: AttachStore;\n get: () => Readonly<State>;\n set: (partial: Partial<State>) => void;\n reportError: (error: unknown) => void;\n}\n\n// ----------------------------------------\n// State Context\n// ----------------------------------------\n\nexport interface StateContext<Target> {\n /** Returns the current target. Throws if not attached. */\n target: () => Target;\n /**\n * Cancellation signals for async operations.\n *\n * - `signals.base` — Aborts on detach or reattach. Use for cleanup.\n * - `signals.supersede(key)` — Returns a signal that aborts when the same key\n * is superseded or when base aborts. Use for operations that should cancel\n * previous in-flight work (e.g., seek superseding seek).\n * - `signals.clear()` — Aborts all keyed signals. Use when starting fresh\n * (e.g., loading a new source cancels pending seeks).\n */\n signals: AbortControllerRegistry;\n /** Read current slice state. Safe to use inside action closures (not during `state()` init). */\n get: () => Readonly<Record<string, unknown>>;\n /** Patch the slice state. Safe to use inside action closures (not during `state()` init). */\n set: (partial: Record<string, unknown>) => void;\n}\n\n// ----------------------------------------\n// Slice\n// ----------------------------------------\n\nexport interface SliceConfig<Target, State> {\n /** Debug label. Used as `displayName` on selectors created from this slice. */\n name?: string;\n state: (ctx: StateContext<Target>) => State;\n attach?: (ctx: AttachContext<Target, State>) => void;\n}\n\nexport type Slice<Target, State> = SliceConfig<Target, State>;\n\nexport type AnySlice<Target = any> = Slice<Target, any>;\n\n// ----------------------------------------\n// Factory\n// ----------------------------------------\n\nexport type SliceFactory<Target> = <State>(config: SliceConfig<Target, State>) => Slice<Target, State>;\n\nexport function defineSlice<Target>(): SliceFactory<Target> {\n return (config) => config;\n}\n\n// ----------------------------------------\n// Inference\n// ----------------------------------------\n\nexport type InferSliceTarget<S> = S extends Slice<infer Target, any> ? Target : never;\n\nexport type InferSliceState<S> = S extends Slice<any, infer State> ? State : never;\n\nexport type UnionSliceState<Slices extends AnySlice[]> = Simplify<UnionToIntersection<InferSliceState<Slices[number]>>>;\n"],"mappings":";AAqEA,SAAgB,cAA4C;CAC1D,QAAQ,WAAW;AACrB"}
+19
View File
@@ -0,0 +1,19 @@
//#region src/core/state.d.ts
type StateChange = () => void;
type UnknownState = Record<string, unknown>;
interface SubscribeOptions {
signal?: AbortSignal;
}
interface State<T> {
readonly current: Readonly<T>;
subscribe(callback: StateChange, options?: SubscribeOptions): () => void;
}
interface WritableState<T> extends State<T> {
patch: (partial: Partial<T>) => void;
}
declare function flush(): void;
declare function createState<T>(initial: T): WritableState<T>;
declare function isState(value: unknown): value is State<object>;
//#endregion
export { State, StateChange, SubscribeOptions, UnknownState, WritableState, createState, flush, isState };
//# sourceMappingURL=state.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"state.d.ts","names":[],"sources":["../../../src/core/state.ts"],"mappings":";KAEY;KAEA,eAAe;UAEV;EACf,SAAS;;UAGM,MAAM;WACZ,SAAS,SAAS;EAC3B,UAAU,UAAU,aAAa,UAAU;;UAG5B,cAAc,WAAW,MAAM;EAC9C,QAAQ,SAAS,QAAQ;;iBAYX;iBA2EA,YAAY,GAAG,SAAS,IAAI,cAAc;iBAI1C,QAAQ,iBAAiB,SAAS"}
+74
View File
@@ -0,0 +1,74 @@
import { noop } from "@videojs/utils/function";
//#region src/core/state.ts
let isFlushScheduled = false;
function scheduleFlush() {
if (isFlushScheduled) return;
isFlushScheduled = true;
queueMicrotask(flush);
}
const pendingContainers = /* @__PURE__ */ new Set();
function flush() {
isFlushScheduled = false;
for (const container of pendingContainers) container.flush();
pendingContainers.clear();
}
const hasOwnProp = Object.prototype.hasOwnProperty;
var StateContainer = class {
#current;
#listeners = /* @__PURE__ */ new Set();
#pending = false;
constructor(initial) {
this.#current = Object.freeze({ ...initial });
}
get current() {
return this.#current;
}
patch(partial) {
const next = { ...this.#current };
let changed = false;
for (const key in partial) {
if (!hasOwnProp.call(partial, key)) continue;
const value = partial[key];
if (!Object.is(this.#current[key], value)) {
next[key] = value;
changed = true;
}
}
if (changed) {
this.#current = Object.freeze(next);
this.#markPending();
}
}
subscribe(callback, options) {
const signal = options?.signal;
if (signal?.aborted) return noop;
this.#listeners.add(callback);
if (!signal) return () => this.#listeners.delete(callback);
const onAbort = () => this.#listeners.delete(callback);
signal.addEventListener("abort", onAbort, { once: true });
return () => {
signal.removeEventListener("abort", onAbort);
this.#listeners.delete(callback);
};
}
flush() {
if (!this.#pending) return;
this.#pending = false;
for (const fn of this.#listeners) fn();
}
#markPending() {
this.#pending = true;
pendingContainers.add(this);
scheduleFlush();
}
};
function createState(initial) {
return new StateContainer(initial);
}
function isState(value) {
return value instanceof StateContainer;
}
//#endregion
export { createState, flush, isState };
//# sourceMappingURL=state.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"state.js","names":["#current","#markPending","#listeners","#pending"],"sources":["../../../src/core/state.ts"],"sourcesContent":["import { noop } from '@videojs/utils/function';\n\nexport type StateChange = () => void;\n\nexport type UnknownState = Record<string, unknown>;\n\nexport interface SubscribeOptions {\n signal?: AbortSignal;\n}\n\nexport interface State<T> {\n readonly current: Readonly<T>;\n subscribe(callback: StateChange, options?: SubscribeOptions): () => void;\n}\n\nexport interface WritableState<T> extends State<T> {\n patch: (partial: Partial<T>) => void;\n}\n\nlet isFlushScheduled = false;\nfunction scheduleFlush(): void {\n if (isFlushScheduled) return;\n isFlushScheduled = true;\n queueMicrotask(flush);\n}\n\nconst pendingContainers = new Set<StateContainer<any>>();\n\nexport function flush(): void {\n isFlushScheduled = false;\n for (const container of pendingContainers) container.flush();\n pendingContainers.clear();\n}\n\nconst hasOwnProp = Object.prototype.hasOwnProperty;\n\nclass StateContainer<T> implements WritableState<T> {\n #current: T;\n #listeners = new Set<StateChange>();\n #pending = false;\n\n constructor(initial: T) {\n this.#current = Object.freeze({ ...initial });\n }\n\n get current(): Readonly<T> {\n return this.#current;\n }\n\n patch(partial: Partial<T>): void {\n const next = { ...this.#current };\n\n let changed = false;\n\n for (const key in partial) {\n if (!hasOwnProp.call(partial, key)) continue;\n\n const value = partial[key];\n\n if (!Object.is(this.#current[key], value)) {\n next[key] = value!;\n changed = true;\n }\n }\n\n if (changed) {\n this.#current = Object.freeze(next);\n this.#markPending();\n }\n }\n\n subscribe(callback: StateChange, options?: SubscribeOptions): () => void {\n const signal = options?.signal;\n if (signal?.aborted) return noop;\n\n this.#listeners.add(callback);\n\n if (!signal) {\n return () => this.#listeners.delete(callback);\n }\n\n const onAbort = () => this.#listeners.delete(callback);\n signal.addEventListener('abort', onAbort, { once: true });\n\n return () => {\n signal.removeEventListener('abort', onAbort);\n this.#listeners.delete(callback);\n };\n }\n\n flush(): void {\n if (!this.#pending) return;\n this.#pending = false;\n for (const fn of this.#listeners) fn();\n }\n\n #markPending(): void {\n this.#pending = true;\n pendingContainers.add(this);\n scheduleFlush();\n }\n}\n\nexport function createState<T>(initial: T): WritableState<T> {\n return new StateContainer(initial);\n}\n\nexport function isState(value: unknown): value is State<object> {\n return value instanceof StateContainer;\n}\n"],"mappings":";;AAmBA,IAAI,mBAAmB;AACvB,SAAS,gBAAsB;CAC7B,IAAI,kBAAkB;CACtB,mBAAmB;CACnB,eAAe,KAAK;AACtB;AAEA,MAAM,oCAAoB,IAAI,IAAyB;AAEvD,SAAgB,QAAc;CAC5B,mBAAmB;CACnB,KAAK,MAAM,aAAa,mBAAmB,UAAU,MAAM;CAC3D,kBAAkB,MAAM;AAC1B;AAEA,MAAM,aAAa,OAAO,UAAU;AAEpC,IAAM,iBAAN,MAAoD;CAClD;CACA,6BAAa,IAAI,IAAiB;CAClC,WAAW;CAEX,YAAY,SAAY;EACtB,KAAKA,WAAW,OAAO,OAAO,EAAE,GAAG,QAAQ,CAAC;CAC9C;CAEA,IAAI,UAAuB;EACzB,OAAO,KAAKA;CACd;CAEA,MAAM,SAA2B;EAC/B,MAAM,OAAO,EAAE,GAAG,KAAKA,SAAS;EAEhC,IAAI,UAAU;EAEd,KAAK,MAAM,OAAO,SAAS;GACzB,IAAI,CAAC,WAAW,KAAK,SAAS,GAAG,GAAG;GAEpC,MAAM,QAAQ,QAAQ;GAEtB,IAAI,CAAC,OAAO,GAAG,KAAKA,SAAS,MAAM,KAAK,GAAG;IACzC,KAAK,OAAO;IACZ,UAAU;GACZ;EACF;EAEA,IAAI,SAAS;GACX,KAAKA,WAAW,OAAO,OAAO,IAAI;GAClC,KAAKC,aAAa;EACpB;CACF;CAEA,UAAU,UAAuB,SAAwC;EACvE,MAAM,SAAS,SAAS;EACxB,IAAI,QAAQ,SAAS,OAAO;EAE5B,KAAKC,WAAW,IAAI,QAAQ;EAE5B,IAAI,CAAC,QACH,aAAa,KAAKA,WAAW,OAAO,QAAQ;EAG9C,MAAM,gBAAgB,KAAKA,WAAW,OAAO,QAAQ;EACrD,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAExD,aAAa;GACX,OAAO,oBAAoB,SAAS,OAAO;GAC3C,KAAKA,WAAW,OAAO,QAAQ;EACjC;CACF;CAEA,QAAc;EACZ,IAAI,CAAC,KAAKC,UAAU;EACpB,KAAKA,WAAW;EAChB,KAAK,MAAM,MAAM,KAAKD,YAAY,GAAG;CACvC;CAEA,eAAqB;EACnB,KAAKC,WAAW;EAChB,kBAAkB,IAAI,IAAI;EAC1B,cAAc;CAChB;AACF;AAEA,SAAgB,YAAe,SAA8B;CAC3D,OAAO,IAAI,eAAe,OAAO;AACnC;AAEA,SAAgB,QAAQ,OAAwC;CAC9D,OAAO,iBAAiB;AAC1B"}
+25
View File
@@ -0,0 +1,25 @@
import { State, StateChange, SubscribeOptions, UnknownState } from "./state.js";
import { Slice } from "./slice.js";
import { StoreCallbacks } from "./config.js";
//#region src/core/store.d.ts
interface StoreOptions<Target, State> extends StoreCallbacks<Target, State> {}
declare function createStore<Target = unknown>(): <State>(slice: Slice<Target, State>, options?: StoreOptions<Target, State>) => Store<Target, State>;
declare function isStore(value: unknown): value is AnyStore;
interface BaseStore<Target = unknown, State$1 = UnknownState> {
[key: string]: unknown;
readonly $state: State<State$1>;
readonly target: Target | null;
readonly destroyed: boolean;
readonly state: State$1;
attach(target: Target): () => void;
destroy(): void;
subscribe(callback: StateChange, options?: SubscribeOptions): () => void;
}
type Store<Target = unknown, State = UnknownState> = BaseStore<Target, State> & State;
type AnyStore<Target = any> = BaseStore<Target, object>;
type UnknownStore<Target = unknown> = Store<Target, UnknownState>;
type InferStoreTarget<S extends AnyStore> = S extends Store<infer T, any> ? T : never;
type InferStoreState<S extends AnyStore> = S extends Store<any, infer State> ? State : never;
//#endregion
export { AnyStore, BaseStore, InferStoreState, InferStoreTarget, Store, StoreOptions, UnknownStore, createStore, isStore };
//# sourceMappingURL=store.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"store.d.ts","names":[],"sources":["../../../src/core/store.ts"],"mappings":";;;;UAUiB,aAAa,QAAQ,eAAe,eAAe,QAAQ;iBAE5D,YAAY,sBAAsB,OAChD,OAAO,MAAM,QAAQ,QACrB,UAAU,aAAa,QAAQ,WAC5B,MAAM,QAAQ;iBAsIH,QAAQ,iBAAiB,SAAS;UAQjC,UAAU,kBAAkB,UAAQ;GAClD;WACQ,QAAQ,MAAe;WACvB,QAAQ;WACR;WACA,OAAO;EAChB,OAAO,QAAQ;EACf;EACA,UAAU,UAAU,aAAa,UAAU;;KAGjC,MAAM,kBAAkB,QAAQ,gBAAgB,UAAU,QAAQ,SAAS;KAE3E,SAAS,gBAAgB,UAAU;KAEnC,aAAa,oBAAoB,MAAM,QAAQ;KAE/C,iBAAiB,UAAU,YAAY,UAAU,YAAY,UAAU;KAEvE,gBAAgB,UAAU,YAAY,UAAU,iBAAiB,SAAS"}
+122
View File
@@ -0,0 +1,122 @@
import { AbortControllerRegistry } from "./abort-controller-registry.js";
import { throwDestroyedError, throwNoTargetError } from "./errors.js";
import { createState } from "./state.js";
import { isNull, isObject } from "@videojs/utils/predicate";
//#region src/core/store.ts
const STORE_SYMBOL = Symbol.for("@videojs/store");
function createStore() {
return (slice, options = {}) => {
let target = null;
let destroyed = false;
const setupAbort = new AbortController();
const signals = new AbortControllerRegistry();
let state;
function validate() {
if (destroyed) throwDestroyedError();
if (!target) throwNoTargetError();
}
const initialState = slice.state({
target: () => {
validate();
return target;
},
signals,
get: () => state.current,
set: (partial) => state.patch(partial)
});
state = createState(initialState);
const store = {
[STORE_SYMBOL]: true,
get $state() {
return state;
},
get target() {
return target;
},
get destroyed() {
return destroyed;
},
get state() {
return state.current;
},
attach,
destroy,
subscribe
};
for (const key of Object.keys(initialState)) Object.defineProperty(store, key, {
get: () => state.current[key],
enumerable: true
});
try {
options.onSetup?.({
store,
signal: setupAbort.signal
});
} catch (error) {
reportError(error);
}
return store;
function attach(newTarget) {
if (destroyed) throwDestroyedError();
signals.reset();
target = newTarget;
const attachContext = {
target: newTarget,
signal: signals.base,
get: () => state.current,
set: (partial) => state.patch(partial),
reportError,
store: {
get state() {
return state.current;
},
subscribe
}
};
try {
slice.attach?.(attachContext);
} catch (error) {
reportError(error);
}
try {
options.onAttach?.({
store,
target: newTarget,
signal: signals.base
});
} catch (error) {
reportError(error);
}
return detach;
}
function detach() {
if (isNull(target)) return;
signals.reset();
target = null;
state.patch(initialState);
}
function destroy() {
if (destroyed) return;
destroyed = true;
detach();
setupAbort.abort();
}
function subscribe(callback, options) {
return state.subscribe(callback, options);
}
function reportError(error) {
if (options.onError) options.onError({
store,
error
});
else console.error("[vjs-store]", error);
}
};
}
function isStore(value) {
return isObject(value) && STORE_SYMBOL in value;
}
//#endregion
export { createStore, isStore };
//# sourceMappingURL=store.js.map
+1
View File
File diff suppressed because one or more lines are too long