From ffc7c4e9b3c7e79041cb90c83fbc042075339d23 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 16 Feb 2026 22:26:23 -0800 Subject: [PATCH] feat(companion): return clearEventSubscriptions result counts --- README.md | 2 +- docs/plans/state.json | 16 ++++++++++++++++ src/companion/index.ts | 1 + src/companion/platformClients.test.ts | 5 +++-- src/companion/platformClients.ts | 13 +++++++------ src/companion/runtimeClient.test.ts | 11 +++++++++-- src/companion/runtimeClient.ts | 14 ++++++++++++-- 7 files changed, 49 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 93da3d2..3653cb8 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()` returning cancelled waiter count, `listKnownEventNames()`, `eventSubscriptionCount`) plus in-flight observability via `pendingRequestCount`, `pendingEventWaitCount`, `hasPendingWork`, `idle`, `getPendingWorkSnapshot()`, `getEventSurfaceSnapshot()`, and `getConnectionSnapshot()`. +- `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()` returning cleared-subscription/cancelled-wait counts, `cancelPendingEventWaits()` returning cancelled waiter count, `listKnownEventNames()`, `eventSubscriptionCount`) plus in-flight observability via `pendingRequestCount`, `pendingEventWaitCount`, `hasPendingWork`, `idle`, `getPendingWorkSnapshot()`, `getEventSurfaceSnapshot()`, and `getConnectionSnapshot()`. - `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 ca7a911..60e639e 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1072,6 +1072,22 @@ ], "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" }, + "companion-clear-event-subscriptions-count-return": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Extended `clearEventSubscriptions()` to return `clearedSubscriptions`/`cancelledWaits` counts on runtime and platform wrappers for explicit teardown observability.", + "files_modified": [ + "src/companion/runtimeClient.ts", + "src/companion/runtimeClient.test.ts", + "src/companion/platformClients.ts", + "src/companion/platformClients.test.ts", + "src/companion/index.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" + }, "companion-runtime-cancel-pending-event-waits": { "status": "completed", "date": "2026-02-17", diff --git a/src/companion/index.ts b/src/companion/index.ts index 8e2c5bd..5531f48 100644 --- a/src/companion/index.ts +++ b/src/companion/index.ts @@ -16,6 +16,7 @@ export type { PendingWorkSnapshot, EventSurfaceSnapshot, ConnectionSnapshot, + ClearEventSubscriptionsResult, CompanionEventHandler, CompanionTypedEventHandler, CompanionEventName, diff --git a/src/companion/platformClients.test.ts b/src/companion/platformClients.test.ts index 3ef2f28..f3a3ffc 100644 --- a/src/companion/platformClients.test.ts +++ b/src/companion/platformClients.test.ts @@ -78,7 +78,7 @@ function createRuntimeMock(): { const subscribeContextWarning = vi.fn(() => () => undefined); const subscribeEvents = vi.fn(() => () => undefined); const subscribeEvent = vi.fn(() => () => undefined); - const clearEventSubscriptions = vi.fn(() => undefined); + const clearEventSubscriptions = vi.fn(() => ({ clearedSubscriptions: 1, cancelledWaits: 0 })); const cancelPendingEventWaits = vi.fn(() => 1); const listKnownEventNames = vi.fn(() => ['agent.stream', 'agent.typing', 'context_warning']); const waitForEvent = vi.fn(async () => ({ token: 'evented' })); @@ -331,9 +331,10 @@ describe('platform companion clients', () => { const mock = createRuntimeMock(); const client = new IOSCompanionClient({ runtime: mock.runtime, nodeId: 'ios-node' }); - client.clearEventSubscriptions(); + const result = client.clearEventSubscriptions(); expect(mock.clearEventSubscriptions).toHaveBeenCalledOnce(); + expect(result).toEqual({ clearedSubscriptions: 1, cancelledWaits: 0 }); }); it('platform cancelPendingEventWaits forwards to runtime client', async () => { diff --git a/src/companion/platformClients.ts b/src/companion/platformClients.ts index 2f5d835..aa1bfb1 100644 --- a/src/companion/platformClients.ts +++ b/src/companion/platformClients.ts @@ -1,6 +1,7 @@ import type { CompanionEventName, CompanionEventEnvelope, + ClearEventSubscriptionsResult, EventSurfaceSnapshot, ConnectionSnapshot, CompanionEventHandler, @@ -267,8 +268,8 @@ export class MacOSCompanionClient { return this.runtime.subscribeEvent(eventName, handler); } - clearEventSubscriptions(): void { - this.runtime.clearEventSubscriptions(); + clearEventSubscriptions(): ClearEventSubscriptionsResult { + return this.runtime.clearEventSubscriptions(); } cancelPendingEventWaits(reason?: string): number { @@ -545,8 +546,8 @@ export class IOSCompanionClient { return this.runtime.subscribeEvent(eventName, handler); } - clearEventSubscriptions(): void { - this.runtime.clearEventSubscriptions(); + clearEventSubscriptions(): ClearEventSubscriptionsResult { + return this.runtime.clearEventSubscriptions(); } cancelPendingEventWaits(reason?: string): number { @@ -821,8 +822,8 @@ export class AndroidCompanionClient { return this.runtime.subscribeEvent(eventName, handler); } - clearEventSubscriptions(): void { - this.runtime.clearEventSubscriptions(); + clearEventSubscriptions(): ClearEventSubscriptionsResult { + return this.runtime.clearEventSubscriptions(); } cancelPendingEventWaits(reason?: string): number { diff --git a/src/companion/runtimeClient.test.ts b/src/companion/runtimeClient.test.ts index f33c88e..ca26478 100644 --- a/src/companion/runtimeClient.test.ts +++ b/src/companion/runtimeClient.test.ts @@ -267,7 +267,7 @@ describe('CompanionRuntimeClient', () => { const handlerB = vi.fn(); client.subscribeEvents(handlerA); client.subscribeEvent('agent.stream', handlerB); - client.clearEventSubscriptions(); + const clearResult = client.clearEventSubscriptions(); (client as unknown as { handleMessage: (raw: string) => void }).handleMessage( JSON.stringify({ @@ -279,6 +279,10 @@ describe('CompanionRuntimeClient', () => { expect(handlerA).not.toHaveBeenCalled(); expect(handlerB).not.toHaveBeenCalled(); + expect(clearResult).toEqual({ + clearedSubscriptions: 2, + cancelledWaits: 0, + }); }); it('dispose clears subscriptions and is safe to call repeatedly', () => { @@ -385,7 +389,10 @@ describe('CompanionRuntimeClient', () => { const awaited = expect( client.waitForEvent('agent.stream', { timeoutMs: 10_000 }), ).rejects.toThrow('Event subscriptions cleared'); - client.clearEventSubscriptions(); + expect(client.clearEventSubscriptions()).toEqual({ + clearedSubscriptions: 1, + cancelledWaits: 1, + }); await awaited; }); diff --git a/src/companion/runtimeClient.ts b/src/companion/runtimeClient.ts index 6ed1f58..16200b6 100644 --- a/src/companion/runtimeClient.ts +++ b/src/companion/runtimeClient.ts @@ -68,6 +68,11 @@ export interface ConnectionSnapshot { idle: boolean; } +export interface ClearEventSubscriptionsResult { + clearedSubscriptions: number; + cancelledWaits: number; +} + export type CompanionEventHandler = (event: string, data: unknown) => void; export type CompanionTypedEventHandler = (data: TData) => void; export type CompanionEventPredicate = (data: TData) => boolean; @@ -464,9 +469,14 @@ export class CompanionRuntimeClient { }; } - clearEventSubscriptions(): void { + clearEventSubscriptions(): ClearEventSubscriptionsResult { + const clearedSubscriptions = this.eventHandlers.size; this.eventHandlers.clear(); - this.rejectEventWaits(new Error('Event subscriptions cleared')); + const cancelledWaits = this.rejectEventWaits(new Error('Event subscriptions cleared')); + return { + clearedSubscriptions, + cancelledWaits, + }; } cancelPendingEventWaits(reason = 'Event waits cancelled'): number {