50 lines
1.9 KiB
TypeScript
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
|
|
}
|