@adhdev/session-host-core 1.0.82-rc.26 → 1.0.82-rc.29
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/dist/index.d.mts +33 -3
- package/dist/index.d.ts +33 -3
- package/dist/index.js +115 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +114 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/ipc.ts +112 -12
- package/src/types.ts +11 -0
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export type {
|
|
2
|
+
MeshWriterAssignment,
|
|
3
|
+
MeshWriterStopTarget,
|
|
4
|
+
StopMeshWriterPayload,
|
|
2
5
|
HostUpgradeLeasePayload,
|
|
3
6
|
AcquireWritePayload,
|
|
4
7
|
AttachSessionPayload,
|
|
@@ -77,6 +80,7 @@ export {
|
|
|
77
80
|
createLineParser,
|
|
78
81
|
createResponseEnvelope,
|
|
79
82
|
disconnectSessionHostClients,
|
|
83
|
+
findConnectedSessionHostClient,
|
|
80
84
|
getDefaultSessionHostEndpoint,
|
|
81
85
|
writeEnvelope,
|
|
82
86
|
} from './ipc.js';
|
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,25 @@ 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`;
|
|
314
|
+
const startedAt = Date.now();
|
|
315
|
+
const startedMonotonicAt = performance.now();
|
|
290
316
|
return new Promise<SessionHostResponse<T>>((resolve, reject) => {
|
|
291
317
|
const timeout = setTimeout(() => {
|
|
292
|
-
const
|
|
318
|
+
const actualElapsedMs = Math.max(0, Math.round(performance.now() - startedMonotonicAt));
|
|
319
|
+
const wallElapsedMs = Math.max(0, Date.now() - startedAt);
|
|
320
|
+
const observerLagMs = Math.max(0, actualElapsedMs - timeoutMs);
|
|
321
|
+
const timeoutError = Object.assign(
|
|
322
|
+
new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`),
|
|
323
|
+
{
|
|
324
|
+
code: 'SESSION_HOST_REQUEST_TIMEOUT',
|
|
325
|
+
requestType: request.type,
|
|
326
|
+
scheduledTimeoutMs: timeoutMs,
|
|
327
|
+
actualElapsedMs,
|
|
328
|
+
wallElapsedMs,
|
|
329
|
+
observerLagMs,
|
|
330
|
+
},
|
|
331
|
+
);
|
|
293
332
|
if (this.socket === socket) {
|
|
294
333
|
this.disconnect(timeoutError);
|
|
295
334
|
} else {
|
|
@@ -307,7 +346,7 @@ export class SessionHostClient {
|
|
|
307
346
|
});
|
|
308
347
|
}
|
|
309
348
|
|
|
310
|
-
async connect(): Promise<void> {
|
|
349
|
+
async connect(timeoutMs = this.requestTimeoutMs): Promise<void> {
|
|
311
350
|
if (this.socket && !this.socket.destroyed) {
|
|
312
351
|
if (this.connectPromise) await this.connectPromise;
|
|
313
352
|
return;
|
|
@@ -381,11 +420,37 @@ export class SessionHostClient {
|
|
|
381
420
|
|
|
382
421
|
const connectTask = (async () => {
|
|
383
422
|
await new Promise<void>((resolve, reject) => {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
423
|
+
const cleanup = () => {
|
|
424
|
+
clearTimeout(timer);
|
|
425
|
+
socket.off('connect', connected);
|
|
426
|
+
socket.off('error', failed);
|
|
427
|
+
socket.off('close', closed);
|
|
428
|
+
};
|
|
429
|
+
const connected = () => { cleanup(); establishedSockets.add(socket); resolve(); };
|
|
430
|
+
const failed = (error: Error) => { cleanup(); reject(error); };
|
|
431
|
+
const closed = () => failed(new Error('Session host connection closed before connect'));
|
|
432
|
+
const connectStartedAt = Date.now();
|
|
433
|
+
const connectStartedMonotonicAt = performance.now();
|
|
434
|
+
const timer = setTimeout(() => {
|
|
435
|
+
const actualElapsedMs = Math.max(0, Math.round(performance.now() - connectStartedMonotonicAt));
|
|
436
|
+
const wallElapsedMs = Math.max(0, Date.now() - connectStartedAt);
|
|
437
|
+
const observerLagMs = Math.max(0, actualElapsedMs - timeoutMs);
|
|
438
|
+
const error = Object.assign(
|
|
439
|
+
new Error(`Session host connect timed out after ${timeoutMs}ms`),
|
|
440
|
+
{
|
|
441
|
+
code: 'SESSION_HOST_CONNECT_TIMEOUT',
|
|
442
|
+
scheduledTimeoutMs: timeoutMs,
|
|
443
|
+
actualElapsedMs,
|
|
444
|
+
wallElapsedMs,
|
|
445
|
+
observerLagMs,
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
failed(error);
|
|
449
|
+
if (this.socket === socket) this.disconnect(error);
|
|
450
|
+
}, timeoutMs);
|
|
451
|
+
socket.once('connect', connected);
|
|
452
|
+
socket.once('error', failed);
|
|
453
|
+
socket.once('close', closed);
|
|
389
454
|
});
|
|
390
455
|
if (this.attachedSessions.size > 0 && this.socket === socket && !socket.destroyed) {
|
|
391
456
|
for (const [sessionId, sub] of [...this.attachedSessions.entries()]) {
|
|
@@ -418,12 +483,47 @@ export class SessionHostClient {
|
|
|
418
483
|
};
|
|
419
484
|
}
|
|
420
485
|
|
|
421
|
-
async request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>> {
|
|
422
|
-
|
|
486
|
+
async request<T = unknown>(request: SessionHostRequest, options: { timeoutMs?: number } = {}): Promise<SessionHostResponse<T>> {
|
|
487
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? this.requestTimeoutMs);
|
|
488
|
+
const deadline = Date.now() + timeoutMs;
|
|
489
|
+
const alreadyConnected = this.isConnected() && !this.connectPromise;
|
|
490
|
+
// A probe must also bound a connect/re-attach already in flight on this
|
|
491
|
+
// client; that task may have started with the normal 30s timeout.
|
|
492
|
+
if (options.timeoutMs !== undefined) {
|
|
493
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
494
|
+
const raceStartedAt = Date.now();
|
|
495
|
+
const raceStartedMonotonicAt = performance.now();
|
|
496
|
+
try {
|
|
497
|
+
await Promise.race([
|
|
498
|
+
this.connect(timeoutMs),
|
|
499
|
+
new Promise<never>((_resolve, reject) => {
|
|
500
|
+
timer = setTimeout(() => {
|
|
501
|
+
const actualElapsedMs = Math.max(0, Math.round(performance.now() - raceStartedMonotonicAt));
|
|
502
|
+
const wallElapsedMs = Math.max(0, Date.now() - raceStartedAt);
|
|
503
|
+
const observerLagMs = Math.max(0, actualElapsedMs - timeoutMs);
|
|
504
|
+
const error = Object.assign(
|
|
505
|
+
new Error(`Session host connect timed out after ${timeoutMs}ms`),
|
|
506
|
+
{
|
|
507
|
+
code: 'SESSION_HOST_CONNECT_TIMEOUT',
|
|
508
|
+
scheduledTimeoutMs: timeoutMs,
|
|
509
|
+
actualElapsedMs,
|
|
510
|
+
wallElapsedMs,
|
|
511
|
+
observerLagMs,
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
reject(error);
|
|
515
|
+
this.disconnect(error);
|
|
516
|
+
}, timeoutMs);
|
|
517
|
+
}),
|
|
518
|
+
]);
|
|
519
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
520
|
+
} else {
|
|
521
|
+
await this.connect(timeoutMs);
|
|
522
|
+
}
|
|
423
523
|
const requestSocket = this.socket;
|
|
424
524
|
if (!requestSocket) throw new Error('Session host socket unavailable');
|
|
425
525
|
|
|
426
|
-
const response = await this.sendRawRequestOnSocket<T>(requestSocket, request);
|
|
526
|
+
const response = await this.sendRawRequestOnSocket<T>(requestSocket, request, options.timeoutMs === undefined || alreadyConnected ? timeoutMs : Math.max(1, deadline - Date.now()));
|
|
427
527
|
if (response.success) {
|
|
428
528
|
if (request.type === 'attach_session') {
|
|
429
529
|
this.attachedSessions.set(request.payload.sessionId, {
|
package/src/types.ts
CHANGED
|
@@ -157,6 +157,15 @@ export interface ResizeSessionPayload {
|
|
|
157
157
|
export interface StopSessionPayload {
|
|
158
158
|
sessionId: string;
|
|
159
159
|
}
|
|
160
|
+
export interface MeshWriterAssignment {
|
|
161
|
+
meshId: string; nodeId: string; taskId: string; attemptId: string;
|
|
162
|
+
generation: number; dispatchNonce: number; ownerDaemonId: string;
|
|
163
|
+
}
|
|
164
|
+
export interface MeshWriterStopTarget {
|
|
165
|
+
runtimeKey: string; startedAt: number; osPid: number; providerType: string;
|
|
166
|
+
assignment: MeshWriterAssignment;
|
|
167
|
+
}
|
|
168
|
+
export interface StopMeshWriterPayload extends MeshWriterStopTarget { sessionId: string; clientId: string }
|
|
160
169
|
|
|
161
170
|
export interface DeleteSessionPayload {
|
|
162
171
|
sessionId: string;
|
|
@@ -317,6 +326,7 @@ export type SessionHostRequest =
|
|
|
317
326
|
| { type: 'send_input'; payload: SendInputPayload }
|
|
318
327
|
| { type: 'resize_session'; payload: ResizeSessionPayload }
|
|
319
328
|
| { type: 'stop_session'; payload: StopSessionPayload }
|
|
329
|
+
| { type: 'stop_mesh_writer'; payload: StopMeshWriterPayload }
|
|
320
330
|
| { type: 'delete_session'; payload: DeleteSessionPayload }
|
|
321
331
|
| { type: 'resume_session'; payload: ResumeSessionPayload }
|
|
322
332
|
| { type: 'acquire_write'; payload: AcquireWritePayload }
|
|
@@ -344,6 +354,7 @@ export const SESSION_HOST_SUPPORTED_REQUEST_TYPES: readonly SessionHostRequestTy
|
|
|
344
354
|
'send_input',
|
|
345
355
|
'resize_session',
|
|
346
356
|
'stop_session',
|
|
357
|
+
'stop_mesh_writer',
|
|
347
358
|
'delete_session',
|
|
348
359
|
'resume_session',
|
|
349
360
|
'acquire_write',
|