import type { CounterfactualAnalysis, CounterfactualSelection, InvalidInputOutcome, LeaveBalanceInput, LeavePlacement, PlanOutcome, PlanningArtifacts, PlanningExplanation, PlanningIncompleteOutcome, PlanningOutcome, PlanningRequest, PlanningSnapshot, PlanningArtifactTimings, PlanningReason, PlanningStateSnapshot, StaleCounterfactualAnalysis, } from './leave-planner.core'; import { planningDateInParis } from './planning-date'; import type { WorkerRequest, WorkerResponse } from './leave-planner.worker-messages'; export type { IfThenPlanningRule, IfThenContribution, ConflictExplanation, CounterfactualAnalysis, CounterfactualSelection, CounterfactualDateResult, CounterfactualReasonCode, InvalidInputOutcome, IncompleteExplanation, LeaveBalanceInput, LeavePlacement, LeaveSelector, LeaveType, DeclaredNonWorkingDay, PlanOutcome, PlanExplanation, PlacementMode, PlanningConflictOutcome, PlanningDate, PlanningHorizon, PlanningIncompleteOutcome, PlanningExplanation, PlanningMetrics, PlanningObjective, PlanningObjectiveComparison, PlanningObjectiveDelta, PlanningObjectiveId, PlanningObjectiveVector, PlanningOutcome, PlanningReason, PlanningRequest, PlanningRule, PlanningSnapshot, PlanningArtifactTimings, PlanningStateSnapshot, PreferenceResult, RemainingBalance, RemainingClassification, RuleForce, TimeOffPeriod, TimeWindowContribution, TimeWindowPlanningRule, OptimizationPlanningRule, } from './leave-planner.core'; // Responsive workers renew this inactivity timer regardless of calculation duration. const WORKER_LIVENESS_TIMEOUT_MS = 30_000; interface UntrustedRecord { horizon?: unknown; start?: unknown; end?: unknown; balances?: unknown; declaredDays?: unknown; rules?: unknown; identities?: unknown; labels?: unknown; deadlineMs?: unknown; progress?: unknown; id?: unknown; name?: unknown; type?: unknown; quantity?: unknown; label?: unknown; availableFrom?: unknown; expiryDate?: unknown; enabled?: unknown; force?: unknown; startDate?: unknown; endDate?: unknown; placeDays?: unknown; placeType?: unknown; dayOfWeek?: unknown; placement?: unknown; rewardDays?: unknown; rewardType?: unknown; configuration?: unknown; } interface TransportSnapshot { calculationDate?: string; horizon?: { start?: unknown; end?: unknown }; balances?: unknown; declaredDays?: unknown; rules?: unknown; identities?: unknown; labels?: unknown; deadlineMs?: unknown; progress?: unknown; } function asRecord(value: unknown): UntrustedRecord | undefined { return typeof value === 'object' && value !== null ? (value as UntrustedRecord) : undefined; } function copyDate(value: unknown): unknown { return value instanceof Date ? new Date(value.getTime()) : value; } function cloneUnknown(value: unknown, seen = new WeakMap()): unknown { if (value instanceof Date) return new Date(value.getTime()); if (typeof value !== 'object' || value === null) return value; const previous = seen.get(value); if (previous) return previous; if (Array.isArray(value)) { const result: unknown[] = []; seen.set(value, result); for (const item of value) result.push(cloneUnknown(item, seen)); return result; } const result: Record = {}; seen.set(value, result); for (const [key, item] of Object.entries(value)) result[key] = cloneUnknown(item, seen); return result; } function snapshotValue(value: unknown): unknown { if (value instanceof Date) return Number.isNaN(value.getTime()) ? 'Invalid Date' : planningDateInParis(value); if (Array.isArray(value)) return value.map(snapshotValue); if (typeof value !== 'object' || value === null) return value; const result: Record = {}; for (const [key, item] of Object.entries(value)) result[key] = snapshotValue(item); return result; } function deepFreeze(value: T, seen = new WeakSet()): T { if (typeof value !== 'object' || value === null || seen.has(value)) return value; seen.add(value); for (const child of Object.values(value as Record)) deepFreeze(child, seen); return Object.freeze(value); } function stringMap(value: unknown): Readonly> { if (typeof value !== 'object' || value === null || Array.isArray(value)) return {}; return Object.fromEntries( Object.entries(value).map(([key, item]) => [ key, typeof item === 'string' ? item : String(item), ]), ); } function publicSnapshot(request: TransportSnapshot): PlanningSnapshot { const horizon = request.horizon ?? {}; const balances = Array.isArray(request.balances) ? request.balances.map((value) => snapshotValue(value) as LeaveBalanceInput) : []; const rules = Array.isArray(request.rules) ? request.rules.map((value) => snapshotValue(value) as PlanningRequest['rules'][number]) : []; const labels = stringMap(request.labels); return deepFreeze({ calculationDate: request.calculationDate, horizon: { start: String(snapshotValue(horizon.start) ?? ''), end: String(snapshotValue(horizon.end) ?? ''), }, balances, ...(request.declaredDays === undefined ? {} : { declaredDays: snapshotValue(request.declaredDays) }), rules, identities: stringMap(request.identities), labels, }) as PlanningSnapshot; } function snapshotRequest(request: unknown): TransportSnapshot { const source = asRecord(request) ?? {}; const horizon = asRecord(source.horizon); const balances = Array.isArray(source.balances) ? source.balances.map((balance) => { const value = asRecord(balance); return { id: value?.id, type: value?.type, quantity: value?.quantity, name: value?.name, label: value?.label, availableFrom: value?.availableFrom === undefined ? undefined : copyDate(value.availableFrom), expiryDate: copyDate(value?.expiryDate), }; }) : source.balances; const rules = Array.isArray(source.rules) ? source.rules.map((rule) => { const value = asRecord(rule); const base = { id: value?.id, name: value?.name, type: value?.type, enabled: value?.enabled, force: value?.force, }; if ( typeof value?.type === 'string' && value.type.toLowerCase() === 'optimization' ) { return { ...base, configuration: cloneUnknown(value.configuration) }; } if (typeof value?.type === 'string' && value.type.toLowerCase() === 'time_window') { return { ...base, startDate: copyDate(value.startDate), endDate: copyDate(value.endDate), placeDays: value.placeDays, placeType: value.placeType, }; } return { ...base, dayOfWeek: value?.dayOfWeek, placeDays: value?.placeDays, placeType: value?.placeType, placement: value?.placement, rewardDays: value?.rewardDays, rewardType: value?.rewardType, }; }) : source.rules; return { horizon: horizon ? { start: copyDate(horizon.start), end: copyDate(horizon.end), } : undefined, balances, declaredDays: cloneUnknown(source.declaredDays), rules, identities: cloneUnknown(source.identities), labels: cloneUnknown(source.labels), deadlineMs: source.deadlineMs, progress: typeof source.progress === 'function' ? source.progress : undefined, }; } function invalidDeadline(request: TransportSnapshot): InvalidInputOutcome | undefined { const deadline = request.deadlineMs; if ( deadline !== undefined && (typeof deadline !== 'number' || !Number.isFinite(deadline) || deadline < 0) ) return { kind: 'InvalidInput', reasons: [ { code: 'deadline', message: 'The planning deadline must be a finite non-negative number.', }, ], }; return undefined; } interface PlanningRun { outcome: Promise; explanation: Promise; explanationError: Promise; artifacts: Promise; timings: Promise; cancel?: () => void; } export type PlanningCalculationTimings = Partial; function interruptedOutcome( checkpoint: PlanOutcome | undefined, reasonCode: string, message: string, ): PlanningIncompleteOutcome { return { kind: 'PlanningIncomplete', incumbent: checkpoint, completedStages: checkpoint?.completedStages ?? [], lastCompletedStage: checkpoint?.lastCompletedStage, reasons: [{ code: reasonCode, message }], }; } 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) => { if (event.data) onMessage(event.data); }; worker.onerror = () => { 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(); latestCounterfactualCancel = undefined; } function planInWorker(snapshot: TransportSnapshot): PlanningRun { let resolveOutcome!: (outcome: PlanningOutcome) => void; let resolveExplanation!: (explanation: PlanningExplanation | undefined) => void; let resolveExplanationError!: (error: PlanningReason | undefined) => void; let resolveArtifacts!: (artifacts: PlanningArtifacts) => void; let resolveTimings!: (timings: PlanningCalculationTimings) => void; const outcome = new Promise((resolve) => { resolveOutcome = resolve; }); const explanation = new Promise((resolve) => { resolveExplanation = resolve; }); const explanationError = new Promise((resolve) => { resolveExplanationError = resolve; }); const artifacts = new Promise((resolve) => { resolveArtifacts = resolve; }); const timings = new Promise((resolve) => { resolveTimings = resolve; }); let cancel: (() => void) | undefined; try { 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; let explanationSettled = false; let artifactsSettled = false; let checkpoint: PlanOutcome | undefined; let checkpointState: PlanningStateSnapshot | undefined; let receivedExplanation: PlanningExplanation | undefined; let receivedExplanationError: PlanningReason | undefined; let receivedFinalState: PlanningStateSnapshot | undefined; let livenessWatchdog: ReturnType; const terminate = () => { job.release(); clearTimeout(livenessWatchdog); }; const finishOutcome = (result: PlanningOutcome) => { if (outcomeSettled) return; if ( cancellationRequested && (result.kind === 'Plan' || result.kind === 'PlanningConflict') ) { cancelledRace = interruptedOutcome( result.kind === 'Plan' ? result : checkpoint, 'user-cancelled', 'The calculation was cancelled by the user.', ); result = cancelledRace; } outcomeSettled = true; clearTimeout(livenessWatchdog); resolveOutcome(result); }; const cancelledExplanation = ( result: PlanningExplanation | undefined, ): PlanningExplanation | undefined => { if (!cancelledRace) return result; const incomplete = { kind: 'IncompleteExplanation' as const, reason: cancelledRace.reasons[0], completedStages: cancelledRace.completedStages, uncompletedStages: [], }; return cancelledRace.incumbent && result?.kind === 'PlanExplanation' ? { ...result, incomplete } : incomplete; }; const finishExplanation = ( result: PlanningExplanation | undefined, error: PlanningReason | undefined, finalState?: PlanningStateSnapshot, ) => { if (explanationSettled) return; if (!error) result = cancelledExplanation(result); explanationSettled = true; receivedExplanation = result; receivedExplanationError = error; receivedFinalState = finalState; resolveExplanation(result); resolveExplanationError(error); }; const finishArtifacts = (result: PlanningArtifacts, finishExplanationPromise = true) => { if (artifactsSettled) return; artifactsSettled = true; const complete = { ...result, explanation: result.explanation ?? receivedExplanation, explanationError: result.explanationError ?? receivedExplanationError, finalState: result.finalState ?? receivedFinalState, }; if (cancelledRace && !complete.explanationError) complete.explanation = cancelledExplanation(complete.explanation); if (finishExplanationPromise) finishExplanation( complete.explanation, complete.explanationError, complete.finalState, ); resolveArtifacts(complete); resolveTimings(complete.timings ?? {}); terminate(); }; const supersede = () => { if (artifactsSettled) return; const hadOutcome = outcomeSettled; const reason = { code: 'stale-calculation', message: 'Planning stopped because a newer calculation started.', }; if (!hadOutcome) finishOutcome(interruptedOutcome(checkpoint, reason.code, reason.message)); finishExplanation( receivedExplanation, hadOutcome ? receivedExplanationError : reason, receivedFinalState ?? checkpointState, ); finishArtifacts({ explanation: receivedExplanation, explanationError: hadOutcome ? receivedExplanationError : reason, finalState: receivedFinalState ?? checkpointState, }); }; const fail = (code: string, message: string) => { const reason = { code, message }; if (!outcomeSettled) finishOutcome(interruptedOutcome(checkpoint, code, message)); if (!explanationSettled && checkpoint && checkpointState) { finishArtifacts({ explanationError: reason, finalState: checkpointState }, false); void explainCheckpointInWorker( publicSnapshot(snapshot), checkpoint, checkpointState, reason, ) .then((result) => finishExplanation( result.explanation, result.explanationError ?? reason, result.finalState ?? checkpointState, ), ) .catch((error: unknown) => finishExplanation( undefined, { code: 'explanation-error', message: error instanceof Error ? error.message : String(error), }, checkpointState, ), ); } else finishArtifacts({ explanation: receivedExplanation, explanationError: receivedExplanationError ?? reason, finalState: receivedFinalState ?? checkpointState, }); }; const scheduleLivenessWatchdog = () => { if (outcomeSettled || artifactsSettled) return; clearTimeout(livenessWatchdog); livenessWatchdog = setTimeout( () => fail( 'worker-error', 'Planning worker stopped responding before returning its outcome.', ), WORKER_LIVENESS_TIMEOUT_MS, ); }; scheduleLivenessWatchdog(); cancel = () => { if (outcomeSettled || cancellationRequested) return; cancellationRequested = true; job.post({ type: 'cancel' }, () => fail('worker-error', 'The cancellation request could not reach the worker.'), ); }; const handleMessage = (message: WorkerResponse) => { if (message.type === 'heartbeat') { scheduleLivenessWatchdog(); return; } if (message.type === 'progress') { scheduleLivenessWatchdog(); if (typeof snapshot.progress === 'function') snapshot.progress(message.stage); return; } if (message.type === 'checkpoint') { scheduleLivenessWatchdog(); checkpoint = message.plan; checkpointState = message.state; return; } if (message.type === 'result') { finishOutcome(message.outcome); if (message.outcome.kind === 'InvalidInput') finishArtifacts({}); return; } if (message.type === 'explanation') { finishExplanation( message.explanation, message.explanationError, message.finalState, ); return; } if (message.type === 'artifacts') { finishArtifacts(message.artifacts); return; } if (message.type === 'error') fail(message.code ?? 'worker-error', message.message); else fail('worker-error', 'Planning worker returned an unexpected response.'); }; 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, 'worker-unavailable', 'Planning could not start because isolated execution is unavailable.', ); resolveOutcome(result); resolveArtifacts({ explanationError: result.reasons[0] }); resolveExplanation(undefined); resolveExplanationError(result.reasons[0]); resolveTimings({}); } return { outcome, explanation, explanationError, artifacts, timings, cancel }; } let calculationSequence = 0; let latestCalculationId = ''; function selectionFor( plan: PlanOutcome, selection: CounterfactualSelection, ): LeavePlacement | undefined { if (typeof selection !== 'string') return plan.placements.find( (candidate) => candidate.date === selection.date && candidate.balanceId === selection.balanceId, ); return plan.placements.find( (candidate) => candidate.date === selection || candidate.balanceId === selection, ); } async function runCounterfactual( context: PlanningCalculationContext, selection: CounterfactualSelection, artifacts: Promise, isCurrent: () => boolean, ): Promise { const stale = (placement: LeavePlacement): StaleCounterfactualAnalysis => ({ kind: 'CounterfactualStale', calculationId: context.id, placement, hypothetical: true, results: [], }); if (context.id !== latestCalculationId) { const plan = await context.outcome; const finalPlan = plan.kind === 'Plan' ? plan : plan.kind === 'PlanningIncomplete' ? plan.incumbent : undefined; const placement = finalPlan ? selectionFor(finalPlan, selection) : undefined; if (!placement) throw new Error('The selected placement is not present in the calculation.'); return stale(placement); } const outcome = await context.outcome; if (context.id !== latestCalculationId || !isCurrent()) { const plan = outcome.kind === 'Plan' ? outcome : outcome.kind === 'PlanningIncomplete' ? outcome.incumbent : undefined; const placement = plan ? selectionFor(plan, selection) : undefined; if (!placement) throw new Error('The selected placement is not present in the calculation.'); return stale(placement); } const plan = outcome.kind === 'Plan' ? outcome : outcome.kind === 'PlanningIncomplete' ? outcome.incumbent : undefined; if (!plan) throw new Error('Counterfactual analysis requires a plan outcome.'); const placement = selectionFor(plan, selection); if (!placement) throw new Error('The selected placement is not present in the calculation.'); const completedArtifacts = await artifacts; if (context.id !== latestCalculationId || !isCurrent()) return stale(placement); if (typeof window !== 'undefined' && typeof Worker !== 'undefined') { let result: CounterfactualAnalysis; try { result = await counterfactualInWorker( context.snapshot, plan, placement, completedArtifacts.finalState, ); } catch (error) { if (context.id !== latestCalculationId || !isCurrent()) return stale(placement); throw error; } if (context.id !== latestCalculationId || !isCurrent()) return stale(placement); return { ...result, calculationId: context.id }; } const core = await import('./leave-planner.core'); const result = core.analyzeCounterfactualInternal( context.snapshot, plan, placement, completedArtifacts.finalState, ); return context.id === latestCalculationId && isCurrent() ? { ...result, calculationId: context.id } : stale(placement); } function counterfactualInWorker( snapshot: PlanningSnapshot, plan: PlanOutcome, placement: LeavePlacement, state?: PlanningStateSnapshot, ): Promise { return new Promise((resolve, reject) => { latestCounterfactualCancel?.(); 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 { 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; } latestCounterfactualCancel = cancel; job.post({ type: 'counterfactual', request: snapshot, plan, placement, state }, (error) => finish(() => reject(error)), ); }); } function explainCheckpointInWorker( snapshot: PlanningSnapshot, plan: PlanOutcome, state: PlanningStateSnapshot, interruption: PlanningReason, ): Promise> { return new Promise((resolve, reject) => { let job: WorkerJob; let settled = false; const finish = (callback: () => void) => { if (settled) return; settled = true; job.release(); callback(); }; try { job = startWorkerJob( () => finish(() => reject(new Error('Checkpoint explanation was superseded.'))), (response) => { if (response.type === 'explanation') finish(() => resolve({ explanation: response.explanation, explanationError: response.explanationError, finalState: response.finalState, }), ); else if (response.type === 'error') finish(() => reject(new Error(response.message))); }, () => finish(() => reject(new Error('Checkpoint explanation worker failed.'))), ); } catch (error) { reject(error); return; } job.post( { type: 'explain-checkpoint', request: snapshot, plan, state, interruption }, (error) => finish(() => reject(error)), ); }); } export interface PlanningCalculationContext { readonly cancel: () => void; readonly id: string; readonly snapshot: PlanningSnapshot; readonly outcome: Promise; readonly explanation: Promise; readonly explanationError: Promise; readonly timings?: Promise; readonly analyzeCounterfactual: ( selection: CounterfactualSelection, ) => Promise; } export function createPlanningCalculation(request: PlanningRequest): PlanningCalculationContext { const calculationDate = planningDateInParis(new Date()); cancelLatestWorkers(); const transport = { ...snapshotRequest(request), calculationDate }; const snapshot = publicSnapshot(transport); const id = `calculation:${++calculationSequence}`; latestCalculationId = id; const deadlineError = invalidDeadline(transport); let run: PlanningRun; if (deadlineError) { run = { outcome: new Promise((resolve) => queueMicrotask(() => resolve(deadlineError))), explanation: Promise.resolve(undefined), explanationError: Promise.resolve(undefined), artifacts: Promise.resolve({}), timings: Promise.resolve({}), }; } else { if (typeof window !== 'undefined') { if (typeof Worker !== 'undefined') { run = planInWorker(transport); } else { let resolveArtifacts!: (artifacts: PlanningArtifacts) => void; let resolveExplanation!: (explanation: PlanningExplanation | undefined) => void; let resolveExplanationError!: (error: PlanningReason | undefined) => void; const artifacts = new Promise( (resolve) => (resolveArtifacts = resolve), ); const explanation = new Promise( (resolve) => (resolveExplanation = resolve), ); const explanationError = new Promise( (resolve) => (resolveExplanationError = resolve), ); const outcome = new Promise((resolve) => { queueMicrotask(async () => { const core = await import('./leave-planner.core'); const validation = core.invalidInputFor(transport); if (validation) { resolve(validation); resolveExplanation(undefined); resolveExplanationError(undefined); resolveArtifacts({}); return; } const result = interruptedOutcome( undefined, 'worker-unavailable', 'Planning could not start because isolated execution is unavailable.', ); resolve(result); const incompleteExplanation: PlanningExplanation = { kind: 'IncompleteExplanation', reason: result.reasons[0], completedStages: [], uncompletedStages: [], }; resolveExplanation(incompleteExplanation); resolveExplanationError(undefined); resolveArtifacts({ explanation: incompleteExplanation, }); }); }); run = { outcome, explanation, explanationError, artifacts, timings: Promise.resolve({}), }; } } else { let cancellationRequested = false; let outcomeSettled = false; let superseded = false; const replacementReason: PlanningReason = { code: 'stale-calculation', message: 'Planning stopped because a newer calculation started.', }; const explanationForRun = (value: PlanningExplanation | undefined) => { if (!superseded) return value; if (value?.kind === 'IncompleteExplanation') return { ...value, reason: replacementReason }; if (value?.kind === 'PlanExplanation' && value.incomplete) return { ...value, incomplete: { ...value.incomplete, reason: replacementReason }, }; return value; }; const supersede = () => { if (!outcomeSettled) { superseded = true; cancellationRequested = true; } }; latestWorkerCancels.add(supersede); let resolveArtifacts!: (artifacts: PlanningArtifacts) => void; let resolveExplanation!: (explanation: PlanningExplanation | undefined) => void; let resolveExplanationError!: (error: PlanningReason | undefined) => void; let resolveTimings!: (timings: PlanningCalculationTimings) => void; const artifacts = new Promise( (resolve) => (resolveArtifacts = resolve), ); const explanation = new Promise( (resolve) => (resolveExplanation = resolve), ); const explanationError = new Promise( (resolve) => (resolveExplanationError = resolve), ); const timings = new Promise( (resolve) => (resolveTimings = resolve), ); const outcome = new Promise((resolve) => { queueMicrotask(async () => { try { const core = await import('./leave-planner.core'); let completedArtifacts: PlanningArtifacts = {}; const result = await core.calculatePlanningAsyncInternal( transport, undefined, (value) => (completedArtifacts = value), (value) => { outcomeSettled = true; resolve( superseded && value.kind === 'PlanningIncomplete' ? { ...value, reasons: [replacementReason], } : value, ); }, (value, error) => { resolveExplanation(explanationForRun(value)); resolveExplanationError(error); }, () => cancellationRequested, ); if (result.kind === 'InvalidInput') { resolveExplanation(undefined); resolveExplanationError(undefined); } resolve(result); queueMicrotask(() => { resolveArtifacts({ ...completedArtifacts, explanation: explanationForRun(completedArtifacts.explanation), }); resolveTimings(completedArtifacts.timings ?? {}); }); } catch (error) { const reason: PlanningReason = { code: 'planner-error', message: error instanceof Error ? error.message : String(error), }; resolve({ kind: 'PlanningIncomplete', completedStages: [], reasons: [reason], }); resolveExplanation(undefined); resolveExplanationError(reason); resolveArtifacts({ explanationError: reason, }); resolveTimings({}); } finally { outcomeSettled = true; latestWorkerCancels.delete(supersede); } }); }); run = { outcome, explanation, explanationError, artifacts, timings, cancel: () => { if (!outcomeSettled) cancellationRequested = true; }, }; } } let counterfactualSequence = 0; const context: PlanningCalculationContext = { id, snapshot, cancel: () => run.cancel?.(), outcome: run.outcome.then((result) => ({ ...result, calculationDate, ...(result.kind === 'PlanningIncomplete' && result.incumbent ? { incumbent: { ...result.incumbent, calculationDate } } : {}), })), explanation: run.explanation.then((result) => result ? { ...result, calculationDate } : undefined, ), explanationError: run.explanationError, timings: run.timings, analyzeCounterfactual: (selection) => { const sequence = ++counterfactualSequence; if (id === latestCalculationId) latestCounterfactualCancel?.(); return runCounterfactual( context, selection, run.artifacts, () => sequence === counterfactualSequence, ); }, }; return context; } export async function planLeave(request: PlanningRequest): Promise { return createPlanningCalculation(request).outcome; }