mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(store): remove platform queue bindings (#360)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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,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,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>>;
|
||||
}
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user