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, OptimizationPlanningRule, 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; 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; } 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; } | { type: 'trace-update'; totalMs: number; update: PlanningTraceUpdate; acknowledge?: boolean; } | { type: 'trace-status-index'; statuses: ReadonlyMap; 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; explanation: Promise; explanationError: Promise; artifacts: Promise; timings: Promise; cancel?: () => void; } export type PlanningCalculationTimings = Partial & { traceTransferMs?: number; }; type MutableTraceDecision = Omit & { candidates: PlanningTraceCandidate[]; deltas: PlanningTraceDelta[]; }; type MutableTracePhase = Omit & { decisions: MutableTraceDecision[]; }; type MutableTrace = Omit & { 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((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 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(); const decisionsById = new Map(); let traceUpdates: (QueuedTraceUpdate | undefined)[] = []; let traceUpdateIndex = 0; let traceDrainTimer: ReturnType | undefined; let traceTransferMs = 0; let terminalTraceTransferMs: number | undefined; let artifactsCompleteReceived = false; let pendingFailure: { code: string; message: string } | undefined; let receivedStatuses: ReadonlyMap | undefined; let receivedNonWorkingDates: readonly string[] = []; let livenessWatchdog: ReturnType; 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) => { 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([ ['ineligible', 0], ['not-examined', 1], ['pruned', 2], ['evaluated-rejected', 3], ['chosen', 4], ]); const historicalStatusCache = new WeakMap>(); const historicalNonWorkingCache = new WeakMap>(); const historicalDecisionCache = new WeakMap< PlanningTrace, { id: string; statuses: Map } >(); async function historicalStatuses( trace: PlanningTrace, decisionId?: string, ): Promise> { 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(); 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((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 { 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; 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(); 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 { 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, isCurrent: () => boolean, ): Promise { 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, 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); 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 { 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 { 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) => { 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> { 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) => { 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; readonly explanation: Promise; readonly explanationError: Promise; readonly trace: Promise; readonly timings?: Promise; readonly analyzeCounterfactual: ( selection: CounterfactualSelection, ) => Promise; readonly historicalEligibility: ( selection: TraceCounterfactualSelection, ) => 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, 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( (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({ 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 { return createPlanningCalculation(request).outcome; }