feat(companion): return clearEventSubscriptions result counts
This commit is contained in:
@@ -1190,7 +1190,7 @@ Methods:
|
|||||||
- `system.capabilities` returns gateway protocol and node policy snapshot.
|
- `system.capabilities` returns gateway protocol and node policy snapshot.
|
||||||
|
|
||||||
Companion runtime helper:
|
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:
|
- `src/companion/platformClients.ts` provides platform-focused wrappers:
|
||||||
- `MacOSCompanionClient` (`platform: "macos"`, APNs push registration)
|
- `MacOSCompanionClient` (`platform: "macos"`, APNs push registration)
|
||||||
- `IOSCompanionClient` (`platform: "ios"`, APNs push registration)
|
- `IOSCompanionClient` (`platform: "ios"`, APNs push registration)
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"companion-runtime-cancel-pending-event-waits": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-17",
|
"date": "2026-02-17",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type {
|
|||||||
PendingWorkSnapshot,
|
PendingWorkSnapshot,
|
||||||
EventSurfaceSnapshot,
|
EventSurfaceSnapshot,
|
||||||
ConnectionSnapshot,
|
ConnectionSnapshot,
|
||||||
|
ClearEventSubscriptionsResult,
|
||||||
CompanionEventHandler,
|
CompanionEventHandler,
|
||||||
CompanionTypedEventHandler,
|
CompanionTypedEventHandler,
|
||||||
CompanionEventName,
|
CompanionEventName,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function createRuntimeMock(): {
|
|||||||
const subscribeContextWarning = vi.fn(() => () => undefined);
|
const subscribeContextWarning = vi.fn(() => () => undefined);
|
||||||
const subscribeEvents = vi.fn(() => () => undefined);
|
const subscribeEvents = vi.fn(() => () => undefined);
|
||||||
const subscribeEvent = 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 cancelPendingEventWaits = vi.fn(() => 1);
|
||||||
const listKnownEventNames = vi.fn(() => ['agent.stream', 'agent.typing', 'context_warning']);
|
const listKnownEventNames = vi.fn(() => ['agent.stream', 'agent.typing', 'context_warning']);
|
||||||
const waitForEvent = vi.fn(async () => ({ token: 'evented' }));
|
const waitForEvent = vi.fn(async () => ({ token: 'evented' }));
|
||||||
@@ -331,9 +331,10 @@ describe('platform companion clients', () => {
|
|||||||
const mock = createRuntimeMock();
|
const mock = createRuntimeMock();
|
||||||
const client = new IOSCompanionClient({ runtime: mock.runtime, nodeId: 'ios-node' });
|
const client = new IOSCompanionClient({ runtime: mock.runtime, nodeId: 'ios-node' });
|
||||||
|
|
||||||
client.clearEventSubscriptions();
|
const result = client.clearEventSubscriptions();
|
||||||
|
|
||||||
expect(mock.clearEventSubscriptions).toHaveBeenCalledOnce();
|
expect(mock.clearEventSubscriptions).toHaveBeenCalledOnce();
|
||||||
|
expect(result).toEqual({ clearedSubscriptions: 1, cancelledWaits: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('platform cancelPendingEventWaits forwards to runtime client', async () => {
|
it('platform cancelPendingEventWaits forwards to runtime client', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
CompanionEventName,
|
CompanionEventName,
|
||||||
CompanionEventEnvelope,
|
CompanionEventEnvelope,
|
||||||
|
ClearEventSubscriptionsResult,
|
||||||
EventSurfaceSnapshot,
|
EventSurfaceSnapshot,
|
||||||
ConnectionSnapshot,
|
ConnectionSnapshot,
|
||||||
CompanionEventHandler,
|
CompanionEventHandler,
|
||||||
@@ -267,8 +268,8 @@ export class MacOSCompanionClient {
|
|||||||
return this.runtime.subscribeEvent(eventName, handler);
|
return this.runtime.subscribeEvent(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEventSubscriptions(): void {
|
clearEventSubscriptions(): ClearEventSubscriptionsResult {
|
||||||
this.runtime.clearEventSubscriptions();
|
return this.runtime.clearEventSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelPendingEventWaits(reason?: string): number {
|
cancelPendingEventWaits(reason?: string): number {
|
||||||
@@ -545,8 +546,8 @@ export class IOSCompanionClient {
|
|||||||
return this.runtime.subscribeEvent(eventName, handler);
|
return this.runtime.subscribeEvent(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEventSubscriptions(): void {
|
clearEventSubscriptions(): ClearEventSubscriptionsResult {
|
||||||
this.runtime.clearEventSubscriptions();
|
return this.runtime.clearEventSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelPendingEventWaits(reason?: string): number {
|
cancelPendingEventWaits(reason?: string): number {
|
||||||
@@ -821,8 +822,8 @@ export class AndroidCompanionClient {
|
|||||||
return this.runtime.subscribeEvent(eventName, handler);
|
return this.runtime.subscribeEvent(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEventSubscriptions(): void {
|
clearEventSubscriptions(): ClearEventSubscriptionsResult {
|
||||||
this.runtime.clearEventSubscriptions();
|
return this.runtime.clearEventSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelPendingEventWaits(reason?: string): number {
|
cancelPendingEventWaits(reason?: string): number {
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ describe('CompanionRuntimeClient', () => {
|
|||||||
const handlerB = vi.fn();
|
const handlerB = vi.fn();
|
||||||
client.subscribeEvents(handlerA);
|
client.subscribeEvents(handlerA);
|
||||||
client.subscribeEvent('agent.stream', handlerB);
|
client.subscribeEvent('agent.stream', handlerB);
|
||||||
client.clearEventSubscriptions();
|
const clearResult = client.clearEventSubscriptions();
|
||||||
|
|
||||||
(client as unknown as { handleMessage: (raw: string) => void }).handleMessage(
|
(client as unknown as { handleMessage: (raw: string) => void }).handleMessage(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -279,6 +279,10 @@ describe('CompanionRuntimeClient', () => {
|
|||||||
|
|
||||||
expect(handlerA).not.toHaveBeenCalled();
|
expect(handlerA).not.toHaveBeenCalled();
|
||||||
expect(handlerB).not.toHaveBeenCalled();
|
expect(handlerB).not.toHaveBeenCalled();
|
||||||
|
expect(clearResult).toEqual({
|
||||||
|
clearedSubscriptions: 2,
|
||||||
|
cancelledWaits: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispose clears subscriptions and is safe to call repeatedly', () => {
|
it('dispose clears subscriptions and is safe to call repeatedly', () => {
|
||||||
@@ -385,7 +389,10 @@ describe('CompanionRuntimeClient', () => {
|
|||||||
const awaited = expect(
|
const awaited = expect(
|
||||||
client.waitForEvent('agent.stream', { timeoutMs: 10_000 }),
|
client.waitForEvent('agent.stream', { timeoutMs: 10_000 }),
|
||||||
).rejects.toThrow('Event subscriptions cleared');
|
).rejects.toThrow('Event subscriptions cleared');
|
||||||
client.clearEventSubscriptions();
|
expect(client.clearEventSubscriptions()).toEqual({
|
||||||
|
clearedSubscriptions: 1,
|
||||||
|
cancelledWaits: 1,
|
||||||
|
});
|
||||||
await awaited;
|
await awaited;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ export interface ConnectionSnapshot {
|
|||||||
idle: boolean;
|
idle: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClearEventSubscriptionsResult {
|
||||||
|
clearedSubscriptions: number;
|
||||||
|
cancelledWaits: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type CompanionEventHandler = (event: string, data: unknown) => void;
|
export type CompanionEventHandler = (event: string, data: unknown) => void;
|
||||||
export type CompanionTypedEventHandler<TData = unknown> = (data: TData) => void;
|
export type CompanionTypedEventHandler<TData = unknown> = (data: TData) => void;
|
||||||
export type CompanionEventPredicate<TData = unknown> = (data: TData) => boolean;
|
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.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 {
|
cancelPendingEventWaits(reason = 'Event waits cancelled'): number {
|
||||||
|
|||||||
Reference in New Issue
Block a user