@zlink-systems/framework 0.28.0 → 0.29.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.
Files changed (112) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/contracts/Configuration/Builders.d.ts +2 -0
  3. package/dist/contracts/Configuration/InternalDefaults.d.ts +3 -0
  4. package/dist/contracts/Configuration/InternalDefaults.js +4 -1
  5. package/dist/contracts/Configuration/RegistrationBuilders.js +9 -0
  6. package/dist/contracts/Configuration/RegistrationNormalizers.d.ts +1 -0
  7. package/dist/contracts/Configuration/RegistrationNormalizers.js +16 -0
  8. package/dist/contracts/Configuration/RegistrationTypes.d.ts +3 -0
  9. package/dist/runtime/actors/actor-mailbox.d.ts +2 -2
  10. package/dist/runtime/actors/actor-mailbox.js +2 -2
  11. package/dist/runtime/actors/actor-runtime-contracts.d.ts +1 -0
  12. package/dist/runtime/actors/index.d.ts +2 -0
  13. package/dist/runtime/actors/index.js +21 -6
  14. package/dist/runtime/backend/node/node-raw-mesh-backend.js +1 -3
  15. package/dist/runtime/execution/serial-execution-queue.d.ts +12 -3
  16. package/dist/runtime/execution/serial-execution-queue.js +116 -28
  17. package/dist/runtime/foundation/operation-registry.d.ts +3 -2
  18. package/dist/runtime/foundation/operation-registry.js +5 -1
  19. package/dist/runtime/foundation/service-authority-payload-codec.d.ts +0 -2
  20. package/dist/runtime/foundation/service-authority-payload-codec.js +0 -43
  21. package/dist/runtime/foundation/service-stateful-registry.d.ts +4 -3
  22. package/dist/runtime/foundation/service-stateful-registry.js +5 -2
  23. package/dist/runtime/foundation/service-stateful-runtime.d.ts +1 -8
  24. package/dist/runtime/foundation/service-stateful-runtime.js +105 -145
  25. package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +0 -11
  26. package/dist/runtime/foundation/service-stateful-wire-codec.js +1 -23
  27. package/dist/runtime/foundation/service-topology-registry.d.ts +5 -0
  28. package/dist/runtime/foundation/service-topology-registry.js +16 -17
  29. package/dist/runtime/handlers/handler-instance-scope.js +1 -1
  30. package/dist/runtime/host/actor-placement-coordinator.d.ts +2 -1
  31. package/dist/runtime/host/actor-placement-coordinator.js +2 -1
  32. package/dist/runtime/host/actor-runtime-options-factory.d.ts +2 -1
  33. package/dist/runtime/host/actor-runtime-options-factory.js +1 -0
  34. package/dist/runtime/host/index.d.ts +1 -1
  35. package/dist/runtime/host/index.js +3 -2
  36. package/dist/runtime/host/instance-activation-authority.d.ts +0 -1
  37. package/dist/runtime/host/instance-activation-authority.js +46 -97
  38. package/dist/runtime/host/spot-address-transport.js +1 -1
  39. package/dist/runtime/host/stateful-authority-route-runtime.d.ts +1 -0
  40. package/dist/runtime/host/stateful-authority-route-runtime.js +17 -9
  41. package/dist/runtime/locations/domain-store-contract.d.ts +3 -1
  42. package/dist/runtime/locations/in-memory-authority-store.d.ts +3 -0
  43. package/dist/runtime/locations/in-memory-authority-store.js +47 -12
  44. package/dist/runtime/locations/in-memory-location-store.d.ts +2 -1
  45. package/dist/runtime/locations/in-memory-location-store.js +15 -10
  46. package/dist/runtime/locations/internal-location-contracts.d.ts +5 -0
  47. package/dist/runtime/locations/internal-store-contracts.d.ts +1 -1
  48. package/dist/runtime/locations/location-store-repository.d.ts +2 -0
  49. package/dist/runtime/locations/location-store-repository.js +130 -54
  50. package/dist/runtime/spots/spot-entry-activation.d.ts +1 -0
  51. package/dist/runtime/spots/spot-entry-activation.js +3 -0
  52. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +1 -0
  53. package/dist/runtime/spots/spot-node-runtime-manager.js +6 -0
  54. package/dist/runtime/spots/spot-serial-executor.d.ts +1 -0
  55. package/dist/runtime/spots/spot-serial-executor.js +14 -2
  56. package/dist/runtime/spots/spot-serial-turn-executor.d.ts +3 -0
  57. package/dist/runtime/spots/spot-serial-turn-executor.js +5 -0
  58. package/dist/runtime/streams/index.d.ts +0 -1
  59. package/dist/runtime/streams/index.js +4 -3
  60. package/dist/runtime/streams/protocol.js +0 -9
  61. package/dist/runtime/streams/session-context.d.ts +0 -4
  62. package/dist/runtime/streams/session-context.js +0 -26
  63. package/dist/runtime/streams/session-serial-executor.d.ts +0 -1
  64. package/dist/runtime/streams/session-serial-executor.js +0 -3
  65. package/dist/runtime/streams/stream-message-utils.d.ts +0 -1
  66. package/dist/runtime/streams/stream-message-utils.js +0 -20
  67. package/dist/runtime/streams/stream-session-runtime.d.ts +6 -3
  68. package/dist/runtime/streams/stream-session-runtime.js +40 -85
  69. package/package.json +3 -3
  70. package/src/contracts/Configuration/Builders.ts +2 -0
  71. package/src/contracts/Configuration/InternalDefaults.ts +4 -0
  72. package/src/contracts/Configuration/RegistrationBuilders.ts +13 -0
  73. package/src/contracts/Configuration/RegistrationNormalizers.ts +25 -0
  74. package/src/contracts/Configuration/RegistrationTypes.ts +3 -0
  75. package/src/runtime/actors/actor-mailbox.ts +7 -2
  76. package/src/runtime/actors/actor-runtime-contracts.ts +4 -0
  77. package/src/runtime/actors/index.ts +77 -42
  78. package/src/runtime/backend/node/node-raw-mesh-backend.ts +1 -7
  79. package/src/runtime/execution/serial-execution-queue.ts +117 -29
  80. package/src/runtime/foundation/operation-registry.ts +16 -9
  81. package/src/runtime/foundation/service-authority-payload-codec.ts +0 -46
  82. package/src/runtime/foundation/service-stateful-registry.ts +10 -5
  83. package/src/runtime/foundation/service-stateful-runtime.ts +152 -224
  84. package/src/runtime/foundation/service-stateful-wire-codec.ts +0 -25
  85. package/src/runtime/foundation/service-topology-registry.ts +30 -20
  86. package/src/runtime/handlers/handler-instance-scope.ts +2 -1
  87. package/src/runtime/host/actor-placement-coordinator.ts +8 -2
  88. package/src/runtime/host/actor-runtime-options-factory.ts +6 -0
  89. package/src/runtime/host/index.ts +6 -2
  90. package/src/runtime/host/instance-activation-authority.ts +58 -125
  91. package/src/runtime/host/spot-address-transport.ts +1 -1
  92. package/src/runtime/host/stateful-authority-route-runtime.ts +22 -9
  93. package/src/runtime/locations/domain-store-contract.ts +6 -1
  94. package/src/runtime/locations/in-memory-authority-store.ts +83 -12
  95. package/src/runtime/locations/in-memory-location-store.ts +22 -10
  96. package/src/runtime/locations/internal-location-contracts.ts +6 -0
  97. package/src/runtime/locations/internal-store-contracts.ts +1 -0
  98. package/src/runtime/locations/location-store-repository.ts +182 -69
  99. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
  100. package/src/runtime/spots/spot-entry-activation.ts +4 -0
  101. package/src/runtime/spots/spot-node-runtime-manager.ts +7 -0
  102. package/src/runtime/spots/spot-serial-executor.ts +14 -5
  103. package/src/runtime/spots/spot-serial-turn-executor.ts +10 -0
  104. package/src/runtime/streams/index.ts +3 -1
  105. package/src/runtime/streams/protocol.ts +0 -13
  106. package/src/runtime/streams/session-context.ts +0 -32
  107. package/src/runtime/streams/session-serial-executor.ts +0 -8
  108. package/src/runtime/streams/stream-message-utils.ts +1 -27
  109. package/src/runtime/streams/stream-session-runtime.ts +54 -97
  110. package/dist/runtime/streams/session-requests.d.ts +0 -25
  111. package/dist/runtime/streams/session-requests.js +0 -101
  112. package/src/runtime/streams/session-requests.ts +0 -110
@@ -1,5 +1,4 @@
1
1
  import type { Message } from '../../contracts/Common/Message';
2
- export declare function copyMessage(message: Message): Message;
3
2
  export declare function simpleMessage(bytes: Uint8Array): unknown;
4
3
  /** Create the structural Message used by the internal receive assembler. */
5
4
  export declare function ownedMessage(bytes: Uint8Array): Message;
@@ -1,30 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.copyMessage = copyMessage;
4
3
  exports.simpleMessage = simpleMessage;
5
4
  exports.ownedMessage = ownedMessage;
6
5
  const framework_json_v1_1 = require("../messaging/framework-json-v1");
7
6
  const protocol_1 = require("./protocol");
8
7
  const EMPTY_MESSAGE_BYTES = new Uint8Array(0);
9
- function copyMessage(message) {
10
- const value = message;
11
- if (value.copy !== undefined) {
12
- return value.copy();
13
- }
14
- if (value.toBytes !== undefined) {
15
- return simpleMessage(value.toBytes());
16
- }
17
- if (value.data !== undefined) {
18
- return simpleMessage(value.data());
19
- }
20
- if (value.bytes !== undefined) {
21
- return simpleMessage(value.bytes);
22
- }
23
- if (value.getString !== undefined) {
24
- return simpleMessage((0, protocol_1.utf8Encode)(value.getString()));
25
- }
26
- throw new Error('Stream response payload cannot be copied.');
27
- }
28
8
  function simpleMessage(bytes) {
29
9
  return createSimpleMessage(new Uint8Array(bytes), false);
30
10
  }
@@ -30,6 +30,9 @@ export interface ZLinkStreamSessionContextFactory {
30
30
  createSessionContext(stream: ZLinkManagedStream, close?: (signal?: AbortSignal) => Promise<void>, providerResolver?: ZLinkProviderResolver): DefaultZLinkSessionContext;
31
31
  }
32
32
  export interface ZLinkStreamSessionRuntimeOptions {
33
+ readonly heartbeatIntervalMs?: number;
34
+ readonly heartbeatTimeoutMs?: number;
35
+ readonly idleTimeoutMs?: number;
33
36
  readonly socket: ZLinkBackendStreamSocket;
34
37
  readonly nativeSessionService?: StreamSessionService;
35
38
  readonly meshCompletions?: ZLinkMeshCompletionTable;
@@ -66,8 +69,10 @@ export declare class ZLinkStreamSessionRuntime {
66
69
  private closeReason;
67
70
  private metricsClosed;
68
71
  private readonly livenessClock;
72
+ private readonly liveness;
69
73
  private lastApplicationActivityAt;
70
74
  private lastInboundFrameAt;
75
+ private lastHeartbeatPingAt;
71
76
  private livenessTimer;
72
77
  constructor(options: ZLinkStreamSessionRuntimeOptions & ZLinkStreamLivenessOptions, routingId: unknown, removeSession?: (sessionId: string, session: ZLinkStreamSessionRuntime) => void);
73
78
  get session(): Promise<ZLinkSession>;
@@ -79,8 +84,7 @@ export declare class ZLinkStreamSessionRuntime {
79
84
  enqueuePacket(payload: Message, decodedHeader: ZLinkStreamFrameHeader, terminalOwner?: ZLinkRetainedStreamOwner, applicationJobPermit?: ApplicationJobPermitPort): void;
80
85
  enqueueDisconnected(error?: unknown): void;
81
86
  /**
82
- * Starts the callback on the session lane and observes its terminal result
83
- * without retaining that lane while application code awaits unrelated work.
87
+ * Retains the session turn through the callback's terminal result.
84
88
  */
85
89
  enqueueActorBindingReplaced(actor: ServiceActorRef, retiredSession: ServiceRetiredBoundSessionRouteFence): void;
86
90
  close(signal?: AbortSignal): Promise<void>;
@@ -105,7 +109,6 @@ export declare class ZLinkStreamSessionRuntime {
105
109
  private queueDisconnect;
106
110
  private cleanup;
107
111
  private enqueueControl;
108
- private enqueueInfrastructure;
109
112
  private enqueueApplication;
110
113
  /**
111
114
  * Spec 26 §2.1/§3.1: a frame discarded because the session serial lane is
@@ -19,19 +19,18 @@ const protocol_1 = require("./protocol");
19
19
  const session_context_1 = require("./session-context");
20
20
  const session_serial_executor_1 = require("./session-serial-executor");
21
21
  const stream_message_utils_1 = require("./stream-message-utils");
22
+ const RegistrationNormalizers_1 = require("../../contracts/Configuration/RegistrationNormalizers");
22
23
  const ZLINK_SEND_DONT_WAIT = 1;
23
24
  const ZLINK_RECV_DONT_WAIT = 1;
24
- const ZLINK_STREAM_HEARTBEAT_INTERVAL_MS = 1_000;
25
- const ZLINK_STREAM_HEARTBEAT_TIMEOUT_MS = 5_000;
26
- const ZLINK_STREAM_APPLICATION_IDLE_TIMEOUT_MS = 30_000;
27
25
  const ZLINK_STREAM_RECEIVE_FRAME_BATCH_LIMIT = 64;
28
26
  const ZLINK_STREAM_ACTOR_BINDING_REPLACEMENT_CLOSE_DELAY_MS = 100;
29
27
  const ZLINK_STREAM_MONITOR_IDLE_MIN_DELAY_MS = 1;
30
28
  const ZLINK_STREAM_MONITOR_IDLE_MAX_DELAY_MS = 20;
31
29
  const ZLINK_STREAM_MONITOR_IDLE_MAX_STEPS = 7;
30
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
32
31
  const systemLivenessClock = {
33
32
  now: () => performance.now(),
34
- setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
33
+ setTimer: (callback, delayMs) => setTimeout(callback, Math.min(delayMs, MAX_TIMER_DELAY_MS)),
35
34
  clearTimer: (timer) => clearTimeout(timer)
36
35
  };
37
36
  class ZLinkStreamReceiveWake {
@@ -69,12 +68,15 @@ class ZLinkStreamSessionRuntime {
69
68
  closeReason = 'client_close';
70
69
  metricsClosed = false;
71
70
  livenessClock;
71
+ liveness;
72
72
  lastApplicationActivityAt = 0;
73
73
  lastInboundFrameAt = 0;
74
+ lastHeartbeatPingAt = 0;
74
75
  livenessTimer;
75
76
  constructor(options, routingId, removeSession = () => { }) {
76
77
  this.options = options;
77
78
  this.removeSession = removeSession;
79
+ this.liveness = (0, RegistrationNormalizers_1.normalizeStreamLiveness)(options);
78
80
  this.livenessClock = options.livenessClock ?? systemLivenessClock;
79
81
  this.stream = new managed_stream_1.ZLinkManagedStream(options.socket, routingId, options.messageSerializers, options.nativeSessionService, options.meshCompletions, undefined, options.nativeSessionRouteForMesh);
80
82
  this.context = options.bindingRuntime.createSessionContext(this.stream, (signal) => this.close(signal), options.providerResolver);
@@ -112,7 +114,7 @@ class ZLinkStreamSessionRuntime {
112
114
  return this.sessionReady;
113
115
  }
114
116
  enqueueConnected(localAddr, remoteAddr) {
115
- this.serial.executeInfrastructure(async () => this.markConnected(localAddr, remoteAddr));
117
+ this.serial.executeControl(async () => this.markConnected(localAddr, remoteAddr));
116
118
  }
117
119
  enqueuePacket(payload, decodedHeader, terminalOwner, applicationJobPermit) {
118
120
  this.lastInboundFrameAt = Math.max(this.lastInboundFrameAt, this.livenessClock.now());
@@ -124,41 +126,20 @@ class ZLinkStreamSessionRuntime {
124
126
  terminalOwner?.close();
125
127
  };
126
128
  if (decodedHeader.kind === protocol_1.ZLinkStreamMessageKind.Control) {
127
- if ((0, protocol_1.messageToBytes)(payload).length === 0 &&
128
- (decodedHeader.name === protocol_1.ZLINK_STREAM_HEARTBEAT_PONG ||
129
- decodedHeader.name === protocol_1.ZLINK_STREAM_HEARTBEAT_PING)) {
130
- void this.handleControl(decodedHeader, payload)
131
- .catch((error) => {
132
- this.options.onError?.(error);
133
- })
134
- .finally(() => {
135
- applicationJobPermit?.releaseAfterInternalProcessing();
136
- payload.close();
137
- releaseTerminal();
138
- });
139
- return;
140
- }
141
- this.enqueueControl(async () => {
142
- try {
143
- const dispatch = () => this.dispatchPacket(payload, decodedHeader);
144
- if (applicationJobPermit === undefined)
145
- await dispatch();
146
- else
147
- await (0, application_job_queue_scope_1.runWithApplicationJobPermit)(applicationJobPermit, dispatch);
148
- }
149
- finally {
150
- releaseTerminal();
151
- }
152
- }, () => {
129
+ void this.handleControl(decodedHeader, payload)
130
+ .catch((error) => {
131
+ this.options.onError?.(error);
132
+ })
133
+ .finally(() => {
153
134
  applicationJobPermit?.releaseAfterInternalProcessing();
154
135
  payload.close();
155
136
  releaseTerminal();
156
- }, () => this.traceShutdownDrop(decodedHeader));
137
+ });
157
138
  return;
158
139
  }
159
140
  if (decodedHeader.kind === protocol_1.ZLinkStreamMessageKind.Response ||
160
141
  decodedHeader.kind === protocol_1.ZLinkStreamMessageKind.Error) {
161
- this.enqueueInfrastructure(async () => {
142
+ this.enqueueControl(async () => {
162
143
  try {
163
144
  await this.dispatchPacket(payload, decodedHeader);
164
145
  }
@@ -198,8 +179,7 @@ class ZLinkStreamSessionRuntime {
198
179
  this.queueDisconnect(error);
199
180
  }
200
181
  /**
201
- * Starts the callback on the session lane and observes its terminal result
202
- * without retaining that lane while application code awaits unrelated work.
182
+ * Retains the session turn through the callback's terminal result.
203
183
  */
204
184
  enqueueActorBindingReplaced(actor, retiredSession) {
205
185
  this.serial.executeControl(async () => {
@@ -209,7 +189,6 @@ class ZLinkStreamSessionRuntime {
209
189
  return;
210
190
  const session = await this.requireSession();
211
191
  let callbackDeadlineTimer;
212
- let callbackTerminal = false;
213
192
  let forcedClose = false;
214
193
  const scheduleClose = () => {
215
194
  if (forcedClose)
@@ -223,25 +202,6 @@ class ZLinkStreamSessionRuntime {
223
202
  void this.close().catch((error) => this.options.onError?.(error));
224
203
  }, ZLINK_STREAM_ACTOR_BINDING_REPLACEMENT_CLOSE_DELAY_MS);
225
204
  };
226
- const completeCallback = () => {
227
- if (callbackTerminal)
228
- return;
229
- callbackTerminal = true;
230
- if (callbackDeadlineTimer !== undefined) {
231
- this.livenessClock.clearTimer(callbackDeadlineTimer);
232
- callbackDeadlineTimer = undefined;
233
- }
234
- scheduleClose();
235
- };
236
- const reportCallbackFailure = (error) => {
237
- try {
238
- this.options.onError?.(error);
239
- }
240
- catch {
241
- // Diagnostics must not prevent the lifecycle terminal transition.
242
- }
243
- completeCallback();
244
- };
245
205
  callbackDeadlineTimer = this.livenessClock.setTimer(() => {
246
206
  if (this.disposed ||
247
207
  this.disconnected ||
@@ -251,19 +211,17 @@ class ZLinkStreamSessionRuntime {
251
211
  forcedClose = true;
252
212
  void this.close().catch((error) => this.options.onError?.(error));
253
213
  }, this.options.replacementCallbackTimeoutMs ?? Registration_1.DEFAULT_SESSION_REPLACEMENT_CALLBACK_TIMEOUT_MS);
254
- const callback = session.onActorBindingReplaced;
255
- if (callback === undefined) {
256
- completeCallback();
257
- return;
258
- }
259
214
  try {
260
- // Start the callback on this lifecycle turn, but do not retain the
261
- // serial lane while application code awaits an unrelated operation.
262
- const result = callback.call(session, this.context, actor.actorId);
263
- void Promise.resolve(result).then(() => completeCallback(), reportCallbackFailure);
215
+ await session.onActorBindingReplaced?.(this.context, actor.actorId);
264
216
  }
265
217
  catch (error) {
266
- reportCallbackFailure(error);
218
+ this.options.onError?.(error);
219
+ }
220
+ finally {
221
+ if (callbackDeadlineTimer !== undefined) {
222
+ this.livenessClock.clearTimer(callbackDeadlineTimer);
223
+ }
224
+ scheduleClose();
267
225
  }
268
226
  });
269
227
  }
@@ -313,6 +271,7 @@ class ZLinkStreamSessionRuntime {
313
271
  this.connected = true;
314
272
  const connectedAt = this.livenessClock.now();
315
273
  this.lastApplicationActivityAt = connectedAt;
274
+ this.lastHeartbeatPingAt = connectedAt;
316
275
  this.lastInboundFrameAt = Math.max(this.lastInboundFrameAt, connectedAt);
317
276
  this.scheduleLivenessCheck();
318
277
  this.options.metrics?.change(runtime_metrics_1.METRIC_NAMES.StreamConnectionsActive, 1, { transport: 'tcp' });
@@ -324,15 +283,8 @@ class ZLinkStreamSessionRuntime {
324
283
  let dispatchPayload = payload;
325
284
  let enteredDispatch = false;
326
285
  try {
327
- if (decodedHeader.kind === protocol_1.ZLinkStreamMessageKind.Control) {
328
- await this.handleControl(decodedHeader, payload);
329
- return;
330
- }
331
286
  this.lastApplicationActivityAt = this.livenessClock.now();
332
287
  dispatchPayload = this.context.payloadForHeader(decodedHeader, payload);
333
- if (this.context.tryCompleteResponse(decodedHeader, dispatchPayload)) {
334
- return;
335
- }
336
288
  const dispatchActor = this.context.actorForSlot(decodedHeader.actorSlot);
337
289
  if (decodedHeader.actorSlot !== undefined && dispatchActor === undefined) {
338
290
  const staleFlowEnabled = this.options.dispatchErrors?.flow.flowCreationEnabled() ?? true;
@@ -466,25 +418,33 @@ class ZLinkStreamSessionRuntime {
466
418
  if (this.disposed || this.disconnected || this.livenessTimer !== undefined) {
467
419
  return;
468
420
  }
421
+ const now = this.livenessClock.now();
422
+ const due = Math.min(this.lastHeartbeatPingAt + this.liveness.heartbeatIntervalMs, this.lastInboundFrameAt + this.liveness.heartbeatTimeoutMs, this.liveness.idleTimeoutMs > 0
423
+ ? this.lastApplicationActivityAt + this.liveness.idleTimeoutMs
424
+ : Infinity);
469
425
  this.livenessTimer = this.livenessClock.setTimer(() => {
470
426
  this.livenessTimer = undefined;
471
- this.serial.executeInfrastructure(async () => this.runLivenessCheck(), {}, (error) => this.options.onError?.(error));
472
- }, ZLINK_STREAM_HEARTBEAT_INTERVAL_MS);
427
+ void this.runLivenessCheck().catch((error) => this.options.onError?.(error));
428
+ }, Math.max(0, due - now));
473
429
  }
474
430
  async runLivenessCheck() {
475
431
  if (this.disposed || this.disconnected || !this.connected) {
476
432
  return;
477
433
  }
478
434
  const now = this.livenessClock.now();
479
- if (now - this.lastApplicationActivityAt >= ZLINK_STREAM_APPLICATION_IDLE_TIMEOUT_MS) {
435
+ if (this.liveness.idleTimeoutMs > 0 &&
436
+ now - this.lastApplicationActivityAt >= this.liveness.idleTimeoutMs) {
480
437
  await this.closeForLiveness(protocol_1.ZLinkStreamCloseReasonCode.IdleTimeout, 'idle_timeout', 'Application idle timeout.');
481
438
  return;
482
439
  }
483
- if (now - this.lastInboundFrameAt >= ZLINK_STREAM_HEARTBEAT_TIMEOUT_MS) {
440
+ if (now - this.lastInboundFrameAt >= this.liveness.heartbeatTimeoutMs) {
484
441
  await this.closeForLiveness(protocol_1.ZLinkStreamCloseReasonCode.HeartbeatTimeout, 'heartbeat_timeout', 'Heartbeat timeout.');
485
442
  return;
486
443
  }
487
- await this.stream.writeControl(protocol_1.ZLINK_STREAM_HEARTBEAT_PING);
444
+ if (now - this.lastHeartbeatPingAt >= this.liveness.heartbeatIntervalMs) {
445
+ this.lastHeartbeatPingAt = now;
446
+ await this.stream.writeControl(protocol_1.ZLINK_STREAM_HEARTBEAT_PING);
447
+ }
488
448
  this.scheduleLivenessCheck();
489
449
  }
490
450
  async closeForLiveness(reason, closeReason, diagnostic) {
@@ -496,7 +456,9 @@ class ZLinkStreamSessionRuntime {
496
456
  this.closeReason = closeReason;
497
457
  this.stopLivenessChecks();
498
458
  await this.stream.closeForReason(reason, diagnostic);
499
- await this.complete(undefined, true);
459
+ void this.serial
460
+ .executeFinal(async () => this.complete(undefined, true))
461
+ .catch((error) => this.options.onError?.(error));
500
462
  }
501
463
  stopLivenessChecks() {
502
464
  if (this.livenessTimer !== undefined) {
@@ -609,13 +571,6 @@ class ZLinkStreamSessionRuntime {
609
571
  onRejected?.();
610
572
  }
611
573
  }
612
- enqueueInfrastructure(work, onRejected, onShutdownRejected) {
613
- if (!this.serial.executeInfrastructure(work, {}, onRejected)) {
614
- // The serial lane only refuses new work after dispose() closed it.
615
- onShutdownRejected?.();
616
- onRejected?.();
617
- }
618
- }
619
574
  enqueueApplication(work, onRejected, onShutdownRejected) {
620
575
  let claim;
621
576
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zlink-systems/framework",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@opentelemetry/api": "^1.9.1",
21
21
  "@opentelemetry/api-logs": "^0.221.0",
22
- "@zlink-systems/stream-wire": "0.28.0",
23
- "@zlink-systems/zlink": "1.15.0"
22
+ "@zlink-systems/stream-wire": "0.29.1",
23
+ "@zlink-systems/zlink": "1.17.0"
24
24
  }
25
25
  }
@@ -187,6 +187,8 @@ export interface ZLinkFanoutChannelBuilder {
187
187
  }
188
188
 
189
189
  export interface ZLinkStreamNodeBuilder {
190
+ setHeartbeat(intervalMs: number, timeoutMs: number): this;
191
+ setIdleTimeout(timeoutMs: number): this;
190
192
  bind(endpointOrPort?: string | number): this;
191
193
  setBindHost(bindHost: string): this;
192
194
  setAdvertiseHost(advertiseHost: string): this;
@@ -22,3 +22,7 @@ export const ZLINK_RELOCATION_MAX_BLOB_REFERENCE_BYTES = 4096;
22
22
  export const AUTHORITY_ENVELOPE_MAX_BYTES = 1024 * 1024;
23
23
  export const CREATION_INTENT_MAX_BYTES = 1024 * 1024;
24
24
  export const CREATION_TERMINAL_ENVELOPE_MAX_BYTES = 1024 * 1024;
25
+
26
+ export const DEFAULT_STREAM_HEARTBEAT_INTERVAL_MS = 1_000;
27
+ export const DEFAULT_STREAM_HEARTBEAT_TIMEOUT_MS = 5_000;
28
+ export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 0;
@@ -689,6 +689,16 @@ class DefaultClientServerChannelServerBuilder implements ZLinkClientServerChanne
689
689
  }
690
690
 
691
691
  class DefaultStreamNodeBuilder implements ZLinkStreamNodeBuilder {
692
+ setHeartbeat(intervalMs: number, timeoutMs: number): this {
693
+ this.streamNode.heartbeatIntervalMs = intervalMs;
694
+ this.streamNode.heartbeatTimeoutMs = timeoutMs;
695
+ return this;
696
+ }
697
+ setIdleTimeout(timeoutMs: number): this {
698
+ this.streamNode.idleTimeoutMs = timeoutMs;
699
+ return this;
700
+ }
701
+
692
702
  constructor(private readonly streamNode: MutableStreamNodeOptions) {}
693
703
 
694
704
  bind(endpointOrPort: string | number = 0): this {
@@ -1605,6 +1615,9 @@ interface MutablePublisherCapabilityOptions {
1605
1615
  }
1606
1616
 
1607
1617
  interface MutableStreamNodeOptions {
1618
+ heartbeatIntervalMs?: number;
1619
+ heartbeatTimeoutMs?: number;
1620
+ idleTimeoutMs?: number;
1608
1621
  bind?: string;
1609
1622
  bindHost?: string;
1610
1623
  advertiseHost?: string;
@@ -21,6 +21,9 @@ import { normalizeEndpoint, parseEndpointHostPort } from './EndpointNotation';
21
21
  import {
22
22
  defaultWorkerMaxThreads,
23
23
  DEFAULT_STREAM_NODE_MAX_MESSAGE_SIZE,
24
+ DEFAULT_STREAM_HEARTBEAT_INTERVAL_MS,
25
+ DEFAULT_STREAM_HEARTBEAT_TIMEOUT_MS,
26
+ DEFAULT_STREAM_IDLE_TIMEOUT_MS,
24
27
  DEFAULT_WORKER_IDLE_TIMEOUT_MS,
25
28
  DEFAULT_WORKER_MIN_THREADS
26
29
  } from './InternalDefaults';
@@ -77,6 +80,27 @@ export function toChannelMap(
77
80
  );
78
81
  }
79
82
 
83
+ export function normalizeStreamLiveness(
84
+ options: ZLinkStreamNodeOptions
85
+ ): Required<
86
+ Pick<ZLinkStreamNodeOptions, 'heartbeatIntervalMs' | 'heartbeatTimeoutMs' | 'idleTimeoutMs'>
87
+ > {
88
+ const heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_STREAM_HEARTBEAT_INTERVAL_MS;
89
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_STREAM_HEARTBEAT_TIMEOUT_MS;
90
+ const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
91
+ if (!Number.isFinite(heartbeatIntervalMs) || heartbeatIntervalMs <= 0)
92
+ throw new ZLinkConfigurationException('STREAM heartbeat interval must be positive.');
93
+ if (!Number.isFinite(heartbeatTimeoutMs) || heartbeatTimeoutMs <= 0)
94
+ throw new ZLinkConfigurationException('STREAM heartbeat timeout must be positive.');
95
+ if (heartbeatTimeoutMs <= heartbeatIntervalMs)
96
+ throw new ZLinkConfigurationException(
97
+ 'STREAM heartbeat timeout must be greater than interval.'
98
+ );
99
+ if (!Number.isFinite(idleTimeoutMs) || idleTimeoutMs < 0)
100
+ throw new ZLinkConfigurationException('STREAM idle timeout must not be negative.');
101
+ return { heartbeatIntervalMs, heartbeatTimeoutMs, idleTimeoutMs };
102
+ }
103
+
80
104
  export function toStreamNodeMap(
81
105
  streamNodes: ZLinkFrameworkRegistrationOptions['streamNodes'],
82
106
  network: ZLinkNetworkOptions
@@ -88,6 +112,7 @@ export function toStreamNodeMap(
88
112
  name,
89
113
  {
90
114
  ...normalized,
115
+ ...normalizeStreamLiveness(streamNode),
91
116
  maxMessageSize: streamNode.maxMessageSize ?? DEFAULT_STREAM_NODE_MAX_MESSAGE_SIZE
92
117
  }
93
118
  ];
@@ -233,6 +233,9 @@ export interface ZLinkRouteChannelOptions {
233
233
  }
234
234
 
235
235
  export interface ZLinkStreamNodeOptions {
236
+ readonly heartbeatIntervalMs?: number;
237
+ readonly heartbeatTimeoutMs?: number;
238
+ readonly idleTimeoutMs?: number;
236
239
  readonly bind?: string;
237
240
  readonly bindHost?: string;
238
241
  readonly advertiseHost?: string;
@@ -17,9 +17,14 @@ export class ZLinkActorSerialExecutor {
17
17
  constructor(
18
18
  private readonly actorId: string,
19
19
  private readonly sourceSpotId: unknown,
20
- options?: ZLinkSerialSchedulerOptions
20
+ options?: ZLinkSerialSchedulerOptions,
21
+ sharedOwner?: ZLinkSerialExecutionQueue
21
22
  ) {
22
- this.scheduler = new ZLinkSerialExecutionQueue((record) => this.runRecord(record), options);
23
+ this.scheduler = new ZLinkSerialExecutionQueue(
24
+ (record) => this.runRecord(record),
25
+ options,
26
+ sharedOwner
27
+ );
23
28
  }
24
29
 
25
30
  execute<T>(operation: () => Promise<T> | T, workOptions?: ZLinkSerialWorkOptions): Promise<T> {
@@ -49,6 +49,10 @@ export interface ZLinkActorManagerOptions {
49
49
  signal?: AbortSignal
50
50
  ) => Promise<ZLinkActorCreateResponse | undefined>;
51
51
  readonly actorDestroyedCleanup?: (actorId: string) => void;
52
+ readonly terminateActorActivation?: (
53
+ actor: ZLinkActor,
54
+ terminal: () => Promise<void>
55
+ ) => Promise<void>;
52
56
  readonly publishActorAuthority?: (
53
57
  actorType: string,
54
58
  actorRef: ActorRef,
@@ -29,6 +29,7 @@ import {
29
29
  wireReplyFailureException
30
30
  } from '../framework-errors-internal';
31
31
  import { disposeLifecycleHandlers } from '../handlers/handler-instance-scope';
32
+ import { currentZLinkActorExecution } from './actor-execution-context';
32
33
  import { encodeFrameworkPayloadMessage } from '../messaging/payload-codec';
33
34
  import { ZLinkActorCreationCoordinator, type ZLinkActorCreateRequest } from './actor-creation';
34
35
  import type { ZLinkActorManagerOptions } from './actor-runtime-contracts';
@@ -217,34 +218,43 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
217
218
  const completions = this.options.nativeActorCompletionTableProvider?.();
218
219
  const entryNodeRid = state.entryNodeRid ?? current.nodeRid;
219
220
  if (node === undefined || completions === undefined) return false;
220
- const destroyTask = state.getOrStartDestroy(entryNodeRid, async (nativeRef) => {
221
- if (nativeRef !== undefined) {
222
- const completion = await completions.submit(() => node.destroyActor(nativeRef, 0), signal);
223
- try {
224
- if (completion.terminalResult !== 0 || completion.failureErrno !== 0) {
225
- // Classify the (terminal, fine) pair via the shared translator
226
- // (spec 32-framework-error-model:83-118, 99-108) instead of
227
- // collapsing every destroy failure to NotFound; genuine
228
- // route-not-found fine codes still yield NotFound through the
229
- // shared table.
230
- throw wireReplyFailureException(
231
- completion.terminalResult,
232
- completion.failureErrno,
233
- `Actor '${actor.actorId}' destroy failed with result '${completion.terminalResult}' and errno '${completion.failureErrno}'.`
234
- );
221
+ const destroyTask = state.getOrStartDestroy(entryNodeRid, (nativeRef) =>
222
+ this.terminateActor(state, async () => {
223
+ if (nativeRef !== undefined) {
224
+ const completion = await completions.submit(
225
+ () => node.destroyActor(nativeRef, 0),
226
+ signal
227
+ );
228
+ try {
229
+ if (completion.terminalResult !== 0 || completion.failureErrno !== 0) {
230
+ // Classify the (terminal, fine) pair via the shared translator
231
+ // (spec 32-framework-error-model:83-118, 99-108) instead of
232
+ // collapsing every destroy failure to NotFound; genuine
233
+ // route-not-found fine codes still yield NotFound through the
234
+ // shared table.
235
+ throw wireReplyFailureException(
236
+ completion.terminalResult,
237
+ completion.failureErrno,
238
+ `Actor '${actor.actorId}' destroy failed with result '${completion.terminalResult}' and errno '${completion.failureErrno}'.`
239
+ );
240
+ }
241
+ } finally {
242
+ closeMeshCompletion(completion);
235
243
  }
236
- } finally {
237
- closeMeshCompletion(completion);
244
+ state.markNativeActorDestroyed(nativeRef);
238
245
  }
239
- state.markNativeActorDestroyed(nativeRef);
240
- }
241
- if (state.actorType !== undefined && state.ownsLocation) {
242
- await this.options.locationLifecycle?.releaseActor(state.actorType, actor.actorId, current);
243
- }
244
- await this.finalizeDestroyedActor(actor.actorId, state);
245
- });
246
+ if (state.actorType !== undefined && state.ownsLocation) {
247
+ await this.options.locationLifecycle?.releaseActor(
248
+ state.actorType,
249
+ actor.actorId,
250
+ current
251
+ );
252
+ }
253
+ await this.finalizeDestroyedActor(actor.actorId, state);
254
+ })
255
+ );
246
256
  try {
247
- await destroyTask;
257
+ await this.awaitActorDestroy(state, destroyTask);
248
258
  } catch (error) {
249
259
  state.clearFailedDestroy(destroyTask);
250
260
  throw error;
@@ -548,6 +558,29 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
548
558
  this.options.metrics?.change(METRIC_NAMES.ActorCount, -1);
549
559
  }
550
560
  }
561
+
562
+ private terminateActor(
563
+ state: ZLinkActorRuntimeState,
564
+ terminal: () => Promise<void>
565
+ ): Promise<void> {
566
+ const actor = state.actor;
567
+ const completion =
568
+ actor === undefined || this.options.terminateActorActivation === undefined
569
+ ? terminal()
570
+ : this.options.terminateActorActivation(actor, terminal);
571
+ return completion;
572
+ }
573
+
574
+ private awaitActorDestroy(
575
+ state: ZLinkActorRuntimeState,
576
+ completion: Promise<void>
577
+ ): Promise<void> {
578
+ if (currentZLinkActorExecution()?.actorId === state.actorId) {
579
+ this.detachedTaskRunner.runDetached('Actor terminal cleanup', () => completion);
580
+ return Promise.resolve();
581
+ }
582
+ return completion;
583
+ }
551
584
  async getOrCreateActor(
552
585
  actorId: string,
553
586
  actorType: string,
@@ -648,25 +681,27 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
648
681
  `Actor '${actor.context.actorId}' must leave its current SPOT before destroy.`
649
682
  );
650
683
  }
651
- const destroyTask = state.getOrStartDestroy(entryNodeRid, async (actorRef) => {
652
- const destroyedActorRef =
653
- actorRef === undefined ? undefined : toFrameworkActorRef(actorRef, state.meshName ?? '');
654
- if (actorRef !== undefined) {
655
- await node.destroyActor(actorRef, 0, destroySignal);
656
- state.markNativeActorDestroyed(actorRef);
657
- }
658
- if (state.actorType !== undefined && state.ownsLocation) {
659
- await this.options.locationLifecycle?.releaseActor(
660
- state.actorType,
661
- actor.context.actorId,
662
- destroyedActorRef
663
- );
664
- }
665
- await this.finalizeDestroyedActor(actor.context.actorId, state);
666
- });
684
+ const destroyTask = state.getOrStartDestroy(entryNodeRid, (actorRef) =>
685
+ this.terminateActor(state, async () => {
686
+ const destroyedActorRef =
687
+ actorRef === undefined ? undefined : toFrameworkActorRef(actorRef, state.meshName ?? '');
688
+ if (actorRef !== undefined) {
689
+ await node.destroyActor(actorRef, 0, destroySignal);
690
+ state.markNativeActorDestroyed(actorRef);
691
+ }
692
+ if (state.actorType !== undefined && state.ownsLocation) {
693
+ await this.options.locationLifecycle?.releaseActor(
694
+ state.actorType,
695
+ actor.context.actorId,
696
+ destroyedActorRef
697
+ );
698
+ }
699
+ await this.finalizeDestroyedActor(actor.context.actorId, state);
700
+ })
701
+ );
667
702
 
668
703
  try {
669
- await destroyTask;
704
+ await this.awaitActorDestroy(state, destroyTask);
670
705
  } catch (error) {
671
706
  state.clearFailedDestroy(destroyTask);
672
707
  throw error;
@@ -297,19 +297,13 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
297
297
  selectObjectPlacement(stableType: string): ZLinkBackendObjectPlacement {
298
298
  const runtime = this.requireRuntime();
299
299
  const localNodeRid = runtime.topology.localDescriptor().nodeRoutingId;
300
- const status = runtime.topology.objectPlacementStatus(
300
+ const { status, descriptor } = runtime.topology.selectObjectPlacementWithStatus(
301
301
  stableType,
302
302
  (candidate) =>
303
303
  candidate.nodeRoutingId === localNodeRid ||
304
304
  runtime.isPeerRouteReady(candidate.nodeRoutingId)
305
305
  );
306
306
  if (status !== 'available') return { kind: status };
307
- const descriptor = runtime.topology.selectObjectPlacement(
308
- stableType,
309
- (candidate) =>
310
- candidate.nodeRoutingId === localNodeRid ||
311
- runtime.isPeerRouteReady(candidate.nodeRoutingId)
312
- );
313
307
  return descriptor === undefined
314
308
  ? { kind: 'unavailable' }
315
309
  : {