Files
tech-log-frontend/src/contracts/web-worker.ts
T

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]);
}