Files
v10/dist/dev/core/tasks/task.js
T

191 lines
5.6 KiB
JavaScript

import { anyAbortSignal } from "@videojs/utils/events";
import { generateId } from "@videojs/utils/string";
//#region src/core/tasks/task.ts
/**
* Generic reusable task that wraps an async run function.
*
* Owns its own AbortController so it can always be aborted independently.
* Optionally composes an external AbortSignal so that a parent's cancellation
* propagates into the task's work without requiring the caller to track the
* task separately.
*
* Ordering guarantee: `value` is written before `status` transitions to `'done'`;
* `error` is written before `status` transitions to `'error'`. Any reader
* observing `status === 'done'` is guaranteed `value` is already present.
*/
var Task = class {
id;
#runFn;
#abortController = new AbortController();
#signal;
#status = "pending";
#value = void 0;
#error = void 0;
constructor(runFn, config) {
this.#runFn = runFn;
const rawId = config?.id;
this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
}
get status() {
return this.#status;
}
get value() {
return this.#value;
}
get error() {
return this.#error;
}
async run() {
this.#status = "running";
try {
const result = await this.#runFn(this.#signal);
this.#value = result;
this.#status = "done";
return result;
} catch (e) {
this.#error = e;
this.#status = "error";
throw e;
}
}
abort() {
this.#abortController.abort();
}
};
/**
* Runs tasks concurrently, deduplicated by task id.
*
* If a task with a given id is already in flight, subsequent schedule() calls
* for that id are silently ignored until the first completes. Tasks are stored
* so abortAll() can cancel any in-flight work (e.g. on engine cleanup).
*/
var ConcurrentRunner = class {
#pending = /* @__PURE__ */ new Map();
#settled = Promise.resolve();
#resolveSettled = null;
#destroyed = false;
schedule(task) {
if (this.#destroyed) return Promise.resolve();
const existing = this.#pending.get(task.id);
if (existing) return existing.promise;
if (this.#pending.size === 0) this.#settled = new Promise((resolve) => {
this.#resolveSettled = resolve;
});
const promise = task.run();
promise.catch(() => {});
const cleanup = () => {
this.#pending.delete(task.id);
if (this.#pending.size === 0) {
this.#resolveSettled?.();
this.#resolveSettled = null;
}
};
promise.then(cleanup, cleanup);
this.#pending.set(task.id, {
task,
promise
});
return promise;
}
/**
* Registers a callback to fire when all currently in-flight tasks settle.
* If the runner is already idle, the callback is never called. If abortAll()
* is called before the batch settles, the callback is superseded and silently
* dropped — no stale callbacks, no generation token required by the caller.
*/
whenSettled(callback) {
if (this.#pending.size === 0) return;
const captured = this.#settled;
captured.then(() => {
if (this.#settled !== captured) return;
callback();
}, () => {});
}
abortAll() {
for (const { task } of this.#pending.values()) task.abort();
this.#pending.clear();
this.#resolveSettled?.();
this.#resolveSettled = null;
this.#settled = Promise.resolve();
}
destroy() {
this.#destroyed = true;
this.abortAll();
}
};
/**
* Runs tasks one at a time in submission order.
*
* Each schedule() call returns a Promise that resolves or rejects with the
* task's result when it is eventually executed. Tasks wait in queue until the
* prior task completes.
*
* Serialization is achieved by chaining each task's run() onto the tail of a
* shared promise chain — no explicit queue or drain loop needed.
*
* abortAll() aborts all pending (not yet started) tasks and the currently
* in-flight task. Pending tasks still run briefly but receive an aborted
* signal and are expected to exit early.
*/
var SerialRunner = class {
#chain = Promise.resolve();
#pending = /* @__PURE__ */ new Set();
#current = null;
#destroyed = false;
schedule(task) {
if (this.#destroyed) return Promise.resolve();
const t = task;
this.#pending.add(t);
const result = this.#chain.then(() => {
this.#pending.delete(t);
this.#current = t;
return task.run();
}).finally(() => {
this.#current = null;
});
this.#chain = result.then(() => {}, () => {});
return result;
}
/**
* A promise that resolves when all currently-scheduled tasks have settled.
* Use the reference as a generation token: capture it after scheduling a
* batch, then check identity in the resolution callback to detect whether
* a subsequent abortAll() + new batch has superseded this one.
*/
get settled() {
return this.#chain;
}
/**
* Registers a callback to fire when all currently-pending tasks settle.
* If the runner is already idle (no pending or running tasks), the callback
* is never called. If new tasks are scheduled before the current batch
* settles, the callback is superseded and silently dropped — no stale
* callbacks, no generation token required by the caller.
*/
whenSettled(callback) {
if (this.#pending.size === 0 && this.#current === null) return;
const currentChain = this.#chain;
currentChain.then(() => {
if (this.#chain !== currentChain) return;
callback();
}, () => {});
}
/** Aborts and clears queued tasks without touching the in-flight task. */
abortPending() {
for (const task of this.#pending) task.abort();
this.#pending.clear();
}
abortAll() {
this.abortPending();
this.#current?.abort();
}
destroy() {
this.#destroyed = true;
this.abortAll();
}
};
//#endregion
export { ConcurrentRunner, SerialRunner, Task };
//# sourceMappingURL=task.js.map