Files
vac-optimizer/src/app/services/leave-planner.service.ts
T
nessar 9851edf906 feat(planning): #62 #63 #64 enhance leave planner with calculation date and entitlement details
- Introduced calculationDate in various service interfaces and snapshots to track the date of planning calculations.
- Added futureEntitlement and includedEntitlement fields to the translation service and respective language files for better user feedback.
- Updated leave-planner.worker to handle calculationDate and declaredDays in requests.
- Enhanced tests to ensure proper handling of calculation dates and artifact retention during planning processes.
- Improved snapshot and explanation outputs to include calculation date for clarity in planning results.
2026-10-07 19:22:46 +02:00

1818 lines
56 KiB
TypeScript

import type {
CounterfactualAnalysis,
CounterfactualBaselineOptions,
CounterfactualSelection,
InvalidInputOutcome,
LeaveBalanceInput,
LeavePlacement,
LeaveType,
DeclaredNonWorkingDay,
PlanOutcome,
PlanningArtifacts,
PlanningExplanation,
PlanningIncompleteOutcome,
PlanningOutcome,
PlanningRequest,
PlanningSnapshot,
PlanningArtifactTimings,
PlanningTrace,
PlanningTraceCandidate,
PlanningTraceDecision,
PlanningTraceDelta,
PlanningTracePhase,
PlanningTraceUpdate,
PlanningReason,
PlanningTraceState,
HistoricalCounterfactualReason,
StaleCounterfactualAnalysis,
TraceCandidateStatus,
TraceCounterfactualSelection,
} from './leave-planner.core';
import { planningDateInParis } from './planning-date';
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,
PlanningTrace,
PlanningTraceCandidate,
PlanningTraceDecision,
PlanningTraceDelta,
PlanningTracePhase,
PlanningTraceState,
PlanningTraceOperation,
PlanningTraceUpdate,
PreferenceResult,
RemainingBalance,
RemainingClassification,
RuleForce,
TimeOffPeriod,
TimeWindowContribution,
TimeWindowPlanningRule,
TraceCounterfactualSelection,
TraceCandidateStatus,
} from './leave-planner.core';
// Responsive workers renew this inactivity timer regardless of calculation duration.
const WORKER_LIVENESS_TIMEOUT_MS = 30_000;
const MAX_TRACE_UPDATES_PER_DRAIN = 16;
const TRACE_DRAIN_TIME_BUDGET_MS = 4;
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;
}
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() === '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;
}
type PlannerWorkerResponse =
| { type: 'heartbeat' }
| { type: 'progress'; stage: string }
| { type: 'trace-overhead'; totalMs: number }
| { type: 'checkpoint'; plan: PlanOutcome; state?: PlanningTraceState }
| { type: 'result'; outcome: PlanningOutcome }
| { type: 'artifacts'; artifacts: PlanningArtifacts }
| {
type: 'explanation';
explanation?: PlanningExplanation;
explanationError?: PlanningReason;
finalState?: PlanningTraceState;
}
| {
type: 'artifacts-start';
artifacts: Omit<
PlanningArtifacts,
'trace' | 'explanation' | 'explanationError' | 'finalState'
>;
trace?: Omit<PlanningTrace, 'phases'>;
}
| {
type: 'trace-update';
totalMs: number;
update: PlanningTraceUpdate;
acknowledge?: boolean;
}
| {
type: 'trace-status-index';
statuses: ReadonlyMap<string, TraceCandidateStatus>;
nonWorkingDates: readonly string[];
}
| { type: 'artifacts-complete'; traceTransferMs?: number }
| {
type: 'historical-eligibility-result';
entries: readonly HistoricalEligibilityReason[];
}
| { type: 'error'; code?: string; message: string };
export type HistoricalEligibilityReason = HistoricalCounterfactualReason;
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> & {
traceTransferMs?: number;
};
type MutableTraceDecision = Omit<PlanningTraceDecision, 'candidates' | 'deltas'> & {
candidates: PlanningTraceCandidate[];
deltas: PlanningTraceDelta[];
};
type MutableTracePhase = Omit<PlanningTracePhase, 'decisions'> & {
decisions: MutableTraceDecision[];
};
type MutableTrace = Omit<PlanningTrace, 'phases'> & { phases: MutableTracePhase[] };
function traceStateFor(snapshot: TransportSnapshot): PlanningTraceState {
const horizon = snapshot.horizon ?? {};
const balances = Array.isArray(snapshot.balances) ? snapshot.balances : [];
return {
calculationDate: snapshot.calculationDate,
horizon: {
start: String(horizon.start ?? ''),
end: String(horizon.end ?? ''),
},
balances: balances.map((item) => {
const balance = asRecord(item);
return {
id: String(balance?.id ?? ''),
type: String(balance?.type ?? 'other') as LeaveType,
quantity: typeof balance?.quantity === 'number' ? balance.quantity : 0,
remaining: typeof balance?.quantity === 'number' ? balance.quantity : 0,
availableFrom: String(balance?.availableFrom ?? horizon.start ?? ''),
expiryDate: String(balance?.expiryDate ?? ''),
earned: false,
};
}),
...(Array.isArray(snapshot.declaredDays)
? { declaredDays: snapshot.declaredDays as DeclaredNonWorkingDay[] }
: {}),
placements: [],
earnedActivations: [],
};
}
function fallbackTrace(
snapshot: TransportSnapshot,
status: PlanningTrace['status'],
error?: PlanningReason,
): PlanningTrace {
return {
status,
initialState: traceStateFor(snapshot),
phases: [],
completedStages: [],
error,
};
}
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;
let latestHistoricalEligibilityCancel: (() => void) | undefined;
function cancelLatestWorkers(): void {
for (const cancel of [...latestWorkerCancels]) cancel();
latestWorkerCancels.clear();
latestCounterfactualCancel = undefined;
latestHistoricalEligibilityCancel = 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 supersede: (() => void) | undefined;
let cancel: (() => void) | undefined;
try {
const worker = new Worker(new URL('./leave-planner.worker', import.meta.url), {
type: 'module',
});
let outcomeSettled = false;
let cancellationRequested = false;
let cancelledRace: PlanningIncompleteOutcome | undefined;
let explanationSettled = false;
let artifactsSettled = false;
let checkpoint: PlanOutcome | undefined;
let checkpointState: PlanningTraceState | undefined;
let receivedArtifacts: Omit<
PlanningArtifacts,
'trace' | 'explanation' | 'explanationError' | 'finalState'
> = {};
let receivedExplanation: PlanningExplanation | undefined;
let receivedExplanationError: PlanningReason | undefined;
let receivedFinalState: PlanningTraceState | undefined;
let receivedTrace: MutableTrace | undefined;
const phasesById = new Map<string, MutableTracePhase>();
const decisionsById = new Map<string, MutableTraceDecision>();
let traceUpdates: (QueuedTraceUpdate | undefined)[] = [];
let traceUpdateIndex = 0;
let traceDrainTimer: ReturnType<typeof setTimeout> | undefined;
let traceTransferMs = 0;
let terminalTraceTransferMs: number | undefined;
let artifactsCompleteReceived = false;
let pendingFailure: { code: string; message: string } | undefined;
let receivedStatuses: ReadonlyMap<string, TraceCandidateStatus> | undefined;
let receivedNonWorkingDates: readonly string[] = [];
let livenessWatchdog: ReturnType<typeof setTimeout>;
let workerStopped = false;
const stopWorker = () => {
if (workerStopped) return;
workerStopped = true;
worker.onmessage = null;
worker.onerror = null;
worker.terminate();
};
const terminate = () => {
stopWorker();
clearTimeout(livenessWatchdog);
if (traceDrainTimer !== undefined) clearTimeout(traceDrainTimer);
traceDrainTimer = undefined;
phasesById.clear();
decisionsById.clear();
};
const finishOutcome = (result: PlanningOutcome) => {
if (!outcomeSettled) {
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?: PlanningTraceState,
) => {
if (!explanationSettled) {
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) {
artifactsSettled = true;
const complete = {
...result,
explanation: result.explanation ?? receivedExplanation,
explanationError: result.explanationError ?? receivedExplanationError,
finalState: result.finalState ?? receivedFinalState,
};
if (cancelledRace) {
if (!complete.explanationError)
complete.explanation = cancelledExplanation(complete.explanation);
if (complete.trace && complete.trace.status !== 'failed')
complete.trace = {
...complete.trace,
status: 'incomplete',
error: cancelledRace.reasons[0],
};
}
if (finishExplanationPromise)
finishExplanation(
complete.explanation,
complete.explanationError,
complete.finalState,
);
resolveArtifacts(complete);
if (complete.trace && receivedStatuses) {
historicalStatusCache.set(complete.trace, new Map(receivedStatuses));
historicalNonWorkingCache.set(complete.trace, new Set(receivedNonWorkingDates));
}
resolveTimings({
...complete.timings,
traceTransferMs:
(terminalTraceTransferMs ?? complete.timings?.traceTransferMs ?? 0) +
traceTransferMs,
});
terminate();
if (supersede) latestWorkerCancels.delete(supersede);
}
};
const partialArtifacts = (
status: PlanningTrace['status'],
error: PlanningReason,
includeError = true,
) => {
const trace = receivedTrace ?? fallbackTrace(snapshot, status, error);
return {
...receivedArtifacts,
trace: { ...trace, status, error },
explanation: receivedExplanation,
finalState: receivedFinalState,
explanationError: includeError
? (receivedExplanationError ?? error)
: receivedExplanationError,
};
};
const applyTraceUpdate = (update: PlanningTraceUpdate): void => {
if (!receivedTrace && update.initialState)
receivedTrace = {
status: 'incomplete',
initialState: update.initialState,
phases: [],
completedStages: [],
nonWorkingDates: update.nonWorkingDates,
};
if (!receivedTrace) throw new Error('Trace update arrived without its initial state.');
if (update.nonWorkingDates) receivedTrace.nonWorkingDates = update.nonWorkingDates;
const phases = receivedTrace.phases;
for (const operation of update.operations) {
if (operation.kind === 'phase') {
const phase = phasesById.get(operation.phase.id);
if (phase) Object.assign(phase, operation.phase);
else {
const created = { ...operation.phase, decisions: [] };
phases.push(created);
phasesById.set(created.id, created);
}
continue;
}
const phase = phasesById.get(operation.phaseId);
if (!phase) throw new Error('Trace update referenced a missing phase.');
const decisions = phase.decisions;
if (operation.kind === 'decision') {
const decision = decisionsById.get(operation.decision.id);
if (decision) Object.assign(decision, operation.decision);
else {
const created = { ...operation.decision, candidates: [], deltas: [] };
decisions.push(created);
decisionsById.set(created.id, created);
}
continue;
}
const decision = decisionsById.get(operation.decisionId);
if (!decision) throw new Error('Trace update referenced a missing decision.');
if (operation.kind === 'candidate') {
const candidates = decision.candidates;
if (operation.mode === 'append' && operation.index === candidates.length)
candidates.push(operation.candidate);
else if (operation.mode === 'replace' && operation.index < candidates.length)
candidates[operation.index] = operation.candidate;
else throw new Error('Trace candidate update arrived out of order.');
continue;
}
const deltas = decision.deltas;
if (operation.index !== deltas.length)
throw new Error('Trace delta update arrived out of order.');
deltas.push(operation.delta);
}
};
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,
);
traceUpdates = [];
traceUpdateIndex = 0;
finishArtifacts(
hadOutcome
? partialArtifacts('failed', reason, false)
: {
trace: fallbackTrace(snapshot, 'failed', reason),
explanationError: reason,
},
);
};
if (supersede) latestWorkerCancels.add(supersede);
const finalizeFailure = (reason: { code: string; message: string }) => {
if (!explanationSettled && checkpoint && checkpointState) {
receivedFinalState ??= checkpointState;
finishArtifacts(partialArtifacts('failed', reason), 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,
),
);
return;
}
finishExplanation(
receivedExplanation,
receivedExplanationError ?? reason,
receivedFinalState ?? checkpointState,
);
finishArtifacts(partialArtifacts('failed', reason));
};
let scheduleTraceDrain: () => void = () => {};
const fail = (code: string, message: string) => {
if (!outcomeSettled) finishOutcome(interruptedOutcome(checkpoint, code, message));
pendingFailure ??= { code, message };
if (traceUpdateIndex < traceUpdates.length) scheduleTraceDrain();
else finalizeFailure(pendingFailure);
};
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,
);
};
const finishDeliveredTrace = () =>
finishArtifacts({
...receivedArtifacts,
trace: receivedTrace,
explanation: receivedExplanation,
explanationError: receivedExplanationError,
finalState: receivedFinalState,
});
scheduleTraceDrain = () => {
if (
traceDrainTimer !== undefined ||
artifactsSettled ||
traceUpdateIndex >= traceUpdates.length
)
return;
traceDrainTimer = setTimeout(() => {
traceDrainTimer = undefined;
const drainStarted = performance.now();
let drained = 0;
let acknowledged = 0;
while (
traceUpdateIndex < traceUpdates.length &&
drained < MAX_TRACE_UPDATES_PER_DRAIN
) {
if (
drained > 0 &&
performance.now() - drainStarted >= TRACE_DRAIN_TIME_BUDGET_MS
)
break;
const updateIndex = traceUpdateIndex++;
const queued = traceUpdates[updateIndex];
traceUpdates[updateIndex] = undefined;
const started = performance.now();
let applied = false;
try {
if (queued) {
applyTraceUpdate(queued.update);
applied = true;
}
} catch (error) {
traceUpdates = [];
traceUpdateIndex = 0;
pendingFailure = {
code: 'trace-error',
message: error instanceof Error ? error.message : String(error),
};
} finally {
const duration = Math.max(0, performance.now() - started);
traceTransferMs += duration;
}
if (applied && queued?.acknowledge) acknowledged++;
drained++;
}
if (traceUpdateIndex >= traceUpdates.length) {
traceUpdates = [];
traceUpdateIndex = 0;
}
if (acknowledged && !workerStopped && !pendingFailure && !artifactsSettled) {
try {
worker.postMessage({ type: 'trace-ack', count: acknowledged });
} catch {
fail('worker-error', 'Planning trace acknowledgements could not be sent.');
}
}
if (artifactsSettled) return;
if (traceUpdateIndex < traceUpdates.length) scheduleTraceDrain();
else if (pendingFailure) finalizeFailure(pendingFailure);
else if (artifactsCompleteReceived) finishDeliveredTrace();
}, 0);
};
scheduleLivenessWatchdog();
cancel = () => {
if (outcomeSettled || cancellationRequested) return;
cancellationRequested = true;
try {
worker.postMessage({ type: 'cancel' });
} catch {
fail('worker-error', 'The cancellation request could not reach the worker.');
}
};
worker.onmessage = (event: MessageEvent<PlannerWorkerResponse>) => {
const message = event.data;
if (!message) return;
if (message.type === 'heartbeat') {
scheduleLivenessWatchdog();
return;
}
if (message.type === 'progress') {
scheduleLivenessWatchdog();
if (typeof snapshot.progress === 'function') snapshot.progress(message.stage);
return;
}
if (message.type === 'trace-overhead') {
scheduleLivenessWatchdog();
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') {
finishExplanation(undefined, undefined);
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 === 'trace-status-index') {
const started = performance.now();
receivedStatuses = message.statuses;
receivedNonWorkingDates = message.nonWorkingDates;
traceTransferMs += Math.max(0, performance.now() - started);
return;
}
if (message.type === 'artifacts-start') {
const started = performance.now();
receivedArtifacts = message.artifacts;
if (message.trace) {
receivedTrace = {
...message.trace,
initialState: receivedTrace?.initialState ?? message.trace.initialState,
nonWorkingDates:
receivedTrace?.nonWorkingDates ?? message.trace.nonWorkingDates,
phases: receivedTrace?.phases ?? [],
};
}
traceTransferMs += Math.max(0, performance.now() - started);
return;
}
if (message.type === 'trace-update') {
if (pendingFailure) return;
traceUpdates.push({
update: message.update,
acknowledge: message.acknowledge === true,
});
scheduleTraceDrain();
return;
}
if (message.type === 'artifacts-complete') {
terminalTraceTransferMs = message.traceTransferMs;
artifactsCompleteReceived = true;
stopWorker();
if (traceUpdateIndex < traceUpdates.length) scheduleTraceDrain();
else finishDeliveredTrace();
return;
}
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.');
}
} catch {
const result = interruptedOutcome(
undefined,
'worker-unavailable',
'Planning could not start because isolated execution is unavailable.',
);
resolveOutcome(result);
resolveArtifacts({
trace: fallbackTrace(snapshot, 'failed', result.reasons[0]),
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 === 'object' && 'decisionId' in selection) return selection.placement;
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,
);
}
function traceSelection(
selection: CounterfactualSelection,
): selection is TraceCounterfactualSelection {
return typeof selection === 'object' && 'decisionId' in selection;
}
const traceStatusPriority = new Map<TraceCandidateStatus, number>([
['ineligible', 0],
['not-examined', 1],
['pruned', 2],
['evaluated-rejected', 3],
['chosen', 4],
]);
const historicalStatusCache = new WeakMap<PlanningTrace, Map<string, TraceCandidateStatus>>();
const historicalNonWorkingCache = new WeakMap<PlanningTrace, Set<string>>();
const historicalDecisionCache = new WeakMap<
PlanningTrace,
{ id: string; statuses: Map<string, TraceCandidateStatus> }
>();
async function historicalStatuses(
trace: PlanningTrace,
decisionId?: string,
): Promise<Map<string, TraceCandidateStatus>> {
if (!decisionId) {
const cached = historicalStatusCache.get(trace);
if (cached) return cached;
} else {
const cached = historicalDecisionCache.get(trace);
if (cached?.id === decisionId) return cached.statuses;
}
const statuses = new Map<string, TraceCandidateStatus>();
let count = 0;
for (const phase of trace.phases) {
for (const decision of phase.decisions) {
if (decisionId && decision.id !== decisionId) continue;
for (const candidate of decision.candidates) {
for (const date of candidate.dates ?? (candidate.date ? [candidate.date] : [])) {
const previous = statuses.get(date);
if (
!previous ||
traceStatusPriority.get(candidate.status)! >
traceStatusPriority.get(previous)!
)
statuses.set(date, candidate.status);
}
if (++count % 256 === 0)
await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
}
}
if (!decisionId) {
historicalStatusCache.set(trace, statuses);
historicalNonWorkingCache.set(
trace,
new Set(trace.nonWorkingDates?.map(({ date }) => date) ?? []),
);
} else historicalDecisionCache.set(trace, { id: decisionId, statuses });
return statuses;
}
function historicalNonWorkingDates(trace: PlanningTrace): ReadonlySet<string> {
const cached = historicalNonWorkingCache.get(trace);
if (cached) return cached;
const dates = new Set(trace.nonWorkingDates?.map(({ date }) => date) ?? []);
historicalNonWorkingCache.set(trace, dates);
return dates;
}
interface ReplayState {
balances: PlanningTraceState['balances'][number][];
placements: Map<string, LeavePlacement>;
earnedActivations: PlanningTraceState['earnedActivations'][number][];
}
interface QueuedTraceUpdate {
update: PlanningTraceUpdate;
acknowledge: boolean;
}
function replayState(trace: PlanningTrace): ReplayState {
return {
balances: trace.initialState.balances.map((balance) => ({ ...balance })),
placements: new Map(
trace.initialState.placements.map((placement) => [placement.date, { ...placement }]),
),
earnedActivations: trace.initialState.earnedActivations.map((activation) => ({
...activation,
dates: [...activation.dates],
})),
};
}
export function applyReplayDeltas(
state: ReplayState,
deltas: readonly PlanningTraceDelta[],
finalState?: PlanningTraceState,
): void {
for (const delta of deltas) {
if (delta.previousPlacement) state.placements.delete(delta.previousPlacement.date);
if (delta.placement)
state.placements.set(delta.placement.date, {
...delta.placement,
ruleIds: [...delta.placement.ruleIds],
});
const balanceId = delta.balanceId ?? delta.balance?.id;
if (delta.kind === 'balance-removed') {
state.balances = state.balances.filter((balance) => balance.id !== balanceId);
continue;
}
if (balanceId) {
let index = state.balances.findIndex((candidate) => candidate.id === balanceId);
const changedBalance = delta.balance?.id === balanceId ? delta.balance : undefined;
if (index < 0 && changedBalance) {
state.balances.push({ ...changedBalance });
index = state.balances.length - 1;
}
if (index < 0 && delta.kind !== 'earned-activated') {
const source = finalState?.balances.find((candidate) => candidate.id === balanceId);
if (source) {
state.balances.push({ ...source });
index = state.balances.length - 1;
}
}
if (index >= 0) {
const current = state.balances[index]!;
state.balances[index] = {
...current,
...changedBalance,
...(delta.remainingAfter === undefined
? {}
: { remaining: delta.remainingAfter }),
};
}
}
if (
delta.kind === 'earned-activated' &&
delta.activation &&
!state.earnedActivations.some((activation) => activation.id === delta.activation!.id)
)
state.earnedActivations.push({
...delta.activation,
dates: [...delta.activation.dates],
});
}
}
function replayedState(trace: PlanningTrace, state: ReplayState): PlanningTraceState {
return {
calculationDate: trace.initialState.calculationDate,
horizon: { ...trace.initialState.horizon },
...(trace.initialState.declaredDays
? { declaredDays: trace.initialState.declaredDays }
: {}),
balances: state.balances,
placements: [...state.placements.values()],
earnedActivations: state.earnedActivations,
};
}
function stateBeforeDecision(
trace: PlanningTrace,
decisionId: string,
finalState?: PlanningTraceState,
): PlanningTraceState | undefined {
const decisions = trace.phases.flatMap((phase) => phase.decisions);
const targetIndex = decisions.findIndex((decision) => decision.id === decisionId);
if (targetIndex < 0) return undefined;
const target = decisions[targetIndex];
if (!target) return undefined;
const decisionsById = new Map(decisions.map((decision) => [decision.id, decision]));
let replayThrough = targetIndex - 1;
const branchAncestors: PlanningTraceDecision[] = [];
if (target.branch) {
let current = target;
const branchSeen = new Set<string>();
let committedParent: PlanningTraceDecision | undefined;
while (current.branch) {
if (branchSeen.has(current.id)) return undefined;
branchSeen.add(current.id);
branchAncestors.unshift(current);
const parentId = current.branch.parentDecisionId;
if (!parentId) break;
const parent = decisionsById.get(parentId);
if (!parent) return undefined;
if (!parent.branch) {
committedParent = parent;
break;
}
current = parent;
}
const branchRootIndex = decisions.findIndex(
(decision) => decision.id === branchAncestors[0]?.id,
);
if (branchRootIndex < 0) return undefined;
replayThrough = committedParent
? decisions.findIndex((decision) => decision.id === committedParent!.id)
: branchRootIndex - 1;
if (replayThrough >= branchRootIndex) return undefined;
}
const state = replayState(trace);
for (let index = 0; index <= replayThrough; index++) {
const decision = decisions[index];
if (decision) applyReplayDeltas(state, decision.deltas, finalState);
}
for (const ancestor of branchAncestors.slice(0, -1))
if (ancestor.branch) applyReplayDeltas(state, ancestor.branch.deltas, finalState);
return replayedState(trace, state);
}
function decisionBaseline(
trace: PlanningTrace,
selection: TraceCounterfactualSelection,
finalState?: PlanningTraceState,
): { state: PlanningTraceState; options: CounterfactualBaselineOptions } | undefined {
let decision: PlanningTraceDecision | undefined;
for (const phase of trace.phases) {
decision = phase.decisions.find((candidate) => candidate.id === selection.decisionId);
if (decision) break;
}
const state = stateBeforeDecision(trace, selection.decisionId, finalState);
if (!decision || !state) return undefined;
const transition = decision.branch?.deltas ?? decision.deltas;
const replacement = transition.find(
(delta) =>
(delta.kind === 'placement-moved' || delta.kind === 'placement-updated') &&
delta.placement?.date === selection.placement.date &&
delta.placement.balanceId === selection.placement.balanceId,
);
return {
state,
options: {
removeDate:
replacement?.kind === 'placement-moved'
? (replacement.fromDate ?? null)
: (replacement?.date ?? null),
compareOriginalState: true,
},
};
}
async function withHistoricalStatuses(
analysis: CounterfactualAnalysis,
trace?: PlanningTrace,
decisionId?: string,
): Promise<CounterfactualAnalysis> {
if (!trace) return analysis;
const statuses = await historicalStatuses(trace, decisionId);
const nonWorkingDates = historicalNonWorkingDates(trace);
return {
...analysis,
results: analysis.results.map((result) => {
const observedStatus = statuses.get(result.date);
const historicalReason = result.historicalReason;
const nonWorking = nonWorkingDates.has(result.date);
return {
...result,
historicalStatus:
observedStatus ??
(trace.status === 'failed'
? undefined
: historicalReason || nonWorking
? 'ineligible'
: 'not-examined'),
historicalReason: observedStatus === undefined ? historicalReason : undefined,
};
}),
};
}
function addHistoricalReasons(
analysis: CounterfactualAnalysis,
entries: readonly HistoricalEligibilityReason[],
): CounterfactualAnalysis {
const reasons = new Map(entries.map(({ date, reason }) => [date, reason]));
return {
...analysis,
results: analysis.results.map((result) => {
const historicalReason = reasons.get(result.date);
return historicalReason ? { ...result, historicalReason } : result;
}),
};
}
async function runHistoricalEligibility(
context: PlanningCalculationContext,
selection: TraceCounterfactualSelection,
artifacts: Promise<PlanningArtifacts>,
isCurrent: () => boolean,
): Promise<readonly HistoricalEligibilityReason[]> {
if (context.id !== latestCalculationId || !isCurrent()) return [];
const completed = await artifacts;
if (context.id !== latestCalculationId || !isCurrent()) return [];
const trace = completed.trace;
const baseline = trace && decisionBaseline(trace, selection, completed.finalState);
if (!baseline) throw new Error('The selected trace decision is unavailable.');
const entries =
typeof window !== 'undefined' && typeof Worker !== 'undefined'
? await historicalEligibilityInWorker(
context.snapshot,
baseline.state,
selection.placement,
baseline.options,
)
: await (async () => {
const core = await import('./leave-planner.core');
return core.historicalCounterfactualReasons(
context.snapshot,
baseline.state,
selection.placement,
baseline.options,
);
})();
return context.id === latestCalculationId && isCurrent() ? entries : [];
}
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);
const selectedDecision = traceSelection(selection)
? completedArtifacts.trace &&
decisionBaseline(completedArtifacts.trace, selection, completedArtifacts.finalState)
: undefined;
if (traceSelection(selection) && !selectedDecision)
throw new Error('The selected trace decision is unavailable.');
const analysisState = selectedDecision?.state ?? completedArtifacts.finalState;
const options = selectedDecision?.options;
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,
analysisState,
options,
);
} catch (error) {
if (context.id !== latestCalculationId || !isCurrent()) return stale(placement);
throw error;
}
if (context.id !== latestCalculationId || !isCurrent()) return stale(placement);
return withHistoricalStatuses(
{ ...result, calculationId: context.id },
completedArtifacts.trace,
traceSelection(selection) ? selection.decisionId : undefined,
);
}
const core = await import('./leave-planner.core');
let result = core.analyzeCounterfactualInternal(
context.snapshot,
plan,
placement,
analysisState,
options,
);
if (selectedDecision)
result = addHistoricalReasons(
result,
core.historicalCounterfactualReasons(
context.snapshot,
selectedDecision.state,
placement,
selectedDecision.options,
),
);
return context.id === latestCalculationId && isCurrent()
? withHistoricalStatuses(
{ ...result, calculationId: context.id },
completedArtifacts.trace,
traceSelection(selection) ? selection.decisionId : undefined,
)
: stale(placement);
}
function counterfactualInWorker(
snapshot: PlanningSnapshot,
plan: PlanOutcome,
placement: LeavePlacement,
state?: PlanningTraceState,
options?: CounterfactualBaselineOptions,
): Promise<CounterfactualAnalysis> {
return new Promise((resolve, reject) => {
latestCounterfactualCancel?.();
let worker: Worker;
try {
worker = new Worker(new URL('./leave-planner.worker', import.meta.url), {
type: 'module',
});
} 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'}).`,
),
),
);
};
worker.onerror = () => finish(() => reject(new Error('Counterfactual worker failed.')));
try {
worker.postMessage({
type: 'counterfactual',
request: snapshot,
plan,
placement,
state,
options,
});
} catch (error) {
finish(() => reject(error));
}
});
}
function historicalEligibilityInWorker(
snapshot: PlanningSnapshot,
state: PlanningTraceState,
placement: LeavePlacement,
options?: CounterfactualBaselineOptions,
): Promise<readonly HistoricalEligibilityReason[]> {
return new Promise((resolve, reject) => {
latestHistoricalEligibilityCancel?.();
let worker: Worker;
try {
worker = new Worker(new URL('./leave-planner.worker', import.meta.url), {
type: 'module',
});
} 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 (latestHistoricalEligibilityCancel === cancel)
latestHistoricalEligibilityCancel = undefined;
callback();
};
const cancel = () =>
finish(() => reject(new Error('Historical eligibility analysis was superseded.')));
latestHistoricalEligibilityCancel = cancel;
latestWorkerCancels.add(cancel);
worker.onmessage = (event: MessageEvent<PlannerWorkerResponse>) => {
const response = event.data;
if (response?.type === 'historical-eligibility-result')
finish(() => resolve(response.entries));
else if (response?.type === 'error')
finish(() =>
reject(
new Error(
response.message ??
`Historical eligibility analysis failed (${response.code ?? 'worker-error'}).`,
),
),
);
};
worker.onerror = () =>
finish(() => reject(new Error('Historical eligibility worker failed.')));
try {
worker.postMessage({
type: 'historical-eligibility',
request: snapshot,
state,
placement,
options,
});
} catch (error) {
finish(() => reject(error));
}
});
}
function explainCheckpointInWorker(
snapshot: PlanningSnapshot,
plan: PlanOutcome,
state: PlanningTraceState,
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 settled = false;
const finish = (callback: () => void) => {
if (settled) return;
settled = true;
worker.onmessage = null;
worker.onerror = null;
worker.terminate();
if (cancel) latestWorkerCancels.delete(cancel);
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')
finish(() =>
resolve({
explanation: response.explanation,
explanationError: response.explanationError,
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,
});
} catch (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 trace: Promise<PlanningTrace | undefined>;
readonly timings?: Promise<PlanningCalculationTimings>;
readonly analyzeCounterfactual: (
selection: CounterfactualSelection,
) => Promise<CounterfactualAnalysis | StaleCounterfactualAnalysis>;
readonly historicalEligibility: (
selection: TraceCounterfactualSelection,
) => Promise<readonly HistoricalEligibilityReason[]>;
}
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,
trace: fallbackTrace(transport, 'failed', result.reasons[0]),
});
});
});
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({
trace: fallbackTrace(transport, 'failed', reason),
explanationError: reason,
});
resolveTimings({});
} finally {
outcomeSettled = true;
latestWorkerCancels.delete(supersede);
}
});
});
run = {
outcome,
explanation,
explanationError,
artifacts,
timings,
cancel: () => {
if (!outcomeSettled) cancellationRequested = true;
},
};
}
}
const retainedArtifacts = run.artifacts.then((artifacts) => ({
...artifacts,
trace:
id === latestCalculationId
? artifacts.trace
: fallbackTrace(transport, 'failed', {
code: 'stale-calculation',
message: 'This trace belongs to an older calculation and was discarded.',
}),
}));
let counterfactualSequence = 0;
let historicalEligibilitySequence = 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,
trace: retainedArtifacts.then((artifacts) => artifacts.trace),
timings: run.timings,
analyzeCounterfactual: (selection) => {
const sequence = ++counterfactualSequence;
if (id === latestCalculationId) latestCounterfactualCancel?.();
return runCounterfactual(
context,
selection,
retainedArtifacts,
() => sequence === counterfactualSequence,
);
},
historicalEligibility: (selection) => {
const sequence = ++historicalEligibilitySequence;
return runHistoricalEligibility(
context,
selection,
retainedArtifacts,
() => sequence === historicalEligibilitySequence,
);
},
};
return context;
}
export async function planLeave(request: PlanningRequest): Promise<PlanningOutcome> {
return createPlanningCalculation(request).outcome;
}