CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m15s
CI / test (push) Successful in 3m18s
CI / benchmark (push) Failing after 5m53s
CI / build (push) Successful in 1m23s
CI / security (push) Successful in 7m6s
CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 43s
CI / test (pull_request) Successful in 3m19s
CI / images (push) Successful in 3m7s
CI / required (push) Successful in 2s
CI / benchmark (pull_request) Failing after 5m28s
CI / security (pull_request) Successful in 6m37s
CI / images (pull_request) Successful in 3m18s
CI / required (pull_request) Successful in 1s
174 lines
4.7 KiB
TypeScript
174 lines
4.7 KiB
TypeScript
/// <reference lib="webworker" />
|
|
|
|
import type {
|
|
CounterfactualAnalysis,
|
|
LeavePlacement,
|
|
PlanOutcome,
|
|
PlanningExplanation,
|
|
PlanningOutcome,
|
|
PlanningReason,
|
|
PlanningSnapshot,
|
|
PlanningStateSnapshot,
|
|
} from './leave-planner.service';
|
|
import {
|
|
analyzeCounterfactualInternal,
|
|
calculatePlanningAsyncInternal,
|
|
explainIncompleteCheckpoint,
|
|
type PlanningArtifacts,
|
|
} from './leave-planner.core';
|
|
|
|
type WorkerPlanningRequest = {
|
|
calculationDate?: string;
|
|
declaredDays?: unknown;
|
|
horizon?: { start?: unknown; end?: unknown };
|
|
balances?: unknown;
|
|
rules?: unknown;
|
|
identities?: unknown;
|
|
labels?: unknown;
|
|
deadlineMs?: unknown;
|
|
progress?: unknown;
|
|
};
|
|
|
|
type WorkerRequest =
|
|
| {
|
|
type: 'plan';
|
|
request: WorkerPlanningRequest;
|
|
}
|
|
| { type: 'cancel' }
|
|
| {
|
|
type: 'counterfactual';
|
|
request: PlanningSnapshot;
|
|
plan: PlanOutcome;
|
|
placement: LeavePlacement;
|
|
state?: PlanningStateSnapshot;
|
|
}
|
|
| {
|
|
type: 'explain-checkpoint';
|
|
request: PlanningSnapshot;
|
|
plan: PlanOutcome;
|
|
state: PlanningStateSnapshot;
|
|
interruption: PlanningReason;
|
|
};
|
|
|
|
type WorkerResponse =
|
|
| { type: 'heartbeat' }
|
|
| { type: 'progress'; stage: string }
|
|
| { type: 'checkpoint'; plan: PlanOutcome; state?: PlanningStateSnapshot }
|
|
| { type: 'result'; outcome: PlanningOutcome }
|
|
| { type: 'artifacts'; artifacts: PlanningArtifacts }
|
|
| {
|
|
type: 'explanation';
|
|
explanation?: PlanningExplanation;
|
|
explanationError?: PlanningReason;
|
|
finalState?: PlanningStateSnapshot;
|
|
}
|
|
| { type: 'counterfactual-result'; analysis: CounterfactualAnalysis }
|
|
| { type: 'error'; code?: string; message: string };
|
|
|
|
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);
|
|
}
|
|
};
|