feat(planner): implement cancellation for planning calculations

- Refactor leave-planner.worker.ts to support cancellation of ongoing calculations.
- Introduce `calculatePlanningAsyncInternal` for asynchronous planning calculations.
- Add cancellation handling in the worker message event.
- Update planning-date.ts with new utility functions for date manipulation.
- Extend translation-service.ts and translation files (en.json, de.json, es.json, fr.json) to include new strings related to planning horizon and cancellation.
- Create documentation for the cancellation feature in planner-cancellation.md.
- Add unit tests for cancellation functionality in leave-planner-cancellation.spec.ts and leave-planner.worker.spec.ts.

#57 #58
This commit is contained in:
nessar committed 2026-10-05 15:34:07 +02:00
1 parent 5c35a59805
commit 2edf2004f5
31 files changed
+2275 -304

No files matched your search

+14 -3
View File
@@ -15,7 +15,7 @@ import type {
} from './leave-planner.service';
import {
analyzeCounterfactualInternal,
calculatePlanningInternal,
calculatePlanningAsyncInternal,
explainIncompleteCheckpoint,
historicalCounterfactualReasons,
type CounterfactualBaselineOptions,
@@ -38,6 +38,7 @@ type WorkerRequest =
request: WorkerPlanningRequest;
}
| { type: 'trace-ack'; count: number }
| { type: 'cancel' }
| {
type: 'counterfactual';
request: PlanningSnapshot;
@@ -120,6 +121,7 @@ interface TraceDelivery {
}
let activeTraceDelivery: TraceDelivery | undefined;
let activeCalculation: { cancelled: boolean } | undefined;
function traceStatusIndex(trace: PlanningTrace): {
statuses: Map<string, TraceCandidateStatus>;
@@ -272,8 +274,12 @@ function pumpTraceUpdates(delivery: TraceDelivery): void {
finishTraceDelivery(delivery);
}
worker.onmessage = (event: MessageEvent<WorkerRequest>) => {
worker.onmessage = async (event: MessageEvent<WorkerRequest>) => {
try {
if (event.data?.type === 'cancel') {
if (activeCalculation) activeCalculation.cancelled = true;
return;
}
if (event.data?.type === 'trace-ack') {
const delivery = activeTraceDelivery;
if (!delivery || !Number.isInteger(event.data.count) || event.data.count <= 0) return;
@@ -282,6 +288,8 @@ worker.onmessage = (event: MessageEvent<WorkerRequest>) => {
return;
}
if (event.data?.type === 'plan') {
const calculation = { cancelled: false };
activeCalculation = calculation;
let artifacts: PlanningArtifacts = {};
let outcomePosted = false;
let outcome: PlanningOutcome;
@@ -294,7 +302,7 @@ worker.onmessage = (event: MessageEvent<WorkerRequest>) => {
};
activeTraceDelivery = traceDelivery;
try {
outcome = calculatePlanningInternal(
outcome = await calculatePlanningAsyncInternal(
{
...event.data.request,
progress: (stage: string) =>
@@ -331,6 +339,7 @@ worker.onmessage = (event: MessageEvent<WorkerRequest>) => {
explanationError,
finalState,
} satisfies WorkerResponse),
() => calculation.cancelled,
);
} catch (error) {
if (activeTraceDelivery === traceDelivery) activeTraceDelivery = undefined;
@@ -341,6 +350,8 @@ worker.onmessage = (event: MessageEvent<WorkerRequest>) => {
message: error instanceof Error ? error.message : String(error),
} satisfies WorkerResponse);
return;
} finally {
if (activeCalculation === calculation) activeCalculation = undefined;
}
if (!outcomePosted)
worker.postMessage({ type: 'result', outcome } satisfies WorkerResponse);