@push.rocks/smartrust 2.5.1 → 2.6.1

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.
@@ -31,10 +31,44 @@ const defaultLogger: IRustBridgeLogger = {
31
31
  log() {},
32
32
  };
33
33
 
34
+ /** A bridge deadline armed by `armDeadline`. */
35
+ interface IBridgeDeadline {
36
+ /** Cancel the deadline, including an expiry that already fired and still waits for its turn. */
37
+ clear(): void;
38
+ }
39
+
40
+ /**
41
+ * Arm a deadline that never beats an answer already waiting to be read.
42
+ *
43
+ * When the process resumes from a stall (a frozen cgroup, a stopped or descheduled VM, a long
44
+ * synchronous task), Node runs every expired timer before it polls for I/O again, so a deadline
45
+ * can expire while the answer it bounds sits unread in the pipe: a frozen Pallet guest timed out a
46
+ * command whose answer its Rust side had written 13.5 ms before the freeze. The expiry therefore
47
+ * acts one event-loop turn later: `setImmediate` runs after the next poll phase, which reads
48
+ * whatever had arrived. Whatever that read settles clears the deadline, and `clear()` cancels the
49
+ * waiting expiry too, so an answer that won is never abandoned afterwards. Every bridge deadline
50
+ * is armed here, never with a bare `setTimeout`.
51
+ */
52
+ const armDeadline = (timeoutMs: number, onExpired: () => void): IBridgeDeadline => {
53
+ let expiry: ReturnType<typeof setImmediate> | null = null;
54
+ const timer = setTimeout(() => {
55
+ expiry = setImmediate(onExpired);
56
+ }, timeoutMs);
57
+ return {
58
+ clear: () => {
59
+ clearTimeout(timer);
60
+ if (expiry) {
61
+ clearImmediate(expiry);
62
+ expiry = null;
63
+ }
64
+ },
65
+ };
66
+ };
67
+
34
68
  interface IPendingRequest {
35
69
  resolve: (value: any) => void;
36
70
  reject: (error: Error) => void;
37
- timer: ReturnType<typeof setTimeout> | null;
71
+ deadline: IBridgeDeadline | null;
38
72
  method: string;
39
73
  abortSignal?: AbortSignal;
40
74
  abortHandler?: () => void;
@@ -155,7 +189,8 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
155
189
  /**
156
190
  * Spawn the Rust binary and wait for it to signal readiness.
157
191
  * Returns true if the binary was found and spawned successfully.
158
- * Rejects with RustBinaryLocatorError for an invalid explicit binaryPath.
192
+ * Rejects with RustBinaryLocatorError for an invalid explicit binaryPath and,
193
+ * inside a single executable application, for an embedded sidecar it cannot provide.
159
194
  */
160
195
  public spawn(): Promise<boolean> {
161
196
  return this.activate(async (generationArg) => {
@@ -289,18 +324,18 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
289
324
 
290
325
  return new Promise<boolean>((resolve) => {
291
326
  let settled = false;
292
- let readyTimeout: ReturnType<typeof setTimeout> | null = null;
293
- let reconnectReadyTimeout: ReturnType<typeof setTimeout> | null = null;
327
+ let readyDeadline: IBridgeDeadline | null = null;
328
+ let reconnectReadyDeadline: IBridgeDeadline | null = null;
294
329
 
295
330
  const isCurrent = (): boolean => this.isCurrentTransport(transportArg, generation);
296
331
  const settle = (valueArg: boolean): void => {
297
- if (readyTimeout) {
298
- clearTimeout(readyTimeout);
299
- readyTimeout = null;
332
+ if (readyDeadline) {
333
+ readyDeadline.clear();
334
+ readyDeadline = null;
300
335
  }
301
- if (reconnectReadyTimeout) {
302
- clearTimeout(reconnectReadyTimeout);
303
- reconnectReadyTimeout = null;
336
+ if (reconnectReadyDeadline) {
337
+ reconnectReadyDeadline.clear();
338
+ reconnectReadyDeadline = null;
304
339
  }
305
340
  if (settled) {
306
341
  return;
@@ -335,9 +370,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
335
370
  if (!isCurrent() || this.lifecycleState !== 'starting') {
336
371
  return;
337
372
  }
338
- if (reconnectReadyTimeout) {
339
- clearTimeout(reconnectReadyTimeout);
340
- reconnectReadyTimeout = null;
373
+ if (reconnectReadyDeadline) {
374
+ reconnectReadyDeadline.clear();
375
+ reconnectReadyDeadline = null;
341
376
  }
342
377
  this.remoteCancelSupported = this.readCancelCapability(readyEvent);
343
378
  this.lifecycleState = 'ready';
@@ -373,9 +408,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
373
408
  this.rejectAllPendingRequests(`${this.options.binaryName} process exited`);
374
409
  this.settleAllRemoteCancellations();
375
410
  if (!intentional && transportArg.reconnecting === true) {
376
- if (reconnectReadyTimeout) {
377
- clearTimeout(reconnectReadyTimeout);
378
- reconnectReadyTimeout = null;
411
+ if (reconnectReadyDeadline) {
412
+ reconnectReadyDeadline.clear();
413
+ reconnectReadyDeadline = null;
379
414
  }
380
415
  this.lifecycleState = 'starting';
381
416
  // Idle capability owners must observe loss before any reconnect timer
@@ -411,10 +446,10 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
411
446
  this.logger.log('info', 'Transport reconnected, waiting for ready event');
412
447
  this.lifecycleState = 'starting';
413
448
  this.remoteCancelSupported = false;
414
- if (reconnectReadyTimeout) {
415
- clearTimeout(reconnectReadyTimeout);
449
+ if (reconnectReadyDeadline) {
450
+ reconnectReadyDeadline.clear();
416
451
  }
417
- reconnectReadyTimeout = setTimeout(() => {
452
+ reconnectReadyDeadline = armDeadline(this.options.readyTimeoutMs, () => {
418
453
  if (!isCurrent() || this.lifecycleState !== 'starting') {
419
454
  return;
420
455
  }
@@ -423,7 +458,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
423
458
  `Reconnected process did not send ready event within ${this.options.readyTimeoutMs}ms`,
424
459
  );
425
460
  requestConfirmedCleanup();
426
- }, this.options.readyTimeoutMs);
461
+ });
427
462
  this.emit('reconnected');
428
463
  });
429
464
 
@@ -431,9 +466,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
431
466
  if (!isCurrent()) {
432
467
  return;
433
468
  }
434
- if (reconnectReadyTimeout) {
435
- clearTimeout(reconnectReadyTimeout);
436
- reconnectReadyTimeout = null;
469
+ if (reconnectReadyDeadline) {
470
+ reconnectReadyDeadline.clear();
471
+ reconnectReadyDeadline = null;
437
472
  }
438
473
  this.isRunning = false;
439
474
  this.transport = null;
@@ -443,7 +478,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
443
478
  settle(false);
444
479
  });
445
480
 
446
- readyTimeout = setTimeout(() => {
481
+ readyDeadline = armDeadline(this.options.readyTimeoutMs, () => {
447
482
  if (!isCurrent() || this.lifecycleState !== 'starting') {
448
483
  return;
449
484
  }
@@ -452,7 +487,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
452
487
  `Process did not send ready event within ${this.options.readyTimeoutMs}ms`,
453
488
  );
454
489
  requestConfirmedCleanup();
455
- }, this.options.readyTimeoutMs);
490
+ });
456
491
 
457
492
  let connectionPromise: Promise<void>;
458
493
  try {
@@ -543,7 +578,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
543
578
  const pending: IPendingRequest = {
544
579
  resolve,
545
580
  reject,
546
- timer: null,
581
+ deadline: null,
547
582
  method,
548
583
  abortSignal: options?.signal,
549
584
  admissionReservation,
@@ -569,13 +604,13 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
569
604
  }
570
605
  }
571
606
 
572
- pending.timer = setTimeout(() => {
607
+ pending.deadline = armDeadline(timeoutMs, () => {
573
608
  this.abandonPendingRequest(
574
609
  id,
575
610
  'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
576
611
  `Command '${method}' timed out after ${timeoutMs}ms`,
577
612
  );
578
- }, timeoutMs);
613
+ });
579
614
 
580
615
  const currentAvailabilityError = this.getRequestAvailabilityError(
581
616
  method,
@@ -743,7 +778,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
743
778
  const pending: IPendingRequest = {
744
779
  resolve,
745
780
  reject,
746
- timer: null,
781
+ deadline: null,
747
782
  method,
748
783
  abortSignal: options?.signal,
749
784
  admissionReservation,
@@ -764,14 +799,14 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
764
799
  };
765
800
  options.signal.addEventListener('abort', pending.abortHandler, { once: true });
766
801
  }
767
- pending.timer = setTimeout(() => {
802
+ pending.deadline = armDeadline(timeoutMs, () => {
768
803
  this.settlePendingRequest(id, {
769
804
  outcome: 'reject',
770
805
  error: new RustBridgeRequestError(`Command '${method}' timed out after ${timeoutMs}ms`, {
771
806
  code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT', requestId: id, method,
772
807
  }),
773
808
  });
774
- }, timeoutMs);
809
+ });
775
810
 
776
811
  void (async (): Promise<void> => {
777
812
  try {
@@ -887,7 +922,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
887
922
  const pending: IPendingRequest = {
888
923
  resolve: (result: any) => streaming.finish(result),
889
924
  reject: (error: Error) => streaming.fail(error),
890
- timer: null,
925
+ deadline: null,
891
926
  method,
892
927
  abortSignal: signal,
893
928
  streaming,
@@ -1151,16 +1186,16 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
1151
1186
 
1152
1187
  /** Start or restart the inactivity deadline of one streaming request. */
1153
1188
  private armStreamingTimeout(id: string, pending: IPendingRequest, afterChunkArg: boolean): void {
1154
- if (pending.timer) {
1155
- clearTimeout(pending.timer);
1156
- }
1189
+ // Clearing also cancels an expiry that fired and waits for its turn: a chunk read in that turn
1190
+ // keeps the stream alive.
1191
+ pending.deadline?.clear();
1157
1192
  const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
1158
1193
  const message = afterChunkArg
1159
1194
  ? `Streaming command '${pending.method}' timed out after ${timeoutMs}ms of inactivity`
1160
1195
  : `Streaming command '${pending.method}' timed out after ${timeoutMs}ms`;
1161
- pending.timer = setTimeout(() => {
1196
+ pending.deadline = armDeadline(timeoutMs, () => {
1162
1197
  this.abandonPendingRequest(id, 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT', message);
1163
- }, timeoutMs);
1198
+ });
1164
1199
  }
1165
1200
 
1166
1201
  /**
@@ -1338,9 +1373,10 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
1338
1373
 
1339
1374
  this.pendingRequests.delete(id);
1340
1375
  this.releasePendingRequestReservation(pending.admissionReservation);
1341
- if (pending.timer) {
1342
- clearTimeout(pending.timer);
1343
- pending.timer = null;
1376
+ // A settled request's deadline does nothing more, even when it fired and waits for its turn.
1377
+ if (pending.deadline) {
1378
+ pending.deadline.clear();
1379
+ pending.deadline = null;
1344
1380
  }
1345
1381
  if (pending.abortSignal && pending.abortHandler) {
1346
1382
  pending.abortSignal.removeEventListener('abort', pending.abortHandler);
package/ts/index.ts CHANGED
@@ -7,7 +7,7 @@ export {
7
7
  type TRustBridgeRequestErrorCode,
8
8
  } from './classes.rustbridgerequesterror.js';
9
9
  export { RustBinaryLocator } from './classes.rustbinarylocator.js';
10
- export { RustBinaryLocatorError } from './classes.rustbinarylocatorerror.js';
10
+ export { RustBinaryLocatorError, type TRustBinaryLocatorErrorCode } from './classes.rustbinarylocatorerror.js';
11
11
  export { StreamingResponse } from './classes.streamingresponse.js';
12
12
  export { StdioTransport } from './classes.stdiotransport.js';
13
13
  export { SocketTransport } from './classes.sockettransport.js';
package/ts/plugins.ts CHANGED
@@ -11,5 +11,6 @@ export { path, fs, childProcess, events, url, net, stream };
11
11
 
12
12
  // @push.rocks scope
13
13
  import * as smartpath from '@push.rocks/smartpath';
14
+ import * as smartsea from '@push.rocks/smartsea';
14
15
 
15
- export { smartpath };
16
+ export { smartpath, smartsea };