@adhdev/session-host-core 1.0.82-rc.26 → 1.0.82-rc.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ipc.ts CHANGED
@@ -124,6 +124,8 @@ export interface SessionHostClientOptions {
124
124
  endpoint?: SessionHostEndpoint;
125
125
  appName?: string;
126
126
  requestTimeoutMs?: number;
127
+ /** Join the shared client registry (default true). Disable for short-lived probes. */
128
+ registerAsShared?: boolean;
127
129
  }
128
130
 
129
131
  /**
@@ -155,6 +157,20 @@ export function disconnectSessionHostClients(endpoint?: SessionHostEndpoint, err
155
157
  }
156
158
  }
157
159
 
160
+ export function findConnectedSessionHostClient(endpoint?: SessionHostEndpoint): SessionHostClient | null {
161
+ for (const ref of activeClientRefs) {
162
+ const client = ref.deref();
163
+ if (!client) {
164
+ activeClientRefs.delete(ref);
165
+ continue;
166
+ }
167
+ if ((!endpoint || client.endpoint.path === endpoint.path) && client.isConnected()) {
168
+ return client;
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
158
174
  export interface SessionHostDisconnectInfo {
159
175
  reason: SessionHostDisconnectReason;
160
176
  endpointPath: string;
@@ -192,13 +208,21 @@ export class SessionHostClient {
192
208
  if (typeof options.requestTimeoutMs === 'number' && options.requestTimeoutMs > 0) {
193
209
  this.requestTimeoutMs = options.requestTimeoutMs;
194
210
  }
195
- activeClientRefs.add(new WeakRef(this));
211
+ if (options.registerAsShared !== false) activeClientRefs.add(new WeakRef(this));
196
212
  }
197
213
 
198
214
  static disconnectClients(endpoint?: SessionHostEndpoint, error?: Error): void {
199
215
  disconnectSessionHostClients(endpoint, error);
200
216
  }
201
217
 
218
+ static findConnectedClient(endpoint?: SessionHostEndpoint): SessionHostClient | null {
219
+ return findConnectedSessionHostClient(endpoint);
220
+ }
221
+
222
+ isConnected(): boolean {
223
+ return Boolean(this.socket && !this.socket.destroyed && establishedSockets.has(this.socket));
224
+ }
225
+
202
226
  /**
203
227
  * Fires when an established connection to the session host goes away.
204
228
  *
@@ -278,6 +302,7 @@ export class SessionHostClient {
278
302
  private sendRawRequestOnSocket<T = unknown>(
279
303
  socket: net.Socket,
280
304
  request: SessionHostRequest,
305
+ timeoutMs = this.requestTimeoutMs,
281
306
  ): Promise<SessionHostResponse<T>> {
282
307
  const requestId = generateUUID();
283
308
  const envelope: SessionHostRequestEnvelope = {
@@ -285,11 +310,10 @@ export class SessionHostClient {
285
310
  requestId,
286
311
  request,
287
312
  };
288
- const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : 30_000;
289
313
  const timeoutLabel = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}s` : `${timeoutMs}ms`;
290
314
  return new Promise<SessionHostResponse<T>>((resolve, reject) => {
291
315
  const timeout = setTimeout(() => {
292
- const timeoutError = new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`);
316
+ const timeoutError = Object.assign(new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`), { code: 'SESSION_HOST_REQUEST_TIMEOUT', requestType: request.type });
293
317
  if (this.socket === socket) {
294
318
  this.disconnect(timeoutError);
295
319
  } else {
@@ -307,7 +331,7 @@ export class SessionHostClient {
307
331
  });
308
332
  }
309
333
 
310
- async connect(): Promise<void> {
334
+ async connect(timeoutMs = this.requestTimeoutMs): Promise<void> {
311
335
  if (this.socket && !this.socket.destroyed) {
312
336
  if (this.connectPromise) await this.connectPromise;
313
337
  return;
@@ -381,11 +405,23 @@ export class SessionHostClient {
381
405
 
382
406
  const connectTask = (async () => {
383
407
  await new Promise<void>((resolve, reject) => {
384
- socket.once('connect', () => {
385
- establishedSockets.add(socket);
386
- resolve();
387
- });
388
- socket.once('error', reject);
408
+ const cleanup = () => {
409
+ clearTimeout(timer);
410
+ socket.off('connect', connected);
411
+ socket.off('error', failed);
412
+ socket.off('close', closed);
413
+ };
414
+ const connected = () => { cleanup(); establishedSockets.add(socket); resolve(); };
415
+ const failed = (error: Error) => { cleanup(); reject(error); };
416
+ const closed = () => failed(new Error('Session host connection closed before connect'));
417
+ const timer = setTimeout(() => {
418
+ const error = Object.assign(new Error(`Session host connect timed out after ${timeoutMs}ms`), { code: 'SESSION_HOST_CONNECT_TIMEOUT' });
419
+ failed(error);
420
+ if (this.socket === socket) this.disconnect(error);
421
+ }, timeoutMs);
422
+ socket.once('connect', connected);
423
+ socket.once('error', failed);
424
+ socket.once('close', closed);
389
425
  });
390
426
  if (this.attachedSessions.size > 0 && this.socket === socket && !socket.destroyed) {
391
427
  for (const [sessionId, sub] of [...this.attachedSessions.entries()]) {
@@ -418,12 +454,33 @@ export class SessionHostClient {
418
454
  };
419
455
  }
420
456
 
421
- async request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>> {
422
- await this.connect();
457
+ async request<T = unknown>(request: SessionHostRequest, options: { timeoutMs?: number } = {}): Promise<SessionHostResponse<T>> {
458
+ const timeoutMs = Math.max(1, options.timeoutMs ?? this.requestTimeoutMs);
459
+ const deadline = Date.now() + timeoutMs;
460
+ const alreadyConnected = this.isConnected() && !this.connectPromise;
461
+ // A probe must also bound a connect/re-attach already in flight on this
462
+ // client; that task may have started with the normal 30s timeout.
463
+ if (options.timeoutMs !== undefined) {
464
+ let timer: ReturnType<typeof setTimeout> | undefined;
465
+ try {
466
+ await Promise.race([
467
+ this.connect(timeoutMs),
468
+ new Promise<never>((_resolve, reject) => {
469
+ timer = setTimeout(() => {
470
+ const error = Object.assign(new Error(`Session host connect timed out after ${timeoutMs}ms`), { code: 'SESSION_HOST_CONNECT_TIMEOUT' });
471
+ reject(error);
472
+ this.disconnect(error);
473
+ }, timeoutMs);
474
+ }),
475
+ ]);
476
+ } finally { if (timer) clearTimeout(timer); }
477
+ } else {
478
+ await this.connect(timeoutMs);
479
+ }
423
480
  const requestSocket = this.socket;
424
481
  if (!requestSocket) throw new Error('Session host socket unavailable');
425
482
 
426
- const response = await this.sendRawRequestOnSocket<T>(requestSocket, request);
483
+ const response = await this.sendRawRequestOnSocket<T>(requestSocket, request, options.timeoutMs === undefined || alreadyConnected ? timeoutMs : Math.max(1, deadline - Date.now()));
427
484
  if (response.success) {
428
485
  if (request.type === 'attach_session') {
429
486
  this.attachedSessions.set(request.payload.sessionId, {