feat(companion): support runtime client autoConnect mode
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user