refactor(store): remove platform queue bindings (#360)

This commit is contained in:
rahim
2026-01-31 18:52:59 +11:00
committed by GitHub
parent be7f87c6fe
commit f620b1782a
13 changed files with 4 additions and 308 deletions
@@ -1,6 +1,5 @@
export type { AsyncStatus } from '../../shared/types';
export { QueueController, type QueueControllerHost } from './queue-controller';
export {
SnapshotController,
type SnapshotControllerHost,
@@ -1,49 +0,0 @@
import type { AnyStore } from '../../core/store';
import type { StoreSource } from '../store-accessor';
import type { SubscriptionControllerHost } from './subscription-controller';
import { SubscriptionController } from './subscription-controller';
export type QueueControllerHost = SubscriptionControllerHost;
/**
* Subscribes to queue task changes.
* Triggers host updates when tasks change.
*
* Accepts either a direct store instance or a context that provides one.
*
* @example Direct store
* ```ts
* class MyElement extends LitElement {
* #queue = new QueueController(this, store);
*
* render() {
* const playTask = this.#queue.value.play;
* const isPending = playTask?.status === 'pending';
* return html`<button ?disabled=${isPending}>Play</button>`;
* }
* }
* ```
*
* @example Context source
* ```ts
* const { context } = createStore({ features: [playbackFeature] });
*
* class MyElement extends LitElement {
* #queue = new QueueController(this, context);
* }
* ```
*/
export class QueueController<Store extends AnyStore> {
readonly #sub: SubscriptionController<Store, Store['queue']['tasks']>;
constructor(host: QueueControllerHost, source: StoreSource<Store>) {
this.#sub = new SubscriptionController(host, source, {
subscribe: (store, onChange) => store.queue.subscribe(onChange),
getValue: (store) => store.queue.tasks,
});
}
get value(): Store['queue']['tasks'] {
return this.#sub.value;
}
}
@@ -1,86 +0,0 @@
import { afterEach, describe, expect, it } from 'vitest';
import { createCoreTestStore, createTestHost } from '../../tests/test-utils';
import { QueueController } from '../queue-controller';
describe('QueueController', () => {
afterEach(() => {
document.body.innerHTML = '';
});
it('returns tasks record', () => {
const { store } = createCoreTestStore();
const host = createTestHost();
const controller = new QueueController(host, store);
expect(controller.value).toEqual({});
});
it('updates when task completes', async () => {
const { store } = createCoreTestStore();
const host = createTestHost();
const controller = new QueueController(host, store);
document.body.appendChild(host);
expect(controller.value.setVolume).toBeUndefined();
await store.request.setVolume!(0.5);
expect(controller.value.setVolume).toBeDefined();
expect(controller.value.setVolume?.status).toBe('success');
expect(host.updateCount).toBeGreaterThan(0);
});
it('unsubscribes on disconnect', async () => {
const { store } = createCoreTestStore();
const host = createTestHost();
const controller = new QueueController(host, store);
document.body.appendChild(host);
host.remove();
const updateCountBefore = host.updateCount;
await store.request.setVolume!(0.5);
expect(host.updateCount).toBe(updateCountBefore);
});
it('handles multiple task updates', async () => {
const { store } = createCoreTestStore();
const host = createTestHost();
const controller = new QueueController(host, store);
document.body.appendChild(host);
await store.request.setVolume!(0.5);
await store.request.setMuted!(true);
expect(controller.value.setVolume).toBeDefined();
expect(controller.value.setMuted).toBeDefined();
expect(controller.value.setVolume?.status).toBe('success');
expect(controller.value.setMuted?.status).toBe('success');
});
it('syncs to current tasks on reconnect', async () => {
const { store } = createCoreTestStore();
const host = createTestHost();
const controller = new QueueController(host, store);
document.body.appendChild(host);
await store.request.setVolume!(0.5);
expect(controller.value.setVolume?.status).toBe('success');
host.remove();
await store.request.setMuted!(true);
// Reconnect
document.body.appendChild(host);
expect(controller.value.setVolume?.status).toBe('success');
expect(controller.value.setMuted?.status).toBe('success');
});
});
@@ -1,8 +1,7 @@
import { describe, expectTypeOf, it } from 'vitest';
import { createState } from '../../../core/state';
import { createCoreTestStore, createTestHost } from '../../tests/test-utils';
import { QueueController } from '../queue-controller';
import { createTestHost } from '../../tests/test-utils';
import { SnapshotController } from '../snapshot-controller';
describe('controller types', () => {
@@ -30,16 +29,4 @@ describe('controller types', () => {
expectTypeOf(controller.value.muted).toEqualTypeOf<boolean>();
});
});
describe('QueueController', () => {
it('value is tasks record', () => {
const { store } = createCoreTestStore();
const host = createTestHost();
const controller = new QueueController(host, store);
// Value type matches store.queue.tasks
expectTypeOf(controller.value).toMatchTypeOf(store.queue.tasks);
});
});
});
+1 -42
View File
@@ -3,18 +3,10 @@ import { ContextConsumer, createContext } from '@lit/context';
import type { ReactiveControllerHost, ReactiveElement } from '@lit/reactive-element';
import { noop } from '@videojs/utils/function';
import type { Constructor } from '@videojs/utils/types';
import type {
AnyFeature,
UnionFeatureRequests,
UnionFeatureState,
UnionFeatureTarget,
UnionFeatureTasks,
} from '../core/feature';
import type { TasksRecord } from '../core/queue';
import type { AnyFeature, UnionFeatureRequests, UnionFeatureState, UnionFeatureTarget } from '../core/feature';
import type { StoreConfig, StoreConsumer, StoreProvider } from '../core/store';
import { Store } from '../core/store';
import { QueueController as QueueControllerBase } from './controllers';
import { createStoreAttachMixin, createStoreMixin, createStoreProviderMixin } from './mixins';
export const contextKey = Symbol('@videojs/store');
@@ -118,32 +110,6 @@ export interface CreateStoreResult<Features extends AnyFeature[]> {
hostConnected: () => void;
hostDisconnected: () => void;
};
/**
* Queue controller bound to this store's context.
* Subscribes to queue task changes.
*
* @example
* ```ts
* const { QueueController } = createStore({ features: [playbackFeature] });
*
* class MyElement extends LitElement {
* #queue = new QueueController(this);
*
* render() {
* const playTask = this.#queue.value.play;
* return html`<button ?disabled=${playTask?.status === 'pending'}>Play</button>`;
* }
* }
* ```
*/
QueueController: new (
host: CreateStoreHost
) => {
value: Readonly<TasksRecord<UnionFeatureTasks<Features>>>;
hostConnected: () => void;
hostDisconnected: () => void;
};
}
/**
@@ -240,12 +206,6 @@ export function createStore<Features extends AnyFeature[]>(
}
}
class QueueController extends QueueControllerBase<ProvidedStore> {
constructor(host: CreateStoreHost) {
super(host, context);
}
}
return {
StoreMixin,
StoreProviderMixin,
@@ -253,6 +213,5 @@ export function createStore<Features extends AnyFeature[]>(
context,
create,
StoreController,
QueueController,
};
}
-1
View File
@@ -1,6 +1,5 @@
export type { AsyncStatus } from './controllers';
export {
QueueController,
SnapshotController,
StoreController,
SubscriptionController,
@@ -113,7 +113,6 @@ describe('createStore', () => {
expect(result).toHaveProperty('context');
expect(result).toHaveProperty('create');
expect(result).toHaveProperty('StoreController');
expect(result).toHaveProperty('QueueController');
});
});
@@ -125,13 +124,6 @@ describe('createStore', () => {
expect(StoreController.prototype).toBeDefined();
});
it('QueueController is a class', () => {
const { QueueController } = createStore({ features: [audioFeature] });
expect(typeof QueueController).toBe('function');
expect(QueueController.prototype).toBeDefined();
});
// Note: Full integration tests with DOM and context would require
// setting up a provider element hierarchy. The bound controllers
// work via context, which is tested in integration tests.
+1 -25
View File
@@ -1,14 +1,7 @@
import { isNull, isUndefined } from '@videojs/utils/predicate';
import type { FC, ReactNode } from 'react';
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
import type {
AnyFeature,
UnionFeatureRequests,
UnionFeatureState,
UnionFeatureTarget,
UnionFeatureTasks,
} from '../core/feature';
import type { TasksRecord } from '../core/queue';
import type { AnyFeature, UnionFeatureRequests, UnionFeatureState, UnionFeatureTarget } from '../core/feature';
import type { StoreConfig } from '../core/store';
import { Store } from '../core/store';
@@ -53,12 +46,6 @@ export interface CreateStoreResult<Features extends AnyFeature[]> {
*/
useStore: () => UseStoreResult<Features>;
/**
* Subscribes to queue task changes.
* Returns the current tasks map from the queue.
*/
useQueue: () => TasksRecord<UnionFeatureTasks<Features>>;
/**
* Creates a new store instance.
* Useful for imperative access or creating a store before render.
@@ -87,7 +74,6 @@ export function createStore<Features extends AnyFeature[]>(
config: CreateStoreConfig<Features>
): CreateStoreResult<Features> {
type Target = UnionFeatureTarget<Features>;
type Tasks = UnionFeatureTasks<Features>;
type StoreType = Store<Target, Features>;
function create(): StoreType {
@@ -152,19 +138,9 @@ export function createStore<Features extends AnyFeature[]>(
);
}
function useQueue(): TasksRecord<Tasks> {
const store = useStoreContext() as StoreType;
return useSyncExternalStore(
(cb) => store.queue.subscribe(cb),
() => store.queue.tasks,
() => store.queue.tasks
);
}
return {
Provider,
useStore,
useQueue,
create,
};
}
-1
View File
@@ -1,5 +1,4 @@
export type { AsyncStatus } from '../../shared/types';
export { useQueue } from './use-queue';
export { useSnapshot } from './use-snapshot';
export { useStore } from './use-store';
@@ -1,30 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useQueue } from '../use-queue';
import { createTestStore } from './test-utils';
describe('useQueue', () => {
it('returns tasks record', () => {
const { store } = createTestStore();
const { result } = renderHook(() => useQueue(store));
expect(result.current).toEqual({});
});
it('updates when task completes', async () => {
const { store } = createTestStore();
const { result } = renderHook(() => useQueue(store));
expect(result.current.setVolume).toBeUndefined();
await act(async () => {
await store.request.setVolume(0.5);
});
expect(result.current.setVolume).toBeDefined();
expect(result.current.setVolume?.status).toBe('success');
});
});
@@ -1,35 +0,0 @@
import { useSyncExternalStore } from 'react';
import type { TasksRecord } from '../../core/queue';
import type { AnyStore, InferStoreTasks } from '../../core/store';
/**
* Subscribe to queue task changes.
*
* Returns a record of all tasks keyed by request name.
* Re-renders when any task is added, updated, or removed.
*
* @example
* ```tsx
* function TaskStatus() {
* const tasks = useQueue(store);
* const playTask = tasks.play;
*
* if (playTask?.status === 'pending') {
* return <span>Loading...</span>;
* }
*
* return <span>Ready</span>;
* }
* ```
*/
export function useQueue<Store extends AnyStore>(store: Store): TasksRecord<InferStoreTasks<Store>> {
return useSyncExternalStore(
(cb) => store.queue.subscribe(cb),
() => store.queue.tasks as TasksRecord<InferStoreTasks<Store>>,
() => store.queue.tasks as TasksRecord<InferStoreTasks<Store>>
);
}
export namespace useQueue {
export type Result<Store extends AnyStore> = TasksRecord<InferStoreTasks<Store>>;
}
+1 -1
View File
@@ -6,4 +6,4 @@ export type {
} from './create-store';
export { createStore } from './create-store';
export { useQueue, useSnapshot, useStore } from './hooks';
export { useSnapshot, useStore } from './hooks';
@@ -124,19 +124,4 @@ describe('createStore', () => {
expect(result.current.volume).toBe(0.5);
});
});
describe('useQueue', () => {
it('returns tasks from context store', () => {
const { Provider, useQueue, create } = createStore({
features: [audioFeature],
});
const store = create();
const { result } = renderHook(() => useQueue(), {
wrapper: ({ children }: { children: ReactNode }) => <Provider store={store}>{children}</Provider>,
});
expect(result.current).toEqual({});
});
});
});