Files
vac-optimizer/src/app/services/leave-planner.service.ts
T
nessarandClaude Opus 5.5 43677ff434 refactor(planning): drop the unreachable late worker message guard
release() already clears the worker handlers, so the released checks
inside them could never run. The lifetime test now states what it
checks: no handler is left for late messages.

Refs #277

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:22:38 +02:00

1014 lines
30 KiB
TypeScript

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<object, unknown>()): 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<string, unknown> = {};
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<string, unknown> = {};
for (const [key, item] of Object.entries(value)) result[key] = snapshotValue(item);
return result;
}
function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
if (typeof value !== 'object' || value === null || seen.has(value)) return value;
seen.add(value);
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child, seen);
return Object.freeze(value);
}
function stringMap(value: unknown): Readonly<Record<string, string>> {
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<PlanningOutcome>;
explanation: Promise<PlanningExplanation | undefined>;
explanationError: Promise<PlanningReason | undefined>;
artifacts: Promise<PlanningArtifacts>;
timings: Promise<PlanningCalculationTimings>;
cancel?: () => void;
}
export type PlanningCalculationTimings = Partial<PlanningArtifactTimings>;
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<WorkerResponse>) => {
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<PlanningOutcome>((resolve) => {
resolveOutcome = resolve;
});
const explanation = new Promise<PlanningExplanation | undefined>((resolve) => {
resolveExplanation = resolve;
});
const explanationError = new Promise<PlanningReason | undefined>((resolve) => {
resolveExplanationError = resolve;
});
const artifacts = new Promise<PlanningArtifacts>((resolve) => {
resolveArtifacts = resolve;
});
const timings = new Promise<PlanningCalculationTimings>((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<typeof setTimeout>;
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<PlanningArtifacts>,
isCurrent: () => boolean,
): Promise<CounterfactualAnalysis | StaleCounterfactualAnalysis> {
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<CounterfactualAnalysis> {
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<Pick<PlanningArtifacts, 'explanation' | 'explanationError' | 'finalState'>> {
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<PlanningOutcome>;
readonly explanation: Promise<PlanningExplanation | undefined>;
readonly explanationError: Promise<PlanningReason | undefined>;
readonly timings?: Promise<PlanningCalculationTimings>;
readonly analyzeCounterfactual: (
selection: CounterfactualSelection,
) => Promise<CounterfactualAnalysis | StaleCounterfactualAnalysis>;
}
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<PlanningArtifacts>(
(resolve) => (resolveArtifacts = resolve),
);
const explanation = new Promise<PlanningExplanation | undefined>(
(resolve) => (resolveExplanation = resolve),
);
const explanationError = new Promise<PlanningReason | undefined>(
(resolve) => (resolveExplanationError = resolve),
);
const outcome = new Promise<PlanningOutcome>((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<PlanningArtifacts>(
(resolve) => (resolveArtifacts = resolve),
);
const explanation = new Promise<PlanningExplanation | undefined>(
(resolve) => (resolveExplanation = resolve),
);
const explanationError = new Promise<PlanningReason | undefined>(
(resolve) => (resolveExplanationError = resolve),
);
const timings = new Promise<PlanningCalculationTimings>(
(resolve) => (resolveTimings = resolve),
);
const outcome = new Promise<PlanningOutcome>((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<PlanningOutcome> {
return createPlanningCalculation(request).outcome;
}