import { isDeepStrictEqual } from 'node:util'; import { appliedProfile, desiredProfile, type RouteRule, type StoredState, } from '../../../shared/contracts/state.js'; import { HarborError } from '../../../shared/errors.js'; import { normalizeRouteRules, ROUTE_RULES_CONTRACT_VERSION, } from '../../../shared/routingRules.js'; import type { RuntimeCommandResult } from '../connection/index.js'; import { finishRollback } from '../../services/rollback.js'; interface RouteRulesDependencies { state: { read(): StoredState; update(mutator: (state: StoredState) => Record): StoredState; }; subscription: { readConfig(profileId: string): unknown | null }; config: { build(subscriptionConfig: unknown, selectedServerId: string, routeRules: RouteRule[]): unknown; read(): string | null; write(value: unknown): void; restore(value: string): void; remove(): void; }; runtime: { isRunning(): Promise; applyCommand(): Promise; restoreRunning(): Promise; }; route?: { isGatewayDirect(): boolean }; serialize(operation: () => Promise): Promise; runOperation(operation: () => Promise): Promise; afterApply?: () => Promise; restoreAppliedActivation?: (state: StoredState) => Promise; } export function createRouteRulesService(dependencies: RouteRulesDependencies) { const applyRules = async (previousState: StoredState, routeRules: RouteRule[]) => { const wasRunning = await dependencies.runtime.isRunning(); const bypassed = dependencies.route?.isGatewayDirect() === true; const targetProfile = wasRunning ? appliedProfile(previousState) : desiredProfile(previousState); const targetServerId = wasRunning ? previousState.appliedServerId : targetProfile?.desiredServerId || ''; const subscriptionConfig = targetProfile ? dependencies.subscription.readConfig(targetProfile.id) : null; if (bypassed || !targetServerId || !subscriptionConfig) { let stateCommitStarted = false; try { stateCommitStarted = true; dependencies.state.update((state) => ({ ...state, routeRules, ...(bypassed ? { appliedRouteRules: [] } : {}), routeRulesRevision: state.routeRulesRevision + 1, })); } catch (error) { await finishRollback(error, [ ...(stateCommitStarted ? [{ run: () => dependencies.state.update(() => previousState) }] : []), ], 'Route rules rollback failed'); } return; } const candidateConfig = dependencies.config.build( subscriptionConfig, targetServerId, routeRules, ); const previousConfig = dependencies.config.read(); const configChanged = previousConfig !== JSON.stringify(candidateConfig, null, 2); let configMutationStarted = false; let runtimeMutationStarted = false; let stateCommitStarted = false; try { if (configChanged) { configMutationStarted = true; dependencies.config.write(candidateConfig); } if (wasRunning && configChanged) { const command = await dependencies.runtime.applyCommand(); runtimeMutationStarted = command.mutationStarted; if (!command.ok) throw command.error; } stateCommitStarted = true; dependencies.state.update((state) => ({ ...state, routeRules, ...(wasRunning ? { appliedRouteRules: routeRules } : {}), routeRulesRevision: state.routeRulesRevision + 1, })); await dependencies.afterApply?.(); } catch (error) { await finishRollback(error, [ ...(configMutationStarted ? [{ run: () => previousConfig === null ? dependencies.config.remove() : dependencies.config.restore(previousConfig), }] : []), ...(wasRunning && runtimeMutationStarted ? [{ run: async () => { await dependencies.runtime.restoreRunning(); await dependencies.restoreAppliedActivation?.(previousState); }, runtime: true, }] : []), ...(stateCommitStarted ? [{ run: () => dependencies.state.update(() => previousState) }] : []), ], 'Route rules rollback failed'); } }; const update = ( rules: unknown, expectedRulesRevision: unknown, rulesContractVersion: unknown, ) => { if (rulesContractVersion !== ROUTE_RULES_CONTRACT_VERSION) { throw new HarborError('REQUEST_INVALID'); } let routeRules: RouteRule[]; try { routeRules = normalizeRouteRules(rules, { strict: true }) as RouteRule[]; } catch (cause) { throw new HarborError('REQUEST_INVALID', { cause }); } if (!Number.isSafeInteger(expectedRulesRevision) || Number(expectedRulesRevision) < 0) { throw new HarborError('REQUEST_INVALID'); } return dependencies.serialize(async () => { const current = dependencies.state.read(); if (current.routeRulesRevision !== expectedRulesRevision) throw new HarborError('STATE_CONFLICT'); if (isDeepStrictEqual(current.routeRules, routeRules)) return; await dependencies.runOperation(() => applyRules(current, routeRules)); }); }; return { update }; } export type RouteRulesService = ReturnType;