diff --git a/.claude/plans/store/queue-simplification.md b/.claude/plans/store/queue-simplification.md index f35d79bd..e21ca573 100644 --- a/.claude/plans/store/queue-simplification.md +++ b/.claude/plans/store/queue-simplification.md @@ -32,7 +32,7 @@ interface Queue { abort(name?: keyof Tasks): void; reset(name?: keyof Tasks): void; destroy(): void; - subscribe(listener: QueueListener): () => void; + subscribe(callback: StateChange>): () => void; } ``` diff --git a/.claude/skills/api/references/extensibility.md b/.claude/skills/api/references/extensibility.md index 7a4cf1ed..35e449af 100644 --- a/.claude/skills/api/references/extensibility.md +++ b/.claude/skills/api/references/extensibility.md @@ -147,7 +147,7 @@ const myPlugin = () => { interface Store { get(): T; set(value: T | ((prev: T) => T)): void; - subscribe(listener: (value: T) => void): () => void; + subscribe(callback: (value: T) => void): () => void; } ``` diff --git a/.claude/skills/api/references/state.md b/.claude/skills/api/references/state.md index ce720329..5a8b6e98 100644 --- a/.claude/skills/api/references/state.md +++ b/.claude/skills/api/references/state.md @@ -24,7 +24,7 @@ The minimal interface any framework can consume: ```ts interface Store { get(): T; - subscribe(listener: (value: T) => void): () => void; + subscribe(callback: (value: T) => void): () => void; } ``` diff --git a/CLAUDE.md b/CLAUDE.md index 33e8dc5c..6434f8f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -309,9 +309,9 @@ function isQueue(value: unknown): value is Queue { Subscriptions return an unsubscribe function: ```ts -subscribe(listener: Listener): () => void { - this.#subscribers.add(listener); - return () => this.#subscribers.delete(listener); +subscribe(callback: Callback): () => void { + this.#subscribers.add(callback); + return () => this.#subscribers.delete(callback); } ``` diff --git a/biome.json b/biome.json index d1ec195e..259bcbad 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,11 @@ "html": { "experimentalFullSupportEnabled": false }, - "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "assist": { + "actions": { + "source": { "organizeImports": "on" } + } + }, "formatter": { "enabled": true, "indentStyle": "space", diff --git a/packages/react/src/media/tests/video.test.tsx b/packages/react/src/media/tests/video.test.tsx index 89d3ef52..1244dc82 100644 --- a/packages/react/src/media/tests/video.test.tsx +++ b/packages/react/src/media/tests/video.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react'; import { createFeature } from '@videojs/store'; -import { createStore } from '@videojs/store/react'; +import { createStore, useStoreContext } from '@videojs/store/react'; import { describe, expect, it, vi } from 'vitest'; import { Video } from '../video'; @@ -75,12 +75,12 @@ describe('video', () => { }); it('attaches video to store on mount', () => { - const { Provider, useStore } = createTestStore(); + const { Provider } = createTestStore(); let attachCalled = false; function TestComponent() { - const store = useStore(); + const store = useStoreContext(); // Spy on attach const originalAttach = store.attach.bind(store); diff --git a/packages/store/src/core/computed.ts b/packages/store/src/core/computed.ts index 8fa1eb96..4851dddc 100644 --- a/packages/store/src/core/computed.ts +++ b/packages/store/src/core/computed.ts @@ -62,9 +62,9 @@ export class Computed { return this.#cached; } - subscribe(listener: () => void): () => void { - this.#listeners.add(listener); - return () => this.#listeners.delete(listener); + subscribe(callback: () => void): () => void { + this.#listeners.add(callback); + return () => this.#listeners.delete(callback); } destroy(): void { diff --git a/packages/store/src/core/queue.ts b/packages/store/src/core/queue.ts index 4204c010..43b54016 100644 --- a/packages/store/src/core/queue.ts +++ b/packages/store/src/core/queue.ts @@ -2,7 +2,7 @@ import { abortable } from '@videojs/utils/events'; import { isUndefined } from '@videojs/utils/predicate'; import { StoreError } from './errors'; import type { Request, RequestMeta, RequestMode } from './request'; -import type { WritableState } from './state'; +import type { StateChange, WritableState } from './state'; import { createState } from './state'; import type { ErrorTask, PendingTask, SuccessTask, Task, TaskContext, TaskKey } from './task'; @@ -47,8 +47,8 @@ export class Queue { } /** Subscribe to task changes. */ - subscribe(listener: (changedKeys: ReadonlySet) => void): () => void { - return this.#tasks.subscribe(listener); + subscribe(callback: StateChange>): () => void { + return this.#tasks.subscribe(callback); } constructor() { diff --git a/packages/store/src/core/state.ts b/packages/store/src/core/state.ts index 5afc3b6b..b3518422 100644 --- a/packages/store/src/core/state.ts +++ b/packages/store/src/core/state.ts @@ -1,8 +1,9 @@ -type Listener = (changedKeys: ReadonlySet) => void; +export type StateChange = (changedKeys: ReadonlySet) => void; export interface State { readonly current: Readonly; - subscribe: ((listener: Listener) => () => void) & ((keys: K[], listener: Listener) => () => void); + subscribe(keys: K[], callback: StateChange): () => void; + subscribe(callback: StateChange): () => void; } export interface WritableState extends State { @@ -19,7 +20,7 @@ function scheduleFlush(): void { queueMicrotask(flush); } -const pendingContainers = new Set>(); +const pendingContainers = new Set>(); export function flush(): void { flushScheduled = false; @@ -33,11 +34,11 @@ export function flush(): void { const hasOwnProp = Object.prototype.hasOwnProperty; -class StateContainer implements WritableState { +class StateContainer implements WritableState { #current: T; - #listeners = new Set(); - #keyListeners = new Map>(); - #pending = new Set(); + #listeners = new Set>(); + #keyListeners = new Map>>(); + #pending = new Set(); constructor(initial: T) { this.#current = Object.freeze({ ...initial }); @@ -50,7 +51,7 @@ class StateContainer implements WritableState { set(key: K, value: T[K]): void { if (Object.is(this.#current[key], value)) return; this.#current = Object.freeze({ ...this.#current, [key]: value }); - this.#pending.add(key); + this.#pending.add(key as any); pendingContainers.add(this); scheduleFlush(); } @@ -59,7 +60,7 @@ class StateContainer implements WritableState { if (!(key in this.#current)) return; const { [key]: _, ...rest } = this.#current; this.#current = Object.freeze(rest as T); - this.#pending.add(key); + this.#pending.add(key as any); pendingContainers.add(this); scheduleFlush(); } @@ -74,7 +75,7 @@ class StateContainer implements WritableState { if (!Object.is(this.#current[key], value)) { next[key] = value!; - this.#pending.add(key); + this.#pending.add(key as any); } } @@ -85,38 +86,39 @@ class StateContainer implements WritableState { } } - subscribe(listener: Listener): () => void; - subscribe(keys: K[], listener: Listener): () => void; - subscribe(first: Listener | PropertyKey[], second?: Listener): () => void { + subscribe(callback: StateChange): () => void; + subscribe(keys: K[], callback: StateChange): () => void; + subscribe(first: StateChange | K[], second?: StateChange): () => void { // Key-specific subscription if (Array.isArray(first)) { const keys = first; - const listener = second!; + const callback = second!; for (const key of keys) { let set = this.#keyListeners.get(key); if (!set) this.#keyListeners.set(key, (set = new Set())); - set.add(listener); + set.add(callback); } return () => { for (const key of keys) { - this.#keyListeners.get(key)?.delete(listener); + this.#keyListeners.get(key)?.delete(callback); } }; } // Global subscription - const listener = first; - this.#listeners.add(listener); + const callback = first; + this.#listeners.add(callback); - return () => this.#listeners.delete(listener); + return () => this.#listeners.delete(callback); } flush(): void { if (this.#pending.size === 0) return; - const keys: ReadonlySet = new Set(this.#pending); + const keys: ReadonlySet = new Set(this.#pending); + this.#pending.clear(); for (const fn of this.#listeners) fn(keys); diff --git a/packages/store/src/core/store.ts b/packages/store/src/core/store.ts index d52ceede..49d1060d 100644 --- a/packages/store/src/core/store.ts +++ b/packages/store/src/core/store.ts @@ -12,7 +12,7 @@ import type { import { Queue } from './queue'; import type { RequestMeta, RequestMetaInit, ResolvedRequestConfig } from './request'; import { CANCEL_ALL, createRequestMeta, resolveRequestCancel, resolveRequestKey } from './request'; -import type { WritableState } from './state'; +import type { StateChange, WritableState } from './state'; import { createState } from './state'; import type { PendingTask, Task, TaskContext } from './task'; @@ -62,22 +62,6 @@ export class Store[] = AnyFeature) => void): () => void; - subscribe>( - keys: K[], - listener: (changedKeys: ReadonlySet) => void - ): () => void; - subscribe( - first: ((changedKeys: ReadonlySet) => void) | (keyof UnionFeatureState)[], - second?: (changedKeys: ReadonlySet) => void - ): () => void { - if (typeof first === 'function') { - return this.#state.subscribe(first); - } - return this.#state.subscribe(first as (keyof (UnionFeatureState & object))[], second!); - } - get request(): UnionFeatureRequests { return this.#request; } @@ -167,6 +151,24 @@ export class Store[] = AnyFeature>): () => void; + + subscribe>( + keys: K[], + callback: StateChange, K> + ): () => void; + + subscribe( + first: StateChange> | (keyof UnionFeatureState)[], + second?: StateChange> + ): () => void { + return this.#state.subscribe( + first as (keyof UnionFeatureState)[], + second as StateChange> + ); + } + #syncAllFeatures(): void { const target = this.#target; if (!target) return; diff --git a/tsconfig.base.json b/tsconfig.base.json index c38bbe72..c9d0df2d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2,8 +2,8 @@ "compilerOptions": { "incremental": true, "composite": true, - "target": "ES2020", - "lib": ["ES2020"], + "target": "ES2022", + "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "bundler", "resolveJsonModule": true,