diff --git a/README.md b/README.md index aaf3240..84e3d40 100644 --- a/README.md +++ b/README.md @@ -1190,7 +1190,7 @@ Methods: - `system.capabilities` returns gateway protocol and node policy snapshot. Companion runtime helper: -- `src/companion/runtimeClient.ts` provides a typed Node/WebSocket client for companion runtimes (macOS/iOS/Android workers) with wrappers for `node.register`, `node.capabilities.get`, `node.location.set/get`, `node.status.set`, `node.push_token.set`, `system.capabilities`, `system.nodes`, and canvas artifact RPCs (`canvas.put/get/list/delete/clear`), plus convenience helpers (`bootstrapNode`, optional `autoConnect`, `dispose()`, `waitForIdle()` for pending-work drain synchronization) and event helpers (`subscribeEvents()`, `subscribeEvent()`, `subscribeAgentStream()`, `subscribeAgentTyping()`, `subscribeContextWarning()`, `waitForEvent()` with timeout/predicate/abort support plus event-name/timeout validation and deterministic teardown cancellation including socket-close rejection, `waitForAnyEvent()` with event-list/timeout validation, `waitForAgentStream()`, `waitForAgentTyping()`, `waitForContextWarning()`, `clearEventSubscriptions()`, `cancelPendingEventWaits()`, `listKnownEventNames()`, `eventSubscriptionCount`) plus in-flight observability via `pendingRequestCount`, `pendingEventWaitCount`, `hasPendingWork`, `idle`, and `getPendingWorkSnapshot()`. +- `src/companion/runtimeClient.ts` provides a typed Node/WebSocket client for companion runtimes (macOS/iOS/Android workers) with wrappers for `node.register`, `node.capabilities.get`, `node.location.set/get`, `node.status.set`, `node.push_token.set`, `system.capabilities`, `system.nodes`, and canvas artifact RPCs (`canvas.put/get/list/delete/clear`), plus convenience helpers (`bootstrapNode`, optional `autoConnect`, `dispose()`, `waitForIdle()` for pending-work drain synchronization) and event helpers (`subscribeEvents()`, `subscribeEvent()`, `subscribeAgentStream()`, `subscribeAgentTyping()`, `subscribeContextWarning()`, `waitForEvent()` with timeout/predicate/abort support plus event-name/timeout validation and deterministic teardown cancellation including socket-close rejection, `waitForAnyEvent()` with event-list/timeout validation, `waitForAgentStream()`, `waitForAgentTyping()`, `waitForContextWarning()`, `clearEventSubscriptions()`, `cancelPendingEventWaits()` returning cancelled waiter count, `listKnownEventNames()`, `eventSubscriptionCount`) plus in-flight observability via `pendingRequestCount`, `pendingEventWaitCount`, `hasPendingWork`, `idle`, and `getPendingWorkSnapshot()`. - `src/companion/platformClients.ts` provides platform-focused wrappers: - `MacOSCompanionClient` (`platform: "macos"`, APNs push registration) - `IOSCompanionClient` (`platform: "ios"`, APNs push registration) diff --git a/docs/plans/state.json b/docs/plans/state.json index 62526ec..a133f37 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1066,6 +1066,21 @@ ], "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-cancel-pending-waits-count-return": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Extended `cancelPendingEventWaits()` to return cancelled waiter count on runtime and platform wrappers for explicit cancellation observability.", + "files_modified": [ + "src/companion/runtimeClient.ts", + "src/companion/runtimeClient.test.ts", + "src/companion/platformClients.ts", + "src/companion/platformClients.test.ts", + "README.md", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/heartbeatLoop.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 2b3a459..dc17b95 100644 --- a/src/companion/platformClients.test.ts +++ b/src/companion/platformClients.test.ts @@ -77,7 +77,7 @@ function createRuntimeMock(): { const subscribeEvents = vi.fn(() => () => undefined); const subscribeEvent = vi.fn(() => () => undefined); const clearEventSubscriptions = vi.fn(() => undefined); - const cancelPendingEventWaits = vi.fn(() => undefined); + const cancelPendingEventWaits = vi.fn(() => 1); const listKnownEventNames = vi.fn(() => ['agent.stream', 'agent.typing', 'context_warning']); const waitForEvent = vi.fn(async () => ({ token: 'evented' })); const waitForIdle = vi.fn(async () => undefined); @@ -321,9 +321,10 @@ describe('platform companion clients', () => { const mock = createRuntimeMock(); const client = new IOSCompanionClient({ runtime: mock.runtime, nodeId: 'ios-node' }); - client.cancelPendingEventWaits('manual'); + const cancelled = client.cancelPendingEventWaits('manual'); expect(mock.cancelPendingEventWaits).toHaveBeenCalledWith('manual'); + expect(cancelled).toBe(1); }); it('platform listKnownEventNames forwards to runtime client', async () => { diff --git a/src/companion/platformClients.ts b/src/companion/platformClients.ts index ecb4e7a..a9e04e2 100644 --- a/src/companion/platformClients.ts +++ b/src/companion/platformClients.ts @@ -269,8 +269,8 @@ export class MacOSCompanionClient { this.runtime.clearEventSubscriptions(); } - cancelPendingEventWaits(reason?: string): void { - this.runtime.cancelPendingEventWaits(reason); + cancelPendingEventWaits(reason?: string): number { + return this.runtime.cancelPendingEventWaits(reason); } listKnownEventNames(): CompanionEventName[] { @@ -539,8 +539,8 @@ export class IOSCompanionClient { this.runtime.clearEventSubscriptions(); } - cancelPendingEventWaits(reason?: string): void { - this.runtime.cancelPendingEventWaits(reason); + cancelPendingEventWaits(reason?: string): number { + return this.runtime.cancelPendingEventWaits(reason); } listKnownEventNames(): CompanionEventName[] { @@ -807,8 +807,8 @@ export class AndroidCompanionClient { this.runtime.clearEventSubscriptions(); } - cancelPendingEventWaits(reason?: string): void { - this.runtime.cancelPendingEventWaits(reason); + cancelPendingEventWaits(reason?: string): number { + return this.runtime.cancelPendingEventWaits(reason); } listKnownEventNames(): CompanionEventName[] { diff --git a/src/companion/runtimeClient.test.ts b/src/companion/runtimeClient.test.ts index 47a8095..79cddb7 100644 --- a/src/companion/runtimeClient.test.ts +++ b/src/companion/runtimeClient.test.ts @@ -399,7 +399,7 @@ describe('CompanionRuntimeClient', () => { const awaited = expect( client.waitForEvent('agent.stream', { timeoutMs: 10_000 }), ).rejects.toThrow('manually cancelled'); - client.cancelPendingEventWaits('manually cancelled'); + expect(client.cancelPendingEventWaits('manually cancelled')).toBe(1); await awaited; (client as unknown as { handleMessage: (raw: string) => void }).handleMessage( @@ -410,6 +410,7 @@ describe('CompanionRuntimeClient', () => { }), ); expect(handler).toHaveBeenCalledWith('agent.stream', { token: 'still-subscribed' }); + expect(client.cancelPendingEventWaits()).toBe(0); }); it('waitForEvent rejects immediately on disconnect', async () => { diff --git a/src/companion/runtimeClient.ts b/src/companion/runtimeClient.ts index e3fa849..5a4faee 100644 --- a/src/companion/runtimeClient.ts +++ b/src/companion/runtimeClient.ts @@ -435,8 +435,8 @@ export class CompanionRuntimeClient { this.rejectEventWaits(new Error('Event subscriptions cleared')); } - cancelPendingEventWaits(reason = 'Event waits cancelled'): void { - this.rejectEventWaits(new Error(reason)); + cancelPendingEventWaits(reason = 'Event waits cancelled'): number { + return this.rejectEventWaits(new Error(reason)); } subscribeEvent( @@ -925,11 +925,13 @@ export class CompanionRuntimeClient { this.pending.clear(); } - private rejectEventWaits(error: Error): void { + private rejectEventWaits(error: Error): number { + const cancelled = this.pendingEventWaits.size; for (const cancel of this.pendingEventWaits) { cancel(error); } this.pendingEventWaits.clear(); + return cancelled; } }