Planning, counterfactual analysis and checkpoint explanation now share one internal worker-job owner for creation, handler cleanup, termination and supersession, and one request/response message contract between the coordinator and the worker. Job-specific progress, checkpoint recovery and completion rules stay explicit. Refs #277 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
/// <reference lib="webworker" />
|
|
|
|
import type { PlanningOutcome } from './leave-planner.service';
|
|
import type { WorkerRequest, WorkerResponse } from './leave-planner.worker-messages';
|
|
import {
|
|
analyzeCounterfactualInternal,
|
|
calculatePlanningAsyncInternal,
|
|
explainIncompleteCheckpoint,
|
|
type PlanningArtifacts,
|
|
} from './leave-planner.core';
|
|
|
|
const worker = self as DedicatedWorkerGlobalScope;
|
|
let activeCalculation: { cancelled: boolean } | undefined;
|
|
|
|
worker.onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
|
try {
|
|
if (event.data?.type === 'cancel') {
|
|
if (activeCalculation) activeCalculation.cancelled = true;
|
|
return;
|
|
}
|
|
if (event.data?.type === 'plan') {
|
|
const calculation = { cancelled: false };
|
|
activeCalculation = calculation;
|
|
let artifacts: PlanningArtifacts = {};
|
|
let outcomePosted = false;
|
|
let outcome: PlanningOutcome;
|
|
// The cooperative driver yields during preparation and search, even within one phase.
|
|
const heartbeat = () =>
|
|
worker.postMessage({ type: 'heartbeat' } satisfies WorkerResponse);
|
|
const heartbeatTimer = setInterval(heartbeat, 1000);
|
|
try {
|
|
heartbeat();
|
|
outcome = await calculatePlanningAsyncInternal(
|
|
{
|
|
...event.data.request,
|
|
progress: (stage: string) =>
|
|
worker.postMessage({
|
|
type: 'progress',
|
|
stage,
|
|
} satisfies WorkerResponse),
|
|
},
|
|
(plan, state) =>
|
|
worker.postMessage({
|
|
type: 'checkpoint',
|
|
plan,
|
|
state,
|
|
} satisfies WorkerResponse),
|
|
(value) => (artifacts = value),
|
|
(value) => {
|
|
outcomePosted = true;
|
|
worker.postMessage({
|
|
type: 'result',
|
|
outcome: value,
|
|
} satisfies WorkerResponse);
|
|
},
|
|
(explanation, explanationError, finalState) =>
|
|
worker.postMessage({
|
|
type: 'explanation',
|
|
explanation,
|
|
explanationError,
|
|
finalState,
|
|
} satisfies WorkerResponse),
|
|
() => calculation.cancelled,
|
|
);
|
|
} catch (error) {
|
|
worker.postMessage({
|
|
type: 'error',
|
|
code: 'planner-error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
} satisfies WorkerResponse);
|
|
return;
|
|
} finally {
|
|
clearInterval(heartbeatTimer);
|
|
if (activeCalculation === calculation) activeCalculation = undefined;
|
|
}
|
|
if (!outcomePosted)
|
|
worker.postMessage({ type: 'result', outcome } satisfies WorkerResponse);
|
|
worker.postMessage({ type: 'artifacts', artifacts } satisfies WorkerResponse);
|
|
return;
|
|
}
|
|
if (event.data?.type === 'explain-checkpoint') {
|
|
const result = explainIncompleteCheckpoint(
|
|
event.data.request,
|
|
event.data.plan,
|
|
event.data.state,
|
|
event.data.interruption,
|
|
);
|
|
worker.postMessage({ type: 'explanation', ...result } satisfies WorkerResponse);
|
|
return;
|
|
}
|
|
if (event.data?.type !== 'counterfactual') return;
|
|
try {
|
|
const analysis = analyzeCounterfactualInternal(
|
|
event.data.request,
|
|
event.data.plan,
|
|
event.data.placement,
|
|
event.data.state,
|
|
);
|
|
worker.postMessage({
|
|
type: 'counterfactual-result',
|
|
analysis,
|
|
} satisfies WorkerResponse);
|
|
} catch (error) {
|
|
worker.postMessage({
|
|
type: 'error',
|
|
code: 'counterfactual-error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
} satisfies WorkerResponse);
|
|
}
|
|
} catch (error) {
|
|
worker.postMessage({
|
|
type: 'error',
|
|
code: 'worker-error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
} satisfies WorkerResponse);
|
|
}
|
|
};
|