feat(companion): add waitForIdle runtime drain helper

This commit is contained in:
William Valentin
2026-02-16 20:56:08 -08:00
parent d14f82cd84
commit ed471072bb
7 changed files with 169 additions and 2 deletions
+74
View File
@@ -41,6 +41,12 @@ export interface CompanionRuntimeClientOptions {
websocketFactory?: (url: string) => WebSocket;
}
export interface WaitForIdleOptions {
timeoutMs?: number;
pollIntervalMs?: number;
signal?: AbortSignal;
}
export type CompanionEventHandler = (event: string, data: unknown) => void;
export type CompanionTypedEventHandler<TData = unknown> = (data: TData) => void;
export type CompanionEventPredicate<TData = unknown> = (data: TData) => boolean;
@@ -607,6 +613,74 @@ export class CompanionRuntimeClient {
return this.waitForEvent<TData>(COMPANION_EVENT_NAMES.contextWarning, options);
}
waitForIdle(options?: WaitForIdleOptions): Promise<void> {
const pollIntervalMs = options?.pollIntervalMs ?? 25;
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
throw new Error('pollIntervalMs must be a positive number');
}
if (!this.hasPendingWork) {
return Promise.resolve();
}
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
const signal = options?.signal;
return new Promise<void>((resolve, reject) => {
let settled = false;
let timeout: NodeJS.Timeout | null = null;
let poll: NodeJS.Timeout | null = null;
let abortCleanup: (() => void) | null = null;
const cleanup = () => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
if (poll) {
clearInterval(poll);
poll = null;
}
if (abortCleanup) {
abortCleanup();
abortCleanup = null;
}
};
const finish = (fn: () => void) => {
if (settled) {
return;
}
settled = true;
cleanup();
fn();
};
const check = () => {
if (!this.hasPendingWork) {
finish(() => resolve());
}
};
timeout = setTimeout(() => {
finish(() => reject(new Error('Timed out waiting for runtime idle state')));
}, timeoutMs);
poll = setInterval(check, pollIntervalMs);
check();
if (signal) {
const onAbort = () => {
finish(() => reject(new Error('Aborted while waiting for runtime idle state')));
};
signal.addEventListener('abort', onAbort, { once: true });
abortCleanup = () => {
signal.removeEventListener('abort', onAbort);
};
if (signal.aborted) {
onAbort();
}
}
});
}
listKnownEventNames(): CompanionEventName[] {
return Object.values(COMPANION_EVENT_NAMES);
}