fix(companion): validate runtime and heartbeat loop options

This commit is contained in:
William Valentin
2026-02-16 18:47:43 -08:00
parent 873dc1ad5b
commit a5c5a320ca
5 changed files with 52 additions and 3 deletions
+10
View File
@@ -37,6 +37,16 @@ describe('CompanionHeartbeatLoop', () => {
loop.stop();
});
it('validates constructor options', () => {
const publisher = { publishHeartbeat: vi.fn(async () => buildStatusResult()) };
expect(() => new CompanionHeartbeatLoop(publisher, { intervalMs: 0 })).toThrow(
'intervalMs must be a positive number',
);
expect(() => new CompanionHeartbeatLoop(publisher, { maxConsecutiveFailures: 0 })).toThrow(
'maxConsecutiveFailures must be >= 1 when specified',
);
});
it('supports delayed first run when runImmediately=false', async () => {
const publishHeartbeat = vi.fn(async () => buildStatusResult());
const loop = new CompanionHeartbeatLoop({ publishHeartbeat }, { intervalMs: 500 });
+14 -2
View File
@@ -31,11 +31,23 @@ export class CompanionHeartbeatLoop {
private consecutiveFailures = 0;
constructor(publisher: HeartbeatPublisher, options: CompanionHeartbeatLoopOptions = {}) {
const intervalMs = options.intervalMs ?? 30_000;
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
throw new Error('intervalMs must be a positive number');
}
const maxConsecutiveFailures = options.maxConsecutiveFailures ?? Number.POSITIVE_INFINITY;
if (!Number.isFinite(maxConsecutiveFailures) && maxConsecutiveFailures !== Number.POSITIVE_INFINITY) {
throw new Error('maxConsecutiveFailures must be a positive number or Infinity');
}
if (Number.isFinite(maxConsecutiveFailures) && maxConsecutiveFailures < 1) {
throw new Error('maxConsecutiveFailures must be >= 1 when specified');
}
this.publisher = publisher;
this.intervalMs = options.intervalMs ?? 30_000;
this.intervalMs = intervalMs;
this.buildHeartbeat = options.buildHeartbeat;
this.onError = options.onError;
this.maxConsecutiveFailures = options.maxConsecutiveFailures ?? Number.POSITIVE_INFINITY;
this.maxConsecutiveFailures = maxConsecutiveFailures;
this.onFailureLimitReached = options.onFailureLimitReached;
}
+9
View File
@@ -96,6 +96,15 @@ afterAll(async () => {
});
describe('CompanionRuntimeClient', () => {
it('validates requestTimeoutMs option', () => {
expect(() => {
new CompanionRuntimeClient({
url: 'ws://127.0.0.1:1',
requestTimeoutMs: 0,
});
}).toThrow('requestTimeoutMs must be a positive number');
});
it('dispatches gateway events to subscribed handlers and supports unsubscribe', () => {
const client = new CompanionRuntimeClient({
url: 'ws://127.0.0.1:1',
+5 -1
View File
@@ -270,9 +270,13 @@ export class CompanionRuntimeClient {
private readonly eventHandlers = new Set<CompanionEventHandler>();
constructor(options: CompanionRuntimeClientOptions) {
const requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) {
throw new Error('requestTimeoutMs must be a positive number');
}
this.url = options.url;
this.token = options.token;
this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
this.requestTimeoutMs = requestTimeoutMs;
this.autoConnect = options.autoConnect ?? false;
this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
}