feat(companion): add manual heartbeat tick helper

This commit is contained in:
William Valentin
2026-02-16 18:46:48 -08:00
parent 985b7bf459
commit 873dc1ad5b
4 changed files with 30 additions and 3 deletions
+1 -1
View File
@@ -1198,7 +1198,7 @@ Companion runtime helper:
- shared `bootstrap()` helper (`register` + `getCapabilities`) for startup handshakes - shared `bootstrap()` helper (`register` + `getCapabilities`) for startup handshakes
- shared `publishHeartbeat()` helper for periodic `node.status.set` updates with safe defaults - shared `publishHeartbeat()` helper for periodic `node.status.set` updates with safe defaults
- optional `defaultSessionId` for canvas helper calls so `sessionId` can be omitted per call - optional `defaultSessionId` for canvas helper calls so `sessionId` can be omitted per call
- `src/companion/heartbeatLoop.ts` provides `CompanionHeartbeatLoop` for periodic heartbeat scheduling (`publishHeartbeat`) with start/stop safety, error hooks, and optional auto-stop after repeated failures. - `src/companion/heartbeatLoop.ts` provides `CompanionHeartbeatLoop` for periodic heartbeat scheduling (`publishHeartbeat`) with start/stop safety, `tickNow()` for manual sends, error hooks, and optional auto-stop after repeated failures.
## Canvas / A2UI Foundation ## Canvas / A2UI Foundation
+13
View File
@@ -355,6 +355,19 @@
], ],
"test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts src/companion/heartbeatLoop.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts src/companion/heartbeatLoop.test.ts + pnpm typecheck passing"
}, },
"companion-heartbeat-loop-manual-tick": {
"status": "completed",
"date": "2026-02-17",
"updated": "2026-02-17",
"summary": "Added `tickNow()` on `CompanionHeartbeatLoop` to allow explicit immediate heartbeat sends independent of the periodic scheduler lifecycle.",
"files_modified": [
"src/companion/heartbeatLoop.ts",
"src/companion/heartbeatLoop.test.ts",
"README.md",
"docs/plans/state.json"
],
"test_status": "pnpm test:run src/companion/heartbeatLoop.test.ts src/companion/runtimeClient.test.ts src/companion/platformClients.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",
+10
View File
@@ -130,4 +130,14 @@ describe('CompanionHeartbeatLoop', () => {
await vi.advanceTimersByTimeAsync(1000); await vi.advanceTimersByTimeAsync(1000);
expect(publishHeartbeat).toHaveBeenCalledTimes(2); expect(publishHeartbeat).toHaveBeenCalledTimes(2);
}); });
it('tickNow sends heartbeat even when loop is not started', async () => {
const publishHeartbeat = vi.fn(async () => buildStatusResult());
const loop = new CompanionHeartbeatLoop({ publishHeartbeat }, { intervalMs: 1000 });
expect(loop.running).toBe(false);
await loop.tickNow();
expect(publishHeartbeat).toHaveBeenCalledTimes(1);
expect(loop.running).toBe(false);
});
}); });
+6 -2
View File
@@ -65,6 +65,10 @@ export class CompanionHeartbeatLoop {
} }
} }
async tickNow(): Promise<void> {
await this.tick(true);
}
private scheduleNext(): void { private scheduleNext(): void {
if (!this.started) { if (!this.started) {
return; return;
@@ -74,8 +78,8 @@ export class CompanionHeartbeatLoop {
}, this.intervalMs); }, this.intervalMs);
} }
private async tick(): Promise<void> { private async tick(force = false): Promise<void> {
if (!this.started || this.inFlight) { if ((!this.started && !force) || this.inFlight) {
return; return;
} }
this.inFlight = true; this.inFlight = true;