mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
chore(packages): remove isolatedDeclarations for store type inference support (#295)
This commit is contained in:
@@ -199,8 +199,8 @@ Before writing new helpers, check `@videojs/utils` for existing utilities.
|
||||
Always return `value is Type` for proper type narrowing:
|
||||
|
||||
```ts
|
||||
function isStoreError(error: unknown): error is StoreError {
|
||||
return error instanceof StoreError;
|
||||
function isStoreError(value: unknown): value is StoreError {
|
||||
return value instanceof StoreError;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -243,3 +243,84 @@ destroy(): void {
|
||||
this.#subscribers.clear();
|
||||
}
|
||||
```
|
||||
|
||||
### No Hungarian Type Notation
|
||||
|
||||
Never prefix type parameters with `T`. Use descriptive names instead:
|
||||
|
||||
```ts
|
||||
// Bad
|
||||
type Mixin<TBase extends Constructor> = ...
|
||||
function createStore<TSlices extends AnySlice[]>(...) { ... }
|
||||
|
||||
// Good
|
||||
type Mixin<Base extends Constructor> = ...
|
||||
function createStore<Slices extends AnySlice[]>(...) { ... }
|
||||
```
|
||||
|
||||
### No Obvious Comments
|
||||
|
||||
Don't write comments that restate what the code does. Comments should explain _why_, not _what_:
|
||||
|
||||
```ts
|
||||
// Bad
|
||||
// Create the store
|
||||
const store = createStore(config);
|
||||
|
||||
// Loop through items
|
||||
for (const item of items) { ... }
|
||||
|
||||
// Good
|
||||
// Create store before rendering to allow pre-hydration
|
||||
const store = createStore(config);
|
||||
```
|
||||
|
||||
### No Pointless Type Casts
|
||||
|
||||
Avoid casts that don't add value. If TypeScript can infer the type, don't cast:
|
||||
|
||||
```ts
|
||||
// Bad - already typed
|
||||
const value = someFunction() as SomeType;
|
||||
|
||||
// Bad - use generic type argument
|
||||
const media = node.querySelector('video, audio') as HTMLMediaElement | null;
|
||||
```
|
||||
|
||||
### Minimal JSDoc
|
||||
|
||||
JSDoc should add value, not restate what TypeScript already shows:
|
||||
|
||||
**No redundant @param/@returns** — TypeScript signatures are the documentation:
|
||||
|
||||
```ts
|
||||
// Bad
|
||||
/**
|
||||
* @param callback - The callback to invoke
|
||||
* @returns A cleanup function
|
||||
*/
|
||||
export function animationFrame(callback: FrameRequestCallback): () => void
|
||||
|
||||
// Good
|
||||
/** Request an animation frame with cleanup. */
|
||||
export function animationFrame(callback: FrameRequestCallback): () => void
|
||||
```
|
||||
|
||||
**Single JSDoc for overloads** — Document the first overload only:
|
||||
|
||||
```ts
|
||||
/** Wait for an event to occur on a target. */
|
||||
export function onEvent<K extends keyof HTMLMediaElementEventMap>(...): Promise<...>;
|
||||
export function onEvent<K extends keyof HTMLElementEventMap>(...): Promise<...>;
|
||||
```
|
||||
|
||||
**One example per function** — Consolidate into a single representative example.
|
||||
|
||||
**No JSDoc for self-documenting code** — Skip JSDoc when names are clear:
|
||||
|
||||
```ts
|
||||
// No JSDoc needed
|
||||
export function supportsIdleCallback(): boolean { ... }
|
||||
get size(): number { ... }
|
||||
add(cleanup: CleanupFn): void { ... }
|
||||
```
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.1.0",
|
||||
"tsdown": "^0.15.9",
|
||||
"tsdown": "^0.15.12",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
|
||||
@@ -11,9 +11,9 @@ import { listen, serializeTimeRanges } from '@videojs/utils/dom';
|
||||
export const bufferSlice = createSlice<HTMLMediaElement>()({
|
||||
initialState: {
|
||||
/** Buffered time ranges as [start, end] tuples. */
|
||||
buffered: [] as Array<[number, number]>,
|
||||
buffered: [] as [number, number][],
|
||||
/** Seekable time ranges as [start, end] tuples. */
|
||||
seekable: [] as Array<[number, number]>,
|
||||
seekable: [] as [number, number][],
|
||||
},
|
||||
|
||||
getSnapshot: ({ target }) => ({
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"declarationDir": "../../types/dom",
|
||||
"isolatedDeclarations": false
|
||||
"declarationDir": "../../types/dom"
|
||||
},
|
||||
"references": [{ "path": "../.." }],
|
||||
"include": ["./**/*.ts"]
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declarationDir": "types",
|
||||
"isolatedDeclarations": false
|
||||
"declarationDir": "types"
|
||||
},
|
||||
"references": [{ "path": "../utils" }, { "path": "../store" }],
|
||||
"include": ["src/core/**/*.ts"]
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@videojs/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.15.9",
|
||||
"tsdown": "^0.15.12",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -21,7 +21,5 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': new URL('./src', import.meta.url).pathname,
|
||||
},
|
||||
dts: {
|
||||
oxc: true,
|
||||
},
|
||||
dts: true,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"files": [],
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.15.9",
|
||||
"tsdown": "^0.15.12",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
"react": "^18.0.0",
|
||||
"tsdown": "^0.15.9",
|
||||
"tsdown": "^0.15.12",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -13,7 +13,5 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': new URL('./src', import.meta.url).pathname,
|
||||
},
|
||||
dts: {
|
||||
oxc: true,
|
||||
},
|
||||
dts: true,
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"jsdom": "^26.1.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"tsdown": "^0.15.9",
|
||||
"tsdown": "^0.15.12",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
|
||||
@@ -20,26 +20,14 @@ export type EnsureTaskKey<T> = T extends string | symbol ? T : never;
|
||||
*/
|
||||
export type TaskScheduler = (flush: () => void) => (() => void) | void;
|
||||
|
||||
/**
|
||||
* Map of task key -> input/output types.
|
||||
*/
|
||||
export type TaskRecord = {
|
||||
[K in TaskKey]: Request<any, any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default loose task types.
|
||||
*/
|
||||
export type DefaultTaskRecord = Record<TaskKey, Request<unknown, unknown>>;
|
||||
|
||||
/**
|
||||
* Ensure T is a TaskRecord.
|
||||
*/
|
||||
export type EnsureTaskRecord<T> = T extends TaskRecord ? T : never;
|
||||
|
||||
/**
|
||||
* Base fields shared by all task states.
|
||||
*/
|
||||
export interface TaskBase<Key extends TaskKey = TaskKey, Input = unknown> {
|
||||
id: symbol;
|
||||
name: string;
|
||||
@@ -49,17 +37,11 @@ export interface TaskBase<Key extends TaskKey = TaskKey, Input = unknown> {
|
||||
meta: RequestMeta | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending task - request in flight.
|
||||
*/
|
||||
export interface PendingTask<Key extends TaskKey = TaskKey, Input = unknown> extends TaskBase<Key, Input> {
|
||||
status: 'pending';
|
||||
abort: AbortController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Success task - completed successfully.
|
||||
*/
|
||||
export interface SuccessTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown> extends TaskBase<
|
||||
Key,
|
||||
Input
|
||||
@@ -69,9 +51,6 @@ export interface SuccessTask<Key extends TaskKey = TaskKey, Input = unknown, Out
|
||||
output: Output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error task - failed or cancelled.
|
||||
*/
|
||||
export interface ErrorTask<Key extends TaskKey = TaskKey, Input = unknown> extends TaskBase<Key, Input> {
|
||||
status: 'error';
|
||||
settledAt: number;
|
||||
@@ -79,32 +58,20 @@ export interface ErrorTask<Key extends TaskKey = TaskKey, Input = unknown> exten
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task with status discriminator.
|
||||
*/
|
||||
export type Task<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown>
|
||||
= | PendingTask<Key, Input>
|
||||
| SuccessTask<Key, Input, Output>
|
||||
| ErrorTask<Key, Input>;
|
||||
|
||||
/**
|
||||
* Settled task (success or error).
|
||||
*/
|
||||
export type SettledTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown>
|
||||
= | SuccessTask<Key, Input, Output>
|
||||
| ErrorTask<Key, Input>;
|
||||
|
||||
/**
|
||||
* Context passed to task handler.
|
||||
*/
|
||||
export interface TaskContext<Input = unknown> {
|
||||
input: Input;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task to enqueue.
|
||||
*/
|
||||
export interface QueueTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown> {
|
||||
name: string;
|
||||
key: Key;
|
||||
@@ -114,9 +81,6 @@ export interface QueueTask<Key extends TaskKey = TaskKey, Input = unknown, Outpu
|
||||
handler: (ctx: TaskContext<Input>) => Promise<Output>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queued task waiting to execute.
|
||||
*/
|
||||
interface QueuedTask<Key extends TaskKey = TaskKey, Input = unknown, Output = unknown> {
|
||||
id: symbol;
|
||||
name: string;
|
||||
@@ -127,7 +91,6 @@ interface QueuedTask<Key extends TaskKey = TaskKey, Input = unknown, Output = un
|
||||
handler: (ctx: TaskContext<Input>) => Promise<Output>;
|
||||
resolve: (value: Output) => void;
|
||||
reject: (error: unknown) => void;
|
||||
/* Cancel scheduled execution. */
|
||||
invalidate?: () => void;
|
||||
}
|
||||
|
||||
@@ -151,18 +114,10 @@ export type QueuedRecord<Tasks extends TaskRecord> = {
|
||||
[K in keyof Tasks]?: QueuedTask<TaskKey<K>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of task key -> task (pending, success, or error).
|
||||
*/
|
||||
export type TasksRecord<Tasks extends TaskRecord> = {
|
||||
[K in keyof Tasks]?: Task<TaskKey<K>, Tasks[K]['input'], Tasks[K]['output']>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Listener callback for task state changes.
|
||||
*
|
||||
* Called when tasks are dispatched, settled, or reset.
|
||||
*/
|
||||
export type QueueListener<Tasks extends TaskRecord> = (tasks: TasksRecord<Tasks>) => void;
|
||||
|
||||
// ----------------------------------------
|
||||
@@ -226,9 +181,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return Object.freeze({ ...this.#queued });
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of task key -> task (pending, success, or error).
|
||||
*/
|
||||
get tasks(): Readonly<TasksRecord<Tasks>> {
|
||||
return Object.freeze({ ...this.#tasks });
|
||||
}
|
||||
@@ -237,35 +189,21 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return this.#destroyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task with the given key is currently pending (executing).
|
||||
*/
|
||||
isPending(key: keyof Tasks): boolean {
|
||||
return this.#tasks[key]?.status === 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task with the given key is currently queued (waiting to execute).
|
||||
*/
|
||||
isQueued(key: keyof Tasks): boolean {
|
||||
return key in this.#queued;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task with the given key is settled (success or error).
|
||||
*/
|
||||
isSettled(key: keyof Tasks): boolean {
|
||||
const task = this.#tasks[key];
|
||||
return task?.status === 'success' || task?.status === 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear settled task(s).
|
||||
*
|
||||
* - If key provided: clears that specific settled task (no-op if pending or doesn't exist)
|
||||
* - If no key: clears all settled tasks (pending tasks are preserved)
|
||||
*
|
||||
* @param key - Optional task key to reset. If omitted, resets all settled tasks.
|
||||
* Clear settled task(s). If key provided, clears that task. If no key, clears all settled.
|
||||
*/
|
||||
reset(key?: keyof Tasks): void {
|
||||
if (!isUndefined(key)) {
|
||||
@@ -278,7 +216,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset all settled tasks
|
||||
let cleared = false;
|
||||
for (const key of Reflect.ownKeys(this.#tasks)) {
|
||||
const task = this.#tasks[key];
|
||||
@@ -293,14 +230,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to task state changes.
|
||||
*
|
||||
* Fires when tasks are dispatched, settled, or reset.
|
||||
*
|
||||
* @param listener - Callback receiving the current tasks map
|
||||
* @returns Unsubscribe function
|
||||
*/
|
||||
subscribe(listener: QueueListener<Tasks>): () => void {
|
||||
this.#subscribers.add(listener);
|
||||
return () => {
|
||||
@@ -330,19 +259,16 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return Promise.reject(new StoreError('DESTROYED'));
|
||||
}
|
||||
|
||||
// Cancel any queued task with same key
|
||||
const queued = this.#queued[key];
|
||||
queued?.invalidate?.();
|
||||
queued?.reject(new StoreError('SUPERSEDED'));
|
||||
delete this.#queued[key];
|
||||
|
||||
// Abort any pending task with same key
|
||||
const existing = this.#tasks[key];
|
||||
if (existing?.status === 'pending') {
|
||||
existing.abort.abort(new StoreError('SUPERSEDED'));
|
||||
}
|
||||
|
||||
// Clear any settled task for this key (new request replaces it)
|
||||
delete this.#tasks[key];
|
||||
|
||||
return new Promise<Tasks[K]['output']>((resolve, reject) => {
|
||||
@@ -365,7 +291,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
try {
|
||||
const scheduleFlush = schedule ?? this.#scheduler;
|
||||
|
||||
// Guard against multiple flushes
|
||||
const safeFlush = () => {
|
||||
if (flushed) return;
|
||||
flushed = true;
|
||||
@@ -374,7 +299,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
|
||||
const cancel = scheduleFlush(safeFlush);
|
||||
|
||||
// Only set invalidate if we haven't already flushed
|
||||
if (!flushed && isFunction(cancel)) {
|
||||
task.invalidate = cancel;
|
||||
}
|
||||
@@ -389,13 +313,7 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel queued task(s) waiting to execute.
|
||||
*
|
||||
* - If key provided: cancels that specific queued task
|
||||
* - If no key: cancels all queued tasks
|
||||
*
|
||||
* @param key - Optional task key to cancel
|
||||
* @returns true if any task was cancelled
|
||||
* Cancel queued task(s). If key provided, cancels that task. If no key, cancels all.
|
||||
*/
|
||||
cancel(key?: keyof Tasks): boolean {
|
||||
if (!isUndefined(key)) {
|
||||
@@ -409,7 +327,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cancel all queued
|
||||
const hadQueued = Object.keys(this.#queued).length > 0;
|
||||
for (const queued of Object.values(this.#queued)) {
|
||||
queued.invalidate?.();
|
||||
@@ -427,28 +344,20 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Flush all
|
||||
const keys = Reflect.ownKeys(this.#queued);
|
||||
await Promise.allSettled(keys.map(k => this.#flushKey(k)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort task(s) - both queued (waiting) and pending (executing).
|
||||
*
|
||||
* - If key provided: aborts that specific task
|
||||
* - If no key: aborts all tasks
|
||||
*
|
||||
* @param key - Optional task key to abort
|
||||
* Abort task(s). If key provided, aborts that task. If no key, aborts all.
|
||||
*/
|
||||
abort(key?: keyof Tasks): void {
|
||||
if (!isUndefined(key)) {
|
||||
// Reject queued
|
||||
const queued = this.#queued[key];
|
||||
queued?.invalidate?.();
|
||||
queued?.reject(new StoreError('ABORTED'));
|
||||
delete this.#queued[key];
|
||||
|
||||
// Abort pending task
|
||||
const task = this.#tasks[key];
|
||||
if (task?.status === 'pending') {
|
||||
task.abort.abort(new StoreError('ABORTED'));
|
||||
@@ -457,10 +366,8 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort all
|
||||
const error = new StoreError('ABORTED');
|
||||
|
||||
// Reject all queued
|
||||
for (const queued of Object.values(this.#queued)) {
|
||||
queued.invalidate?.();
|
||||
queued.reject(error);
|
||||
@@ -468,7 +375,6 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
|
||||
this.#queued = {};
|
||||
|
||||
// Abort all pending tasks
|
||||
for (const task of Object.values(this.#tasks)) {
|
||||
if (task?.status === 'pending') {
|
||||
task.abort.abort(error);
|
||||
@@ -540,7 +446,7 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
};
|
||||
|
||||
// Only update if we're still the current task for this key
|
||||
if (this.#tasks[key as keyof Tasks] === pendingTask) {
|
||||
if (this.#tasks[key] === pendingTask) {
|
||||
this.#tasks[key as keyof Tasks] = successTask;
|
||||
this.#notifySubscribers();
|
||||
}
|
||||
@@ -558,7 +464,7 @@ export class Queue<Tasks extends TaskRecord = DefaultTaskRecord> {
|
||||
};
|
||||
|
||||
// Only update if we're still the current task for this key
|
||||
if (this.#tasks[key as keyof Tasks] === pendingTask) {
|
||||
if (this.#tasks[key] === pendingTask) {
|
||||
this.#tasks[key as keyof Tasks] = errorTask;
|
||||
this.#notifySubscribers();
|
||||
}
|
||||
|
||||
@@ -23,41 +23,23 @@ export type RequestRecord = {
|
||||
[K in string]: Request<any, any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default loose request types.
|
||||
*/
|
||||
export type DefaultRequestRecord = Record<string, Request>;
|
||||
|
||||
/**
|
||||
* Context passed to request handlers.
|
||||
*/
|
||||
export interface RequestContext<Target> {
|
||||
target: Target;
|
||||
signal: AbortSignal;
|
||||
meta: RequestMeta | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request key - static or derived from input.
|
||||
*/
|
||||
export type RequestKey<Input = unknown> = TaskKey | ((input: Input) => TaskKey);
|
||||
|
||||
/**
|
||||
* Request cancel config.
|
||||
*/
|
||||
export type RequestCancel<Input = unknown> = TaskKey | TaskKey[] | ((input: Input) => TaskKey | TaskKey[]);
|
||||
|
||||
/**
|
||||
* Request handler function.
|
||||
*/
|
||||
export type RequestHandler<Target, Input = unknown, Output = unknown> = (
|
||||
input: Input,
|
||||
ctx: RequestContext<Target>,
|
||||
) => Output | Promise<Output>;
|
||||
|
||||
/**
|
||||
* Full request config.
|
||||
*/
|
||||
export interface RequestConfig<Target, Input = unknown, Output = unknown> {
|
||||
key?: RequestKey<Input>;
|
||||
schedule?: TaskScheduler;
|
||||
@@ -66,9 +48,6 @@ export interface RequestConfig<Target, Input = unknown, Output = unknown> {
|
||||
handler: RequestHandler<Target, Input, Output>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved request config (after normalization).
|
||||
*/
|
||||
export interface ResolvedRequestConfig<Target, Input = unknown, Output = unknown> {
|
||||
key: RequestKey<Input>;
|
||||
schedule?: TaskScheduler | undefined;
|
||||
@@ -81,19 +60,12 @@ export type RequestHandlerRecord = {
|
||||
[K in string]: RequestHandler<any, any, any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of request names to handlers or configs. This is the config passed to `createSlice`.
|
||||
*/
|
||||
export type RequestConfigMap<Target, Requests extends { [K in keyof Requests]: Request<any, any> }> = {
|
||||
[K in keyof Requests]: Requests[K] extends Request<infer I, infer O>
|
||||
? RequestHandler<Target, I, O> | RequestConfig<Target, I, O>
|
||||
: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of request config objects to resolved configs. This is the config stored internally in
|
||||
* the store.
|
||||
*/
|
||||
export type ResolvedRequestConfigMap<Target, Requests extends { [K in keyof Requests]: Request<any, any> }> = {
|
||||
[K in keyof Requests]: Requests[K] extends Request<infer I, infer O> ? ResolvedRequestConfig<Target, I, O> : never;
|
||||
};
|
||||
@@ -102,9 +74,6 @@ export type ResolvedRequestConfigMap<Target, Requests extends { [K in keyof Requ
|
||||
// Type Inference
|
||||
// ----------------------------------------
|
||||
|
||||
/**
|
||||
* Infer the input type of a RequestHandler.
|
||||
*/
|
||||
export type InferRequestHandlerInput<Handler> = Handler extends () => any
|
||||
? void
|
||||
: Handler extends (input: infer I, ctx?: any) => any
|
||||
@@ -115,25 +84,16 @@ export type InferRequestHandlerInput<Handler> = Handler extends () => any
|
||||
? I
|
||||
: void;
|
||||
|
||||
/**
|
||||
* Infer the output type of a RequestHandler.
|
||||
*/
|
||||
export type InferRequestHandlerOutput<Handler> = Handler extends (...args: any[]) => infer O
|
||||
? Awaited<O>
|
||||
: Handler extends { handler: (...args: any[]) => infer O }
|
||||
? Awaited<O>
|
||||
: void;
|
||||
|
||||
/**
|
||||
* Resolve a RequestHandlerRecord to a RequestRecord.
|
||||
*/
|
||||
export type ResolveRequestMap<Requests> = {
|
||||
[K in keyof Requests]: Request<InferRequestHandlerInput<Requests[K]>, InferRequestHandlerOutput<Requests[K]>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a Request (input/output) to its function signature.
|
||||
*/
|
||||
export type ResolveRequestHandler<R>
|
||||
= R extends Request<infer I, infer O>
|
||||
? [I] extends [void]
|
||||
@@ -203,16 +163,10 @@ export function createRequestMeta<Context = unknown>(init: RequestMetaInit<Conte
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a RequestMeta object.
|
||||
*/
|
||||
export function isRequestMeta(value: unknown): value is RequestMeta {
|
||||
return isObject(value) && REQUEST_META in value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an event-like object to RequestMeta.
|
||||
*/
|
||||
export function createRequestMetaFromEvent<Context = unknown>(
|
||||
event: EventLike,
|
||||
context?: Context,
|
||||
|
||||
@@ -30,7 +30,6 @@ export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[
|
||||
|
||||
this.#queue = config.queue ?? new Queue<UnionSliceTasks<Slices>>();
|
||||
|
||||
// Use provided factory or default
|
||||
const factory = config.state ?? (initial => new State(initial));
|
||||
this.#state = factory(this.#createInitialState());
|
||||
|
||||
@@ -157,12 +156,10 @@ export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[
|
||||
maybeListener?: (selected: Selected) => void,
|
||||
options?: SubscribeOptions<Selected>,
|
||||
): () => void {
|
||||
// Full state subscription (single argument)
|
||||
if (!maybeListener) {
|
||||
return this.#state.subscribe(selectorOrListener);
|
||||
}
|
||||
|
||||
// Selector-based subscription
|
||||
const selector = selectorOrListener as Selector<UnionSliceState<Slices>, Selected>;
|
||||
const listener = maybeListener;
|
||||
const equalityFn = options?.equalityFn ?? Object.is;
|
||||
@@ -176,11 +173,9 @@ export class Store<Target, Slices extends AnySlice<Target>[] = AnySlice<Target>[
|
||||
}
|
||||
};
|
||||
|
||||
// Optimization: use key-based subscription if selector returns object
|
||||
const keys = getSelectorKeys(selector, this.#state.value);
|
||||
|
||||
if (keys) {
|
||||
// Note: subscribeKeys listener receives full state at runtime, type is narrowed for safety
|
||||
return this.#state.subscribeKeys(
|
||||
keys as (keyof UnionSliceState<Slices>)[],
|
||||
handler as (state: Pick<UnionSliceState<Slices>, keyof UnionSliceState<Slices>>) => void,
|
||||
@@ -359,19 +354,9 @@ export function createStore<Slices extends AnySlice[]>(
|
||||
|
||||
export type AnyStore<Target = any> = Store<Target, AnySlice<Target>[]>;
|
||||
|
||||
/**
|
||||
* A selector function that extracts a subset of state.
|
||||
*/
|
||||
export type Selector<State, Selected> = (state: State) => Selected;
|
||||
|
||||
/**
|
||||
* Options for selector-based subscriptions.
|
||||
*/
|
||||
export interface SubscribeOptions<T> {
|
||||
/**
|
||||
* Custom equality function for comparing selected values.
|
||||
* Defaults to `Object.is`.
|
||||
*/
|
||||
equalityFn?: (a: T, b: T) => boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,28 +3,11 @@ import type { TaskScheduler } from '../core/queue';
|
||||
import { animationFrame, idleCallback } from '@videojs/utils/dom';
|
||||
|
||||
/**
|
||||
* Create a scheduler that delays task execution until the next animation frame.
|
||||
*
|
||||
* Uses `requestAnimationFrame` under the hood. Ideal for UI updates that should
|
||||
* sync with the browser's repaint cycle.
|
||||
*
|
||||
* @returns A TaskScheduler for use with the queue
|
||||
* Scheduler using `requestAnimationFrame`. Ideal for UI updates.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createQueue } from '@videojs/store';
|
||||
* import { raf } from '@videojs/store/dom';
|
||||
*
|
||||
* const queue = createQueue();
|
||||
*
|
||||
* queue.enqueue({
|
||||
* name: 'update-ui',
|
||||
* key: 'ui',
|
||||
* schedule: raf(),
|
||||
* handler: async () => {
|
||||
* // This runs on the next animation frame
|
||||
* },
|
||||
* });
|
||||
* queue.enqueue({ key: 'ui', schedule: raf(), handler: async () => {} });
|
||||
* ```
|
||||
*/
|
||||
export function raf(): TaskScheduler {
|
||||
@@ -32,42 +15,11 @@ export function raf(): TaskScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scheduler that delays task execution until the browser is idle.
|
||||
*
|
||||
* Uses `requestIdleCallback` under the hood (with `setTimeout` fallback for Safari).
|
||||
* Ideal for non-critical background work.
|
||||
*
|
||||
* @param options - Optional idle callback options (e.g., timeout)
|
||||
* @returns A TaskScheduler for use with the queue
|
||||
* Scheduler using `requestIdleCallback`. Ideal for background work.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createQueue } from '@videojs/store';
|
||||
* import { idle } from '@videojs/store/dom';
|
||||
*
|
||||
* const queue = createQueue();
|
||||
*
|
||||
* queue.enqueue({
|
||||
* name: 'analytics',
|
||||
* key: 'analytics',
|
||||
* schedule: idle(),
|
||||
* handler: async () => {
|
||||
* // This runs when the browser is idle
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With timeout to ensure execution within 2 seconds
|
||||
* queue.enqueue({
|
||||
* name: 'critical-background',
|
||||
* key: 'bg',
|
||||
* schedule: idle({ timeout: 2000 }),
|
||||
* handler: async () => {
|
||||
* // Runs when idle, or after 2 seconds
|
||||
* },
|
||||
* });
|
||||
* queue.enqueue({ key: 'bg', schedule: idle({ timeout: 2000 }), handler: async () => {} });
|
||||
* ```
|
||||
*/
|
||||
export function idle(options?: IdleRequestOptions): TaskScheduler {
|
||||
|
||||
@@ -15,9 +15,6 @@ import { useRequest as useRequestBase, useSelector as useSelectorBase, useTasks
|
||||
// Types
|
||||
// ----------------------------------------
|
||||
|
||||
/**
|
||||
* Configuration for `createStore`.
|
||||
*/
|
||||
export interface CreateStoreConfig<Slices extends AnySlice[]> extends StoreConfig<UnionSliceTarget<Slices>, Slices> {
|
||||
/**
|
||||
* Display name for React DevTools.
|
||||
@@ -25,9 +22,6 @@ export interface CreateStoreConfig<Slices extends AnySlice[]> extends StoreConfi
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Provider component returned by `createStore`.
|
||||
*/
|
||||
export interface ProviderProps<Slices extends AnySlice[]> {
|
||||
children: ReactNode;
|
||||
/**
|
||||
@@ -44,9 +38,6 @@ export interface ProviderProps<Slices extends AnySlice[]> {
|
||||
inherit?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of `createStore`.
|
||||
*/
|
||||
export interface CreateStoreResult<Slices extends AnySlice[]> {
|
||||
/**
|
||||
* Provider component that creates and manages the store lifecycle.
|
||||
@@ -100,21 +91,6 @@ export interface CreateStoreResult<Slices extends AnySlice[]> {
|
||||
* const { Provider, useStore, useSelector, useRequest, useTasks, create } = createStore({
|
||||
* slices: [playbackSlice, presentationSlice],
|
||||
* });
|
||||
*
|
||||
* function App() {
|
||||
* return (
|
||||
* <Provider>
|
||||
* <Video />
|
||||
* <Controls />
|
||||
* </Provider>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function Controls() {
|
||||
* const paused = useSelector((s) => s.paused);
|
||||
* const play = useRequest((r) => r.play);
|
||||
* return <button onClick={() => play()}>{paused ? 'Play' : 'Pause'}</button>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function createStore<Slices extends AnySlice[]>(config: CreateStoreConfig<Slices>): CreateStoreResult<Slices> {
|
||||
@@ -124,9 +100,6 @@ export function createStore<Slices extends AnySlice[]>(config: CreateStoreConfig
|
||||
type Tasks = UnionSliceTasks<Slices>;
|
||||
type StoreType = Store<Target, Slices>;
|
||||
|
||||
/**
|
||||
* Creates a new store instance.
|
||||
*/
|
||||
function create(): StoreType {
|
||||
return new Store(config);
|
||||
}
|
||||
@@ -174,31 +147,20 @@ export function createStore<Slices extends AnySlice[]>(config: CreateStoreConfig
|
||||
Provider.displayName = `${config.displayName}.Provider`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the typed store instance from context.
|
||||
*/
|
||||
function useStore(): StoreType {
|
||||
return useStoreContext() as StoreType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to a selected portion of state.
|
||||
*/
|
||||
function useSelector<T>(selector: (state: State) => T): T {
|
||||
const store = useStore();
|
||||
return useSelectorBase(store, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request map or a selected request.
|
||||
*/
|
||||
function useRequest(): Requests;
|
||||
function useRequest<T>(selector: (requests: Requests) => T): T;
|
||||
function useRequest<T>(selector?: (requests: Requests) => T): Requests | T {
|
||||
const store = useStore();
|
||||
// useRequestBase doesn't use React hooks internally, but we always call it
|
||||
// to maintain consistent hook call order (even though it's technically not required)
|
||||
const requests = useRequestBase(store) as Requests;
|
||||
const requests = useRequestBase(store);
|
||||
|
||||
if (isUndefined(selector)) {
|
||||
return requests;
|
||||
@@ -207,9 +169,6 @@ export function createStore<Slices extends AnySlice[]>(config: CreateStoreConfig
|
||||
return selector(requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to task state changes.
|
||||
*/
|
||||
function useTasks(): TasksRecord<Tasks> {
|
||||
const store = useStore();
|
||||
return useTasksBase(store);
|
||||
|
||||
@@ -6,12 +6,7 @@ import { isUndefined } from '@videojs/utils/predicate';
|
||||
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
||||
|
||||
/**
|
||||
* Subscribes to a selected portion of state.
|
||||
* Re-renders only when the selected value changes.
|
||||
*
|
||||
* @param store - The store instance
|
||||
* @param selector - Function to select a portion of state
|
||||
* @returns The selected value
|
||||
* Subscribe to selected state. Re-renders only when selected value changes.
|
||||
*/
|
||||
export function useSelector<S extends AnyStore, T>(store: S, selector: (state: InferStoreState<S>) => T): T {
|
||||
const subscribe = useCallback(
|
||||
@@ -26,22 +21,10 @@ export function useSelector<S extends AnyStore, T>(store: S, selector: (state: I
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request map from the store.
|
||||
*
|
||||
* @param store - The store instance
|
||||
* @returns The request map
|
||||
* Get request map or select a specific request.
|
||||
*/
|
||||
export function useRequest<S extends AnyStore>(store: S): InferStoreRequests<S>;
|
||||
|
||||
/**
|
||||
* Returns a selected request from the store.
|
||||
*
|
||||
* @param store - The store instance
|
||||
* @param selector - Function to select a request
|
||||
* @returns The selected request
|
||||
*/
|
||||
export function useRequest<S extends AnyStore, T>(store: S, selector: (requests: InferStoreRequests<S>) => T): T;
|
||||
|
||||
export function useRequest<S extends AnyStore, T>(
|
||||
store: S,
|
||||
selector?: (requests: InferStoreRequests<S>) => T,
|
||||
@@ -56,14 +39,9 @@ export function useRequest<S extends AnyStore, T>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to task state changes.
|
||||
* Returns the current tasks map from the queue.
|
||||
*
|
||||
* @param store - The store instance
|
||||
* @returns The tasks record
|
||||
* Subscribe to task state changes.
|
||||
*/
|
||||
export function useTasks<S extends AnyStore>(store: S): TasksRecord<InferStoreTasks<S>> {
|
||||
// Cache the tasks snapshot to ensure referential stability
|
||||
const tasksRef = useRef(store.queue.tasks);
|
||||
|
||||
const subscribe = useCallback(
|
||||
|
||||
@@ -14,7 +14,5 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': new URL('./src/core', import.meta.url).pathname,
|
||||
},
|
||||
dts: {
|
||||
oxc: true,
|
||||
},
|
||||
dts: true,
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"clean": "rm -rf dist types"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.15.9",
|
||||
"tsdown": "^0.15.12",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
/**
|
||||
* Request an animation frame and return a cleanup function to cancel it.
|
||||
*
|
||||
* @param callback - The callback to invoke on the next animation frame
|
||||
* @returns A cleanup function that cancels the animation frame request
|
||||
* Request an animation frame with cleanup.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cancel = animationFrame((time) => {
|
||||
* console.log('Frame at', time);
|
||||
* });
|
||||
*
|
||||
* // Later, cancel if needed
|
||||
* cancel();
|
||||
* const cancel = animationFrame((time) => console.log('Frame at', time));
|
||||
* cancel(); // Cancel if needed
|
||||
* ```
|
||||
*/
|
||||
export function animationFrame(callback: FrameRequestCallback): () => void {
|
||||
|
||||
@@ -10,10 +10,10 @@ export interface OnEventOptions extends AddEventListenerOptions {
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (HTMLMediaElement)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
* @example
|
||||
* ```ts
|
||||
* const event = await onEvent(video, 'seeked');
|
||||
* ```
|
||||
*/
|
||||
export function onEvent<K extends keyof HTMLMediaElementEventMap>(
|
||||
target: HTMLMediaElement,
|
||||
@@ -21,79 +21,24 @@ export function onEvent<K extends keyof HTMLMediaElementEventMap>(
|
||||
options?: OnEventOptions,
|
||||
): Promise<HTMLMediaElementEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (HTMLElement)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof HTMLElementEventMap>(
|
||||
target: HTMLElement,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<HTMLElementEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (Window)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof WindowEventMap>(
|
||||
target: Window,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<WindowEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (Document)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<DocumentEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Wait for video to be seeked
|
||||
* const event = await onEvent(video, 'seeked');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With AbortSignal for cancellation
|
||||
* const controller = new AbortController();
|
||||
*
|
||||
* try {
|
||||
* const event = await onEvent(video, 'seeked', { signal: controller.signal });
|
||||
* } catch (e) {
|
||||
* if (e.name === 'AbortError') {
|
||||
* console.log('Cancelled waiting for event');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Cancel from elsewhere
|
||||
* controller.abort();
|
||||
* ```
|
||||
*/
|
||||
export function onEvent(target: EventTarget, type: string, options?: OnEventOptions): Promise<Event>;
|
||||
|
||||
export function onEvent(target: EventTarget, type: string, options?: OnEventOptions): Promise<Event> {
|
||||
|
||||
@@ -1,29 +1,12 @@
|
||||
import { supportsIdleCallback } from './supports';
|
||||
|
||||
/**
|
||||
* Request an idle callback and return a cleanup function to cancel it.
|
||||
*
|
||||
* Falls back to `setTimeout` with 1ms delay in environments that don't
|
||||
* support `requestIdleCallback` (e.g., Safari).
|
||||
*
|
||||
* @param callback - The callback to invoke when the browser is idle
|
||||
* @param options - Optional idle callback options (timeout, etc.)
|
||||
* @returns A cleanup function that cancels the idle callback request
|
||||
* Request an idle callback with cleanup. Falls back to setTimeout for Safari.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cancel = idleCallback((deadline) => {
|
||||
* console.log('Time remaining:', deadline.timeRemaining());
|
||||
* });
|
||||
*
|
||||
* // Later, cancel if needed
|
||||
* cancel();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With timeout option
|
||||
* const cancel = idleCallback(doWork, { timeout: 1000 });
|
||||
* cancel(); // Cancel if needed
|
||||
* ```
|
||||
*/
|
||||
export function idleCallback(callback: IdleRequestCallback, options?: IdleRequestOptions): () => void {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (HTMLMediaElement)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
* @example
|
||||
* ```ts
|
||||
* const cleanup = listen(video, 'play', () => console.log('playing'));
|
||||
* cleanup(); // Remove listener
|
||||
* ```
|
||||
*/
|
||||
export function listen<K extends keyof HTMLMediaElementEventMap>(
|
||||
target: HTMLMediaElement,
|
||||
@@ -14,15 +14,6 @@ export function listen<K extends keyof HTMLMediaElementEventMap>(
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (HTMLElement)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof HTMLElementEventMap>(
|
||||
target: HTMLElement,
|
||||
type: K,
|
||||
@@ -30,15 +21,6 @@ export function listen<K extends keyof HTMLElementEventMap>(
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (Window)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof WindowEventMap>(
|
||||
target: Window,
|
||||
type: K,
|
||||
@@ -46,15 +28,6 @@ export function listen<K extends keyof WindowEventMap>(
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (Document)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
type: K,
|
||||
@@ -62,39 +35,6 @@ export function listen<K extends keyof DocumentEventMap>(
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cleanup = listen(video, 'play', () => console.log('playing'));
|
||||
*
|
||||
* // Later, remove the listener
|
||||
* cleanup();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With options
|
||||
* const cleanup = listen(video, 'play', handler, { once: true, passive: true });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With AbortSignal (native browser support)
|
||||
* const controller = new AbortController();
|
||||
* listen(video, 'play', handler, { signal: controller.signal });
|
||||
*
|
||||
* // Later, abort to remove the listener
|
||||
* controller.abort();
|
||||
* ```
|
||||
*/
|
||||
export function listen(
|
||||
target: EventTarget,
|
||||
type: string,
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
/**
|
||||
* Check if `requestIdleCallback` is supported.
|
||||
*
|
||||
* @returns `true` if `requestIdleCallback` is available
|
||||
*/
|
||||
export function supportsIdleCallback(): boolean {
|
||||
return typeof requestIdleCallback === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `requestAnimationFrame` is supported.
|
||||
*
|
||||
* @returns `true` if `requestAnimationFrame` is available
|
||||
*/
|
||||
export function supportsAnimationFrame(): boolean {
|
||||
return typeof requestAnimationFrame === 'function';
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
/**
|
||||
* Converts a TimeRanges object to an array of [start, end] tuples.
|
||||
*
|
||||
* @param ranges - The TimeRanges object to serialize
|
||||
* @returns An array of [start, end] tuples
|
||||
*/
|
||||
/** Converts a TimeRanges object to an array of [start, end] tuples. */
|
||||
export function serializeTimeRanges(ranges: TimeRanges): Array<[number, number]> {
|
||||
const result: Array<[number, number]> = [];
|
||||
|
||||
|
||||
@@ -25,26 +25,15 @@ export type CleanupFn = () => void | Promise<void>;
|
||||
export class Disposer {
|
||||
#cleanups = new Set<CleanupFn>();
|
||||
|
||||
/**
|
||||
* Number of registered cleanup functions.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.#cleanups.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a cleanup function to the collection.
|
||||
*/
|
||||
add(cleanup: CleanupFn): void {
|
||||
this.#cleanups.add(cleanup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all cleanup functions synchronously.
|
||||
*
|
||||
* Note: If any cleanup functions return promises, they will not be awaited.
|
||||
* Use `disposeAsync()` if you have async cleanup functions.
|
||||
*/
|
||||
/** Run all cleanups sync. Use `disposeAsync()` for async cleanups. */
|
||||
dispose(): void {
|
||||
for (const cleanup of this.#cleanups) {
|
||||
cleanup();
|
||||
@@ -52,9 +41,6 @@ export class Disposer {
|
||||
this.#cleanups.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all cleanup functions, awaiting any promises.
|
||||
*/
|
||||
async disposeAsync(): Promise<void> {
|
||||
await Promise.all([...this.#cleanups].map(cleanup => cleanup()));
|
||||
this.#cleanups.clear();
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/**
|
||||
* Wrap a function to catch and handle errors instead of throwing.
|
||||
*
|
||||
* @param fn - Function to wrap (can be undefined)
|
||||
* @param onError - Error handler (defaults to console.error)
|
||||
* @returns Wrapped function that never throws, or undefined if fn is undefined
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const safeFn = tryCatch(riskyFn, (e) => logger.error(e));
|
||||
|
||||
@@ -6,18 +6,10 @@ import { isObject } from '../predicate/predicate';
|
||||
export type Selector<State, Selected> = (state: State) => Selected;
|
||||
|
||||
/**
|
||||
* Extracts the state keys a selector depends on by running it once
|
||||
* and inspecting the result object's keys.
|
||||
*
|
||||
* Returns `null` if the selector returns a primitive or array
|
||||
* (keys cannot be determined).
|
||||
* Extract state keys a selector depends on. Returns null for primitives/arrays.
|
||||
*
|
||||
* @example
|
||||
* const selector = (s: State) => ({ volume: s.volume, muted: s.muted });
|
||||
* getSelectorKeys(selector, state); // ['volume', 'muted']
|
||||
*
|
||||
* const primitiveSelector = (s: State) => s.volume;
|
||||
* getSelectorKeys(primitiveSelector, state); // null
|
||||
* getSelectorKeys((s) => ({ volume: s.volume }), state); // ['volume']
|
||||
*/
|
||||
export function getSelectorKeys<State, Selected>(
|
||||
selector: Selector<State, Selected>,
|
||||
|
||||
@@ -19,8 +19,8 @@ describe('pick', () => {
|
||||
});
|
||||
|
||||
it('ignores non-existent keys', () => {
|
||||
const obj = { a: 1, b: 2 } as Record<string, number>;
|
||||
expect(pick(obj, ['a', 'nonexistent'] as (keyof typeof obj)[])).toEqual({ a: 1 });
|
||||
const obj = { a: 1, b: 2 };
|
||||
expect(pick(obj, ['a', 'nonexistent' as keyof typeof obj])).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it('handles nested objects (shallow copy)', () => {
|
||||
|
||||
@@ -17,7 +17,5 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': new URL('./src', import.meta.url).pathname,
|
||||
},
|
||||
dts: {
|
||||
oxc: true,
|
||||
},
|
||||
dts: true,
|
||||
});
|
||||
|
||||
Generated
+6
-6
@@ -319,7 +319,7 @@ importers:
|
||||
specifier: ^26.1.0
|
||||
version: 26.1.0
|
||||
tsdown:
|
||||
specifier: ^0.15.9
|
||||
specifier: ^0.15.12
|
||||
version: 0.15.12(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
@@ -341,7 +341,7 @@ importers:
|
||||
version: link:../utils
|
||||
devDependencies:
|
||||
tsdown:
|
||||
specifier: ^0.15.9
|
||||
specifier: ^0.15.12
|
||||
version: 0.15.12(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
@@ -350,7 +350,7 @@ importers:
|
||||
packages/icons:
|
||||
devDependencies:
|
||||
tsdown:
|
||||
specifier: ^0.15.9
|
||||
specifier: ^0.15.12
|
||||
version: 0.15.12(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
@@ -375,7 +375,7 @@ importers:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1
|
||||
tsdown:
|
||||
specifier: ^0.15.9
|
||||
specifier: ^0.15.12
|
||||
version: 0.15.12(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
@@ -409,7 +409,7 @@ importers:
|
||||
specifier: ^19.2.1
|
||||
version: 19.2.3(react@19.2.3)
|
||||
tsdown:
|
||||
specifier: ^0.15.9
|
||||
specifier: ^0.15.12
|
||||
version: 0.15.12(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
@@ -421,7 +421,7 @@ importers:
|
||||
packages/utils:
|
||||
devDependencies:
|
||||
tsdown:
|
||||
specifier: ^0.15.9
|
||||
specifier: ^0.15.12
|
||||
version: 0.15.12(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedDeclarations": true,
|
||||
"isolatedDeclarations": false,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true
|
||||
|
||||
Reference in New Issue
Block a user