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
+382
View File
@@ -0,0 +1,382 @@
# @videojs/store
[![package-badge]][package]
> **⚠️ Beta** Close to stable. Experimental adoption in real projects.
A reactive store for managing state owned by external systems. Built for media players, streaming libraries, and real-time systems where you don't own the state.
```bash
npm install @videojs/store
```
## Why?
[Traditional state management](#how-its-different) assumes you own the state. But when working with a `<video>` element, Web Sockets, streaming libraries, and real-time systems, the external system is the authority. You observe it, send requests to it, and react to its changes.
`@videojs/store` embraces this model:
- **Read Path**: Observe external state, sync to reactive store
- **Write Path**: Send requests to the target, handle failures
```ts
import { createStore, defineSlice } from '@videojs/store';
const volumeSlice = defineSlice<HTMLMediaElement>()({
state: () => ({ volume: 1 }),
attach: ({ target, set, signal }) => {
const sync = () => set({ volume: target.volume });
target.addEventListener('volumechange', sync, { signal });
},
});
const store = createStore<HTMLMediaElement>()(volumeSlice);
store.attach(videoElement);
// State is flat on the store
const { volume } = store;
```
## Core Concepts
### Target
The target is a reference to the external system. Slices read from and write to it.
```ts
const videoElement = document.querySelector('video');
store.attach(videoElement);
```
### Slices
A slice defines state, how to sync it from the target, and actions to modify the target.
```ts
import { defineSlice } from '@videojs/store';
import { listen } from '@videojs/utils/dom';
const volumeSlice = defineSlice<HTMLMediaElement>()({
state: ({ target }) => ({
volume: 1,
muted: false,
// Sync - use target() directly
setVolume(value: number) {
const media = target();
media.volume = Math.max(0, Math.min(1, value));
},
// Action - directly updates target
toggleMuted() {
const media = target();
media.muted = !media.muted;
return media.muted;
},
}),
attach({ target, signal, set }) {
const sync = () => set({ volume: target.volume, muted: target.muted });
sync();
listen(target, 'volumechange', sync, { signal });
},
});
```
### Slice Type Inference
State types are fully inferred from the slice config:
```ts
import type { InferSliceState } from '@videojs/store';
const volumeSlice = defineSlice<HTMLMediaElement>()({
state: () => ({ volume: 1, muted: false, /* actions */ }),
// ...
});
// Infer types from the slice
type VolumeState = InferSliceState<typeof volumeSlice>;
// { volume: number; muted: boolean; setVolume: ...; toggleMuted: ... }
```
### Combining Slices
Use `combine` to merge multiple slices into one:
```ts
import { combine, createStore, defineSlice } from '@videojs/store';
const volumeSlice = defineSlice<HTMLMediaElement>()({ /* ... */ });
const playbackSlice = defineSlice<HTMLMediaElement>()({ /* ... */ });
// Combine into a single slice
const mediaSlice = combine(volumeSlice, playbackSlice);
const store = createStore<HTMLMediaElement>()(mediaSlice);
```
Behavior:
- State factories are called in order, results merged (last wins on conflict)
- All attach handlers run; errors are caught and reported via `reportError`
- Use `UnionSliceState<Slices>` for combined state type inference
```ts
import type { UnionSliceState } from '@videojs/store';
const slices = [volumeSlice, playbackSlice] as const;
type MediaState = UnionSliceState<typeof slices>;
```
### Actions
Actions modify the target. You can call `target()` to access the attached target.
```ts
state: ({ target }) => ({
volume: 1,
// Action
setVolume(volume: number) {
const media = target();
media.volume = volume;
return media.volume;
},
// Fire-and-forget
logVolume() {
console.log('Current volume:', target().volume);
},
}),
```
## Store
The store connects a slice to a target.
```ts
// Simple
const store = createStore<HTMLMediaElement>()(volumeSlice);
// With combined slices and options
const store = createStore<HTMLMediaElement>()(
combine(volumeSlice, playbackSlice),
{
onSetup: ({ store, signal }) => {
// Called when store is created
},
onAttach: ({ store, target, signal }) => {
// Called when target is attached
},
onError: ({ error, store }) => {
// Global error handler
},
}
);
```
### Type Inference
```ts
import type { InferStoreState, InferStoreTarget } from '@videojs/store';
const store = createStore<HTMLMediaElement>()(volumeSlice);
type State = InferStoreState<typeof store>;
type Target = InferStoreTarget<typeof store>;
```
### Attaching a Target
```ts
const detach = store.attach(videoElement);
// State syncs from target (flat access)
const { paused, volume } = store;
// Actions go to target (flat access)
store.play();
store.setVolume(0.5);
// Detach when done
detach();
```
### Destroying a Store
Clean up when the store is no longer needed:
```ts
// Detaches target, aborts signals, cleans up
store.destroy();
```
### Subscribing to State
State is reactive—subscribe to be notified when any property changes:
```ts
const unsubscribe = store.subscribe(() => {
const { volume } = store;
console.log('State changed:', volume);
});
```
Mutations are auto-batched—multiple changes in the same tick trigger only one notification.
## Cancellation Signals
Use `signals` to manage cancellation for async operations. The store provides an `AbortControllerRegistry` instance that tracks the attach lifecycle and supports keyed cancellation for superseding work.
```ts
state: ({ target, signals }) => ({
// Supersede pattern: new seek cancels previous seek
async seek(time: number) {
const signal = signals.supersede(signalKeys.seek);
// ...
},
// Cancel all pending operations (e.g., when loading new source)
loadSource(src: string) {
signals.clear();
// ...
},
}),
```
**API:**
| Method | Description |
|--------|-------------|
| `signals.base` | Attach-scoped signal. Aborts on detach or reattach. |
| `signals.supersede(key)` | Returns signal that aborts when same key is superseded or base aborts. |
| `signals.clear()` | Aborts all keyed signals, leaving base intact. |
Define shared keys for cross-slice coordination:
```ts
export const signalKeys = {
seek: Symbol.for('@videojs/seek'),
} as const;
```
## Error Handling
Handle errors locally via `try/catch`, or globally via `onError`:
```ts
import { isStoreError } from '@videojs/store';
// Global error handling
const store = createStore<HTMLMediaElement>()(volumeSlice, {
onError: ({ error, store }) => {
console.error('Store error:', error);
},
});
// Local error handling
try {
await store.play();
} catch (error) {
if (isStoreError(error)) {
switch (error.code) {
case 'NO_TARGET':
// No media element attached
break;
default:
console.error(`[${error.code}]`, error.message);
}
}
}
```
All store errors include a `code` for programmatic handling:
| Code | Description |
| ------------ | ---------------------------- |
| `DESTROYED` | Store destroyed |
| `NO_TARGET` | No target attached |
## State Primitives
The store uses explicit state containers internally. You can use these primitives directly:
```ts
import { createState, flush, isState } from '@videojs/store';
// Create state container
const state = createState({ volume: 1, muted: false });
// Read via .current
const { volume } = state.current; // 1
// Mutate via patch() - changes are auto-batched
state.patch({ volume: 0.5 });
state.patch({ volume: 0.5, muted: true });
// Only ONE notification fires (after microtask)
// Subscribe to changes
state.subscribe(() => {
const { volume } = state.current;
console.log('Changed:', volume);
});
// Optional abort signal for cleanup
const controller = new AbortController();
state.subscribe(() => {}, { signal: controller.signal });
controller.abort();
// Check if value is state
isState(state); // true
// Force immediate notification (mainly for tests)
flush();
```
## How It's Different
| | Redux/Zustand | React Query | @videojs/store |
| ----------------- | ---------------- | --------------------- | -------------------------- |
| **Authority** | You own state | Server owns state | External system owns state |
| **Mutations** | Sync reducers | Async server requests | Actions to target |
| **State source** | Internal store | HTTP cache | Synced from target |
| **Subscriptions** | To store changes | To query cache | To target events |
| **Use case** | App state | Server data | Media, WebSocket, hardware |
**Redux/Zustand**: Great for state you control. But when a `<video>` element is the source of truth, you end up fighting the pattern—syncing external state into the store, handling race conditions between your state and the element's actual state.
**React Query**: Perfect for server state with request/response. But media elements aren't request/response—they're live, event-driven systems with their own lifecycle.
**@videojs/store**: Built for external authority. The target is the source of truth. You observe it, request changes, and react to its events.
```ts
// Redux approach - fighting the abstraction
dispatch(play());
// Hope the video actually plays...
// Manually sync video.paused back to store...
// Handle race conditions...
// @videojs/store - working with the abstraction
store.play(); // Actions call into the target
const { paused } = store; // Always reflects video.paused
```
## Community
If you need help with anything related to Video.js v10, or if you'd like to casually chat with other
members:
- [Join Discord Server][discord]
- [See GitHub Discussions][gh-discussions]
## License
[Apache-2.0](./LICENSE)
[package]: https://www.npmjs.com/package/@videojs/store
[package-badge]: https://img.shields.io/npm/v/@videojs/store?label=@videojs/store
[discord]: https://discord.gg/JBqHh485uF
[gh-discussions]: https://github.com/videojs/v10/discussions
+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"}
+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
+6
View File
@@ -0,0 +1,6 @@
import { SnapshotController, SnapshotControllerHost } from "./html/controllers/snapshot-controller.js";
import { StoreAccessor, StoreAccessorHost, StoreSource } from "./html/store-accessor.js";
import { StoreController, StoreControllerHost } from "./html/controllers/store-controller.js";
import { SubscriptionController, SubscriptionControllerHost } from "./html/controllers/subscription-controller.js";
import "./html/controllers/index.js";
export { SnapshotController, type SnapshotControllerHost, StoreAccessor, StoreAccessorHost, StoreController, type StoreControllerHost, StoreSource, SubscriptionController, type SubscriptionControllerHost };
+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 };
+4
View File
@@ -0,0 +1,4 @@
import { SnapshotController, SnapshotControllerHost } from "./snapshot-controller.js";
import { StoreController, StoreControllerHost } from "./store-controller.js";
import { SubscriptionController, SubscriptionControllerHost } from "./subscription-controller.js";
export { SnapshotController, type SnapshotControllerHost, StoreController, type StoreControllerHost, SubscriptionController, type SubscriptionControllerHost };
+43
View File
@@ -0,0 +1,43 @@
import { State } from "../../core/state.js";
import { Selector } from "../../core/shallow-equal.js";
import { ReactiveController, ReactiveControllerHost } from "@videojs/element";
//#region src/html/controllers/snapshot-controller.d.ts
type SnapshotControllerHost = ReactiveControllerHost & HTMLElement;
/**
* 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);
* ```
*/
declare class SnapshotController<T extends object, R = T> implements ReactiveController {
#private;
/**
* @label Without Selector
* @param host - The host element that owns this controller.
* @param state - The State container to subscribe to.
*/
constructor(host: ReactiveControllerHost, state: State<T>);
/**
* @label With Selector
* @param host - The host element that owns this controller.
* @param state - The State container to subscribe to.
* @param selector - Derives a value from the state.
*/
constructor(host: ReactiveControllerHost, state: State<T>, selector: Selector<T, R>);
get value(): R;
/** Switch to tracking a different state container. */
track(state: State<T>): void;
hostConnected(): void;
hostDisconnected(): void;
}
declare namespace SnapshotController {
type Host = SnapshotControllerHost;
}
//#endregion
export { SnapshotController, SnapshotControllerHost };
//# sourceMappingURL=snapshot-controller.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"snapshot-controller.d.ts","names":[],"sources":["../../../../src/html/controllers/snapshot-controller.ts"],"mappings":";;;;KAMY,yBAAyB,yBAAyB;;;;;;;;;;;;cAajD,mBAAmB,kBAAkB,IAAI,cAAc;;;;;;;EAalE,YAAY,MAAM,wBAAwB,OAAO,MAAM;;;;;;;EAOvD,YAAY,MAAM,wBAAwB,OAAO,MAAM,IAAI,UAAU,SAAS,GAAG;MAQ7E,SAAS;;EAUb,MAAM,OAAO,MAAM;EAKnB;EAIA;;kBA2Be;OACH,OAAO"}
+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
+1
View File
@@ -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"}
+61
View File
@@ -0,0 +1,61 @@
import { AnyStore, InferStoreState } from "../../core/store.js";
import { Selector } from "../../core/shallow-equal.js";
import { StoreSource } from "../store-accessor.js";
import { ReactiveController, ReactiveControllerHost } from "@videojs/element";
//#region src/html/controllers/store-controller.d.ts
type StoreControllerHost = ReactiveControllerHost & HTMLElement;
/**
* 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>`;
* }
* }
* ```
*/
declare class StoreController<Store extends AnyStore, Result = Store> implements ReactiveController {
#private;
/**
* @label Without Selector
* @param host - The host element that owns this controller.
* @param source - Store instance or context to resolve the store from.
*/
constructor(host: StoreControllerHost, source: StoreSource<Store>);
/**
* @label With Selector
* @param host - The host element that owns this controller.
* @param source - Store instance or context to resolve the store from.
* @param selector - Derives a value from the store state.
*/
constructor(host: StoreControllerHost, source: StoreSource<Store>, selector: Selector<InferStoreState<Store>, Result>);
get value(): Result;
hostConnected(): void;
}
declare namespace StoreController {
type Host = StoreControllerHost;
}
//#endregion
export { StoreController, StoreControllerHost };
//# sourceMappingURL=store-controller.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"store-controller.d.ts","names":[],"sources":["../../../../src/html/controllers/store-controller.ts"],"mappings":";;;;;KAOY,sBAAsB,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiC9C,gBAAgB,cAAc,UAAU,SAAS,kBAAkB;;;;;;;EAY9E,YAAY,MAAM,qBAAqB,QAAQ,YAAY;;;;;;;EAO3D,YACE,MAAM,qBACN,QAAQ,YAAY,QACpB,UAAU,SAAS,gBAAgB,QAAQ;MAazC,SAAS;EAgBb;;kBAee;OACH,OAAO"}
+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"}
+51
View File
@@ -0,0 +1,51 @@
import { AnyStore } from "../../core/store.js";
import { StoreSource } from "../store-accessor.js";
import { ReactiveController, ReactiveControllerHost } from "@videojs/element";
//#region src/html/controllers/subscription-controller.d.ts
type SubscriptionControllerHost = ReactiveControllerHost & HTMLElement;
interface SubscriptionControllerConfig<Store extends AnyStore, Value> {
getValue: (store: Store) => Value;
subscribe: (store: Store, onChange: () => void) => () => void;
}
/**
* 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;
* }
* }
* ```
*/
declare class SubscriptionController<Store extends AnyStore, Value> implements ReactiveController {
#private;
/**
* @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: SubscriptionControllerHost, source: StoreSource<Store>, config: SubscriptionControllerConfig<Store, Value>);
get value(): Value;
hostDisconnected(): void;
}
declare namespace SubscriptionController {
type Host = SubscriptionControllerHost;
type Config<Store extends AnyStore, Value> = SubscriptionControllerConfig<Store, Value>;
}
//#endregion
export { SubscriptionController, SubscriptionControllerConfig, SubscriptionControllerHost };
//# sourceMappingURL=subscription-controller.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"subscription-controller.d.ts","names":[],"sources":["../../../../src/html/controllers/subscription-controller.ts"],"mappings":";;;;KAMY,6BAA6B,yBAAyB;UAEjD,6BAA6B,cAAc,UAAU;EACpE,WAAW,OAAO,UAAU;EAC5B,YAAY,OAAO,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;cA2Bf,uBAAuB,cAAc,UAAU,kBAAkB;;;;;;;EAY5E,YACE,MAAM,4BACN,QAAQ,YAAY,QACpB,QAAQ,6BAA6B,OAAO;MAS1C,SAAS;EAUb;;kBAae;OACH,OAAO;OACP,OAAO,cAAc,UAAU,SAAS,6BAA6B,OAAO"}
+64
View File
@@ -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"}
+34
View File
@@ -0,0 +1,34 @@
import { AnyStore } from "../core/store.js";
import { Context } from "@videojs/element/context";
import { ReactiveController, ReactiveControllerHost } from "@videojs/element";
//#region src/html/store-accessor.d.ts
type StoreSource<Store extends AnyStore> = Store | Context<unknown, Store>;
type StoreAccessorHost = ReactiveControllerHost & HTMLElement;
/**
* 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
* ```
*/
declare class StoreAccessor<Store extends AnyStore> implements ReactiveController {
#private;
constructor(host: StoreAccessorHost, source: StoreSource<Store>, onAvailable?: (store: Store) => void);
/** Returns the store, or null if not yet available from context. */
get value(): Store | null;
hostConnected(): void;
}
//#endregion
export { StoreAccessor, StoreAccessorHost, StoreSource };
//# sourceMappingURL=store-accessor.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"store-accessor.d.ts","names":[],"sources":["../../../src/html/store-accessor.ts"],"mappings":";;;;KAOY,YAAY,cAAc,YAAY,QAAQ,iBAAiB;KAE/D,oBAAoB,yBAAyB;;;;;;;;;;;;;;;;;;;cAoB5C,cAAc,cAAc,qBAAqB;;EAM5D,YAAY,MAAM,mBAAmB,QAAQ,YAAY,QAAQ,eAAe,OAAO;;MAoBnF,SAAS;EAQb"}
+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"}
+10
View File
@@ -0,0 +1,10 @@
import { AbortControllerRegistry, SignalKey } from "./core/abort-controller-registry.js";
import { State, StateChange, SubscribeOptions, UnknownState, WritableState, createState, flush, isState } from "./core/state.js";
import { AnySlice, Attach, AttachContext, AttachStore, InferSliceState, InferSliceTarget, Slice, SliceConfig, SliceFactory, StateContext, UnionSliceState, defineSlice } from "./core/slice.js";
import { combine } from "./core/combine.js";
import { AnyStore, BaseStore, InferStoreState, InferStoreTarget, Store, StoreOptions, UnknownStore, createStore, isStore } from "./core/store.js";
import { StoreAttachContext, StoreCallbacks, StoreErrorContext, StoreSetupContext } from "./core/config.js";
import { StoreError, StoreErrorCode, StoreErrorOptions, isStoreError, throwDestroyedError, throwNoTargetError } from "./core/errors.js";
import { Comparator, Selector, shallowEqual } from "./core/shallow-equal.js";
import { createSelector } from "./core/selector.js";
export { AbortControllerRegistry, AnySlice, AnyStore, Attach, AttachContext, AttachStore, BaseStore, type Comparator, InferSliceState, InferSliceTarget, InferStoreState, InferStoreTarget, type Selector, SignalKey, Slice, SliceConfig, SliceFactory, State, StateChange, StateContext, Store, StoreAttachContext, StoreCallbacks, StoreError, StoreErrorCode, StoreErrorContext, StoreErrorOptions, StoreOptions, StoreSetupContext, SubscribeOptions, UnionSliceState, UnknownState, UnknownStore, WritableState, combine, createSelector, createState, createStore, defineSlice, flush, isState, isStore, isStoreError, shallowEqual, throwDestroyedError, throwNoTargetError };
+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 };
+5
View File
@@ -0,0 +1,5 @@
import { Comparator, Selector } from "./core/shallow-equal.js";
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 { type Comparator, type Selector, useSelector, useSnapshot, useStore };
+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 };
+16
View File
@@ -0,0 +1,16 @@
import { Comparator, Selector } from "../../core/shallow-equal.js";
//#region src/react/hooks/use-selector.d.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`.
*/
declare function useSelector<S, R>(subscribe: (cb: () => void) => () => void, getSnapshot: () => S, selector: Selector<S, R>, isEqual?: Comparator<R>): R;
//#endregion
export { type Comparator, type Selector, useSelector };
//# sourceMappingURL=use-selector.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"use-selector.d.ts","names":[],"sources":["../../../../src/react/hooks/use-selector.ts"],"mappings":";;;;;;;;;;;;iBAegB,YAAY,GAAG,GAC7B,YAAY,+BACZ,mBAAmB,GACnB,UAAU,SAAS,GAAG,IACtB,UAAS,WAAW,KACnB"}
+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"}
+24
View File
@@ -0,0 +1,24 @@
import { State } from "../../core/state.js";
import { Comparator, Selector } from "../../core/shallow-equal.js";
import "./use-selector.js";
//#region src/react/hooks/use-snapshot.d.ts
/**
* Subscribe to a State container's current value.
*
* @param state - The State container to subscribe to.
* @param selector - Derives a value from state.
* @param isEqual - Custom equality function. Defaults to `shallowEqual`.
*/
/** @label Without Selector */
declare function useSnapshot<T extends object>(state: State<T>): T;
/**
* Select a value from state. Re-renders when the selected value changes.
*
* @label With Selector
* @param selector - Derives a value from state.
* @param isEqual - Custom equality function. Defaults to `shallowEqual`.
*/
declare function useSnapshot<T extends object, R>(state: State<T>, selector: Selector<T, R>, isEqual?: Comparator<R>): R;
//#endregion
export { useSnapshot };
//# sourceMappingURL=use-snapshot.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"use-snapshot.d.ts","names":[],"sources":["../../../../src/react/hooks/use-snapshot.ts"],"mappings":";;;;;;;;;;;;iBAYgB,YAAY,kBAAkB,OAAO,MAAM,KAAK;;;;;;;;iBAShD,YAAY,kBAAkB,GAAG,OAAO,MAAM,IAAI,UAAU,SAAS,GAAG,IAAI,UAAU,WAAW,KAAK"}
+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"}
+40
View File
@@ -0,0 +1,40 @@
import { AnyStore, InferStoreState } from "../../core/store.js";
import { Comparator, Selector } from "../../core/shallow-equal.js";
import "./use-selector.js";
//#region src/react/hooks/use-store.d.ts
/**
* Access store state and actions.
*
* Without selector: Returns the store, does NOT subscribe to changes.
* With selector: Returns selected state, re-renders when selected state changes (shallowEqual).
*
* @example
* ```tsx
* // Store access (no subscription) - access actions, subscribe without re-render
* function Controls() {
* const { setVolume } = useStore(store);
* }
*
* // Selector-based subscription - re-renders when paused changes
* function PlayButton() {
* const paused = useStore(store, (s) => s.paused);
* return <button>{paused ? 'Play' : 'Pause'}</button>;
* }
* ```
*/
/** @label Without Selector */
declare function useStore<S extends AnyStore>(store: S): S;
/**
* Select a value from the store. Re-renders when the selected value changes (shallowEqual).
*
* @label With Selector
* @param selector - Derives a value from the store state.
* @param isEqual - Custom equality function. Defaults to `shallowEqual`.
*/
declare function useStore<S extends AnyStore, R>(store: S, selector: Selector<InferStoreState<S>, R>, isEqual?: Comparator<R>): R;
declare namespace useStore {
type Result<S extends AnyStore> = S;
}
//#endregion
export { useStore };
//# sourceMappingURL=use-store.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"use-store.d.ts","names":[],"sources":["../../../../src/react/hooks/use-store.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;iBA2BgB,SAAS,UAAU,UAAU,OAAO,IAAI;;;;;;;;iBASxC,SAAS,UAAU,UAAU,GAC3C,OAAO,GACP,UAAU,SAAS,gBAAgB,IAAI,IACvC,UAAU,WAAW,KACpB;kBASc;OACH,OAAO,UAAU,YAAY"}
+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

Some files were not shown because too many files have changed in this diff Show More