mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
build(store): build26 from 45504a2b
This commit is contained in:
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
Vendored
+26
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+24
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+39
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { shallowEqual } from "@videojs/utils/object";
|
||||
export { shallowEqual };
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
//#region src/core/slice.ts
|
||||
function defineSlice() {
|
||||
return (config) => config;
|
||||
}
|
||||
//#endregion
|
||||
export { defineSlice };
|
||||
|
||||
//# sourceMappingURL=slice.js.map
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+74
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+122
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+5
@@ -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 };
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
Vendored
+54
@@ -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
@@ -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"}
|
||||
Vendored
+9
@@ -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 };
|
||||
Vendored
+4
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
Vendored
+12
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+33
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+23
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+21
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+24
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+22
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"selector.d.ts","names":[],"sources":["../../../src/core/selector.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBA4BgB,eAAe,UAAU,UAAU,OAAO,IAAI,iBAAiB,gBAAgB"}
|
||||
Vendored
+39
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+10
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { shallowEqual } from "@videojs/utils/object";
|
||||
export { shallowEqual };
|
||||
Vendored
+52
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
//#region src/core/slice.ts
|
||||
function defineSlice() {
|
||||
return (config) => config;
|
||||
}
|
||||
//#endregion
|
||||
export { defineSlice };
|
||||
|
||||
//# sourceMappingURL=slice.js.map
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+19
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+74
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+25
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+122
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -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 };
|
||||
Vendored
+5
@@ -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 };
|
||||
Vendored
+4
@@ -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 };
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
+61
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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,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"}
|
||||
@@ -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"}
|
||||
Vendored
+34
@@ -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
@@ -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"}
|
||||
Vendored
+54
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+10
@@ -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 };
|
||||
Vendored
+9
@@ -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 };
|
||||
Vendored
+5
@@ -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 };
|
||||
Vendored
+4
@@ -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
@@ -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
@@ -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"}
|
||||
Vendored
+27
@@ -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
@@ -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
@@ -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
@@ -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"}
|
||||
Vendored
+10
@@ -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
@@ -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"}
|
||||
Vendored
+40
@@ -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
@@ -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"}
|
||||
Vendored
+11
@@ -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
Reference in New Issue
Block a user