refactor(planning): give planning workers one lifetime owner
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>
This commit is contained in:
1 parent
7fc2b33587
commit
b69b3c3690
4 files changed
+337
-176
No files matched your search
@@ -0,0 +1,172 @@
|
||||
import { vi } from 'vitest';
|
||||
import {
|
||||
createPlanningCalculation,
|
||||
type LeavePlacement,
|
||||
type PlanningCalculationContext,
|
||||
} from './leave-planner.service';
|
||||
|
||||
const request = { horizon: { start: '2024-01-01', end: '2025-01-01' }, balances: [], rules: [] };
|
||||
const plan = {
|
||||
kind: 'Plan',
|
||||
placements: [{ date: '2024-01-05', balanceId: 'cp' } as unknown as LeavePlacement],
|
||||
completedStages: [],
|
||||
};
|
||||
const state = { marker: 'state' };
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
class FakeWorker {
|
||||
static all: FakeWorker[] = [];
|
||||
static constructThrows = false;
|
||||
static postThrows = false;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
terminate = vi.fn();
|
||||
postMessage = vi.fn(() => {
|
||||
if (this.throwOnPost) throw new Error('post failed');
|
||||
});
|
||||
private throwOnPost = FakeWorker.postThrows;
|
||||
constructor() {
|
||||
if (FakeWorker.constructThrows) throw new Error('no worker');
|
||||
FakeWorker.all.push(this);
|
||||
}
|
||||
emit(data: unknown) {
|
||||
this.onmessage?.({ data } as MessageEvent);
|
||||
}
|
||||
}
|
||||
|
||||
interface Started {
|
||||
worker?: FakeWorker;
|
||||
succeeded: Promise<boolean>;
|
||||
deliver: () => void;
|
||||
late: () => void;
|
||||
}
|
||||
type Flags = { construct?: boolean; post?: boolean };
|
||||
const arm = (flags: Flags) => {
|
||||
FakeWorker.constructThrows = !!flags.construct;
|
||||
FakeWorker.postThrows = !!flags.post;
|
||||
};
|
||||
|
||||
const jobs: Record<string, (flags: Flags) => Promise<Started>> = {
|
||||
plan: async (flags) => {
|
||||
arm(flags);
|
||||
const progress = vi.fn();
|
||||
const calculation = createPlanningCalculation({ ...request, progress });
|
||||
const worker = FakeWorker.all[0];
|
||||
return {
|
||||
worker,
|
||||
succeeded: calculation.outcome.then((outcome) => outcome.kind === 'Plan'),
|
||||
deliver: () => {
|
||||
worker?.emit({ type: 'result', outcome: plan });
|
||||
worker?.emit({ type: 'artifacts', artifacts: {} });
|
||||
},
|
||||
late: () => {
|
||||
worker?.emit({ type: 'progress', stage: 'late' });
|
||||
expect(progress).not.toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
},
|
||||
counterfactual: async (flags) => {
|
||||
const calculation = createPlanningCalculation(request);
|
||||
FakeWorker.all[0].emit({ type: 'result', outcome: plan });
|
||||
FakeWorker.all[0].emit({ type: 'artifacts', artifacts: {} });
|
||||
await calculation.timings;
|
||||
arm(flags);
|
||||
const analysis = (calculation as PlanningCalculationContext).analyzeCounterfactual(
|
||||
plan.placements[0],
|
||||
);
|
||||
const succeeded = analysis.then(
|
||||
(result) => result.kind !== 'CounterfactualStale',
|
||||
() => false,
|
||||
);
|
||||
await flush();
|
||||
const worker = FakeWorker.all[1];
|
||||
return {
|
||||
worker,
|
||||
succeeded,
|
||||
deliver: () =>
|
||||
worker?.emit({ type: 'counterfactual-result', analysis: { results: [] } }),
|
||||
late: () => worker?.emit({ type: 'error', message: 'late' }),
|
||||
};
|
||||
},
|
||||
'explain-checkpoint': async (flags) => {
|
||||
const calculation = createPlanningCalculation(request);
|
||||
const first = FakeWorker.all[0];
|
||||
first.emit({ type: 'checkpoint', plan, state });
|
||||
arm(flags);
|
||||
first.emit({ type: 'error', message: 'planner crashed' });
|
||||
const worker = FakeWorker.all[1];
|
||||
return {
|
||||
worker,
|
||||
succeeded: calculation.explanation.then((explanation) => explanation !== undefined),
|
||||
deliver: () =>
|
||||
worker?.emit({
|
||||
type: 'explanation',
|
||||
explanation: { kind: 'IncompleteExplanation' },
|
||||
}),
|
||||
late: () => worker?.emit({ type: 'error', message: 'late' }),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
describe('worker job lifetime', () => {
|
||||
beforeEach(() => {
|
||||
FakeWorker.all = [];
|
||||
arm({});
|
||||
vi.stubGlobal('window', {});
|
||||
vi.stubGlobal('Worker', FakeWorker);
|
||||
});
|
||||
afterEach(() => {
|
||||
arm({});
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe.each(Object.keys(jobs))('%s job', (name) => {
|
||||
const start = jobs[name];
|
||||
const released = (worker: FakeWorker) => {
|
||||
expect(worker.terminate).toHaveBeenCalledOnce();
|
||||
expect(worker.onmessage).toBeNull();
|
||||
expect(worker.onerror).toBeNull();
|
||||
};
|
||||
|
||||
it('releases the worker once on success and ignores late messages', async () => {
|
||||
const job = await start({});
|
||||
job.deliver();
|
||||
expect(await job.succeeded).toBe(true);
|
||||
released(job.worker!);
|
||||
job.late();
|
||||
job.deliver();
|
||||
released(job.worker!);
|
||||
});
|
||||
|
||||
it('fails without a worker when construction throws', async () => {
|
||||
const job = await start({ construct: true });
|
||||
expect(job.worker).toBeUndefined();
|
||||
expect(await job.succeeded).toBe(false);
|
||||
});
|
||||
|
||||
it('releases the worker once when postMessage throws', async () => {
|
||||
const job = await start({ post: true });
|
||||
expect(await job.succeeded).toBe(false);
|
||||
released(job.worker!);
|
||||
});
|
||||
|
||||
it('releases the worker once on a runtime error', async () => {
|
||||
const job = await start({});
|
||||
job.worker!.onerror!();
|
||||
expect(await job.succeeded).toBe(false);
|
||||
released(job.worker!);
|
||||
job.deliver();
|
||||
expect(await job.succeeded).toBe(false);
|
||||
released(job.worker!);
|
||||
});
|
||||
|
||||
it('releases the worker once when a newer calculation supersedes it', async () => {
|
||||
const job = await start({});
|
||||
createPlanningCalculation(request);
|
||||
expect(await job.succeeded).toBe(false);
|
||||
released(job.worker!);
|
||||
job.deliver();
|
||||
released(job.worker!);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
StaleCounterfactualAnalysis,
|
||||
} from './leave-planner.core';
|
||||
import { planningDateInParis } from './planning-date';
|
||||
import type { WorkerRequest, WorkerResponse } from './leave-planner.worker-messages';
|
||||
|
||||
export type {
|
||||
IfThenPlanningRule,
|
||||
@@ -278,20 +279,6 @@ function invalidDeadline(request: TransportSnapshot): InvalidInputOutcome | unde
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type PlannerWorkerResponse =
|
||||
| { 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: 'error'; code?: string; message: string };
|
||||
|
||||
interface PlanningRun {
|
||||
outcome: Promise<PlanningOutcome>;
|
||||
explanation: Promise<PlanningExplanation | undefined>;
|
||||
@@ -320,6 +307,47 @@ function interruptedOutcome(
|
||||
const latestWorkerCancels = new Set<() => void>();
|
||||
let latestCounterfactualCancel: (() => void) | undefined;
|
||||
|
||||
interface WorkerJob {
|
||||
post(message: WorkerRequest, onFailure: (error: unknown) => void): void;
|
||||
release(): void;
|
||||
}
|
||||
|
||||
// Single owner of a worker's lifetime: creation, handlers, cancellation registration and an idempotent release.
|
||||
function startWorkerJob(
|
||||
supersede: () => void,
|
||||
onMessage: (message: WorkerResponse) => void,
|
||||
onError: () => void,
|
||||
): WorkerJob {
|
||||
const worker = new Worker(new URL('./leave-planner.worker', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
let released = false;
|
||||
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
||||
if (!released && event.data) onMessage(event.data);
|
||||
};
|
||||
worker.onerror = () => {
|
||||
if (!released) onError();
|
||||
};
|
||||
latestWorkerCancels.add(supersede);
|
||||
return {
|
||||
post(message, onFailure) {
|
||||
try {
|
||||
worker.postMessage(message);
|
||||
} catch (error) {
|
||||
onFailure(error);
|
||||
}
|
||||
},
|
||||
release() {
|
||||
if (released) return;
|
||||
released = true;
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
worker.terminate();
|
||||
latestWorkerCancels.delete(supersede);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function cancelLatestWorkers(): void {
|
||||
for (const cancel of [...latestWorkerCancels]) cancel();
|
||||
latestWorkerCancels.clear();
|
||||
@@ -349,9 +377,11 @@ function planInWorker(snapshot: TransportSnapshot): PlanningRun {
|
||||
});
|
||||
let cancel: (() => void) | undefined;
|
||||
try {
|
||||
const worker = new Worker(new URL('./leave-planner.worker', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
const job = startWorkerJob(
|
||||
() => supersede(),
|
||||
(message) => handleMessage(message),
|
||||
() => fail('worker-error', 'Planning worker failed before returning explanation data.'),
|
||||
);
|
||||
let outcomeSettled = false;
|
||||
let cancellationRequested = false;
|
||||
let cancelledRace: PlanningIncompleteOutcome | undefined;
|
||||
@@ -364,9 +394,7 @@ function planInWorker(snapshot: TransportSnapshot): PlanningRun {
|
||||
let receivedFinalState: PlanningStateSnapshot | undefined;
|
||||
let livenessWatchdog: ReturnType<typeof setTimeout>;
|
||||
const terminate = () => {
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
worker.terminate();
|
||||
job.release();
|
||||
clearTimeout(livenessWatchdog);
|
||||
};
|
||||
const finishOutcome = (result: PlanningOutcome) => {
|
||||
@@ -434,7 +462,6 @@ function planInWorker(snapshot: TransportSnapshot): PlanningRun {
|
||||
resolveArtifacts(complete);
|
||||
resolveTimings(complete.timings ?? {});
|
||||
terminate();
|
||||
latestWorkerCancels.delete(supersede);
|
||||
};
|
||||
const supersede = () => {
|
||||
if (artifactsSettled) return;
|
||||
@@ -456,7 +483,6 @@ function planInWorker(snapshot: TransportSnapshot): PlanningRun {
|
||||
finalState: receivedFinalState ?? checkpointState,
|
||||
});
|
||||
};
|
||||
latestWorkerCancels.add(supersede);
|
||||
const fail = (code: string, message: string) => {
|
||||
const reason = { code, message };
|
||||
if (!outcomeSettled) finishOutcome(interruptedOutcome(checkpoint, code, message));
|
||||
@@ -508,15 +534,11 @@ function planInWorker(snapshot: TransportSnapshot): PlanningRun {
|
||||
cancel = () => {
|
||||
if (outcomeSettled || cancellationRequested) return;
|
||||
cancellationRequested = true;
|
||||
try {
|
||||
worker.postMessage({ type: 'cancel' });
|
||||
} catch {
|
||||
fail('worker-error', 'The cancellation request could not reach the worker.');
|
||||
}
|
||||
job.post({ type: 'cancel' }, () =>
|
||||
fail('worker-error', 'The cancellation request could not reach the worker.'),
|
||||
);
|
||||
};
|
||||
worker.onmessage = (event: MessageEvent<PlannerWorkerResponse>) => {
|
||||
const message = event.data;
|
||||
if (!message) return;
|
||||
const handleMessage = (message: WorkerResponse) => {
|
||||
if (message.type === 'heartbeat') {
|
||||
scheduleLivenessWatchdog();
|
||||
return;
|
||||
@@ -552,13 +574,9 @@ function planInWorker(snapshot: TransportSnapshot): PlanningRun {
|
||||
if (message.type === 'error') fail(message.code ?? 'worker-error', message.message);
|
||||
else fail('worker-error', 'Planning worker returned an unexpected response.');
|
||||
};
|
||||
worker.onerror = () =>
|
||||
fail('worker-error', 'Planning worker failed before returning explanation data.');
|
||||
try {
|
||||
worker.postMessage({ type: 'plan', request: { ...snapshot, progress: undefined } });
|
||||
} catch {
|
||||
fail('worker-error', 'Planning input could not be sent to the isolated worker.');
|
||||
}
|
||||
job.post({ type: 'plan', request: { ...snapshot, progress: undefined } }, () =>
|
||||
fail('worker-error', 'Planning input could not be sent to the isolated worker.'),
|
||||
);
|
||||
} catch {
|
||||
const result = interruptedOutcome(
|
||||
undefined,
|
||||
@@ -677,62 +695,43 @@ function counterfactualInWorker(
|
||||
): Promise<CounterfactualAnalysis> {
|
||||
return new Promise((resolve, reject) => {
|
||||
latestCounterfactualCancel?.();
|
||||
let worker: Worker;
|
||||
let job: WorkerJob;
|
||||
const finish = (callback: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
job.release();
|
||||
if (latestCounterfactualCancel === cancel) latestCounterfactualCancel = undefined;
|
||||
callback();
|
||||
};
|
||||
let settled = false;
|
||||
const cancel = () =>
|
||||
finish(() => reject(new Error('Counterfactual analysis was superseded.')));
|
||||
try {
|
||||
worker = new Worker(new URL('./leave-planner.worker', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
job = startWorkerJob(
|
||||
cancel,
|
||||
(message) => {
|
||||
if (message.type === 'counterfactual-result' && message.analysis)
|
||||
finish(() => resolve(message.analysis));
|
||||
else if (message.type === 'error')
|
||||
finish(() =>
|
||||
reject(
|
||||
new Error(
|
||||
message.message ??
|
||||
`Counterfactual analysis failed (${message.code ?? 'worker-error'}).`,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
() => finish(() => reject(new Error('Counterfactual worker failed.'))),
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
let settled = false;
|
||||
const finish = (callback: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
worker.terminate();
|
||||
if (cancel) latestWorkerCancels.delete(cancel);
|
||||
if (latestCounterfactualCancel === cancel) latestCounterfactualCancel = undefined;
|
||||
callback();
|
||||
};
|
||||
const cancel = () =>
|
||||
finish(() => reject(new Error('Counterfactual analysis was superseded.')));
|
||||
latestCounterfactualCancel = cancel;
|
||||
latestWorkerCancels.add(cancel);
|
||||
worker.onmessage = (
|
||||
event: MessageEvent<{
|
||||
type: string;
|
||||
analysis?: CounterfactualAnalysis;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}>,
|
||||
) => {
|
||||
if (event.data?.type === 'counterfactual-result' && event.data.analysis)
|
||||
finish(() => resolve(event.data.analysis!));
|
||||
else if (event.data?.type === 'error')
|
||||
finish(() =>
|
||||
reject(
|
||||
new Error(
|
||||
event.data.message ??
|
||||
`Counterfactual analysis failed (${event.data.code ?? 'worker-error'}).`,
|
||||
),
|
||||
),
|
||||
job.post({ type: 'counterfactual', request: snapshot, plan, placement, state }, (error) =>
|
||||
finish(() => reject(error)),
|
||||
);
|
||||
};
|
||||
worker.onerror = () => finish(() => reject(new Error('Counterfactual worker failed.')));
|
||||
try {
|
||||
worker.postMessage({
|
||||
type: 'counterfactual',
|
||||
request: snapshot,
|
||||
plan,
|
||||
placement,
|
||||
state,
|
||||
});
|
||||
} catch (error) {
|
||||
finish(() => reject(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -743,31 +742,19 @@ function explainCheckpointInWorker(
|
||||
interruption: PlanningReason,
|
||||
): Promise<Pick<PlanningArtifacts, 'explanation' | 'explanationError' | 'finalState'>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let worker: Worker;
|
||||
try {
|
||||
worker = new Worker(new URL('./leave-planner.worker', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
let job: WorkerJob;
|
||||
let settled = false;
|
||||
const finish = (callback: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
worker.terminate();
|
||||
if (cancel) latestWorkerCancels.delete(cancel);
|
||||
job.release();
|
||||
callback();
|
||||
};
|
||||
const cancel = () =>
|
||||
finish(() => reject(new Error('Checkpoint explanation was superseded.')));
|
||||
latestWorkerCancels.add(cancel);
|
||||
worker.onmessage = (event: MessageEvent<PlannerWorkerResponse>) => {
|
||||
const response = event.data;
|
||||
if (response?.type === 'explanation')
|
||||
try {
|
||||
job = startWorkerJob(
|
||||
() => finish(() => reject(new Error('Checkpoint explanation was superseded.'))),
|
||||
(response) => {
|
||||
if (response.type === 'explanation')
|
||||
finish(() =>
|
||||
resolve({
|
||||
explanation: response.explanation,
|
||||
@@ -775,21 +762,19 @@ function explainCheckpointInWorker(
|
||||
finalState: response.finalState,
|
||||
}),
|
||||
);
|
||||
else if (response?.type === 'error') finish(() => reject(new Error(response.message)));
|
||||
};
|
||||
worker.onerror = () =>
|
||||
finish(() => reject(new Error('Checkpoint explanation worker failed.')));
|
||||
try {
|
||||
worker.postMessage({
|
||||
type: 'explain-checkpoint',
|
||||
request: snapshot,
|
||||
plan,
|
||||
state,
|
||||
interruption,
|
||||
});
|
||||
else if (response.type === 'error')
|
||||
finish(() => reject(new Error(response.message)));
|
||||
},
|
||||
() => finish(() => reject(new Error('Checkpoint explanation worker failed.'))),
|
||||
);
|
||||
} catch (error) {
|
||||
finish(() => reject(error));
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
job.post(
|
||||
{ type: 'explain-checkpoint', request: snapshot, plan, state, interruption },
|
||||
(error) => finish(() => reject(error)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
CounterfactualAnalysis,
|
||||
LeavePlacement,
|
||||
PlanOutcome,
|
||||
PlanningExplanation,
|
||||
PlanningOutcome,
|
||||
PlanningReason,
|
||||
PlanningSnapshot,
|
||||
PlanningStateSnapshot,
|
||||
} from './leave-planner.service';
|
||||
import type { PlanningArtifacts } from './leave-planner.core';
|
||||
|
||||
// Message contract shared by the planning coordinator and its worker; types only.
|
||||
export type WorkerPlanningRequest = {
|
||||
calculationDate?: string;
|
||||
declaredDays?: unknown;
|
||||
horizon?: { start?: unknown; end?: unknown };
|
||||
balances?: unknown;
|
||||
rules?: unknown;
|
||||
identities?: unknown;
|
||||
labels?: unknown;
|
||||
deadlineMs?: unknown;
|
||||
progress?: unknown;
|
||||
};
|
||||
|
||||
export 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;
|
||||
};
|
||||
|
||||
export 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 };
|
||||
@@ -1,15 +1,7 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import type {
|
||||
CounterfactualAnalysis,
|
||||
LeavePlacement,
|
||||
PlanOutcome,
|
||||
PlanningExplanation,
|
||||
PlanningOutcome,
|
||||
PlanningReason,
|
||||
PlanningSnapshot,
|
||||
PlanningStateSnapshot,
|
||||
} from './leave-planner.service';
|
||||
import type { PlanningOutcome } from './leave-planner.service';
|
||||
import type { WorkerRequest, WorkerResponse } from './leave-planner.worker-messages';
|
||||
import {
|
||||
analyzeCounterfactualInternal,
|
||||
calculatePlanningAsyncInternal,
|
||||
@@ -17,54 +9,6 @@ import {
|
||||
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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user