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
+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"}
+26
View File
@@ -0,0 +1,26 @@
//#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));
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;GAcrD,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"}
+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"}
+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"}
+2
View File
@@ -0,0 +1,2 @@
import { shallowEqual } from "@videojs/utils/object";
export { shallowEqual };
+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"}
+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"}
+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
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
import { SnapshotController } from "./html/controllers/snapshot-controller.js";
import { StoreAccessor } from "./html/store-accessor.js";
import { StoreController } from "./html/controllers/store-controller.js";
import { SubscriptionController } from "./html/controllers/subscription-controller.js";
export { SnapshotController, StoreAccessor, StoreController, SubscriptionController };
+65
View File
@@ -0,0 +1,65 @@
import { shallowEqual } from "../../core/shallow-equal.js";
import { noop } from "@videojs/utils/function";
//#region src/html/controllers/snapshot-controller.ts
/**
* Subscribe to a `State<T>` container with optional selector.
*
* Without selector: returns full state, re-renders on any state change.
* With selector: returns selected slice, re-renders only when the slice changes (shallowEqual).
*
* @example
* ```ts
* #state = new SnapshotController(this, sliderState, (s) => s.value);
* ```
*/
var SnapshotController = class {
#host;
#selector;
#state;
#cached;
#unsubscribe = noop;
constructor(host, state, selector) {
this.#host = host;
this.#state = state;
this.#selector = selector;
host.addController(this);
}
get value() {
if (!this.#selector) return this.#state.current;
this.#cached ??= this.#selector(this.#state.current);
return this.#cached;
}
/** Switch to tracking a different state container. */
track(state) {
this.#state = state;
this.#subscribe();
}
hostConnected() {
this.#subscribe();
}
hostDisconnected() {
this.#unsubscribe();
this.#unsubscribe = noop;
this.#cached = void 0;
}
#subscribe() {
this.#unsubscribe();
if (!this.#selector) {
this.#unsubscribe = this.#state.subscribe(() => this.#host.requestUpdate());
return;
}
const selector = this.#selector;
this.#cached = selector(this.#state.current);
this.#unsubscribe = this.#state.subscribe(() => {
const next = selector(this.#state.current);
if (!shallowEqual(this.#cached, next)) {
this.#cached = next;
this.#host.requestUpdate();
}
});
}
};
//#endregion
export { SnapshotController };
//# sourceMappingURL=snapshot-controller.js.map
@@ -0,0 +1 @@
{"version":3,"file":"snapshot-controller.js","names":["#host","#selector","#state","#cached","#subscribe","#unsubscribe"],"sources":["../../../../src/html/controllers/snapshot-controller.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from '@videojs/element';\nimport { noop } from '@videojs/utils/function';\nimport type { Selector } from '../../core/shallow-equal';\nimport { shallowEqual } from '../../core/shallow-equal';\nimport type { State } from '../../core/state';\n\nexport type SnapshotControllerHost = ReactiveControllerHost & HTMLElement;\n\n/**\n * Subscribe to a `State<T>` container with optional selector.\n *\n * Without selector: returns full state, re-renders on any state change.\n * With selector: returns selected slice, re-renders only when the slice changes (shallowEqual).\n *\n * @example\n * ```ts\n * #state = new SnapshotController(this, sliderState, (s) => s.value);\n * ```\n */\nexport class SnapshotController<T extends object, R = T> implements ReactiveController {\n readonly #host: ReactiveControllerHost;\n readonly #selector: Selector<T, R> | undefined;\n\n #state: State<T>;\n #cached: R | undefined;\n #unsubscribe = noop;\n\n /**\n * @label Without Selector\n * @param host - The host element that owns this controller.\n * @param state - The State container to subscribe to.\n */\n constructor(host: ReactiveControllerHost, state: State<T>);\n /**\n * @label With Selector\n * @param host - The host element that owns this controller.\n * @param state - The State container to subscribe to.\n * @param selector - Derives a value from the state.\n */\n constructor(host: ReactiveControllerHost, state: State<T>, selector: Selector<T, R>);\n constructor(host: ReactiveControllerHost, state: State<T>, selector?: Selector<T, R>) {\n this.#host = host;\n this.#state = state;\n this.#selector = selector;\n host.addController(this);\n }\n\n get value(): R {\n if (!this.#selector) {\n return this.#state.current as unknown as R;\n }\n\n this.#cached ??= this.#selector(this.#state.current);\n return this.#cached;\n }\n\n /** Switch to tracking a different state container. */\n track(state: State<T>): void {\n this.#state = state;\n this.#subscribe();\n }\n\n hostConnected(): void {\n this.#subscribe();\n }\n\n hostDisconnected(): void {\n this.#unsubscribe();\n this.#unsubscribe = noop;\n this.#cached = undefined;\n }\n\n #subscribe(): void {\n this.#unsubscribe();\n\n if (!this.#selector) {\n this.#unsubscribe = this.#state.subscribe(() => this.#host.requestUpdate());\n return;\n }\n\n const selector = this.#selector;\n this.#cached = selector(this.#state.current);\n\n this.#unsubscribe = this.#state.subscribe(() => {\n const next = selector(this.#state.current);\n if (!shallowEqual(this.#cached, next)) {\n this.#cached = next;\n this.#host.requestUpdate();\n }\n });\n }\n}\n\nexport namespace SnapshotController {\n export type Host = SnapshotControllerHost;\n}\n"],"mappings":";;;;;;;;;;;;;;AAmBA,IAAa,qBAAb,MAAuF;CACrF;CACA;CAEA;CACA;CACA,eAAe;CAef,YAAY,MAA8B,OAAiB,UAA2B;EACpF,KAAKA,QAAQ;EACb,KAAKE,SAAS;EACd,KAAKD,YAAY;EACjB,KAAK,cAAc,IAAI;CACzB;CAEA,IAAI,QAAW;EACb,IAAI,CAAC,KAAKA,WACR,OAAO,KAAKC,OAAO;EAGrB,KAAKC,YAAY,KAAKF,UAAU,KAAKC,OAAO,OAAO;EACnD,OAAO,KAAKC;CACd;;CAGA,MAAM,OAAuB;EAC3B,KAAKD,SAAS;EACd,KAAKE,WAAW;CAClB;CAEA,gBAAsB;EACpB,KAAKA,WAAW;CAClB;CAEA,mBAAyB;EACvB,KAAKC,aAAa;EAClB,KAAKA,eAAe;EACpB,KAAKF,UAAU,KAAA;CACjB;CAEA,aAAmB;EACjB,KAAKE,aAAa;EAElB,IAAI,CAAC,KAAKJ,WAAW;GACnB,KAAKI,eAAe,KAAKH,OAAO,gBAAgB,KAAKF,MAAM,cAAc,CAAC;GAC1E;EACF;EAEA,MAAM,WAAW,KAAKC;EACtB,KAAKE,UAAU,SAAS,KAAKD,OAAO,OAAO;EAE3C,KAAKG,eAAe,KAAKH,OAAO,gBAAgB;GAC9C,MAAM,OAAO,SAAS,KAAKA,OAAO,OAAO;GACzC,IAAI,CAAC,aAAa,KAAKC,SAAS,IAAI,GAAG;IACrC,KAAKA,UAAU;IACf,KAAKH,MAAM,cAAc;GAC3B;EACF,CAAC;CACH;AACF"}
+63
View File
@@ -0,0 +1,63 @@
import { SnapshotController } from "./snapshot-controller.js";
import { StoreAccessor } from "../store-accessor.js";
import { isNull, isUndefined } from "@videojs/utils/predicate";
//#region src/html/controllers/store-controller.ts
/**
* Access store state and actions.
*
* Without selector: Returns the store, does NOT subscribe to changes.
* With selector: Returns selected state, triggers update when selected state changes (shallowEqual).
*
* @example
* ```ts
* // Store access (no subscription) - access actions
* class Controls extends LitElement {
* #store = new StoreController(this, storeSource);
*
* handleClick() {
* this.#store.value.setVolume(0.5);
* }
* }
*
* // Selector-based subscription - re-renders when playback changes
* class PlayButton extends LitElement {
* #playback = new StoreController(this, storeSource, selectPlayback);
*
* render() {
* const playback = this.#playback.value;
* if (!playback) return nothing;
* return html`<button @click=${playback.toggle}>
* ${playback.paused ? 'Play' : 'Pause'}
* </button>`;
* }
* }
* ```
*/
var StoreController = class {
#host;
#selector;
#accessor;
#snapshot = null;
constructor(host, source, selector) {
this.#host = host;
this.#selector = selector;
this.#accessor = new StoreAccessor(host, source, (store) => this.#connect(store));
host.addController(this);
}
get value() {
const store = this.#accessor.value;
if (isNull(store)) throw new Error("Store not available");
if (isUndefined(this.#selector)) return store;
return this.#snapshot.value;
}
hostConnected() {}
#connect(store) {
if (isUndefined(this.#selector)) return;
if (!this.#snapshot) this.#snapshot = new SnapshotController(this.#host, store.$state, this.#selector);
else this.#snapshot.track(store.$state);
}
};
//#endregion
export { StoreController };
//# sourceMappingURL=store-controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"store-controller.js","names":["#host","#selector","#accessor","#connect","#snapshot"],"sources":["../../../../src/html/controllers/store-controller.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from '@videojs/element';\nimport { isNull, isUndefined } from '@videojs/utils/predicate';\nimport type { Selector } from '../../core/shallow-equal';\nimport type { AnyStore, InferStoreState } from '../../core/store';\nimport { StoreAccessor, type StoreSource } from '../store-accessor';\nimport { SnapshotController } from './snapshot-controller';\n\nexport type StoreControllerHost = ReactiveControllerHost & HTMLElement;\n\n/**\n * Access store state and actions.\n *\n * Without selector: Returns the store, does NOT subscribe to changes.\n * With selector: Returns selected state, triggers update when selected state changes (shallowEqual).\n *\n * @example\n * ```ts\n * // Store access (no subscription) - access actions\n * class Controls extends LitElement {\n * #store = new StoreController(this, storeSource);\n *\n * handleClick() {\n * this.#store.value.setVolume(0.5);\n * }\n * }\n *\n * // Selector-based subscription - re-renders when playback changes\n * class PlayButton extends LitElement {\n * #playback = new StoreController(this, storeSource, selectPlayback);\n *\n * render() {\n * const playback = this.#playback.value;\n * if (!playback) return nothing;\n * return html`<button @click=${playback.toggle}>\n * ${playback.paused ? 'Play' : 'Pause'}\n * </button>`;\n * }\n * }\n * ```\n */\nexport class StoreController<Store extends AnyStore, Result = Store> implements ReactiveController {\n readonly #host: StoreControllerHost;\n readonly #selector: Selector<InferStoreState<Store>, Result> | undefined;\n readonly #accessor: StoreAccessor<Store>;\n\n #snapshot: SnapshotController<object, Result> | null = null;\n\n /**\n * @label Without Selector\n * @param host - The host element that owns this controller.\n * @param source - Store instance or context to resolve the store from.\n */\n constructor(host: StoreControllerHost, source: StoreSource<Store>);\n /**\n * @label With Selector\n * @param host - The host element that owns this controller.\n * @param source - Store instance or context to resolve the store from.\n * @param selector - Derives a value from the store state.\n */\n constructor(\n host: StoreControllerHost,\n source: StoreSource<Store>,\n selector: Selector<InferStoreState<Store>, Result>\n );\n constructor(\n host: StoreControllerHost,\n source: StoreSource<Store>,\n selector?: Selector<InferStoreState<Store>, Result>\n ) {\n this.#host = host;\n this.#selector = selector;\n this.#accessor = new StoreAccessor(host, source, (store) => this.#connect(store));\n host.addController(this);\n }\n\n get value(): Result {\n const store = this.#accessor.value;\n\n if (isNull(store)) {\n throw new Error('Store not available');\n }\n\n // Without selector: return store\n if (isUndefined(this.#selector)) {\n return store as unknown as Result;\n }\n\n // With selector: delegate to snapshot controller\n return this.#snapshot!.value;\n }\n\n hostConnected(): void {\n // StoreAccessor + SnapshotController handle their own lifecycle.\n }\n\n #connect(store: Store): void {\n if (isUndefined(this.#selector)) return;\n\n if (!this.#snapshot) {\n this.#snapshot = new SnapshotController(this.#host, store.$state, this.#selector as Selector<object, Result>);\n } else {\n this.#snapshot.track(store.$state);\n }\n }\n}\n\nexport namespace StoreController {\n export type Host = StoreControllerHost;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAa,kBAAb,MAAmG;CACjG;CACA;CACA;CAEA,YAAuD;CAmBvD,YACE,MACA,QACA,UACA;EACA,KAAKA,QAAQ;EACb,KAAKC,YAAY;EACjB,KAAKC,YAAY,IAAI,cAAc,MAAM,SAAS,UAAU,KAAKC,SAAS,KAAK,CAAC;EAChF,KAAK,cAAc,IAAI;CACzB;CAEA,IAAI,QAAgB;EAClB,MAAM,QAAQ,KAAKD,UAAU;EAE7B,IAAI,OAAO,KAAK,GACd,MAAM,IAAI,MAAM,qBAAqB;EAIvC,IAAI,YAAY,KAAKD,SAAS,GAC5B,OAAO;EAIT,OAAO,KAAKG,UAAW;CACzB;CAEA,gBAAsB,CAEtB;CAEA,SAAS,OAAoB;EAC3B,IAAI,YAAY,KAAKH,SAAS,GAAG;EAEjC,IAAI,CAAC,KAAKG,WACR,KAAKA,YAAY,IAAI,mBAAmB,KAAKJ,OAAO,MAAM,QAAQ,KAAKC,SAAqC;OAE5G,KAAKG,UAAU,MAAM,MAAM,MAAM;CAErC;AACF"}
@@ -0,0 +1,64 @@
import { StoreAccessor } from "../store-accessor.js";
import { noop } from "@videojs/utils/function";
import { isNull } from "@videojs/utils/predicate";
//#region src/html/controllers/subscription-controller.ts
/**
* Resolves a store from context or direct source and manages subscription lifecycle.
*
* Combines store resolution (direct or context) with subscription management.
* Use as a building block for controllers that need store access with subscriptions.
*
* @example
* ```ts
* class MyController<Store extends AnyStore> {
* #ctrl: SubscriptionController<Store, Tasks>;
*
* constructor(host: Host, source: StoreSource<Store>) {
* this.#ctrl = new SubscriptionController(host, source, {
* subscribe: (store, onChange) => store.queue.subscribe(onChange),
* getValue: (store) => store.queue.tasks,
* });
* }
*
* get value() {
* return this.#ctrl.value;
* }
* }
* ```
*/
var SubscriptionController = class {
#host;
#config;
#accessor;
#unsubscribe = noop;
/**
* @param host - The host element that owns this controller.
* @param source - Store instance or context to resolve the store from.
* @param config - Subscription and value extraction configuration.
*/
constructor(host, source, config) {
this.#host = host;
this.#config = config;
this.#accessor = new StoreAccessor(host, source, (store) => this.#connect(store));
host.addController(this);
}
get value() {
const store = this.#accessor.value;
if (isNull(store)) throw new Error("Store not available");
return this.#config.getValue(store);
}
hostDisconnected() {
this.#unsubscribe();
this.#unsubscribe = noop;
}
#connect(store) {
this.#unsubscribe();
this.#unsubscribe = this.#config.subscribe(store, () => {
this.#host.requestUpdate();
});
}
};
//#endregion
export { SubscriptionController };
//# sourceMappingURL=subscription-controller.js.map
@@ -0,0 +1 @@
{"version":3,"file":"subscription-controller.js","names":["#host","#config","#accessor","#connect","#unsubscribe"],"sources":["../../../../src/html/controllers/subscription-controller.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from '@videojs/element';\nimport { noop } from '@videojs/utils/function';\nimport { isNull } from '@videojs/utils/predicate';\nimport type { AnyStore } from '../../core/store';\nimport { StoreAccessor, type StoreSource } from '../store-accessor';\n\nexport type SubscriptionControllerHost = ReactiveControllerHost & HTMLElement;\n\nexport interface SubscriptionControllerConfig<Store extends AnyStore, Value> {\n getValue: (store: Store) => Value;\n subscribe: (store: Store, onChange: () => void) => () => void;\n}\n\n/**\n * Resolves a store from context or direct source and manages subscription lifecycle.\n *\n * Combines store resolution (direct or context) with subscription management.\n * Use as a building block for controllers that need store access with subscriptions.\n *\n * @example\n * ```ts\n * class MyController<Store extends AnyStore> {\n * #ctrl: SubscriptionController<Store, Tasks>;\n *\n * constructor(host: Host, source: StoreSource<Store>) {\n * this.#ctrl = new SubscriptionController(host, source, {\n * subscribe: (store, onChange) => store.queue.subscribe(onChange),\n * getValue: (store) => store.queue.tasks,\n * });\n * }\n *\n * get value() {\n * return this.#ctrl.value;\n * }\n * }\n * ```\n */\nexport class SubscriptionController<Store extends AnyStore, Value> implements ReactiveController {\n readonly #host: SubscriptionControllerHost;\n readonly #config: SubscriptionControllerConfig<Store, Value>;\n readonly #accessor: StoreAccessor<Store>;\n\n #unsubscribe = noop;\n\n /**\n * @param host - The host element that owns this controller.\n * @param source - Store instance or context to resolve the store from.\n * @param config - Subscription and value extraction configuration.\n */\n constructor(\n host: SubscriptionControllerHost,\n source: StoreSource<Store>,\n config: SubscriptionControllerConfig<Store, Value>\n ) {\n this.#host = host;\n this.#config = config;\n this.#accessor = new StoreAccessor(host, source, (store) => this.#connect(store));\n\n host.addController(this);\n }\n\n get value(): Value {\n const store = this.#accessor.value;\n\n if (isNull(store)) {\n throw new Error('Store not available');\n }\n\n return this.#config.getValue(store);\n }\n\n hostDisconnected(): void {\n this.#unsubscribe();\n this.#unsubscribe = noop;\n }\n\n #connect(store: Store): void {\n this.#unsubscribe();\n this.#unsubscribe = this.#config.subscribe(store, () => {\n this.#host.requestUpdate();\n });\n }\n}\n\nexport namespace SubscriptionController {\n export type Host = SubscriptionControllerHost;\n export type Config<Store extends AnyStore, Value> = SubscriptionControllerConfig<Store, Value>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,IAAa,yBAAb,MAAiG;CAC/F;CACA;CACA;CAEA,eAAe;;;;;;CAOf,YACE,MACA,QACA,QACA;EACA,KAAKA,QAAQ;EACb,KAAKC,UAAU;EACf,KAAKC,YAAY,IAAI,cAAc,MAAM,SAAS,UAAU,KAAKC,SAAS,KAAK,CAAC;EAEhF,KAAK,cAAc,IAAI;CACzB;CAEA,IAAI,QAAe;EACjB,MAAM,QAAQ,KAAKD,UAAU;EAE7B,IAAI,OAAO,KAAK,GACd,MAAM,IAAI,MAAM,qBAAqB;EAGvC,OAAO,KAAKD,QAAQ,SAAS,KAAK;CACpC;CAEA,mBAAyB;EACvB,KAAKG,aAAa;EAClB,KAAKA,eAAe;CACtB;CAEA,SAAS,OAAoB;EAC3B,KAAKA,aAAa;EAClB,KAAKA,eAAe,KAAKH,QAAQ,UAAU,aAAa;GACtD,KAAKD,MAAM,cAAc;EAC3B,CAAC;CACH;AACF"}
+54
View File
@@ -0,0 +1,54 @@
import { isStore } from "../core/store.js";
import { noop } from "@videojs/utils/function";
import { ContextConsumer } from "@videojs/element/context";
//#region src/html/store-accessor.ts
/**
* Resolves a store from either a direct instance or context.
*
* When given a direct store, provides immediate access.
* When given a context, sets up a ContextConsumer to receive the store.
*
* @example Direct store
* ```ts
* const accessor = new StoreAccessor(host, store, (s) => console.log('available', s));
* accessor.value; // Store (immediately available)
* ```
*
* @example Context source
* ```ts
* const accessor = new StoreAccessor(host, context, (s) => console.log('available', s));
* accessor.value; // null until context provides store
* ```
*/
var StoreAccessor = class {
#onAvailable;
#consumer;
#directStore;
constructor(host, source, onAvailable) {
this.#onAvailable = onAvailable ?? noop;
if (isStore(source)) {
this.#directStore = source;
this.#consumer = null;
} else {
this.#directStore = null;
this.#consumer = new ContextConsumer(host, {
context: source,
callback: (store) => this.#onAvailable(store),
subscribe: false
});
}
host.addController(this);
}
/** Returns the store, or null if not yet available from context. */
get value() {
if (this.#consumer) return this.#consumer.value ?? null;
return this.#directStore;
}
hostConnected() {
if (this.#directStore) this.#onAvailable(this.#directStore);
}
};
//#endregion
export { StoreAccessor };
//# sourceMappingURL=store-accessor.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"store-accessor.js","names":["#onAvailable","#consumer","#directStore"],"sources":["../../../src/html/store-accessor.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from '@videojs/element';\nimport type { Context } from '@videojs/element/context';\nimport { ContextConsumer } from '@videojs/element/context';\nimport { noop } from '@videojs/utils/function';\nimport type { AnyStore } from '../core/store';\nimport { isStore } from '../core/store';\n\nexport type StoreSource<Store extends AnyStore> = Store | Context<unknown, Store>;\n\nexport type StoreAccessorHost = ReactiveControllerHost & HTMLElement;\n\n/**\n * Resolves a store from either a direct instance or context.\n *\n * When given a direct store, provides immediate access.\n * When given a context, sets up a ContextConsumer to receive the store.\n *\n * @example Direct store\n * ```ts\n * const accessor = new StoreAccessor(host, store, (s) => console.log('available', s));\n * accessor.value; // Store (immediately available)\n * ```\n *\n * @example Context source\n * ```ts\n * const accessor = new StoreAccessor(host, context, (s) => console.log('available', s));\n * accessor.value; // null until context provides store\n * ```\n */\nexport class StoreAccessor<Store extends AnyStore> implements ReactiveController {\n readonly #onAvailable: (store: Store) => void;\n readonly #consumer: ContextConsumer<Context<unknown, Store>, StoreAccessorHost> | null;\n\n #directStore: Store | null;\n\n constructor(host: StoreAccessorHost, source: StoreSource<Store>, onAvailable?: (store: Store) => void) {\n this.#onAvailable = onAvailable ?? noop;\n\n // Check if source is a store (object with subscribe) or context (symbol/string)\n if (isStore(source)) {\n this.#directStore = source as Store;\n this.#consumer = null;\n } else {\n this.#directStore = null;\n this.#consumer = new ContextConsumer(host, {\n context: source,\n callback: (store) => this.#onAvailable(store),\n subscribe: false,\n });\n }\n\n host.addController(this);\n }\n\n /** Returns the store, or null if not yet available from context. */\n get value(): Store | null {\n if (this.#consumer) {\n return this.#consumer.value ?? null;\n }\n\n return this.#directStore;\n }\n\n hostConnected(): void {\n // For direct store, trigger onAvailable on connect/reconnect\n // Context consumer handles its own reconnect via callback\n if (this.#directStore) {\n this.#onAvailable(this.#directStore);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,gBAAb,MAAiF;CAC/E;CACA;CAEA;CAEA,YAAY,MAAyB,QAA4B,aAAsC;EACrG,KAAKA,eAAe,eAAe;EAGnC,IAAI,QAAQ,MAAM,GAAG;GACnB,KAAKE,eAAe;GACpB,KAAKD,YAAY;EACnB,OAAO;GACL,KAAKC,eAAe;GACpB,KAAKD,YAAY,IAAI,gBAAgB,MAAM;IACzC,SAAS;IACT,WAAW,UAAU,KAAKD,aAAa,KAAK;IAC5C,WAAW;GACb,CAAC;EACH;EAEA,KAAK,cAAc,IAAI;CACzB;;CAGA,IAAI,QAAsB;EACxB,IAAI,KAAKC,WACP,OAAO,KAAKA,UAAU,SAAS;EAGjC,OAAO,KAAKC;CACd;CAEA,gBAAsB;EAGpB,IAAI,KAAKA,cACP,KAAKF,aAAa,KAAKE,YAAY;CAEvC;AACF"}
+9
View File
@@ -0,0 +1,9 @@
import { AbortControllerRegistry } from "./core/abort-controller-registry.js";
import { combine } from "./core/combine.js";
import { StoreError, isStoreError, throwDestroyedError, throwNoTargetError } from "./core/errors.js";
import { createSelector } from "./core/selector.js";
import { shallowEqual } from "./core/shallow-equal.js";
import { defineSlice } from "./core/slice.js";
import { createState, flush, isState } from "./core/state.js";
import { createStore, isStore } from "./core/store.js";
export { AbortControllerRegistry, StoreError, combine, createSelector, createState, createStore, defineSlice, flush, isState, isStore, isStoreError, shallowEqual, throwDestroyedError, throwNoTargetError };
+4
View File
@@ -0,0 +1,4 @@
import { useSelector } from "./react/hooks/use-selector.js";
import { useSnapshot } from "./react/hooks/use-snapshot.js";
import { useStore } from "./react/hooks/use-store.js";
export { useSelector, useSnapshot, useStore };
+27
View File
@@ -0,0 +1,27 @@
import { shallowEqual } from "../../core/shallow-equal.js";
import { useRef, useSyncExternalStore } from "react";
//#region src/react/hooks/use-selector.ts
/**
* Subscribe to derived state with customizable equality check.
*
* Low-level hook used internally by `useStore` and `useSnapshot`.
*
* @param subscribe - Subscribe function that returns an unsubscribe callback.
* @param getSnapshot - Returns the current snapshot value.
* @param selector - Derives a value from the snapshot.
* @param isEqual - Custom equality function. Defaults to `shallowEqual`.
*/
function useSelector(subscribe, getSnapshot, selector, isEqual = shallowEqual) {
const cache = useRef(void 0);
const getSelectedSnapshot = () => {
const next = selector(getSnapshot());
if (cache.current !== void 0 && isEqual(cache.current, next)) return cache.current;
cache.current = next;
return next;
};
return useSyncExternalStore(subscribe, getSelectedSnapshot, getSelectedSnapshot);
}
//#endregion
export { useSelector };
//# sourceMappingURL=use-selector.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"use-selector.js","names":[],"sources":["../../../../src/react/hooks/use-selector.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react';\nimport { type Comparator, type Selector, shallowEqual } from '../../core/shallow-equal';\n\nexport type { Comparator, Selector };\n\n/**\n * Subscribe to derived state with customizable equality check.\n *\n * Low-level hook used internally by `useStore` and `useSnapshot`.\n *\n * @param subscribe - Subscribe function that returns an unsubscribe callback.\n * @param getSnapshot - Returns the current snapshot value.\n * @param selector - Derives a value from the snapshot.\n * @param isEqual - Custom equality function. Defaults to `shallowEqual`.\n */\nexport function useSelector<S, R>(\n subscribe: (cb: () => void) => () => void,\n getSnapshot: () => S,\n selector: Selector<S, R>,\n isEqual: Comparator<R> = shallowEqual\n): R {\n const cache = useRef<R | undefined>(undefined);\n\n const getSelectedSnapshot = () => {\n const next = selector(getSnapshot());\n\n if (cache.current !== undefined && isEqual(cache.current, next)) {\n return cache.current;\n }\n\n cache.current = next;\n\n return next;\n };\n\n return useSyncExternalStore(subscribe, getSelectedSnapshot, getSelectedSnapshot);\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,SAAgB,YACd,WACA,aACA,UACA,UAAyB,cACtB;CACH,MAAM,QAAQ,OAAsB,KAAA,CAAS;CAE7C,MAAM,4BAA4B;EAChC,MAAM,OAAO,SAAS,YAAY,CAAC;EAEnC,IAAI,MAAM,YAAY,KAAA,KAAa,QAAQ,MAAM,SAAS,IAAI,GAC5D,OAAO,MAAM;EAGf,MAAM,UAAU;EAEhB,OAAO;CACT;CAEA,OAAO,qBAAqB,WAAW,qBAAqB,mBAAmB;AACjF"}
+10
View File
@@ -0,0 +1,10 @@
import { useSelector } from "./use-selector.js";
import { identity } from "@videojs/utils/function";
//#region src/react/hooks/use-snapshot.ts
function useSnapshot(state, selector, isEqual) {
return useSelector((cb) => state.subscribe(cb), () => state.current, selector ?? identity, isEqual);
}
//#endregion
export { useSnapshot };
//# sourceMappingURL=use-snapshot.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"use-snapshot.js","names":[],"sources":["../../../../src/react/hooks/use-snapshot.ts"],"sourcesContent":["import { identity } from '@videojs/utils/function';\nimport type { State } from '../../core/state';\nimport { type Comparator, type Selector, useSelector } from './use-selector';\n\n/**\n * Subscribe to a State container's current value.\n *\n * @param state - The State container to subscribe to.\n * @param selector - Derives a value from state.\n * @param isEqual - Custom equality function. Defaults to `shallowEqual`.\n */\n/** @label Without Selector */\nexport function useSnapshot<T extends object>(state: State<T>): T;\n\n/**\n * Select a value from state. Re-renders when the selected value changes.\n *\n * @label With Selector\n * @param selector - Derives a value from state.\n * @param isEqual - Custom equality function. Defaults to `shallowEqual`.\n */\nexport function useSnapshot<T extends object, R>(state: State<T>, selector: Selector<T, R>, isEqual?: Comparator<R>): R;\n\nexport function useSnapshot(state: State<object>, selector?: Selector<any, any>, isEqual?: Comparator<any>) {\n return useSelector(\n (cb) => state.subscribe(cb),\n () => state.current,\n selector ?? identity,\n isEqual\n );\n}\n"],"mappings":";;;AAuBA,SAAgB,YAAY,OAAsB,UAA+B,SAA2B;CAC1G,OAAO,aACJ,OAAO,MAAM,UAAU,EAAE,SACpB,MAAM,SACZ,YAAY,UACZ,OACF;AACF"}
+11
View File
@@ -0,0 +1,11 @@
import { useSelector } from "./use-selector.js";
import { identity, noop } from "@videojs/utils/function";
//#region src/react/hooks/use-store.ts
const noopSubscribe = () => noop;
function useStore(store, selector, isEqual) {
return useSelector(selector ? (cb) => store.subscribe(cb) : noopSubscribe, selector ? () => store.state : () => store, selector ?? identity, isEqual);
}
//#endregion
export { useStore };
//# sourceMappingURL=use-store.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"use-store.js","names":[],"sources":["../../../../src/react/hooks/use-store.ts"],"sourcesContent":["import { identity, noop } from '@videojs/utils/function';\nimport type { AnyStore, InferStoreState } from '../../core/store';\nimport { type Comparator, type Selector, useSelector } from './use-selector';\n\nconst noopSubscribe = () => noop;\n\n/**\n * Access store state and actions.\n *\n * Without selector: Returns the store, does NOT subscribe to changes.\n * With selector: Returns selected state, re-renders when selected state changes (shallowEqual).\n *\n * @example\n * ```tsx\n * // Store access (no subscription) - access actions, subscribe without re-render\n * function Controls() {\n * const { setVolume } = useStore(store);\n * }\n *\n * // Selector-based subscription - re-renders when paused changes\n * function PlayButton() {\n * const paused = useStore(store, (s) => s.paused);\n * return <button>{paused ? 'Play' : 'Pause'}</button>;\n * }\n * ```\n */\n/** @label Without Selector */\nexport function useStore<S extends AnyStore>(store: S): S;\n\n/**\n * Select a value from the store. Re-renders when the selected value changes (shallowEqual).\n *\n * @label With Selector\n * @param selector - Derives a value from the store state.\n * @param isEqual - Custom equality function. Defaults to `shallowEqual`.\n */\nexport function useStore<S extends AnyStore, R>(\n store: S,\n selector: Selector<InferStoreState<S>, R>,\n isEqual?: Comparator<R>\n): R;\n\nexport function useStore(store: AnyStore, selector?: Selector<any, any>, isEqual?: Comparator<any>) {\n const subscribe = selector ? (cb: () => void) => store.subscribe(cb) : noopSubscribe,\n getSnapshot = selector ? () => store.state : () => store;\n\n return useSelector(subscribe, getSnapshot, selector ?? identity, isEqual);\n}\n\nexport namespace useStore {\n export type Result<S extends AnyStore> = S;\n}\n"],"mappings":";;;AAIA,MAAM,sBAAsB;AAsC5B,SAAgB,SAAS,OAAiB,UAA+B,SAA2B;CAIlG,OAAO,YAHW,YAAY,OAAmB,MAAM,UAAU,EAAE,IAAI,eACvD,iBAAiB,MAAM,cAAc,OAEV,YAAY,UAAU,OAAO;AAC1E"}