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.
|
- `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`).
|
- `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:
|
- `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)
|
||||||
|
|||||||
@@ -1430,5 +1430,5 @@ For more implementation details, see:
|
|||||||
- Protocol types: `src/gateway/protocol.ts`
|
- Protocol types: `src/gateway/protocol.ts`
|
||||||
- Handlers: `src/gateway/handlers/`
|
- Handlers: `src/gateway/handlers/`
|
||||||
- Gateway server: `src/gateway/server.ts`
|
- 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`
|
- 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"
|
"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": {
|
"browser-tools-activation-clarity": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-17",
|
"date": "2026-02-17",
|
||||||
|
|||||||
@@ -268,4 +268,31 @@ describe('CompanionRuntimeClient', () => {
|
|||||||
client.disconnect();
|
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;
|
url: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
requestTimeoutMs?: number;
|
requestTimeoutMs?: number;
|
||||||
|
autoConnect?: boolean;
|
||||||
websocketFactory?: (url: string) => WebSocket;
|
websocketFactory?: (url: string) => WebSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,9 +250,11 @@ export class CompanionRuntimeClient {
|
|||||||
private readonly url: string;
|
private readonly url: string;
|
||||||
private readonly token?: string;
|
private readonly token?: string;
|
||||||
private readonly requestTimeoutMs: number;
|
private readonly requestTimeoutMs: number;
|
||||||
|
private readonly autoConnect: boolean;
|
||||||
private readonly websocketFactory: (url: string) => WebSocket;
|
private readonly websocketFactory: (url: string) => WebSocket;
|
||||||
|
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
|
private connectPromise: Promise<void> | null = null;
|
||||||
private nextId = 1;
|
private nextId = 1;
|
||||||
private pending = new Map<number, PendingRequest>();
|
private pending = new Map<number, PendingRequest>();
|
||||||
|
|
||||||
@@ -259,6 +262,7 @@ export class CompanionRuntimeClient {
|
|||||||
this.url = options.url;
|
this.url = options.url;
|
||||||
this.token = options.token;
|
this.token = options.token;
|
||||||
this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
|
||||||
|
this.autoConnect = options.autoConnect ?? false;
|
||||||
this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
|
this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +275,19 @@ export class CompanionRuntimeClient {
|
|||||||
return;
|
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));
|
const ws = this.websocketFactory(withToken(this.url, this.token));
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -326,6 +343,13 @@ export class CompanionRuntimeClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async call<T>(method: string, params?: Record<string, unknown>): Promise<T> {
|
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) {
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
throw new Error('WebSocket is not connected');
|
throw new Error('WebSocket is not connected');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user