mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(store): queue simplification (#302)
This commit is contained in:
@@ -28,8 +28,6 @@ export type StoreErrorCode
|
||||
| 'NO_TARGET'
|
||||
/** Guard condition returned falsy - request preconditions not met. */
|
||||
| 'REJECTED'
|
||||
/** Task was removed from queue via `dequeue()` or `clear()`. */
|
||||
| 'REMOVED'
|
||||
/** Request was replaced by a newer request with the same key. */
|
||||
| 'SUPERSEDED'
|
||||
/** Guard condition timed out waiting for a truthy result. */
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './request';
|
||||
export * from './slice';
|
||||
export * from './state';
|
||||
export * from './store';
|
||||
export * from './task';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, RequestMeta } from './request';
|
||||
import type { ErrorTask, PendingTask, SuccessTask, Task, TaskContext, TaskKey } from './task';
|
||||
|
||||
import { tryCatch } from '@videojs/utils/function';
|
||||
import { isFunction, isUndefined } from '@videojs/utils/predicate';
|
||||
import { isUndefined } from '@videojs/utils/predicate';
|
||||
|
||||
import { StoreError } from './errors';
|
||||
|
||||
@@ -9,17 +9,6 @@ import { StoreError } from './errors';
|
||||
// Types
|
||||
// ----------------------------------------
|
||||
|
||||
export type TaskKey<T = string | symbol> = T & (string | symbol);
|
||||
|
||||
export type EnsureTaskKey<T> = T extends string | symbol ? T : never;
|
||||
|
||||
/**
|
||||
* A task scheduler controls when a task flushes.
|
||||
*
|
||||
* Returns an optional cancel function.
|
||||
*/
|
||||
export type TaskScheduler = (flush: () => void) => (() => void) | void;
|
||||
|
||||
export type TaskRecord = {
|
||||
[K in TaskKey]: Request<any, any>;
|
||||
};
|
||||
@@ -28,56 +17,11 @@ export type DefaultTaskRecord = Record<TaskKey, Request<unknown, unknown>>;
|
||||
|
||||
export type EnsureTaskRecord<T> = T extends TaskRecord ? T : never;
|
||||
|
||||
export interface TaskBase<Key extends TaskKey = TaskKey, Input = unknown> {
|
||||
id: symbol;
|
||||
name: string;
|
||||
key: Key;
|
||||
input: Input;
|
||||
startedAt: number;
|
||||
meta: RequestMeta | null;
|
||||
}
|
||||
|
||||
export interface PendingTask<Key extends TaskKey = TaskKey, Input = unknown> extends TaskBase<Key, Input> {
|
||||
status: 'pending';
|
||||
abort: AbortController;
|
||||
}
|
||||
|
||||
export interface SuccessTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown> extends TaskBase<
|
||||
Key,
|
||||
Input
|
||||
> {
|
||||
status: 'success';
|
||||
settledAt: number;
|
||||
output: Output;
|
||||
}
|
||||
|
||||
export interface ErrorTask<Key extends TaskKey = TaskKey, Input = unknown> extends TaskBase<Key, Input> {
|
||||
status: 'error';
|
||||
settledAt: number;
|
||||
error: unknown;
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
export type Task<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown>
|
||||
= | PendingTask<Key, Input>
|
||||
| SuccessTask<Key, Input, Output>
|
||||
| ErrorTask<Key, Input>;
|
||||
|
||||
export type SettledTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown>
|
||||
= | SuccessTask<Key, Input, Output>
|
||||
| ErrorTask<Key, Input>;
|
||||
|
||||
export interface TaskContext<Input = unknown> {
|
||||
input: Input;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface QueueTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown> {
|
||||
name: string;
|
||||
key: Key;
|
||||
input?: Input;
|
||||
meta?: RequestMeta | null;
|
||||
schedule?: TaskScheduler | undefined;
|
||||
handler: (ctx: TaskContext<Input>) => Promise<Output>;
|
||||
}
|
||||
|
||||
@@ -87,30 +31,12 @@ interface QueuedTask<Key extends TaskKey = TaskKey, Input = unknown, Output = un
|
||||
key: Key;
|
||||
input: Input;
|
||||
meta: RequestMeta | null;
|
||||
schedule: TaskScheduler | undefined;
|
||||
handler: (ctx: TaskContext<Input>) => Promise<Output>;
|
||||
resolve: (value: Output) => void;
|
||||
reject: (error: unknown) => void;
|
||||
invalidate?: () => void;
|
||||
}
|
||||
|
||||
export interface QueueConfig<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
/** Default scheduler when task has no schedule */
|
||||
scheduler?: TaskScheduler;
|
||||
onDispatch?: <K extends keyof Tasks>(task: PendingTask<TaskKey<K>, Tasks[K]['input']>) => void;
|
||||
onSettled?: <K extends keyof Tasks>(task: SettledTask<TaskKey<K>, Tasks[K]['input'], Tasks[K]['output']>) => void;
|
||||
}
|
||||
|
||||
export interface QueuedTaskId<Key extends TaskKey = TaskKey> {
|
||||
key: Key;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type PublicQueuedRecord<Tasks extends TaskRecord> = {
|
||||
readonly [K in keyof Tasks]?: QueuedTaskId<TaskKey<K>>;
|
||||
};
|
||||
|
||||
export type QueuedRecord<Tasks extends TaskRecord> = {
|
||||
type QueuedRecord<Tasks extends TaskRecord> = {
|
||||
[K in keyof Tasks]?: QueuedTask<TaskKey<K>>;
|
||||
};
|
||||
|
||||
@@ -120,66 +46,17 @@ export type TasksRecord<Tasks extends TaskRecord> = {
|
||||
|
||||
export type QueueListener<Tasks extends TaskRecord> = (tasks: TasksRecord<Tasks>) => void;
|
||||
|
||||
// ----------------------------------------
|
||||
// Schedulers
|
||||
// ----------------------------------------
|
||||
|
||||
/**
|
||||
* Default scheduler, delay to next microtask.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide}
|
||||
*/
|
||||
export const microtask: TaskScheduler = (flush) => {
|
||||
let cancelled = false;
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (!cancelled) flush();
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Delay execution by ms. Resets on each new task.
|
||||
*
|
||||
* @param ms - Milliseconds to delay
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout}
|
||||
*/
|
||||
export function delay(ms: number): TaskScheduler {
|
||||
return (flush) => {
|
||||
const id = setTimeout(flush, ms);
|
||||
return () => clearTimeout(id);
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Implementation
|
||||
// ----------------------------------------
|
||||
|
||||
export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
readonly #scheduler: TaskScheduler;
|
||||
readonly #onDispatch: QueueConfig<Tasks>['onDispatch'];
|
||||
readonly #onSettled: QueueConfig<Tasks>['onSettled'];
|
||||
readonly #subscribers = new Set<QueueListener<Tasks>>();
|
||||
|
||||
#queued: QueuedRecord<Tasks> = {};
|
||||
#tasks: TasksRecord<Tasks> = {};
|
||||
#destroyed = false;
|
||||
|
||||
constructor(config: QueueConfig<Tasks> = {}) {
|
||||
this.#scheduler = config.scheduler ?? microtask;
|
||||
|
||||
// Wrap callbacks to catch errors and prevent breaking queue/scheduler
|
||||
const logError = (e: unknown) => console.error('[vjs-queue]', e);
|
||||
this.#onDispatch = tryCatch(config.onDispatch, logError);
|
||||
this.#onSettled = tryCatch(config.onSettled, logError);
|
||||
}
|
||||
|
||||
get queued(): Readonly<PublicQueuedRecord<Tasks>> {
|
||||
return Object.freeze({ ...this.#queued });
|
||||
}
|
||||
#flushScheduled = false;
|
||||
|
||||
get tasks(): Readonly<TasksRecord<Tasks>> {
|
||||
return Object.freeze({ ...this.#tasks });
|
||||
@@ -189,26 +66,7 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return this.#destroyed;
|
||||
}
|
||||
|
||||
isPending(name: keyof Tasks): boolean {
|
||||
return this.#tasks[name]?.status === 'pending';
|
||||
}
|
||||
|
||||
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(name: keyof Tasks): boolean {
|
||||
const task = this.#tasks[name];
|
||||
return task?.status === 'success' || task?.status === 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear settled task(s). If name provided, clears that task. If no name, clears all settled.
|
||||
*/
|
||||
/** Clear settled task(s). If name provided, clears that task. If no name, clears all settled. */
|
||||
reset(name?: keyof Tasks): void {
|
||||
if (!isUndefined(name)) {
|
||||
const task = this.#tasks[name];
|
||||
@@ -257,7 +115,7 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
enqueue<K extends keyof Tasks>(
|
||||
task: QueueTask<TaskKey<K>, Tasks[K]['input'], Tasks[K]['output']>,
|
||||
): Promise<Tasks[K]['output']> {
|
||||
const { name, key, input, schedule, meta = null, handler } = task;
|
||||
const { name, key, input, meta = null, handler } = task;
|
||||
|
||||
if (this.#destroyed) {
|
||||
return Promise.reject(new StoreError('DESTROYED'));
|
||||
@@ -265,7 +123,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
|
||||
// Supersede any queued task with the same key
|
||||
const queued = this.#queued[key];
|
||||
queued?.invalidate?.();
|
||||
queued?.reject(new StoreError('SUPERSEDED'));
|
||||
delete this.#queued[key];
|
||||
|
||||
@@ -278,99 +135,51 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
}
|
||||
|
||||
return new Promise<Tasks[K]['output']>((resolve, reject) => {
|
||||
const task: QueuedTask = {
|
||||
const queuedTask: QueuedTask = {
|
||||
id: Symbol('@videojs/task'),
|
||||
name,
|
||||
key,
|
||||
input,
|
||||
meta,
|
||||
schedule,
|
||||
handler,
|
||||
resolve,
|
||||
reject,
|
||||
};
|
||||
|
||||
this.#queued[key as keyof Tasks] = task;
|
||||
|
||||
let flushed = false;
|
||||
|
||||
try {
|
||||
const scheduleFlush = schedule ?? this.#scheduler;
|
||||
|
||||
const safeFlush = () => {
|
||||
if (flushed) return;
|
||||
flushed = true;
|
||||
this.#flushKey(key);
|
||||
};
|
||||
|
||||
const cancel = scheduleFlush(safeFlush);
|
||||
|
||||
if (!flushed && isFunction(cancel)) {
|
||||
task.invalidate = cancel;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!flushed) {
|
||||
delete this.#queued[key];
|
||||
}
|
||||
|
||||
reject(err);
|
||||
}
|
||||
this.#queued[key as keyof Tasks] = queuedTask;
|
||||
this.#scheduleFlush();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel queued task(s). If name provided, cancels that task. If no name, cancels all.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
#scheduleFlush(): void {
|
||||
if (this.#flushScheduled) return;
|
||||
|
||||
const hadQueued = Object.keys(this.#queued).length > 0;
|
||||
for (const queued of Object.values(this.#queued)) {
|
||||
queued.invalidate?.();
|
||||
queued.reject(new StoreError('REMOVED'));
|
||||
}
|
||||
|
||||
this.#queued = {};
|
||||
|
||||
return hadQueued;
|
||||
this.#flushScheduled = true;
|
||||
queueMicrotask(() => {
|
||||
this.#flushScheduled = false;
|
||||
this.#flushAll();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
#flushAll(): void {
|
||||
if (this.#destroyed) return;
|
||||
|
||||
const keys = Reflect.ownKeys(this.#queued);
|
||||
await Promise.allSettled(keys.map(k => this.#flushKey(k)));
|
||||
const keys = Reflect.ownKeys(this.#queued) as (keyof Tasks)[];
|
||||
for (const key of keys) {
|
||||
const task = this.#queued[key];
|
||||
if (task) {
|
||||
delete this.#queued[key];
|
||||
this.#executeNow(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort task(s). If name provided, aborts that task. If no name, aborts all.
|
||||
*/
|
||||
/** Abort task(s). If name provided, aborts that task. If no name, aborts all. */
|
||||
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;
|
||||
@@ -389,7 +198,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
const error = new StoreError('ABORTED');
|
||||
|
||||
for (const queued of Object.values(this.#queued)) {
|
||||
queued.invalidate?.();
|
||||
queued.reject(error);
|
||||
}
|
||||
|
||||
@@ -411,17 +219,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
this.#tasks = {};
|
||||
}
|
||||
|
||||
async #flushKey(key: keyof Tasks): Promise<void> {
|
||||
if (this.#destroyed) return;
|
||||
|
||||
const task = this.#queued[key];
|
||||
if (!task) return;
|
||||
|
||||
delete this.#queued[key];
|
||||
|
||||
await this.#executeNow(task);
|
||||
}
|
||||
|
||||
async #executeNow<K extends keyof Tasks>(
|
||||
task: QueuedTask<TaskKey<K>, Tasks[K]['input'], Tasks[K]['output']>,
|
||||
): Promise<void> {
|
||||
@@ -444,7 +241,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
// 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);
|
||||
|
||||
try {
|
||||
if (abort.signal.aborted) {
|
||||
@@ -471,8 +267,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
this.#tasks[name as keyof Tasks] = successTask;
|
||||
this.#notifySubscribers();
|
||||
}
|
||||
|
||||
this.#onSettled?.(successTask);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -489,8 +283,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
this.#tasks[name as keyof Tasks] = errorTask;
|
||||
this.#notifySubscribers();
|
||||
}
|
||||
|
||||
this.#onSettled?.(errorTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,7 +295,7 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
* Create a queue for managing task execution.
|
||||
*
|
||||
* - Same key = supersede previous (cancel queued, abort pending)
|
||||
* - Tasks scheduled via schedule function (default: microtask)
|
||||
* - Tasks batched via microtask for supersession
|
||||
*
|
||||
* @example
|
||||
* // Loose typing (default)
|
||||
@@ -516,8 +308,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
* 'volume': Request<number>;
|
||||
* }>();
|
||||
*/
|
||||
export function createQueue<Tasks extends TaskRecord = DefaultTaskRecord>(
|
||||
config: QueueConfig<Tasks> = {},
|
||||
): Queue<Tasks> {
|
||||
return new Queue<Tasks>(config);
|
||||
export function createQueue<Tasks extends TaskRecord = DefaultTaskRecord>(): Queue<Tasks> {
|
||||
return new Queue<Tasks>();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EventLike } from '@videojs/utils/events';
|
||||
import type { Guard } from './guard';
|
||||
import type { TaskKey, TaskScheduler } from './queue';
|
||||
import type { TaskKey } from './task';
|
||||
|
||||
import { isFunction, isObject } from '@videojs/utils/predicate';
|
||||
|
||||
@@ -42,7 +42,6 @@ export type RequestHandler<Target, Input = unknown, Output = unknown> = (
|
||||
|
||||
export interface RequestConfig<Target, Input = unknown, Output = unknown> {
|
||||
key?: RequestKey<Input>;
|
||||
schedule?: TaskScheduler;
|
||||
guard?: Guard<Target> | Guard<Target>[];
|
||||
cancel?: RequestCancel<Input>;
|
||||
handler: RequestHandler<Target, Input, Output>;
|
||||
@@ -50,7 +49,6 @@ export interface RequestConfig<Target, Input = unknown, Output = unknown> {
|
||||
|
||||
export interface ResolvedRequestConfig<Target, Input = unknown, Output = unknown> {
|
||||
key: RequestKey<Input>;
|
||||
schedule?: TaskScheduler | undefined;
|
||||
guard: Guard<Target>[];
|
||||
cancel?: RequestCancel<Input> | undefined;
|
||||
handler: RequestHandler<Target, Input, Output>;
|
||||
@@ -120,7 +118,6 @@ export function resolveRequests<Target, Requests extends { [K in keyof Requests]
|
||||
} else {
|
||||
resolved[name] = {
|
||||
key: config.key ?? name,
|
||||
schedule: config.schedule,
|
||||
cancel: config.cancel,
|
||||
handler: config.handler,
|
||||
guard: config.guard ? (Array.isArray(config.guard) ? config.guard : [config.guard]) : [],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { PendingTask, Task, TaskContext } from './queue';
|
||||
import type { RequestMeta, RequestMetaInit, ResolvedRequestConfig } from './request';
|
||||
import type {
|
||||
AnySlice,
|
||||
@@ -9,6 +8,7 @@ import type {
|
||||
UnionSliceTasks,
|
||||
} from './slice';
|
||||
import type { StateFactory } from './state';
|
||||
import type { PendingTask, Task, TaskContext } from './task';
|
||||
|
||||
import { getSelectorKeys } from '@videojs/utils/object';
|
||||
import { isNull } from '@videojs/utils/predicate';
|
||||
@@ -306,7 +306,6 @@ export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[
|
||||
key,
|
||||
input,
|
||||
meta,
|
||||
schedule: config.schedule,
|
||||
handler,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { RequestMeta } from './request';
|
||||
|
||||
// ----------------------------------------
|
||||
// Types
|
||||
// ----------------------------------------
|
||||
|
||||
export type TaskKey<T = string | symbol> = T & (string | symbol);
|
||||
|
||||
export type EnsureTaskKey<T> = T extends string | symbol ? T : never;
|
||||
|
||||
export interface TaskBase<Key extends TaskKey = TaskKey, Input = unknown> {
|
||||
id: symbol;
|
||||
name: string;
|
||||
key: Key;
|
||||
input: Input;
|
||||
startedAt: number;
|
||||
meta: RequestMeta | null;
|
||||
}
|
||||
|
||||
export interface PendingTask<Key extends TaskKey = TaskKey, Input = unknown> extends TaskBase<Key, Input> {
|
||||
status: 'pending';
|
||||
abort: AbortController;
|
||||
}
|
||||
|
||||
export interface SuccessTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown> extends TaskBase<
|
||||
Key,
|
||||
Input
|
||||
> {
|
||||
status: 'success';
|
||||
settledAt: number;
|
||||
output: Output;
|
||||
}
|
||||
|
||||
export interface ErrorTask<Key extends TaskKey = TaskKey, Input = unknown> extends TaskBase<Key, Input> {
|
||||
status: 'error';
|
||||
settledAt: number;
|
||||
error: unknown;
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
export type Task<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown>
|
||||
= | PendingTask<Key, Input>
|
||||
| SuccessTask<Key, Input, Output>
|
||||
| ErrorTask<Key, Input>;
|
||||
|
||||
export type SettledTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown>
|
||||
= | SuccessTask<Key, Input, Output>
|
||||
| ErrorTask<Key, Input>;
|
||||
|
||||
export interface TaskContext<Input = unknown> {
|
||||
input: Input;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Type Guards
|
||||
// ----------------------------------------
|
||||
|
||||
/** Check if task is pending (in-flight). */
|
||||
export function isPendingTask<K extends TaskKey, I, O>(task: Task<K, I, O> | undefined): task is PendingTask<K, I> {
|
||||
return task?.status === 'pending';
|
||||
}
|
||||
|
||||
/** Check if task is settled (success or error). */
|
||||
export function isSettledTask<K extends TaskKey, I, O>(task: Task<K, I, O> | undefined): task is SettledTask<K, I, O> {
|
||||
return task?.status === 'success' || task?.status === 'error';
|
||||
}
|
||||
|
||||
/** Check if task is a success. */
|
||||
export function isSuccessTask<K extends TaskKey, I, O>(task: Task<K, I, O> | undefined): task is SuccessTask<K, I, O> {
|
||||
return task?.status === 'success';
|
||||
}
|
||||
|
||||
/** Check if task is an error. */
|
||||
export function isErrorTask<K extends TaskKey, I>(task: Task<K, I> | undefined): task is ErrorTask<K, I> {
|
||||
return task?.status === 'error';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createSlice, createStore, delay } from '../../index';
|
||||
import { createSlice, createStore } from '../../index';
|
||||
|
||||
describe('store lifecycle integration', () => {
|
||||
it('full lifecycle: create → attach → use → detach → destroy', async () => {
|
||||
@@ -54,9 +54,7 @@ describe('store lifecycle integration', () => {
|
||||
expect(store.destroyed).toBe(true);
|
||||
});
|
||||
|
||||
it('request with guards and scheduling', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
it('request with guards', async () => {
|
||||
let ready = false;
|
||||
const isReady = () => ready;
|
||||
|
||||
@@ -65,8 +63,7 @@ describe('store lifecycle integration', () => {
|
||||
getSnapshot: () => ({}),
|
||||
subscribe: () => {},
|
||||
request: {
|
||||
delayedAction: {
|
||||
schedule: delay(100),
|
||||
guardedAction: {
|
||||
guard: [isReady],
|
||||
handler: (_, _ctx) => 'completed',
|
||||
},
|
||||
@@ -81,17 +78,13 @@ describe('store lifecycle integration', () => {
|
||||
store.attach({});
|
||||
|
||||
// Test 1: Guard rejects when not ready
|
||||
const failPromise = store.request.delayedAction().catch(e => e);
|
||||
await vi.runAllTimersAsync();
|
||||
const failPromise = store.request.guardedAction().catch(e => e);
|
||||
await expect(failPromise).resolves.toMatchObject({ code: 'REJECTED' });
|
||||
|
||||
// Test 2: Guard passes when ready
|
||||
ready = true;
|
||||
const successPromise = store.request.delayedAction();
|
||||
await vi.runAllTimersAsync();
|
||||
const successPromise = store.request.guardedAction();
|
||||
await expect(successPromise).resolves.toBe('completed');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +1,10 @@
|
||||
import type { ErrorTask, PendingTask, SuccessTask, Task, TasksRecord } from '../queue';
|
||||
import type { TasksRecord } from '../queue';
|
||||
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import { createQueue } from '../queue';
|
||||
|
||||
describe('queue types', () => {
|
||||
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();
|
||||
@@ -47,27 +15,6 @@ describe('queue types', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -75,13 +22,6 @@ describe('queue types', () => {
|
||||
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();
|
||||
|
||||
@@ -89,13 +29,6 @@ describe('queue types', () => {
|
||||
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();
|
||||
|
||||
|
||||
@@ -49,10 +49,6 @@ describe('slice', () => {
|
||||
it('preserves full config options', () => {
|
||||
const guard = () => true;
|
||||
|
||||
const schedule = (flush: () => void) => {
|
||||
setTimeout(flush, 100);
|
||||
};
|
||||
|
||||
const slice = createSlice({
|
||||
initialState: {},
|
||||
getSnapshot: () => ({}),
|
||||
@@ -61,7 +57,6 @@ describe('slice', () => {
|
||||
configured: {
|
||||
key: 'custom-key',
|
||||
guard: [guard],
|
||||
schedule,
|
||||
handler: () => {},
|
||||
},
|
||||
},
|
||||
@@ -69,7 +64,6 @@ describe('slice', () => {
|
||||
|
||||
expect(slice.request.configured.key).toBe('custom-key');
|
||||
expect(slice.request.configured.guard).toEqual([guard]);
|
||||
expect(slice.request.configured.schedule).toBe(schedule);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,28 +188,6 @@ describe('store types', () => {
|
||||
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();
|
||||
|
||||
@@ -218,14 +196,6 @@ describe('store types', () => {
|
||||
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();
|
||||
|
||||
@@ -234,14 +204,6 @@ describe('store types', () => {
|
||||
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>;
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import type { ErrorTask, PendingTask, SuccessTask, Task } from '../task';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isErrorTask, isPendingTask, isSettledTask, isSuccessTask } from '../task';
|
||||
|
||||
describe('task', () => {
|
||||
describe('isPendingTask', () => {
|
||||
it('returns true for pending task', () => {
|
||||
const task: Task = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'pending',
|
||||
abort: new AbortController(),
|
||||
};
|
||||
|
||||
expect(isPendingTask(task)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for success task', () => {
|
||||
const task: Task = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'success',
|
||||
settledAt: Date.now(),
|
||||
output: 'result',
|
||||
};
|
||||
|
||||
expect(isPendingTask(task)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for error task', () => {
|
||||
const task: Task = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'error',
|
||||
settledAt: Date.now(),
|
||||
error: new Error('test'),
|
||||
cancelled: false,
|
||||
};
|
||||
|
||||
expect(isPendingTask(task)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isPendingTask(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSettledTask', () => {
|
||||
it('returns true for success task', () => {
|
||||
const task: Task = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'success',
|
||||
settledAt: Date.now(),
|
||||
output: 'result',
|
||||
};
|
||||
|
||||
expect(isSettledTask(task)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for error task', () => {
|
||||
const task: Task = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'error',
|
||||
settledAt: Date.now(),
|
||||
error: new Error('test'),
|
||||
cancelled: false,
|
||||
};
|
||||
|
||||
expect(isSettledTask(task)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for pending task', () => {
|
||||
const task: Task = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'pending',
|
||||
abort: new AbortController(),
|
||||
};
|
||||
|
||||
expect(isSettledTask(task)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isSettledTask(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSuccessTask', () => {
|
||||
it('returns true for success task', () => {
|
||||
const task: SuccessTask = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'success',
|
||||
settledAt: Date.now(),
|
||||
output: 'result',
|
||||
};
|
||||
|
||||
expect(isSuccessTask(task)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for error task', () => {
|
||||
const task: ErrorTask = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'error',
|
||||
settledAt: Date.now(),
|
||||
error: new Error('test'),
|
||||
cancelled: false,
|
||||
};
|
||||
|
||||
expect(isSuccessTask(task)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for pending task', () => {
|
||||
const task: PendingTask = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'pending',
|
||||
abort: new AbortController(),
|
||||
};
|
||||
|
||||
expect(isSuccessTask(task)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isSuccessTask(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isErrorTask', () => {
|
||||
it('returns true for error task', () => {
|
||||
const task: ErrorTask = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'error',
|
||||
settledAt: Date.now(),
|
||||
error: new Error('test'),
|
||||
cancelled: false,
|
||||
};
|
||||
|
||||
expect(isErrorTask(task)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for cancelled error task', () => {
|
||||
const task: ErrorTask = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'error',
|
||||
settledAt: Date.now(),
|
||||
error: new Error('aborted'),
|
||||
cancelled: true,
|
||||
};
|
||||
|
||||
expect(isErrorTask(task)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for success task', () => {
|
||||
const task: SuccessTask = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'success',
|
||||
settledAt: Date.now(),
|
||||
output: 'result',
|
||||
};
|
||||
|
||||
expect(isErrorTask(task)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for pending task', () => {
|
||||
const task: PendingTask = {
|
||||
id: Symbol('task'),
|
||||
name: 'test',
|
||||
key: 'test',
|
||||
input: undefined,
|
||||
startedAt: Date.now(),
|
||||
meta: null,
|
||||
status: 'pending',
|
||||
abort: new AbortController(),
|
||||
};
|
||||
|
||||
expect(isErrorTask(task)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isErrorTask(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ErrorTask, PendingTask, SuccessTask, Task } from '../task';
|
||||
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import { isErrorTask, isPendingTask, isSettledTask, isSuccessTask } from '../task';
|
||||
|
||||
describe('task types', () => {
|
||||
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('type guards', () => {
|
||||
it('isPendingTask narrows to PendingTask', () => {
|
||||
const task: Task = {} as Task;
|
||||
|
||||
if (isPendingTask(task)) {
|
||||
expectTypeOf(task).toExtend<PendingTask>();
|
||||
}
|
||||
});
|
||||
|
||||
it('isSettledTask narrows to SuccessTask | ErrorTask', () => {
|
||||
const task: Task = {} as Task;
|
||||
|
||||
if (isSettledTask(task)) {
|
||||
expectTypeOf(task.settledAt).toBeNumber();
|
||||
}
|
||||
});
|
||||
|
||||
it('isSuccessTask narrows to SuccessTask', () => {
|
||||
const task: Task = {} as Task;
|
||||
|
||||
if (isSuccessTask(task)) {
|
||||
expectTypeOf(task).toExtend<SuccessTask>();
|
||||
expectTypeOf(task.output).toBeUnknown();
|
||||
}
|
||||
});
|
||||
|
||||
it('isErrorTask narrows to ErrorTask', () => {
|
||||
const task: Task = {} as Task;
|
||||
|
||||
if (isErrorTask(task)) {
|
||||
expectTypeOf(task).toExtend<ErrorTask>();
|
||||
expectTypeOf(task.error).toBeUnknown();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { idle, raf } from './schedulers';
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { TaskScheduler } from '../core/queue';
|
||||
|
||||
import { animationFrame, idleCallback } from '@videojs/utils/dom';
|
||||
|
||||
/**
|
||||
* Scheduler using `requestAnimationFrame`. Ideal for UI updates.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* queue.enqueue({ key: 'ui', schedule: raf(), handler: async () => {} });
|
||||
* ```
|
||||
*/
|
||||
export function raf(): TaskScheduler {
|
||||
return flush => animationFrame(flush);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduler using `requestIdleCallback`. Ideal for background work.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* queue.enqueue({ key: 'bg', schedule: idle({ timeout: 2000 }), handler: async () => {} });
|
||||
* ```
|
||||
*/
|
||||
export function idle(options?: IdleRequestOptions): TaskScheduler {
|
||||
return flush => idleCallback(flush, options);
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createQueue } from '../../core/queue';
|
||||
import { idle, raf } from '../schedulers';
|
||||
|
||||
describe('dom schedulers', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('raf', () => {
|
||||
it('creates a TaskScheduler', () => {
|
||||
const scheduler = raf();
|
||||
expect(scheduler).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('schedules flush on animation frame', async () => {
|
||||
const flush = vi.fn();
|
||||
const scheduler = raf();
|
||||
|
||||
scheduler(flush);
|
||||
|
||||
expect(flush).not.toHaveBeenCalled();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(flush).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('returns cancel function', async () => {
|
||||
const flush = vi.fn();
|
||||
const scheduler = raf();
|
||||
|
||||
const cancel = scheduler(flush);
|
||||
|
||||
expect(cancel).toBeTypeOf('function');
|
||||
cancel!();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(flush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('works with queue', async () => {
|
||||
const queue = createQueue();
|
||||
const handler = vi.fn().mockResolvedValue('result');
|
||||
|
||||
const promise = queue.enqueue({
|
||||
name: 'raf-task',
|
||||
key: 'raf',
|
||||
schedule: raf(),
|
||||
handler,
|
||||
});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('result');
|
||||
});
|
||||
});
|
||||
|
||||
describe('idle', () => {
|
||||
it('creates a TaskScheduler', () => {
|
||||
const scheduler = idle();
|
||||
expect(scheduler).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('schedules flush when idle', async () => {
|
||||
const flush = vi.fn();
|
||||
const scheduler = idle();
|
||||
|
||||
scheduler(flush);
|
||||
|
||||
expect(flush).not.toHaveBeenCalled();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(flush).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('returns cancel function', async () => {
|
||||
const flush = vi.fn();
|
||||
const scheduler = idle();
|
||||
|
||||
const cancel = scheduler(flush);
|
||||
|
||||
expect(cancel).toBeTypeOf('function');
|
||||
cancel!();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(flush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts options', async () => {
|
||||
const flush = vi.fn();
|
||||
const scheduler = idle({ timeout: 1000 });
|
||||
|
||||
scheduler(flush);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
expect(flush).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('works with queue', async () => {
|
||||
const queue = createQueue();
|
||||
const handler = vi.fn().mockResolvedValue('idle-result');
|
||||
|
||||
const promise = queue.enqueue({
|
||||
name: 'idle-task',
|
||||
key: 'idle',
|
||||
schedule: idle(),
|
||||
handler,
|
||||
});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('idle-result');
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration', () => {
|
||||
it('different schedulers can coexist in same queue', async () => {
|
||||
const queue = createQueue();
|
||||
const order: string[] = [];
|
||||
|
||||
queue.enqueue({
|
||||
name: 'raf-task',
|
||||
key: 'raf',
|
||||
schedule: raf(),
|
||||
handler: async () => {
|
||||
order.push('raf');
|
||||
},
|
||||
});
|
||||
|
||||
queue.enqueue({
|
||||
name: 'idle-task',
|
||||
key: 'idle',
|
||||
schedule: idle(),
|
||||
handler: async () => {
|
||||
order.push('idle');
|
||||
},
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(order).toContain('raf');
|
||||
expect(order).toContain('idle');
|
||||
});
|
||||
|
||||
it('superseding works with raf scheduler', async () => {
|
||||
const queue = createQueue();
|
||||
const first = vi.fn().mockResolvedValue('first');
|
||||
const second = vi.fn().mockResolvedValue('second');
|
||||
|
||||
const promise1 = queue.enqueue({
|
||||
name: 'first',
|
||||
key: 'shared',
|
||||
schedule: raf(),
|
||||
handler: first,
|
||||
});
|
||||
|
||||
const promise2 = queue.enqueue({
|
||||
name: 'second',
|
||||
key: 'shared',
|
||||
schedule: raf(),
|
||||
handler: second,
|
||||
});
|
||||
|
||||
// Handle the rejection immediately to avoid unhandled rejection warning
|
||||
promise1.catch(() => {});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
await expect(promise1).rejects.toThrow();
|
||||
await expect(promise2).resolves.toBe('second');
|
||||
expect(first).not.toHaveBeenCalled();
|
||||
expect(second).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"declarationDir": "../../types/dom"
|
||||
},
|
||||
"references": [{ "path": "../.." }],
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactiveController, ReactiveControllerHost } from '@lit/reactive-element';
|
||||
import type { EnsureFunction } from '@videojs/utils/types';
|
||||
import type { Task } from '../../core/queue';
|
||||
import type { AnyStore, InferStoreRequests } from '../../core/store';
|
||||
import type { Task } from '../../core/task';
|
||||
import type { MutationResult } from '../../shared/types';
|
||||
import type { StoreSource } from '../store-accessor';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactiveController, ReactiveControllerHost } from '@lit/reactive-element';
|
||||
import type { EnsureFunction } from '@videojs/utils/types';
|
||||
import type { Task } from '../../core/queue';
|
||||
import type { AnyStore, InferStoreRequests, InferStoreState } from '../../core/store';
|
||||
import type { Task } from '../../core/task';
|
||||
import type { OptimisticResult } from '../../shared/types';
|
||||
import type { StoreSource } from '../store-accessor';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EnsureFunction } from '@videojs/utils/types';
|
||||
import type { Task } from '../../core/queue';
|
||||
import type { AnyStore, InferStoreRequests } from '../../core/store';
|
||||
import type { Task } from '../../core/task';
|
||||
import type { MutationResult } from '../../shared/types';
|
||||
|
||||
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EnsureFunction } from '@videojs/utils/types';
|
||||
import type { Task } from '../../core/queue';
|
||||
import type { AnyStore, InferStoreRequests, InferStoreState } from '../../core/store';
|
||||
import type { Task } from '../../core/task';
|
||||
import type { OptimisticResult } from '../../shared/types';
|
||||
|
||||
import { useCallback, useReducer, useRef, useSyncExternalStore } from 'react';
|
||||
|
||||
Reference in New Issue
Block a user