diff --git a/index.ts b/index.ts index 8db2cd5..6747a68 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,6 @@ export { default as isTimeToYield } from './src/isTimeToYield' export type { default as SchedulingPriority } from './src/SchedulingPriority' // utility -export { default as Deferred } from './src/utils/Deferred' export { default as queueTask } from './src/utils/queueTask' +export { default as withResolvers } from './src/utils/withResolvers' export { default as afterFrame } from './src/utils/requestAfterFrame' diff --git a/src/state.ts b/src/state.ts index 44f7986..729f380 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,11 +1,11 @@ import { Task } from './tasks' -import Deferred from './utils/Deferred' +import withResolvers, { PromiseWithResolvers } from './utils/withResolvers' type State = { tasks: Task[] frameTimeElapsed: boolean - onIdleCallback: Deferred - onAnimationFrame: Deferred + onIdleCallback: PromiseWithResolvers + onAnimationFrame: PromiseWithResolvers idleDeadline: IdleDeadline | undefined workStartTimeThisFrame: number | undefined } @@ -14,8 +14,8 @@ const state: State = { tasks: [], idleDeadline: undefined, frameTimeElapsed: false, - onIdleCallback: new Deferred(), - onAnimationFrame: new Deferred(), + onIdleCallback: withResolvers(), + onAnimationFrame: withResolvers(), workStartTimeThisFrame: undefined, } diff --git a/src/tasks.ts b/src/tasks.ts index c0b1cf5..05af719 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -1,11 +1,10 @@ import state from './state' -import Deferred from './utils/Deferred' import { startTracking } from './tracking' import SchedulingPriority from './SchedulingPriority' +import withResolvers, { PromiseWithResolvers } from './utils/withResolvers' -export type Task = { +export type Task = PromiseWithResolvers & { priority: SchedulingPriority - deferred: Deferred } /** @@ -13,7 +12,7 @@ export type Task = { * @param priority {SchedulingPriority} The priority of the new task. */ export function createTask(priority: SchedulingPriority): Task { - const item = { priority, deferred: new Deferred() } + const item = { ...withResolvers(), priority } const insertIndex = priority === 'user-blocking' ? 0 @@ -56,6 +55,6 @@ export function removeTask(task: Task): void { export function nextTask(): void { const task = state.tasks[0] if (task !== undefined) { - task.deferred.resolve() + task.resolve() } } diff --git a/src/tracking.ts b/src/tracking.ts index cb11f5a..e6f8c8c 100644 --- a/src/tracking.ts +++ b/src/tracking.ts @@ -1,5 +1,5 @@ import state from './state' -import Deferred from './utils/Deferred' +import withResolvers from './utils/withResolvers' let isTracking = false let idleCallbackId: number | undefined @@ -26,7 +26,7 @@ export function startTracking(): void { state.onIdleCallback.resolve() - state.onIdleCallback = new Deferred() + state.onIdleCallback = withResolvers() }) } @@ -35,7 +35,7 @@ export function startTracking(): void { state.onAnimationFrame.resolve() - state.onAnimationFrame = new Deferred() + state.onAnimationFrame = withResolvers() if (state.tasks.length === 0) { isTracking = false diff --git a/src/utils/Deferred.ts b/src/utils/Deferred.ts deleted file mode 100644 index a7fc205..0000000 --- a/src/utils/Deferred.ts +++ /dev/null @@ -1,66 +0,0 @@ -// inspired by Deno's implementation: https://deno.land/std@0.170.0/async/deferred.ts?source - -/** - * Creates a Promise with additional `reject` and `resolve` methods. - * It also adds a `state` property. - * - * @example - * ```typescript - * const deferred = new Deferred(); - * // ... - * deferred.resolve(42); - * ``` - */ -export default class Deferred extends Promise { - #resolve: (value: T | PromiseLike) => void - #reject: (reason?: unknown) => void - #state: 'pending' | 'fulfilled' | 'rejected' - - constructor( - executor?: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: unknown) => void, - ) => void, - ) { - let resolve: (value: T | PromiseLike) => void - let reject: (reason?: unknown) => void - - super((resolveLocal, rejectLocal) => { - executor?.(resolveLocal, rejectLocal) - - resolve = resolveLocal - reject = rejectLocal - }) - - this.#state = 'pending' - this.#resolve = resolve! - this.#reject = reject! - } - - get state(): 'pending' | 'fulfilled' | 'rejected' { - return this.#state - } - - resolve(value: T): void - resolve(value: PromiseLike): Promise - resolve(value: T | PromiseLike): void | Promise { - if (value instanceof Promise) { - // eslint-disable-next-line promise/prefer-await-to-then - return value.then(() => { - this.#state = 'fulfilled' - - this.#resolve(value as T) - }) - } - - this.#state = 'fulfilled' - - this.#resolve(value) - } - - reject(reason?: unknown): void { - this.#state = 'rejected' - - this.#reject(reason) - } -} diff --git a/src/utils/withResolvers.ts b/src/utils/withResolvers.ts new file mode 100644 index 0000000..3edbfad --- /dev/null +++ b/src/utils/withResolvers.ts @@ -0,0 +1,21 @@ +export interface PromiseWithResolvers { + promise: Promise + resolve: (value: T) => void + reject: (reason?: any) => void +} + +export default function withResolvers(): PromiseWithResolvers { + let resolve: (value: T) => void + let reject: () => void + + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + + return { + promise, + resolve: resolve!, + reject: reject!, + } +} diff --git a/src/yieldControl.ts b/src/yieldControl.ts index 94b36c8..e2a11df 100644 --- a/src/yieldControl.ts +++ b/src/yieldControl.ts @@ -32,7 +32,7 @@ export default async function yieldControl( await schedule(priority) if (state.tasks[0] !== task) { - await task.deferred + await task.promise if (isTimeToYield(priority)) { await schedule(priority) @@ -50,7 +50,7 @@ export default async function yieldControl( async function schedule(priority: SchedulingPriority): Promise { if (state.frameTimeElapsed) { - await state.onAnimationFrame + await state.onAnimationFrame.promise } if ( @@ -67,7 +67,7 @@ async function schedule(priority: SchedulingPriority): Promise { state.workStartTimeThisFrame = Date.now() } } else { - await state.onIdleCallback + await state.onIdleCallback.promise // not checking for `navigator.scheduling?.isInputPending?.()` here because idle callbacks // ensure no input is pending