refactor(store): clean up

This commit is contained in:
Rahim
2026-01-24 00:29:29 +11:00
parent 1363f766a4
commit d0b79ef588
11 changed files with 63 additions and 55 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ interface Queue<Tasks> {
abort(name?: keyof Tasks): void;
reset(name?: keyof Tasks): void;
destroy(): void;
subscribe(listener: QueueListener<Tasks>): () => void;
subscribe(callback: StateChange<TasksRecord<Tasks>>): () => void;
}
```
@@ -147,7 +147,7 @@ const myPlugin = () => {
interface Store<T> {
get(): T;
set(value: T | ((prev: T) => T)): void;
subscribe(listener: (value: T) => void): () => void;
subscribe(callback: (value: T) => void): () => void;
}
```
+1 -1
View File
@@ -24,7 +24,7 @@ The minimal interface any framework can consume:
```ts
interface Store<T> {
get(): T;
subscribe(listener: (value: T) => void): () => void;
subscribe(callback: (value: T) => void): () => void;
}
```
+3 -3
View File
@@ -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);
}
```
+5 -1
View File
@@ -3,7 +3,11 @@
"html": {
"experimentalFullSupportEnabled": false
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"assist": {
"actions": {
"source": { "organizeImports": "on" }
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
@@ -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);
+3 -3
View File
@@ -62,9 +62,9 @@ export class Computed<T extends object, K extends keyof T, R> {
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 {
+3 -3
View File
@@ -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<Tasks extends TaskRecord = DefaultTaskRecord> {
}
/** Subscribe to task changes. */
subscribe(listener: (changedKeys: ReadonlySet<PropertyKey>) => void): () => void {
return this.#tasks.subscribe(listener);
subscribe(callback: StateChange<TasksRecord<Tasks>>): () => void {
return this.#tasks.subscribe(callback);
}
constructor() {
+22 -20
View File
@@ -1,8 +1,9 @@
type Listener = (changedKeys: ReadonlySet<PropertyKey>) => void;
export type StateChange<T, K extends keyof T = keyof T> = (changedKeys: ReadonlySet<K>) => void;
export interface State<T extends object> {
readonly current: Readonly<T>;
subscribe: ((listener: Listener) => () => void) & (<K extends keyof T>(keys: K[], listener: Listener) => () => void);
subscribe<K extends keyof T>(keys: K[], callback: StateChange<T, K>): () => void;
subscribe(callback: StateChange<T>): () => void;
}
export interface WritableState<T extends object> extends State<T> {
@@ -19,7 +20,7 @@ function scheduleFlush(): void {
queueMicrotask(flush);
}
const pendingContainers = new Set<StateContainer<any>>();
const pendingContainers = new Set<StateContainer<any, any>>();
export function flush(): void {
flushScheduled = false;
@@ -33,11 +34,11 @@ export function flush(): void {
const hasOwnProp = Object.prototype.hasOwnProperty;
class StateContainer<T extends object> implements WritableState<T> {
class StateContainer<T extends object, K extends keyof T> implements WritableState<T> {
#current: T;
#listeners = new Set<Listener>();
#keyListeners = new Map<PropertyKey, Set<Listener>>();
#pending = new Set<PropertyKey>();
#listeners = new Set<StateChange<T>>();
#keyListeners = new Map<K, Set<StateChange<T>>>();
#pending = new Set<K>();
constructor(initial: T) {
this.#current = Object.freeze({ ...initial });
@@ -50,7 +51,7 @@ class StateContainer<T extends object> implements WritableState<T> {
set<K extends keyof T>(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<T extends object> implements WritableState<T> {
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<T extends object> implements WritableState<T> {
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<T extends object> implements WritableState<T> {
}
}
subscribe(listener: Listener): () => void;
subscribe<K extends keyof T>(keys: K[], listener: Listener): () => void;
subscribe(first: Listener | PropertyKey[], second?: Listener): () => void {
subscribe(callback: StateChange<T>): () => void;
subscribe<K extends keyof T>(keys: K[], callback: StateChange<T, K>): () => void;
subscribe(first: StateChange<T> | K[], second?: StateChange<T>): () => 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<PropertyKey> = new Set(this.#pending);
const keys: ReadonlySet<K> = new Set(this.#pending);
this.#pending.clear();
for (const fn of this.#listeners) fn(keys);
+19 -17
View File
@@ -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<Target, Features extends AnyFeature<Target>[] = AnyFeature<Ta
return this.#state.current;
}
/** Subscribe to state changes. */
subscribe(listener: (changedKeys: ReadonlySet<PropertyKey>) => void): () => void;
subscribe<K extends keyof UnionFeatureState<Features>>(
keys: K[],
listener: (changedKeys: ReadonlySet<PropertyKey>) => void
): () => void;
subscribe(
first: ((changedKeys: ReadonlySet<PropertyKey>) => void) | (keyof UnionFeatureState<Features>)[],
second?: (changedKeys: ReadonlySet<PropertyKey>) => void
): () => void {
if (typeof first === 'function') {
return this.#state.subscribe(first);
}
return this.#state.subscribe(first as (keyof (UnionFeatureState<Features> & object))[], second!);
}
get request(): UnionFeatureRequests<Features> {
return this.#request;
}
@@ -167,6 +151,24 @@ export class Store<Target, Features extends AnyFeature<Target>[] = AnyFeature<Ta
// State
// ----------------------------------------
/** Subscribe to state changes. */
subscribe(callback: StateChange<UnionFeatureState<Features>>): () => void;
subscribe<K extends keyof UnionFeatureState<Features>>(
keys: K[],
callback: StateChange<UnionFeatureState<Features>, K>
): () => void;
subscribe(
first: StateChange<UnionFeatureState<Features>> | (keyof UnionFeatureState<Features>)[],
second?: StateChange<UnionFeatureState<Features>>
): () => void {
return this.#state.subscribe(
first as (keyof UnionFeatureState<Features>)[],
second as StateChange<UnionFeatureState<Features>>
);
}
#syncAllFeatures(): void {
const target = this.#target;
if (!target) return;
+2 -2
View File
@@ -2,8 +2,8 @@
"compilerOptions": {
"incremental": true,
"composite": true,
"target": "ES2020",
"lib": ["ES2020"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,