import { isDeepStrictEqual } from 'node:util'; import type { RouteRule, StoredState } from '../../../shared/contracts/state.js'; import { HarborError } from '../../../shared/errors.js'; import { normalizeRouteRules } 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(): 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; }; serialize(operation: () => Promise): Promise; runOperation(operation: () => Promise): Promise; } export function createRouteRulesService(dependencies: RouteRulesDependencies) { const applyRules = async (previousState: StoredState, routeRules: RouteRule[]) => { const subscriptionConfig = dependencies.subscription.readConfig(); if (!previousState.selectedServerId || !subscriptionConfig) { let stateCommitStarted = false; try { stateCommitStarted = true; dependencies.state.update((state) => ({ ...state, routeRules, 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, previousState.selectedServerId, routeRules, ); const previousConfig = dependencies.config.read(); const wasRunning = await dependencies.runtime.isRunning(); let configMutationStarted = false; let runtimeMutationStarted = false; let stateCommitStarted = false; try { configMutationStarted = true; dependencies.config.write(candidateConfig); if (wasRunning) { 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, })); } catch (error) { await finishRollback(error, [ ...(configMutationStarted ? [{ run: () => previousConfig === null ? dependencies.config.remove() : dependencies.config.restore(previousConfig), }] : []), ...(wasRunning && runtimeMutationStarted ? [{ run: () => dependencies.runtime.restoreRunning(), runtime: true, }] : []), ...(stateCommitStarted ? [{ run: () => dependencies.state.update(() => previousState) }] : []), ], 'Route rules rollback failed'); } }; const update = (rules: unknown, expectedRulesRevision: unknown, expectedRevision: unknown) => { let routeRules: RouteRule[]; try { routeRules = normalizeRouteRules(rules, { strict: true }) as RouteRule[]; } catch (cause) { throw new HarborError('REQUEST_INVALID', { cause }); } const rulesRevision = expectedRulesRevision ?? expectedRevision; if (!Number.isSafeInteger(rulesRevision) || Number(rulesRevision) < 0) { throw new HarborError('REQUEST_INVALID'); } return dependencies.serialize(async () => { const current = dependencies.state.read(); const currentRevision = expectedRulesRevision == null ? current.revision : current.routeRulesRevision; if (currentRevision !== rulesRevision) throw new HarborError('STATE_CONFLICT'); if (isDeepStrictEqual(current.routeRules, routeRules)) return; await dependencies.runOperation(() => applyRules(current, routeRules)); }); }; return { update }; } export type RouteRulesService = ReturnType;