feat(companion): support runtime client autoConnect mode

This commit is contained in:
William Valentin
2026-02-16 18:36:50 -08:00
parent 5db7beeb53
commit 8d123cf859
5 changed files with 67 additions and 2 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`).
- `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 optional `autoConnect` mode for one-shot RPC calls.
- `src/companion/platformClients.ts` provides platform-focused wrappers:
- `MacOSCompanionClient` (`platform: "macos"`, APNs push registration)
- `IOSCompanionClient` (`platform: "ios"`, APNs push registration)
+1 -1
View File
@@ -1430,5 +1430,5 @@ For more implementation details, see:
- Protocol types: `src/gateway/protocol.ts`
- Handlers: `src/gateway/handlers/`
- Gateway server: `src/gateway/server.ts`
- Companion runtime client helper: `src/companion/runtimeClient.ts` (node + system + `canvas.*` typed RPC wrappers)
- Companion runtime client helper: `src/companion/runtimeClient.ts` (node + system + `canvas.*` typed RPC wrappers, optional `autoConnect`)
- Platform companion wrappers: `src/companion/platformClients.ts`
+14
View File
@@ -232,6 +232,20 @@
],
"test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
},
"companion-runtime-client-autoconnect": {
"status": "completed",
"date": "2026-02-17",
"updated": "2026-02-17",
"summary": "Added optional `autoConnect` support to `CompanionRuntimeClient` so RPC calls can establish connections on demand, including connect de-duplication to avoid concurrent open races, plus integration coverage.",
"files_modified": [
"src/companion/runtimeClient.ts",
"src/companion/runtimeClient.test.ts",
"README.md",
"docs/api/PROTOCOL.md",
"docs/plans/state.json"
],
"test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
},
"browser-tools-activation-clarity": {
"status": "completed",
"date": "2026-02-17",
+27
View File
@@ -268,4 +268,31 @@ describe('CompanionRuntimeClient', () => {
client.disconnect();
}
});
it('supports autoConnect mode for one-shot RPC usage', async () => {
if (!LISTEN_ALLOWED) {
return;
}
const client = new CompanionRuntimeClient({
url: `ws://127.0.0.1:${TEST_PORT}`,
token: TEST_TOKEN,
autoConnect: true,
});
expect(client.connected).toBe(false);
try {
const register = await client.registerNode({
nodeId: 'auto-connect-node',
role: 'companion',
capabilities: ['ui.canvas'],
});
expect(register.registered).toBe(true);
expect(client.connected).toBe(true);
} finally {
client.disconnect();
}
});
});
+24
View File
@@ -37,6 +37,7 @@ export interface CompanionRuntimeClientOptions {
url: string;
token?: string;
requestTimeoutMs?: number;
autoConnect?: boolean;
websocketFactory?: (url: string) => WebSocket;
}
@@ -249,9 +250,11 @@ export class CompanionRuntimeClient {
private readonly url: string;
private readonly token?: string;
private readonly requestTimeoutMs: number;
private readonly autoConnect: boolean;
private readonly websocketFactory: (url: string) => WebSocket;
private ws: WebSocket | null = null;
private connectPromise: Promise<void> | null = null;
private nextId = 1;
private pending = new Map<number, PendingRequest>();
@@ -259,6 +262,7 @@ export class CompanionRuntimeClient {
this.url = options.url;
this.token = options.token;
this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
this.autoConnect = options.autoConnect ?? false;
this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
}
@@ -271,6 +275,19 @@ export class CompanionRuntimeClient {
return;
}
if (this.connectPromise) {
return this.connectPromise;
}
this.connectPromise = this.openConnection();
try {
await this.connectPromise;
} finally {
this.connectPromise = null;
}
}
private async openConnection(): Promise<void> {
const ws = this.websocketFactory(withToken(this.url, this.token));
await new Promise<void>((resolve, reject) => {
@@ -326,6 +343,13 @@ export class CompanionRuntimeClient {
}
async call<T>(method: string, params?: Record<string, unknown>): Promise<T> {
if (!this.connected) {
if (!this.autoConnect) {
throw new Error('WebSocket is not connected');
}
await this.connect();
}
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket is not connected');
}