fix(companion): dedupe heartbeat loop scheduled timers

This commit is contained in:
William Valentin
2026-02-16 19:39:29 -08:00
parent ebb62ffb65
commit 61533bd816
3 changed files with 33 additions and 0 deletions
+12
View File
@@ -760,6 +760,18 @@
], ],
"test_status": "pnpm test:run src/companion/platformClients.integration.test.ts src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/companion/platformClients.integration.test.ts src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.test.ts + pnpm typecheck passing"
}, },
"companion-heartbeat-loop-timer-dedup": {
"status": "completed",
"date": "2026-02-17",
"updated": "2026-02-17",
"summary": "Prevented duplicate pending timers in `CompanionHeartbeatLoop` by clearing any existing scheduled timeout before re-scheduling, with regression coverage for `tickNow()` during active loops.",
"files_modified": [
"src/companion/heartbeatLoop.ts",
"src/companion/heartbeatLoop.test.ts",
"docs/plans/state.json"
],
"test_status": "pnpm test:run src/companion/heartbeatLoop.test.ts src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
},
"browser-tools-activation-clarity": { "browser-tools-activation-clarity": {
"status": "completed", "status": "completed",
"date": "2026-02-17", "date": "2026-02-17",
+17
View File
@@ -217,4 +217,21 @@ describe('CompanionHeartbeatLoop', () => {
lastFailure: null, lastFailure: null,
}); });
}); });
it('tickNow while running does not accumulate duplicate scheduled timers', async () => {
const publishHeartbeat = vi.fn(async () => buildStatusResult());
const loop = new CompanionHeartbeatLoop({ publishHeartbeat }, { intervalMs: 1000 });
loop.start(false);
expect(vi.getTimerCount()).toBe(1);
await loop.tickNow();
expect(vi.getTimerCount()).toBe(1);
expect(publishHeartbeat).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1000);
expect(publishHeartbeat).toHaveBeenCalledTimes(2);
loop.stop();
});
}); });
+4
View File
@@ -138,6 +138,10 @@ export class CompanionHeartbeatLoop {
if (!this.started) { if (!this.started) {
return; return;
} }
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const delay = this.computeDelayMs(); const delay = this.computeDelayMs();
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
void this.tick(); void this.tick();