Files
MikrotikManager/backend/src/services/routeros-snapshot-read.ts
T

50 lines
1.9 KiB
TypeScript

import { isRouterOsPreHeaderGetReset } from "./mikrotik.js"
interface SnapshotGetClient {
get<T>(path: string, timeoutMs?: number, signal?: AbortSignal, maxResponseBytes?: number, beforeDispatch?: () => Promise<unknown>): Promise<T>
}
/** A single catalogue read, pinned to the original client/path. Only a transport-
* branded reset before any response headers can spend its one retry slot. */
export async function readRouterOsSnapshotGet<T>(input: {
client: SnapshotGetClient
path: string
assertCurrent(): Promise<unknown>
timeoutMs?: number
signal?: AbortSignal
now?: () => number
}): Promise<T> {
const { client, path, assertCurrent, signal } = input
const timeoutMs = input.timeoutMs ?? 10_000, now = input.now ?? (() => performance.now())
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) throw new RangeError("Invalid RouterOS snapshot read timeout")
const deadline = now() + timeoutMs
let originalReset: unknown
const remaining = () => {
signal?.throwIfAborted()
const budget = deadline - now()
if (budget <= 0) throw originalReset ?? new DOMException("RouterOS snapshot read deadline expired", "TimeoutError")
return Math.ceil(budget)
}
for (let attempt = 0; attempt < 2; attempt++) {
remaining()
await assertCurrent()
const budget = remaining()
const deadlineSignal = AbortSignal.timeout(budget)
const requestSignal = signal ? AbortSignal.any([signal, deadlineSignal]) : deadlineSignal
try {
const result = await client.get<T>(path, budget, requestSignal, undefined, async () => {
await assertCurrent()
remaining()
requestSignal.throwIfAborted()
})
remaining()
return result
} catch (error) {
if (attempt !== 0 || !isRouterOsPreHeaderGetReset(error) || signal?.aborted) throw error
originalReset = error
remaining()
}
}
throw originalReset
}