mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(store): lit bindings (#289)
This commit is contained in:
@@ -13,6 +13,13 @@ export type TaskKey<T = string | symbol> = T & (string | symbol);
|
||||
|
||||
export type EnsureTaskKey<T> = T extends string | symbol ? T : never;
|
||||
|
||||
/**
|
||||
* Status for async operations (mutations, optimistic updates).
|
||||
*
|
||||
* Used by framework bindings (React, Lit) for tracking request lifecycle.
|
||||
*/
|
||||
export type AsyncStatus = 'idle' | 'pending' | 'success' | 'error';
|
||||
|
||||
/**
|
||||
* A task scheduler controls when a task flushes.
|
||||
*
|
||||
@@ -189,28 +196,32 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return this.#destroyed;
|
||||
}
|
||||
|
||||
isPending(key: keyof Tasks): boolean {
|
||||
return this.#tasks[key]?.status === 'pending';
|
||||
isPending(name: keyof Tasks): boolean {
|
||||
return this.#tasks[name]?.status === 'pending';
|
||||
}
|
||||
|
||||
isQueued(key: keyof Tasks): boolean {
|
||||
return key in this.#queued;
|
||||
isQueued(name: keyof Tasks): boolean {
|
||||
// Note: #queued is keyed by key, but we need to search by name
|
||||
for (const task of Object.values(this.#queued)) {
|
||||
if (task?.name === name) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isSettled(key: keyof Tasks): boolean {
|
||||
const task = this.#tasks[key];
|
||||
isSettled(name: keyof Tasks): boolean {
|
||||
const task = this.#tasks[name];
|
||||
return task?.status === 'success' || task?.status === 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear settled task(s). If key provided, clears that task. If no key, clears all settled.
|
||||
* Clear settled task(s). If name provided, clears that task. If no name, clears all settled.
|
||||
*/
|
||||
reset(key?: keyof Tasks): void {
|
||||
if (!isUndefined(key)) {
|
||||
const task = this.#tasks[key];
|
||||
reset(name?: keyof Tasks): void {
|
||||
if (!isUndefined(name)) {
|
||||
const task = this.#tasks[name];
|
||||
if (!task || task.status === 'pending') return;
|
||||
|
||||
delete this.#tasks[key];
|
||||
delete this.#tasks[name];
|
||||
this.#notifySubscribers();
|
||||
|
||||
return;
|
||||
@@ -259,18 +270,20 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return Promise.reject(new StoreError('DESTROYED'));
|
||||
}
|
||||
|
||||
// Supersede any queued task with the same key
|
||||
const queued = this.#queued[key];
|
||||
queued?.invalidate?.();
|
||||
queued?.reject(new StoreError('SUPERSEDED'));
|
||||
delete this.#queued[key];
|
||||
|
||||
const existing = this.#tasks[key];
|
||||
if (existing?.status === 'pending') {
|
||||
existing.abort.abort(new StoreError('SUPERSEDED'));
|
||||
// Supersede any pending task with the same key (may have different name)
|
||||
// Don't delete - let error handler update status to 'error' so controllers can see it
|
||||
for (const task of Object.values(this.#tasks)) {
|
||||
if (task?.key === key && task.status === 'pending') {
|
||||
task.abort.abort(new StoreError('SUPERSEDED'));
|
||||
}
|
||||
}
|
||||
|
||||
delete this.#tasks[key];
|
||||
|
||||
return new Promise<Tasks[K]['output']>((resolve, reject) => {
|
||||
const task: QueuedTask = {
|
||||
id: Symbol('@videojs/task'),
|
||||
@@ -313,18 +326,20 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel queued task(s). If key provided, cancels that task. If no key, cancels all.
|
||||
* Cancel queued task(s). If name provided, cancels that task. If no name, cancels all.
|
||||
*/
|
||||
cancel(key?: keyof Tasks): boolean {
|
||||
if (!isUndefined(key)) {
|
||||
const queued = this.#queued[key];
|
||||
if (!queued) return false;
|
||||
|
||||
queued.invalidate?.();
|
||||
queued.reject(new StoreError('REMOVED'));
|
||||
delete this.#queued[key];
|
||||
|
||||
return true;
|
||||
cancel(name?: keyof Tasks): boolean {
|
||||
if (!isUndefined(name)) {
|
||||
// Find queued task by name (#queued is keyed by key)
|
||||
for (const [key, queued] of Object.entries(this.#queued)) {
|
||||
if (queued?.name === name) {
|
||||
queued.invalidate?.();
|
||||
queued.reject(new StoreError('REMOVED'));
|
||||
delete this.#queued[key as keyof Tasks];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const hadQueued = Object.keys(this.#queued).length > 0;
|
||||
@@ -338,9 +353,15 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return hadQueued;
|
||||
}
|
||||
|
||||
async flush(key?: keyof Tasks): Promise<void> {
|
||||
if (!isUndefined(key)) {
|
||||
await this.#flushKey(key);
|
||||
async flush(name?: keyof Tasks): Promise<void> {
|
||||
if (!isUndefined(name)) {
|
||||
// Find queued task by name and flush by its key
|
||||
for (const [key, queued] of Object.entries(this.#queued)) {
|
||||
if (queued?.name === name) {
|
||||
await this.#flushKey(key as keyof Tasks);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -349,16 +370,22 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort task(s). If key provided, aborts that task. If no key, aborts all.
|
||||
* Abort task(s). If name provided, aborts that task. If no name, aborts all.
|
||||
*/
|
||||
abort(key?: keyof Tasks): void {
|
||||
if (!isUndefined(key)) {
|
||||
const queued = this.#queued[key];
|
||||
queued?.invalidate?.();
|
||||
queued?.reject(new StoreError('ABORTED'));
|
||||
delete this.#queued[key];
|
||||
abort(name?: keyof Tasks): void {
|
||||
if (!isUndefined(name)) {
|
||||
// Find and abort queued task by name
|
||||
for (const [key, queued] of Object.entries(this.#queued)) {
|
||||
if (queued?.name === name) {
|
||||
queued.invalidate?.();
|
||||
queued.reject(new StoreError('ABORTED'));
|
||||
delete this.#queued[key as keyof Tasks];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const task = this.#tasks[key];
|
||||
// Abort pending task (stored by name)
|
||||
const task = this.#tasks[name];
|
||||
if (task?.status === 'pending') {
|
||||
task.abort.abort(new StoreError('ABORTED'));
|
||||
}
|
||||
@@ -421,7 +448,8 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
meta,
|
||||
};
|
||||
|
||||
this.#tasks[key as keyof Tasks] = pendingTask;
|
||||
// Store tasks by name for controller access (different names can share same key)
|
||||
this.#tasks[name as keyof Tasks] = pendingTask;
|
||||
this.#notifySubscribers();
|
||||
this.#onDispatch?.(pendingTask);
|
||||
|
||||
@@ -445,9 +473,9 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
output: result,
|
||||
};
|
||||
|
||||
// Only update if we're still the current task for this key
|
||||
if (this.#tasks[key] === pendingTask) {
|
||||
this.#tasks[key as keyof Tasks] = successTask;
|
||||
// Only update if we're still the current task for this name
|
||||
if (this.#tasks[name as keyof Tasks] === pendingTask) {
|
||||
this.#tasks[name as keyof Tasks] = successTask;
|
||||
this.#notifySubscribers();
|
||||
}
|
||||
|
||||
@@ -463,9 +491,9 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
cancelled: abort.signal.aborted,
|
||||
};
|
||||
|
||||
// Only update if we're still the current task for this key
|
||||
if (this.#tasks[key] === pendingTask) {
|
||||
this.#tasks[key as keyof Tasks] = errorTask;
|
||||
// Only update if we're still the current task for this name
|
||||
if (this.#tasks[name as keyof Tasks] === pendingTask) {
|
||||
this.#tasks[name as keyof Tasks] = errorTask;
|
||||
this.#notifySubscribers();
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export function resolveRequestKey(keyConfig: RequestKey<any>, input: unknown): T
|
||||
return isFunction(keyConfig) ? keyConfig(input) : keyConfig;
|
||||
}
|
||||
|
||||
export function resolveRequestCancelKeys(cancel: RequestCancel<any> | undefined, input: unknown): TaskKey[] {
|
||||
export function resolveRequestCancel(cancel: RequestCancel<any> | undefined, input: unknown): TaskKey[] {
|
||||
if (!cancel) return [];
|
||||
const result = isFunction(cancel) ? cancel(input) : cancel;
|
||||
return Array.isArray(result) ? result : [result];
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isNull } from '@videojs/utils/predicate';
|
||||
|
||||
import { StoreError } from './errors';
|
||||
import { Queue } from './queue';
|
||||
import { createRequestMeta, resolveRequestCancelKeys, resolveRequestKey } from './request';
|
||||
import { createRequestMeta, resolveRequestCancel, resolveRequestKey } from './request';
|
||||
import { State } from './state';
|
||||
|
||||
export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[]> {
|
||||
@@ -274,8 +274,8 @@ export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[
|
||||
): Promise<unknown> {
|
||||
const key = resolveRequestKey(config.key, input);
|
||||
|
||||
for (const cancelKey of resolveRequestCancelKeys(config.cancel, input)) {
|
||||
this.#queue.abort(cancelKey);
|
||||
for (const requestName of resolveRequestCancel(config.cancel, input)) {
|
||||
this.#queue.abort(requestName);
|
||||
}
|
||||
|
||||
const handler = async ({ input, signal }: TaskContext) => {
|
||||
@@ -311,7 +311,7 @@ export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[
|
||||
});
|
||||
} catch (error) {
|
||||
const tasks = this.#queue.tasks as Record<string | symbol, Task | undefined>;
|
||||
const task = tasks[key];
|
||||
const task = tasks[name];
|
||||
|
||||
this.#handleError({
|
||||
request: task?.status === 'pending' ? task : undefined,
|
||||
@@ -385,6 +385,14 @@ export interface StoreErrorContext<Target, Slices extends AnySlice<Target>[]> {
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export interface StoreProvider<Slices extends AnySlice[]> {
|
||||
store: Store<UnionSliceTarget<Slices>, Slices>;
|
||||
}
|
||||
|
||||
export interface StoreConsumer<Slices extends AnySlice[]> {
|
||||
readonly store: Store<UnionSliceTarget<Slices>, Slices> | null;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Type Inference
|
||||
// ----------------------------------------
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('queue', () => {
|
||||
});
|
||||
|
||||
describe('cancel', () => {
|
||||
it('cancel(key) removes specific queued task', async () => {
|
||||
it('cancel(name) removes specific queued task', async () => {
|
||||
const queue = createQueue({
|
||||
scheduler: delay(100),
|
||||
});
|
||||
@@ -172,8 +172,8 @@ describe('queue', () => {
|
||||
const handler = vi.fn();
|
||||
const promise = queue.enqueue({ name: 'test', key: 'k', handler });
|
||||
|
||||
expect(queue.cancel('k')).toBe(true);
|
||||
expect(queue.cancel('k')).toBe(false);
|
||||
expect(queue.cancel('test')).toBe(true);
|
||||
expect(queue.cancel('test')).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
await expect(promise).rejects.toMatchObject({ code: 'REMOVED' });
|
||||
@@ -214,7 +214,7 @@ describe('queue', () => {
|
||||
await expect(promise).resolves.toBe('ok');
|
||||
});
|
||||
|
||||
it('flush(key) executes specific task', async () => {
|
||||
it('flush(name) executes specific task', async () => {
|
||||
vi.useRealTimers();
|
||||
|
||||
const queue = createQueue({ scheduler: delay(1000) });
|
||||
@@ -233,7 +233,7 @@ describe('queue', () => {
|
||||
});
|
||||
|
||||
describe('abort', () => {
|
||||
it('abort(key) cancels queued and aborts pending', async () => {
|
||||
it('abort(name) cancels queued and aborts pending', async () => {
|
||||
vi.useRealTimers();
|
||||
|
||||
const queue = createQueue();
|
||||
@@ -254,7 +254,7 @@ describe('queue', () => {
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
queue.abort('k');
|
||||
queue.abort('test');
|
||||
|
||||
await expect(promise).rejects.toMatchObject({ code: 'ABORTED' });
|
||||
expect(aborted).toBe(true);
|
||||
@@ -392,7 +392,7 @@ describe('queue', () => {
|
||||
handler: vi.fn().mockResolvedValue('result1'),
|
||||
});
|
||||
|
||||
// Queue should have the first task
|
||||
// Queue should have the first task (#queued is keyed by key 'shared')
|
||||
expect(Reflect.ownKeys(queue.queued).length).toBe(1);
|
||||
expect(queue.queued.shared?.name).toBe('task1');
|
||||
|
||||
@@ -406,7 +406,7 @@ describe('queue', () => {
|
||||
// First should be superseded
|
||||
await expect(promise1).rejects.toMatchObject({ code: 'SUPERSEDED' });
|
||||
|
||||
// Queue should only have the second task (first was explicitly deleted)
|
||||
// Queue should only have the second task (#queued keyed by key 'shared')
|
||||
expect(Reflect.ownKeys(queue.queued).length).toBe(1);
|
||||
expect(queue.queued.shared?.name).toBe('task2');
|
||||
|
||||
@@ -438,7 +438,7 @@ describe('queue', () => {
|
||||
|
||||
// Wait for task to start
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
expect(queue.tasks.k?.status).toBe('pending');
|
||||
expect(queue.tasks.task?.status).toBe('pending');
|
||||
|
||||
// Destroy queue
|
||||
queue.destroy();
|
||||
@@ -449,7 +449,7 @@ describe('queue', () => {
|
||||
expect(cleanupSpy).toHaveBeenCalledWith('cleanup');
|
||||
|
||||
// After destroy, all tasks are cleared for memory cleanup
|
||||
expect(queue.tasks.k).toBeUndefined();
|
||||
expect(queue.tasks.task).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles scheduler error without double-cleanup when already flushed', async () => {
|
||||
@@ -520,12 +520,12 @@ describe('queue', () => {
|
||||
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
|
||||
expect(queue.isPending('test-key')).toBe(true);
|
||||
expect(queue.isPending('other-key')).toBe(false);
|
||||
expect(queue.isPending('test')).toBe(true);
|
||||
expect(queue.isPending('other')).toBe(false);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(queue.isPending('test-key')).toBe(false);
|
||||
expect(queue.isPending('test')).toBe(false);
|
||||
});
|
||||
|
||||
it('isQueued returns true when task is waiting to execute', async () => {
|
||||
@@ -537,13 +537,13 @@ describe('queue', () => {
|
||||
handler: vi.fn().mockResolvedValue('result'),
|
||||
});
|
||||
|
||||
expect(queue.isQueued('test-key')).toBe(true);
|
||||
expect(queue.isQueued('other-key')).toBe(false);
|
||||
expect(queue.isQueued('test')).toBe(true);
|
||||
expect(queue.isQueued('other')).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(queue.isQueued('test-key')).toBe(false);
|
||||
expect(queue.isQueued('test')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -602,11 +602,11 @@ describe('queue', () => {
|
||||
// Wait for dispatch
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
|
||||
// First call should have pending task
|
||||
// First call should have pending task (keyed by name 'test')
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
const pendingSnapshot = listener.mock.calls[0]![0] as Record<string, { status: string }>;
|
||||
expect(Reflect.ownKeys(pendingSnapshot).length).toBe(1);
|
||||
expect(pendingSnapshot['test-key']?.status).toBe('pending');
|
||||
expect(pendingSnapshot.test?.status).toBe('pending');
|
||||
|
||||
// Complete the handler
|
||||
resolveHandler!();
|
||||
@@ -616,7 +616,7 @@ describe('queue', () => {
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
const settledSnapshot = listener.mock.calls[1]![0] as Record<string, { status: string }>;
|
||||
expect(Reflect.ownKeys(settledSnapshot).length).toBe(1);
|
||||
expect(settledSnapshot['test-key']?.status).toBe('success');
|
||||
expect(settledSnapshot.test?.status).toBe('success');
|
||||
});
|
||||
|
||||
it('unsubscribe stops notifications', async () => {
|
||||
@@ -724,9 +724,9 @@ describe('queue', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Task should be pending
|
||||
// Task should be pending (keyed by name 'task')
|
||||
await new Promise(r => setTimeout(r, 5));
|
||||
const pendingTask = queue.tasks.k;
|
||||
const pendingTask = queue.tasks.task;
|
||||
expect(pendingTask?.status).toBe('pending');
|
||||
expect(pendingTask?.name).toBe('task');
|
||||
|
||||
@@ -734,7 +734,7 @@ describe('queue', () => {
|
||||
await promise;
|
||||
|
||||
// Task should be success
|
||||
const successTask = queue.tasks.k;
|
||||
const successTask = queue.tasks.task;
|
||||
expect(successTask?.status).toBe('success');
|
||||
if (successTask?.status === 'success') {
|
||||
expect(successTask.output).toBe('result');
|
||||
@@ -759,13 +759,13 @@ describe('queue', () => {
|
||||
|
||||
// Task should be pending
|
||||
await new Promise(r => setTimeout(r, 5));
|
||||
expect(queue.tasks.k?.status).toBe('pending');
|
||||
expect(queue.tasks.task?.status).toBe('pending');
|
||||
|
||||
// Wait for failure
|
||||
await expect(promise).rejects.toThrow('test error');
|
||||
|
||||
// Task should be error
|
||||
const errorTask = queue.tasks.k;
|
||||
const errorTask = queue.tasks.task;
|
||||
expect(errorTask?.status).toBe('error');
|
||||
if (errorTask?.status === 'error') {
|
||||
expect(errorTask.error).toBe(error);
|
||||
@@ -791,14 +791,14 @@ describe('queue', () => {
|
||||
|
||||
// Wait for task to start
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
expect(queue.tasks.k?.status).toBe('pending');
|
||||
expect(queue.tasks.task?.status).toBe('pending');
|
||||
|
||||
// Abort the task
|
||||
queue.abort('k');
|
||||
// Abort the task (by name)
|
||||
queue.abort('task');
|
||||
await promise.catch(() => {});
|
||||
|
||||
// Task should be error with cancelled=true
|
||||
const errorTask = queue.tasks.k;
|
||||
const errorTask = queue.tasks.task;
|
||||
expect(errorTask?.status).toBe('error');
|
||||
if (errorTask?.status === 'error') {
|
||||
expect(errorTask.cancelled).toBe(true);
|
||||
@@ -817,22 +817,24 @@ describe('queue', () => {
|
||||
handler: async () => 'first-result',
|
||||
});
|
||||
|
||||
expect(queue.tasks.k?.status).toBe('success');
|
||||
if (queue.tasks.k?.status === 'success') {
|
||||
expect(queue.tasks.k.output).toBe('first-result');
|
||||
// Keyed by name 'first'
|
||||
expect(queue.tasks.first?.status).toBe('success');
|
||||
if (queue.tasks.first?.status === 'success') {
|
||||
expect(queue.tasks.first.output).toBe('first-result');
|
||||
}
|
||||
|
||||
// Second request replaces settled task
|
||||
// Second request (different name, same key) - doesn't replace first since different name
|
||||
await queue.enqueue({
|
||||
name: 'second',
|
||||
key: 'k',
|
||||
handler: async () => 'second-result',
|
||||
});
|
||||
|
||||
expect(queue.tasks.k?.status).toBe('success');
|
||||
if (queue.tasks.k?.status === 'success') {
|
||||
expect(queue.tasks.k.output).toBe('second-result');
|
||||
expect(queue.tasks.k.name).toBe('second');
|
||||
// Both tasks exist at different keys (names)
|
||||
expect(queue.tasks.second?.status).toBe('success');
|
||||
if (queue.tasks.second?.status === 'success') {
|
||||
expect(queue.tasks.second.output).toBe('second-result');
|
||||
expect(queue.tasks.second.name).toBe('second');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -849,11 +851,11 @@ describe('queue', () => {
|
||||
handler: async () => 'result',
|
||||
});
|
||||
|
||||
expect(queue.tasks.k?.status).toBe('success');
|
||||
expect(queue.tasks.task?.status).toBe('success');
|
||||
|
||||
queue.reset('k');
|
||||
queue.reset('task');
|
||||
|
||||
expect(queue.tasks.k).toBeUndefined();
|
||||
expect(queue.tasks.task).toBeUndefined();
|
||||
});
|
||||
|
||||
it('is no-op when task is pending', async () => {
|
||||
@@ -872,11 +874,11 @@ describe('queue', () => {
|
||||
|
||||
// Wait for task to start
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
expect(queue.tasks.k?.status).toBe('pending');
|
||||
expect(queue.tasks.task?.status).toBe('pending');
|
||||
|
||||
// Reset should be no-op
|
||||
queue.reset('k');
|
||||
expect(queue.tasks.k?.status).toBe('pending');
|
||||
queue.reset('task');
|
||||
expect(queue.tasks.task?.status).toBe('pending');
|
||||
|
||||
await promise;
|
||||
});
|
||||
@@ -904,11 +906,11 @@ describe('queue', () => {
|
||||
|
||||
queue.subscribe(listener);
|
||||
|
||||
queue.reset('k');
|
||||
queue.reset('task');
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
const snapshot = listener.mock.calls[0]![0] as Record<string, unknown>;
|
||||
expect(snapshot.k).toBeUndefined();
|
||||
expect(snapshot.task).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not notify subscribers when task does not exist', () => {
|
||||
@@ -979,7 +981,7 @@ describe('queue', () => {
|
||||
const queue = createQueue();
|
||||
await queue.enqueue({ name: 'task', key: 'k', handler: async () => 'result' });
|
||||
|
||||
expect(queue.isSettled('k')).toBe(true);
|
||||
expect(queue.isSettled('task')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for error task', async () => {
|
||||
@@ -996,7 +998,7 @@ describe('queue', () => {
|
||||
|
||||
await promise.catch(() => {});
|
||||
|
||||
expect(queue.isSettled('k')).toBe(true);
|
||||
expect(queue.isSettled('task')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for pending task', async () => {
|
||||
@@ -1014,7 +1016,7 @@ describe('queue', () => {
|
||||
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
|
||||
expect(queue.isSettled('k')).toBe(false);
|
||||
expect(queue.isSettled('task')).toBe(false);
|
||||
|
||||
await promise;
|
||||
});
|
||||
@@ -1033,7 +1035,7 @@ describe('queue', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
await queue.enqueue({ name: 'task', key: 'k', handler: async () => 'result' });
|
||||
expect(queue.tasks.k?.status).toBe('success');
|
||||
expect(queue.tasks.task?.status).toBe('success');
|
||||
|
||||
queue.destroy();
|
||||
|
||||
@@ -1065,31 +1067,32 @@ describe('queue', () => {
|
||||
|
||||
const snapshot2 = queue.tasks;
|
||||
|
||||
// Snapshots should be independent
|
||||
// Tasks keyed by name - first and second are different entries
|
||||
expect(snapshot1).not.toBe(snapshot2);
|
||||
if (snapshot1.k?.status === 'success' && snapshot2.k?.status === 'success') {
|
||||
expect(snapshot1.k.output).toBe('first');
|
||||
expect(snapshot2.k.output).toBe('second');
|
||||
if (snapshot1.first?.status === 'success' && snapshot2.second?.status === 'success') {
|
||||
expect(snapshot1.first.output).toBe('first');
|
||||
expect(snapshot2.second.output).toBe('second');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('symbol keys', () => {
|
||||
it('supports symbol keys', async () => {
|
||||
it('supports symbol names', async () => {
|
||||
vi.useRealTimers();
|
||||
|
||||
const queue = createQueue();
|
||||
const key = Symbol('task');
|
||||
const name = Symbol('task');
|
||||
|
||||
await queue.enqueue({
|
||||
name: 'task',
|
||||
key,
|
||||
name: name as unknown as string,
|
||||
key: name,
|
||||
handler: async () => 'result',
|
||||
});
|
||||
|
||||
expect(queue.tasks[key]?.status).toBe('success');
|
||||
if (queue.tasks[key]?.status === 'success') {
|
||||
expect(queue.tasks[key].output).toBe('result');
|
||||
const task = queue.tasks[name as unknown as string];
|
||||
expect(task?.status).toBe('success');
|
||||
if (task?.status === 'success') {
|
||||
expect(task.output).toBe('result');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1106,7 +1109,7 @@ describe('queue', () => {
|
||||
handler: async () => 'result',
|
||||
});
|
||||
|
||||
expect(queue.tasks.k?.meta).toBeNull();
|
||||
expect(queue.tasks.task?.meta).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { AsyncStatus, ErrorTask, PendingTask, SuccessTask, Task, TasksRecord } from '../queue';
|
||||
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import { createQueue } from '../queue';
|
||||
|
||||
describe('queue types', () => {
|
||||
describe('AsyncStatus', () => {
|
||||
it('includes all status values', () => {
|
||||
const idle: AsyncStatus = 'idle';
|
||||
const pending: AsyncStatus = 'pending';
|
||||
const success: AsyncStatus = 'success';
|
||||
const error: AsyncStatus = 'error';
|
||||
|
||||
expectTypeOf(idle).toExtend<AsyncStatus>();
|
||||
expectTypeOf(pending).toExtend<AsyncStatus>();
|
||||
expectTypeOf(success).toExtend<AsyncStatus>();
|
||||
expectTypeOf(error).toExtend<AsyncStatus>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task', () => {
|
||||
it('is discriminated union of task states', () => {
|
||||
const task: Task = {} as Task;
|
||||
|
||||
if (task.status === 'pending') {
|
||||
expectTypeOf(task).toExtend<PendingTask>();
|
||||
expectTypeOf(task.abort).toExtend<AbortController>();
|
||||
}
|
||||
|
||||
if (task.status === 'success') {
|
||||
expectTypeOf(task).toExtend<SuccessTask>();
|
||||
expectTypeOf(task.output).toBeUnknown();
|
||||
expectTypeOf(task.settledAt).toBeNumber();
|
||||
}
|
||||
|
||||
if (task.status === 'error') {
|
||||
expectTypeOf(task).toExtend<ErrorTask>();
|
||||
expectTypeOf(task.error).toBeUnknown();
|
||||
expectTypeOf(task.cancelled).toBeBoolean();
|
||||
expectTypeOf(task.settledAt).toBeNumber();
|
||||
}
|
||||
});
|
||||
|
||||
it('has common properties across all states', () => {
|
||||
const task: Task = {} as Task;
|
||||
|
||||
expectTypeOf(task.id).toEqualTypeOf<symbol>();
|
||||
expectTypeOf(task.name).toEqualTypeOf<string>();
|
||||
expectTypeOf(task.key).toExtend<string | symbol>();
|
||||
expectTypeOf(task.startedAt).toBeNumber();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createQueue', () => {
|
||||
it('returns Queue with default task record', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.tasks).toExtend<TasksRecord<any>>();
|
||||
expectTypeOf(queue.destroyed).toBeBoolean();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queue methods', () => {
|
||||
it('isPending takes name parameter', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.isPending).toBeFunction();
|
||||
expectTypeOf(queue.isPending).returns.toBeBoolean();
|
||||
});
|
||||
|
||||
it('isQueued takes name parameter', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.isQueued).toBeFunction();
|
||||
expectTypeOf(queue.isQueued).returns.toBeBoolean();
|
||||
});
|
||||
|
||||
it('isSettled takes name parameter', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.isSettled).toBeFunction();
|
||||
expectTypeOf(queue.isSettled).returns.toBeBoolean();
|
||||
});
|
||||
|
||||
it('reset takes optional name parameter', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.reset).toBeFunction();
|
||||
expectTypeOf(queue.reset).returns.toBeVoid();
|
||||
});
|
||||
|
||||
it('cancel takes optional name parameter and returns boolean', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.cancel).toBeFunction();
|
||||
expectTypeOf(queue.cancel).returns.toBeBoolean();
|
||||
});
|
||||
|
||||
it('abort takes optional name parameter', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.abort).toBeFunction();
|
||||
expectTypeOf(queue.abort).returns.toBeVoid();
|
||||
});
|
||||
|
||||
it('flush takes optional name parameter and returns promise', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.flush).toBeFunction();
|
||||
expectTypeOf(queue.flush).returns.toExtend<Promise<void>>();
|
||||
});
|
||||
|
||||
it('subscribe takes listener and returns unsubscribe', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.subscribe).toBeFunction();
|
||||
expectTypeOf(queue.subscribe).returns.toExtend<() => void>();
|
||||
});
|
||||
|
||||
it('destroy returns void', () => {
|
||||
const queue = createQueue();
|
||||
|
||||
expectTypeOf(queue.destroy).toBeFunction();
|
||||
expectTypeOf(queue.destroy).returns.toBeVoid();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enqueue', () => {
|
||||
it('returns promise', async () => {
|
||||
const queue = createQueue();
|
||||
|
||||
const result = queue.enqueue({
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
handler: async () => 42,
|
||||
});
|
||||
|
||||
// Verify it's a promise by checking it has then
|
||||
expectTypeOf(result.then).toBeFunction();
|
||||
});
|
||||
|
||||
it('handler receives TaskContext with signal', async () => {
|
||||
const queue = createQueue();
|
||||
|
||||
await queue.enqueue({
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
handler: async (ctx) => {
|
||||
expectTypeOf(ctx.signal).toEqualTypeOf<AbortSignal>();
|
||||
return 'done';
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
createRequestMetaFromEvent,
|
||||
isRequestMeta,
|
||||
REQUEST_META,
|
||||
resolveRequestCancelKeys,
|
||||
resolveRequestCancel,
|
||||
resolveRequestKey,
|
||||
} from '../request';
|
||||
|
||||
@@ -128,22 +128,22 @@ describe('request', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRequestCancelKeys', () => {
|
||||
describe('resolveRequestCancelNames', () => {
|
||||
it('returns empty array for undefined', () => {
|
||||
expect(resolveRequestCancelKeys(undefined, null)).toEqual([]);
|
||||
expect(resolveRequestCancel(undefined, null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('wraps single key in array', () => {
|
||||
expect(resolveRequestCancelKeys('key', null)).toEqual(['key']);
|
||||
it('wraps single name in array', () => {
|
||||
expect(resolveRequestCancel('seek', null)).toEqual(['seek']);
|
||||
});
|
||||
|
||||
it('returns array directly', () => {
|
||||
expect(resolveRequestCancelKeys(['a', 'b'], null)).toEqual(['a', 'b']);
|
||||
expect(resolveRequestCancel(['seek', 'load'], null)).toEqual(['seek', 'load']);
|
||||
});
|
||||
|
||||
it('calls function with input', () => {
|
||||
const cancelFn = (type: string) => [`${type}-loading`, `${type}-fetch`];
|
||||
expect(resolveRequestCancelKeys(cancelFn, 'video')).toEqual(['video-loading', 'video-fetch']);
|
||||
const cancelFn = (type: string) => [`${type}Loading`, `${type}Fetch`];
|
||||
expect(resolveRequestCancel(cancelFn, 'video')).toEqual(['videoLoading', 'videoFetch']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { InferSliceRequests, InferSliceState, Slice } from '../slice';
|
||||
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import { createSlice } from '../slice';
|
||||
|
||||
interface MockTarget {
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
describe('slice types', () => {
|
||||
describe('createSlice', () => {
|
||||
it('returns Slice type with inferred state', () => {
|
||||
const slice = createSlice<MockTarget>()({
|
||||
initialState: { volume: 1, muted: false },
|
||||
getSnapshot: ({ target }) => ({ volume: target.volume, muted: target.muted }),
|
||||
subscribe: () => {},
|
||||
request: {},
|
||||
});
|
||||
|
||||
expectTypeOf(slice).toExtend<Slice<MockTarget, {
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
}, object>>();
|
||||
});
|
||||
|
||||
it('infers request handler types from simple handlers', () => {
|
||||
const _slice = createSlice<MockTarget>()({
|
||||
initialState: { volume: 1 },
|
||||
getSnapshot: ({ target }) => ({ volume: target.volume }),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
setVolume: (volume: number, { target }) => {
|
||||
target.volume = volume;
|
||||
return volume;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type Requests = InferSliceRequests<typeof _slice>;
|
||||
|
||||
expectTypeOf<Requests>().toHaveProperty('setVolume');
|
||||
|
||||
expectTypeOf<Requests['setVolume']>().toExtend<{ input: number; output: number }>();
|
||||
});
|
||||
|
||||
it('infers async request handler types', () => {
|
||||
const _slice = createSlice<MockTarget>()({
|
||||
initialState: { volume: 1 },
|
||||
getSnapshot: ({ target }) => ({ volume: target.volume }),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
asyncSetVolume: async (volume: number, { target }): Promise<number> => {
|
||||
target.volume = volume;
|
||||
return volume;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type Requests = InferSliceRequests<typeof _slice>;
|
||||
|
||||
expectTypeOf<Requests['asyncSetVolume']>().toExtend<{ input: number; output: number }>();
|
||||
});
|
||||
|
||||
it('infers request config with custom key', () => {
|
||||
const _slice = createSlice<MockTarget>()({
|
||||
initialState: { volume: 1, muted: false },
|
||||
getSnapshot: ({ target }) => ({ volume: target.volume, muted: target.muted }),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
setVolume: {
|
||||
key: 'audio',
|
||||
handler: (volume: number, { target }) => {
|
||||
target.volume = volume;
|
||||
return volume;
|
||||
},
|
||||
},
|
||||
setMuted: {
|
||||
key: 'audio',
|
||||
handler: (muted: boolean, { target }) => {
|
||||
target.muted = muted;
|
||||
return muted;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type Requests = InferSliceRequests<typeof _slice>;
|
||||
|
||||
expectTypeOf<Requests['setVolume']>().toExtend<{ input: number; output: number }>();
|
||||
expectTypeOf<Requests['setMuted']>().toExtend<{ input: boolean; output: boolean }>();
|
||||
});
|
||||
|
||||
it('infers void input when handler takes no arguments', () => {
|
||||
const _slice = createSlice<MockTarget>()({
|
||||
initialState: { muted: false },
|
||||
getSnapshot: ({ target }) => ({ muted: target.muted }),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
toggleMute: ({ target }) => {
|
||||
target.muted = !target.muted;
|
||||
return target.muted;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type Requests = InferSliceRequests<typeof _slice>;
|
||||
|
||||
expectTypeOf<Requests['toggleMute']>().toExtend<{ input: void; output: boolean }>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InferSliceState', () => {
|
||||
it('extracts state type from slice', () => {
|
||||
const _slice = createSlice<MockTarget>()({
|
||||
initialState: { volume: 1, muted: false, label: 'test' },
|
||||
getSnapshot: ({ initialState }) => initialState,
|
||||
subscribe: () => {},
|
||||
request: {},
|
||||
});
|
||||
|
||||
type State = InferSliceState<typeof _slice>;
|
||||
|
||||
expectTypeOf<State>().toEqualTypeOf<{ volume: number; muted: boolean; label: string }>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InferSliceRequests', () => {
|
||||
it('extracts request types from slice', () => {
|
||||
const _slice = createSlice<MockTarget>()({
|
||||
initialState: { volume: 1 },
|
||||
getSnapshot: ({ target }) => ({ volume: target.volume }),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
setVolume: (v: number) => v,
|
||||
reset: () => 1,
|
||||
},
|
||||
});
|
||||
|
||||
type Requests = InferSliceRequests<typeof _slice>;
|
||||
|
||||
expectTypeOf<Requests>().toHaveProperty('setVolume');
|
||||
expectTypeOf<Requests>().toHaveProperty('reset');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
import type { Queue, TasksRecord } from '../queue';
|
||||
import type { InferStoreRequests, InferStoreState, InferStoreTarget, InferStoreTasks } from '../store';
|
||||
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import { createSlice } from '../slice';
|
||||
import { createStore } from '../store';
|
||||
|
||||
interface MockTarget {
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
const audioSlice = createSlice<MockTarget>()({
|
||||
initialState: { volume: 1, muted: false },
|
||||
getSnapshot: ({ target }) => ({ volume: target.volume, muted: target.muted }),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
setVolume: (volume: number, { target }) => {
|
||||
target.volume = volume;
|
||||
return volume;
|
||||
},
|
||||
setMuted: (muted: boolean, { target }) => {
|
||||
target.muted = muted;
|
||||
return muted;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const playbackSlice = createSlice<MockTarget>()({
|
||||
initialState: { playing: false },
|
||||
getSnapshot: () => ({ playing: false }),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
play: () => true,
|
||||
pause: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
function createTestStore() {
|
||||
return createStore({ slices: [audioSlice, playbackSlice] });
|
||||
}
|
||||
|
||||
function createSingleSliceStore() {
|
||||
return createStore({ slices: [audioSlice] });
|
||||
}
|
||||
|
||||
describe('store types', () => {
|
||||
describe('createStore', () => {
|
||||
it('state has union of all slice states', () => {
|
||||
const store = createTestStore();
|
||||
|
||||
expectTypeOf(store.state.volume).toEqualTypeOf<number>();
|
||||
expectTypeOf(store.state.muted).toEqualTypeOf<boolean>();
|
||||
expectTypeOf(store.state.playing).toEqualTypeOf<boolean>();
|
||||
});
|
||||
|
||||
it('request has union of all slice requests', () => {
|
||||
const store = createTestStore();
|
||||
|
||||
expectTypeOf(store.request).toHaveProperty('setVolume');
|
||||
expectTypeOf(store.request).toHaveProperty('setMuted');
|
||||
expectTypeOf(store.request).toHaveProperty('play');
|
||||
expectTypeOf(store.request).toHaveProperty('pause');
|
||||
});
|
||||
|
||||
it('request methods have correct signatures', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
expectTypeOf(store.request.setVolume).toBeFunction();
|
||||
expectTypeOf(store.request.setVolume).parameter(0).toEqualTypeOf<number>();
|
||||
expectTypeOf(store.request.setVolume).returns.toEqualTypeOf<Promise<number>>();
|
||||
|
||||
expectTypeOf(store.request.setMuted).toBeFunction();
|
||||
expectTypeOf(store.request.setMuted).parameter(0).toEqualTypeOf<boolean>();
|
||||
expectTypeOf(store.request.setMuted).returns.toEqualTypeOf<Promise<boolean>>();
|
||||
});
|
||||
|
||||
it('queue has correctly typed tasks', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
expectTypeOf(store.queue).toExtend<Queue<any>>();
|
||||
expectTypeOf(store.queue.tasks).toExtend<TasksRecord<any>>();
|
||||
});
|
||||
|
||||
it('target is nullable before attach', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
expectTypeOf(store.target).toEqualTypeOf<MockTarget | null>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InferStoreTarget', () => {
|
||||
it('extracts target type from store', () => {
|
||||
const _store = createSingleSliceStore();
|
||||
type Target = InferStoreTarget<typeof _store>;
|
||||
const _target: Target = {} as Target;
|
||||
|
||||
expectTypeOf(_target).toEqualTypeOf<MockTarget>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InferStoreState', () => {
|
||||
it('extracts state type from store', () => {
|
||||
const _store = createTestStore();
|
||||
type State = InferStoreState<typeof _store>;
|
||||
const _state: State = {} as State;
|
||||
|
||||
expectTypeOf(_state.volume).toEqualTypeOf<number>();
|
||||
expectTypeOf(_state.muted).toEqualTypeOf<boolean>();
|
||||
expectTypeOf(_state.playing).toEqualTypeOf<boolean>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InferStoreRequests', () => {
|
||||
it('extracts request types from store', () => {
|
||||
const _store = createSingleSliceStore();
|
||||
type Requests = InferStoreRequests<typeof _store>;
|
||||
|
||||
expectTypeOf<Requests>().toHaveProperty('setVolume');
|
||||
expectTypeOf<Requests>().toHaveProperty('setMuted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('InferStoreTasks', () => {
|
||||
it('extracts task types from store', () => {
|
||||
const _store = createSingleSliceStore();
|
||||
type Tasks = InferStoreTasks<typeof _store>;
|
||||
|
||||
expectTypeOf<Tasks>().toHaveProperty('setVolume');
|
||||
expectTypeOf<Tasks>().toHaveProperty('setMuted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('listener receives full state', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.subscribe((state) => {
|
||||
expectTypeOf(state).toEqualTypeOf<{ volume: number; muted: boolean }>();
|
||||
});
|
||||
});
|
||||
|
||||
it('selector listener receives selected value', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.subscribe(
|
||||
s => s.volume,
|
||||
(volume) => {
|
||||
expectTypeOf(volume).toEqualTypeOf<number>();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store queue integration types', () => {
|
||||
it('queue.tasks has keys matching request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
expectTypeOf(store.queue.tasks).toHaveProperty('setVolume');
|
||||
expectTypeOf(store.queue.tasks).toHaveProperty('setMuted');
|
||||
});
|
||||
|
||||
it('task input type matches request parameter', () => {
|
||||
const store = createSingleSliceStore();
|
||||
const task = store.queue.tasks.setVolume;
|
||||
|
||||
if (task) {
|
||||
expectTypeOf(task.input).toEqualTypeOf<number>();
|
||||
}
|
||||
});
|
||||
|
||||
it('task output type matches request return on success', () => {
|
||||
const store = createSingleSliceStore();
|
||||
const task = store.queue.tasks.setVolume;
|
||||
|
||||
if (task?.status === 'success') {
|
||||
expectTypeOf(task.output).toEqualTypeOf<number>();
|
||||
}
|
||||
});
|
||||
|
||||
it('multi-slice store has combined queue task types', () => {
|
||||
const store = createTestStore();
|
||||
|
||||
expectTypeOf(store.queue.tasks).toHaveProperty('setVolume');
|
||||
expectTypeOf(store.queue.tasks).toHaveProperty('setMuted');
|
||||
expectTypeOf(store.queue.tasks).toHaveProperty('play');
|
||||
expectTypeOf(store.queue.tasks).toHaveProperty('pause');
|
||||
});
|
||||
|
||||
it('queue.isPending accepts request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
// Should compile - valid request names
|
||||
store.queue.isPending('setVolume');
|
||||
store.queue.isPending('setMuted');
|
||||
});
|
||||
|
||||
it('queue.isQueued accepts request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.queue.isQueued('setVolume');
|
||||
store.queue.isQueued('setMuted');
|
||||
});
|
||||
|
||||
it('queue.isSettled accepts request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.queue.isSettled('setVolume');
|
||||
store.queue.isSettled('setMuted');
|
||||
});
|
||||
|
||||
it('queue.reset accepts request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.queue.reset('setVolume');
|
||||
store.queue.reset('setMuted');
|
||||
store.queue.reset(); // all
|
||||
});
|
||||
|
||||
it('queue.cancel accepts request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.queue.cancel('setVolume');
|
||||
store.queue.cancel('setMuted');
|
||||
store.queue.cancel(); // all
|
||||
});
|
||||
|
||||
it('queue.abort accepts request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.queue.abort('setVolume');
|
||||
store.queue.abort('setMuted');
|
||||
store.queue.abort(); // all
|
||||
});
|
||||
|
||||
it('queue.flush accepts request names', () => {
|
||||
const store = createSingleSliceStore();
|
||||
|
||||
store.queue.flush('setVolume');
|
||||
store.queue.flush('setMuted');
|
||||
store.queue.flush(); // all
|
||||
});
|
||||
|
||||
it('InferStoreTasks matches queue task record keys', () => {
|
||||
const _store = createSingleSliceStore();
|
||||
type Tasks = InferStoreTasks<typeof _store>;
|
||||
|
||||
expectTypeOf<Tasks>().toHaveProperty('setVolume');
|
||||
expectTypeOf<Tasks>().toHaveProperty('setMuted');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user