140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
/**
|
|
* §16. Generic CPU Web Worker contract.
|
|
*
|
|
* The capability is `NOT_SELECTED` by default (§16.2). These types exist so a
|
|
* selection can be expressed and validated, but no production worker entry is
|
|
* created until a measured, CPU-bound task with an owner is contributed. This
|
|
* runtime is separate from the OPFS dedicated worker and the Service Worker.
|
|
*/
|
|
|
|
export const WEB_WORKER_PROTOCOL = "CA_WEB_WORKER_V1" as const;
|
|
|
|
export const WEB_WORKER_BOUNDS = Object.freeze({
|
|
taskGroups: 16,
|
|
tasksPerGroup: 32,
|
|
activeGroups: 4,
|
|
queuedPerGroup: 32,
|
|
queuedBytesPerGroup: 16 * 1024 * 1024,
|
|
defaultInputBytes: 1024 * 1024,
|
|
hardInputBytes: 8 * 1024 * 1024,
|
|
defaultOutputBytes: 1024 * 1024,
|
|
hardOutputBytes: 8 * 1024 * 1024,
|
|
defaultDeadlineMs: 5_000,
|
|
hardDeadlineMs: 30_000,
|
|
cancelGraceMs: 250,
|
|
idleTerminateMs: 60_000,
|
|
restartsPerWindow: 3,
|
|
restartWindowMs: 300_000,
|
|
transferables: 16,
|
|
mainThreadChunkBudgetMs: 8,
|
|
mainThreadFallbackInputBytes: 1024 * 1024,
|
|
});
|
|
|
|
export interface InstalledWorkerTask {
|
|
readonly taskId: string;
|
|
readonly taskVersion: 1;
|
|
readonly maximumInputBytes: number;
|
|
readonly maximumOutputBytes: number;
|
|
readonly deadlineMs: number;
|
|
}
|
|
|
|
export interface InstalledWebWorkerContribution {
|
|
readonly taskGroupId: string;
|
|
readonly tasks: readonly InstalledWorkerTask[];
|
|
readonly fallback: "MAIN_THREAD_CHUNKED" | "UNSUPPORTED";
|
|
}
|
|
|
|
export type WorkerRequestMessage = Readonly<{
|
|
protocol: typeof WEB_WORKER_PROTOCOL;
|
|
kind: "EXECUTE";
|
|
taskId: string;
|
|
taskVersion: 1;
|
|
requestId: string;
|
|
workerGeneration: number;
|
|
deadlineEpochMs: number;
|
|
payload: unknown;
|
|
}>;
|
|
|
|
export type WorkerCancelMessage = Readonly<{
|
|
protocol: typeof WEB_WORKER_PROTOCOL;
|
|
kind: "CANCEL";
|
|
requestId: string;
|
|
workerGeneration: number;
|
|
}>;
|
|
|
|
export type WorkerFailureCode =
|
|
| "TASK_UNKNOWN"
|
|
| "VERSION_UNSUPPORTED"
|
|
| "INPUT_INVALID"
|
|
| "INPUT_TOO_LARGE"
|
|
| "OUTPUT_INVALID"
|
|
| "OUTPUT_TOO_LARGE"
|
|
| "QUEUE_FULL"
|
|
| "DEADLINE_EXCEEDED"
|
|
| "CANCELLED"
|
|
| "CRASHED"
|
|
| "TRANSFER_FAILED"
|
|
| "STALE_RESULT"
|
|
| "RUNTIME_PROTOCOL_FAILURE";
|
|
|
|
export type WorkerResponseMessage =
|
|
| Readonly<{
|
|
protocol: typeof WEB_WORKER_PROTOCOL;
|
|
kind: "SUCCESS";
|
|
requestId: string;
|
|
workerGeneration: number;
|
|
payload: unknown;
|
|
}>
|
|
| Readonly<{
|
|
protocol: typeof WEB_WORKER_PROTOCOL;
|
|
kind: "FAILURE";
|
|
requestId: string;
|
|
workerGeneration: number;
|
|
code: WorkerFailureCode;
|
|
}>;
|
|
|
|
export function validateWebWorkerContributions(
|
|
contributions: readonly InstalledWebWorkerContribution[],
|
|
): readonly InstalledWebWorkerContribution[] {
|
|
const bounds = WEB_WORKER_BOUNDS;
|
|
if (contributions.length > bounds.taskGroups) {
|
|
throw new TypeError("Web Worker task groups exceed their bound.");
|
|
}
|
|
const groupIds = new Set<string>();
|
|
const taskIds = new Set<string>();
|
|
for (const contribution of contributions) {
|
|
if (!contribution.taskGroupId || groupIds.has(contribution.taskGroupId)) {
|
|
throw new TypeError("Web Worker task group identity is invalid.");
|
|
}
|
|
groupIds.add(contribution.taskGroupId);
|
|
if (
|
|
contribution.tasks.length === 0 ||
|
|
contribution.tasks.length > bounds.tasksPerGroup
|
|
) {
|
|
throw new TypeError("Web Worker task count is out of range.");
|
|
}
|
|
for (const task of contribution.tasks) {
|
|
const qualified = `${contribution.taskGroupId}/${task.taskId}`;
|
|
if (!task.taskId || taskIds.has(qualified)) {
|
|
throw new TypeError("Duplicate Web Worker task.");
|
|
}
|
|
taskIds.add(qualified);
|
|
if (
|
|
task.taskVersion !== 1 ||
|
|
!Number.isSafeInteger(task.maximumInputBytes) ||
|
|
task.maximumInputBytes < 1 ||
|
|
task.maximumInputBytes > bounds.hardInputBytes ||
|
|
!Number.isSafeInteger(task.maximumOutputBytes) ||
|
|
task.maximumOutputBytes < 1 ||
|
|
task.maximumOutputBytes > bounds.hardOutputBytes ||
|
|
!Number.isSafeInteger(task.deadlineMs) ||
|
|
task.deadlineMs < 1 ||
|
|
task.deadlineMs > bounds.hardDeadlineMs
|
|
) {
|
|
throw new TypeError(`Web Worker task bounds invalid: ${qualified}`);
|
|
}
|
|
}
|
|
}
|
|
return Object.freeze([...contributions]);
|
|
}
|