Files
pikasTech-unidesk/scripts/src/read-only-long-poll.ts
T
2026-07-15 06:35:25 +02:00

64 lines
2.7 KiB
TypeScript

export const DEFAULT_READ_ONLY_LONG_POLL_TIMEOUT_MS = 120_000;
export const READ_ONLY_LONG_POLL_TRANSPORT_MARGIN_MS = 15_000;
export interface ReadOnlyLongPollClock {
now(): number;
}
export interface ReadOnlyLongPollResult<T> {
value: T;
completed: boolean;
timedOut: boolean;
cancelled: boolean;
elapsedMs: number;
observations: number;
cursor: string;
}
export function parseStrictDuration(value: string, label = "duration"): number {
const match = /^(\d+)(ms|s|m)$/u.exec(value);
if (match === null) throw new Error(`${label} must match <integer>ms|s|m`);
const amount = Number.parseInt(match[1]!, 10);
if (!Number.isSafeInteger(amount) || amount <= 0) throw new Error(`${label} must be greater than zero`);
const multiplier = match[2] === "ms" ? 1 : match[2] === "s" ? 1_000 : 60_000;
const durationMs = amount * multiplier;
if (!Number.isSafeInteger(durationMs) || durationMs > 3_600_000) throw new Error(`${label} must not exceed 60m`);
return durationMs;
}
export function readOnlyLongPollTransportTimeoutMs(timeoutMs: number): number {
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 3_600_000) throw new Error("long-poll timeoutMs must be between 1ms and 60m");
return timeoutMs + READ_ONLY_LONG_POLL_TRANSPORT_MARGIN_MS;
}
export async function runReadOnlyLongPoll<T>(options: {
observe: () => Promise<T>;
completed: (value: T) => boolean;
waitForChange: (remainingMs: number, signal?: AbortSignal) => Promise<void>;
cursor: (value: T) => string;
timeoutMs?: number;
clock?: ReadOnlyLongPollClock;
signal?: AbortSignal;
}): Promise<ReadOnlyLongPollResult<T>> {
const timeoutMs = options.timeoutMs ?? DEFAULT_READ_ONLY_LONG_POLL_TIMEOUT_MS;
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("timeoutMs must be a positive integer");
const clock = options.clock ?? { now: () => Date.now() };
const startedAt = clock.now();
let observations = 0;
let value = await options.observe();
observations += 1;
while (!options.completed(value)) {
if (options.signal?.aborted === true) {
return { value, completed: false, timedOut: false, cancelled: true, elapsedMs: clock.now() - startedAt, observations, cursor: options.cursor(value) };
}
const remainingMs = timeoutMs - (clock.now() - startedAt);
if (remainingMs <= 0) {
return { value, completed: false, timedOut: true, cancelled: false, elapsedMs: clock.now() - startedAt, observations, cursor: options.cursor(value) };
}
await options.waitForChange(remainingMs, options.signal);
value = await options.observe();
observations += 1;
}
return { value, completed: true, timedOut: false, cancelled: false, elapsedMs: clock.now() - startedAt, observations, cursor: options.cursor(value) };
}