feat(companion): return clearEventSubscriptions result counts

This commit is contained in:
William Valentin
2026-02-16 22:26:23 -08:00
parent 06bdb27f70
commit ffc7c4e9b3
7 changed files with 49 additions and 13 deletions
+1 -1
View File
@@ -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)
+16
View File
@@ -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",
+1
View File
@@ -16,6 +16,7 @@ export type {
PendingWorkSnapshot,
EventSurfaceSnapshot,
ConnectionSnapshot,
ClearEventSubscriptionsResult,
CompanionEventHandler,
CompanionTypedEventHandler,
CompanionEventName,
+3 -2
View File
@@ -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 () => {
+7 -6
View File
@@ -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 {
+9 -2
View File
@@ -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;
});
+12 -2
View File
@@ -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<TData = unknown> = (data: TData) => void;
export type CompanionEventPredicate<TData = unknown> = (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 {