From 4c7ba6df23bb5ff58c60d9e7488e15103c979f86 Mon Sep 17 00:00:00 2001 From: Rahim Date: Sun, 4 Jan 2026 15:43:06 +1100 Subject: [PATCH] docs(store): update readme --- CLAUDE.md | 5 +- packages/store/README.md | 138 +++++++++++++++---------------- packages/store/src/core/slice.ts | 2 +- packages/store/src/core/store.ts | 12 +-- 4 files changed, 77 insertions(+), 80 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3dbc62ea..9770c1bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,8 @@ pnpm install # Run all demos/sites in parallel pnpm dev -# Typecheck across repo (fast) +# Typecheck across repo (fast - uses TypeScript project references) +# Always run from root, not per-package pnpm typecheck # Build all packages/apps @@ -84,7 +85,7 @@ pnpm -F test src/core # Lint all workspace packages pnpm lint # Lint and fix a single file -pnpm lint:file:fix +pnpm lint:fix:file # Remove all dist and types outputs pnpm clean diff --git a/packages/store/README.md b/packages/store/README.md index 2870e744..72e44c97 100644 --- a/packages/store/README.md +++ b/packages/store/README.md @@ -78,9 +78,37 @@ const audioSlice = createSlice()({ }); ``` -### Explicit Types +### Slice Type Inference -For shared type definitions, use `Request`: +State and request types are fully inferred from the slice config: + +```ts +import type { InferSliceRequests, InferSliceState } from '@videojs/store'; + +const audioSlice = createSlice()({ + initialState: { volume: 1, muted: false }, + // ... +}); + +// Infer types from the slice +type AudioState = InferSliceState; +type AudioRequests = InferSliceRequests; +``` + +For stores with multiple slices: + +```ts +import type { UnionSliceRequests, UnionSliceState } from '@videojs/store'; + +const slices = [audioSlice, playbackSlice] as const; + +type MediaState = UnionSliceState; +type MediaRequests = UnionSliceRequests; +``` + +### Explicit Slice Types + +For upfront type definitions, use `Request`: ```ts import type { Request } from '@videojs/store'; @@ -93,10 +121,10 @@ interface AudioState { } interface AudioRequests { - setVolume: Request; // (volume: number) => void - setMuted: Request; // (muted: boolean) => void - play: Request; // () => void - getDuration: Request; // () => number + setVolume: Request; // (volume: number) => Promise + setMuted: Request; // (muted: boolean) => Promise + play: Request; // () => Promise + getDuration: Request; // () => Promise } const audioSlice = createSlice({ @@ -109,6 +137,8 @@ const audioSlice = createSlice({ Requests are operations against the target. Use function shorthand for simple cases, or full config for guards and scheduling. ```ts +import { onEvent } from '@videojs/utils/events'; + request: { // Shorthand - just the handler setVolume(volume, { target }) { @@ -187,6 +217,17 @@ const store = createStore({ }); ``` +### Type Inference + +```ts +import type { InferStoreRequests, InferStoreState } from '@videojs/store'; + +const store = createStore({ slices: [audioSlice, playbackSlice] }); + +type State = InferStoreState; +type Requests = InferStoreRequests; +``` + ### Attaching a Target ```ts @@ -203,6 +244,15 @@ store.request.play(); detach(); ``` +### Destroying a Store + +Clean up when the store is no longer needed: + +```ts +// Detaches target, aborts pending requests, clears queue +store.destroy(); +``` + ### Subscribing to State ```ts @@ -325,14 +375,20 @@ Schedule controls _when_ a request executes. The schedule function receives a `f optionally returns a cancel function. Default schedule is microtask (executes at end of current tick). ```ts -import { delay } from '@videojs/store'; +import { delay, microtask } from '@videojs/store'; import { raf, idle } from '@videojs/store/dom'; request: { - // Debounce 100ms - good for sliders + // Microtask - default, executes at end of current tick setVolume: { + schedule: microtask, + handler: (volume, { target }) => { target.volume = volume; }, + }, + + // Debounce 100ms - good for sliders + seek: { schedule: delay(100), - handler: (volume, { target }) => { target.media.volume = volume; }, + handler: (time, { target }) => { target.currentTime = time; }, }, // Sync with animation frame @@ -385,6 +441,8 @@ request: { ```ts import type { Guard } from '@videojs/store'; +import { onEvent } from '@videojs/utils/events'; + const canMediaPlay: Guard = ({ target, signal }) => { if (target.readyState >= HAVE_ENOUGH_DATA) return true; return onEvent(target, 'canplay', { signal }); // wait for canplay @@ -499,60 +557,6 @@ await queue.enqueue({ }); ``` -## React - -```ts -import { useStore, useSlice, useRequest, usePending } from '@videojs/store/react'; - -// Full state - re-renders on any change -function Player() { - const state = useStore(store); - return
{state.paused ? 'Paused' : 'Playing'}
; -} - -// Selector - re-renders only when volume changes -function VolumeDisplay() { - const volume = useStore(store, (s) => s.volume); - return
{Math.round(volume * 100)}%
; -} - -// Multiple values -function AudioControls() { - const { volume, muted } = useStore(store, (s) => ({ - volume: s.volume, - muted: s.muted - })); - - return
{volume} {muted ? '🔇' : '🔊'}
; -} - -// Track request state -function PlayButton() { - const { dispatch, isPending, error } = useRequest(store.request.play); - - return ( - - ); -} - -// Check pending by key -function SeekBar() { - const isSeeking = usePending(store, 'seek'); - return ; -} - -// Check if slice exists -function QualityMenu() { - const quality = useSlice(store, qualitySlice); - - if (!quality) return null; - - return ; -} -``` - ## Advanced ### Custom State @@ -645,14 +649,6 @@ function QualityMenu() { } ``` -## Exports - -```md -@videojs/store # Core: createStore, createSlice, createQueue, Request -@videojs/store/dom # raf(), idle() schedulers -@videojs/store/react # useStore, useSlice, useRequest, usePending -``` - ## How It's Different | | Redux/Zustand | React Query | @videojs/store | diff --git a/packages/store/src/core/slice.ts b/packages/store/src/core/slice.ts index 2ccf5e68..c060059b 100644 --- a/packages/store/src/core/slice.ts +++ b/packages/store/src/core/slice.ts @@ -71,7 +71,7 @@ export type UnionSliceState[]> = UnionToInte InferSliceState >; -export type UnionSliceRequest[]> = UnionToIntersection< +export type UnionSliceRequests[]> = UnionToIntersection< ResolveSliceRequestHandlers >; diff --git a/packages/store/src/core/store.ts b/packages/store/src/core/store.ts index 196f1fac..52535d6b 100644 --- a/packages/store/src/core/store.ts +++ b/packages/store/src/core/store.ts @@ -1,6 +1,6 @@ import type { PendingTask, TaskContext } from './queue'; import type { RequestMeta, RequestMetaInit, ResolvedRequestConfig } from './request'; -import type { AnySlice, InferSliceTarget, Slice, UnionSliceRequest, UnionSliceState, UnionSliceTasks } from './slice'; +import type { AnySlice, InferSliceTarget, Slice, UnionSliceRequests, UnionSliceState, UnionSliceTasks } from './slice'; import type { StateFactory } from './state'; import { getSelectorKeys } from '@videojs/utils/object'; @@ -16,7 +16,7 @@ export class Store[] = AnySlice[ readonly #slices: Slices; readonly #queue: Queue>; readonly #state: State>; - readonly #request: UnionSliceRequest; + readonly #request: UnionSliceRequests; readonly #requestConfigs: Map>; readonly #setupAbort = new AbortController(); @@ -59,7 +59,7 @@ export class Store[] = AnySlice[ return this.#state.value; } - get request(): UnionSliceRequest { + get request(): UnionSliceRequests { return this.#request; } @@ -258,7 +258,7 @@ export class Store[] = AnySlice[ return configs; } - #buildRequestProxy(): UnionSliceRequest { + #buildRequestProxy(): UnionSliceRequests { const proxy: Record Promise> = {}; for (const [name, config] of this.#requestConfigs) { @@ -271,7 +271,7 @@ export class Store[] = AnySlice[ }; } - return proxy as UnionSliceRequest; + return proxy as UnionSliceRequests; } async #execute( @@ -410,6 +410,6 @@ export type InferStoreSlices = S extends Store = UnionSliceState>; -export type InferStoreRequests = UnionSliceRequest>; +export type InferStoreRequests = UnionSliceRequests>; export type InferStoreTasks = UnionSliceTasks>;