diff --git a/README.md b/README.md index 8bbb184..60bf41a 100644 --- a/README.md +++ b/README.md @@ -1197,6 +1197,7 @@ Companion runtime helper: - `AndroidCompanionClient` (`platform: "android"`, FCM push registration) - shared `bootstrap()` helper (`register` + `getCapabilities`) for startup handshakes - shared `publishHeartbeat()` helper for periodic `node.status.set` updates with safe defaults + - `createHeartbeatLoop()` convenience helper that returns a bound `CompanionHeartbeatLoop` - 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, `tickNow()` for manual sends, error hooks, and optional auto-stop after repeated failures. diff --git a/docs/plans/state.json b/docs/plans/state.json index 6551803..2b40722 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -382,6 +382,19 @@ ], "test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing" }, + "companion-platform-heartbeat-loop-factory": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Added `createHeartbeatLoop()` to macOS/iOS/Android platform clients, returning a client-bound `CompanionHeartbeatLoop` for minimal heartbeat wiring in companion app runtimes.", + "files_modified": [ + "src/companion/platformClients.ts", + "src/companion/platformClients.test.ts", + "README.md", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/heartbeatLoop.test.ts src/companion/runtimeClient.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/platformClients.test.ts b/src/companion/platformClients.test.ts index 4807d05..8ebf21e 100644 --- a/src/companion/platformClients.test.ts +++ b/src/companion/platformClients.test.ts @@ -223,4 +223,19 @@ describe('platform companion clients', () => { 'sessionId is required (provide one or configure defaultSessionId)', ); }); + + it('creates a bound heartbeat loop helper from platform clients', async () => { + const mock = createRuntimeMock(); + const client = new IOSCompanionClient({ runtime: mock.runtime, nodeId: 'ios-node' }); + + const loop = client.createHeartbeatLoop(); + await loop.tickNow(); + + expect(mock.setNodeStatus).toHaveBeenCalledWith( + expect.objectContaining({ + platform: 'ios', + statusText: 'heartbeat', + }), + ); + }); }); diff --git a/src/companion/platformClients.ts b/src/companion/platformClients.ts index 9ab0bcd..a0c733b 100644 --- a/src/companion/platformClients.ts +++ b/src/companion/platformClients.ts @@ -18,6 +18,12 @@ import type { SystemCapabilitiesResult, SystemNodesResult, } from './runtimeClient.js'; +import { + CompanionHeartbeatLoop, +} from './heartbeatLoop.js'; +import type { + CompanionHeartbeatLoopOptions, +} from './heartbeatLoop.js'; export interface PlatformClientOptions { runtime: CompanionRuntimeClient; @@ -129,6 +135,10 @@ export class MacOSCompanionClient { }); } + createHeartbeatLoop(options: CompanionHeartbeatLoopOptions = {}): CompanionHeartbeatLoop { + return new CompanionHeartbeatLoop(this, options); + } + setLocation(location: SetNodeLocationInput): Promise { return this.runtime.setNodeLocation(location); } @@ -261,6 +271,10 @@ export class IOSCompanionClient { }); } + createHeartbeatLoop(options: CompanionHeartbeatLoopOptions = {}): CompanionHeartbeatLoop { + return new CompanionHeartbeatLoop(this, options); + } + setLocation(location: SetNodeLocationInput): Promise { return this.runtime.setNodeLocation(location); } @@ -393,6 +407,10 @@ export class AndroidCompanionClient { }); } + createHeartbeatLoop(options: CompanionHeartbeatLoopOptions = {}): CompanionHeartbeatLoop { + return new CompanionHeartbeatLoop(this, options); + } + setLocation(location: SetNodeLocationInput): Promise { return this.runtime.setNodeLocation(location); }