diff --git a/README.md b/README.md index 087a502..8bbb184 100644 --- a/README.md +++ b/README.md @@ -1198,7 +1198,7 @@ Companion runtime helper: - shared `bootstrap()` helper (`register` + `getCapabilities`) for startup handshakes - 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 -- `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 diff --git a/docs/plans/state.json b/docs/plans/state.json index 4155c9d..b47a7f3 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -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" }, + "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": { "status": "completed", "date": "2026-02-17", diff --git a/src/companion/heartbeatLoop.test.ts b/src/companion/heartbeatLoop.test.ts index 7aa6a40..3532cde 100644 --- a/src/companion/heartbeatLoop.test.ts +++ b/src/companion/heartbeatLoop.test.ts @@ -130,4 +130,14 @@ describe('CompanionHeartbeatLoop', () => { await vi.advanceTimersByTimeAsync(1000); 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); + }); }); diff --git a/src/companion/heartbeatLoop.ts b/src/companion/heartbeatLoop.ts index b040941..2e2c889 100644 --- a/src/companion/heartbeatLoop.ts +++ b/src/companion/heartbeatLoop.ts @@ -65,6 +65,10 @@ export class CompanionHeartbeatLoop { } } + async tickNow(): Promise { + await this.tick(true); + } + private scheduleNext(): void { if (!this.started) { return; @@ -74,8 +78,8 @@ export class CompanionHeartbeatLoop { }, this.intervalMs); } - private async tick(): Promise { - if (!this.started || this.inFlight) { + private async tick(force = false): Promise { + if ((!this.started && !force) || this.inFlight) { return; } this.inFlight = true;