@zlink-systems/framework 0.28.0 → 0.29.0

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 (87) 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/backend/node/node-raw-mesh-backend.js +1 -3
  12. package/dist/runtime/execution/serial-execution-queue.d.ts +12 -3
  13. package/dist/runtime/execution/serial-execution-queue.js +116 -28
  14. package/dist/runtime/foundation/operation-registry.d.ts +3 -2
  15. package/dist/runtime/foundation/operation-registry.js +5 -1
  16. package/dist/runtime/foundation/service-authority-payload-codec.d.ts +0 -2
  17. package/dist/runtime/foundation/service-authority-payload-codec.js +0 -43
  18. package/dist/runtime/foundation/service-stateful-registry.d.ts +4 -3
  19. package/dist/runtime/foundation/service-stateful-registry.js +5 -2
  20. package/dist/runtime/foundation/service-stateful-runtime.d.ts +1 -8
  21. package/dist/runtime/foundation/service-stateful-runtime.js +78 -126
  22. package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +0 -11
  23. package/dist/runtime/foundation/service-stateful-wire-codec.js +1 -23
  24. package/dist/runtime/foundation/service-topology-registry.d.ts +5 -0
  25. package/dist/runtime/foundation/service-topology-registry.js +16 -17
  26. package/dist/runtime/host/instance-activation-authority.d.ts +0 -1
  27. package/dist/runtime/host/instance-activation-authority.js +46 -97
  28. package/dist/runtime/host/spot-address-transport.js +1 -1
  29. package/dist/runtime/host/stateful-authority-route-runtime.d.ts +1 -0
  30. package/dist/runtime/host/stateful-authority-route-runtime.js +17 -9
  31. package/dist/runtime/locations/domain-store-contract.d.ts +1 -0
  32. package/dist/runtime/locations/in-memory-authority-store.d.ts +2 -0
  33. package/dist/runtime/locations/in-memory-authority-store.js +22 -8
  34. package/dist/runtime/locations/in-memory-location-store.d.ts +1 -0
  35. package/dist/runtime/locations/in-memory-location-store.js +3 -0
  36. package/dist/runtime/locations/internal-store-contracts.d.ts +1 -1
  37. package/dist/runtime/locations/location-store-repository.d.ts +1 -0
  38. package/dist/runtime/locations/location-store-repository.js +92 -49
  39. package/dist/runtime/spots/spot-serial-executor.js +4 -1
  40. package/dist/runtime/spots/spot-serial-turn-executor.d.ts +3 -0
  41. package/dist/runtime/spots/spot-serial-turn-executor.js +5 -0
  42. package/dist/runtime/streams/index.d.ts +0 -1
  43. package/dist/runtime/streams/index.js +4 -3
  44. package/dist/runtime/streams/protocol.js +0 -9
  45. package/dist/runtime/streams/session-context.d.ts +0 -4
  46. package/dist/runtime/streams/session-context.js +0 -26
  47. package/dist/runtime/streams/session-serial-executor.d.ts +0 -1
  48. package/dist/runtime/streams/session-serial-executor.js +0 -3
  49. package/dist/runtime/streams/stream-message-utils.d.ts +0 -1
  50. package/dist/runtime/streams/stream-message-utils.js +0 -20
  51. package/dist/runtime/streams/stream-session-runtime.d.ts +6 -3
  52. package/dist/runtime/streams/stream-session-runtime.js +40 -85
  53. package/package.json +3 -3
  54. package/src/contracts/Configuration/Builders.ts +2 -0
  55. package/src/contracts/Configuration/InternalDefaults.ts +4 -0
  56. package/src/contracts/Configuration/RegistrationBuilders.ts +13 -0
  57. package/src/contracts/Configuration/RegistrationNormalizers.ts +25 -0
  58. package/src/contracts/Configuration/RegistrationTypes.ts +3 -0
  59. package/src/runtime/actors/actor-mailbox.ts +7 -2
  60. package/src/runtime/backend/node/node-raw-mesh-backend.ts +1 -7
  61. package/src/runtime/execution/serial-execution-queue.ts +117 -29
  62. package/src/runtime/foundation/operation-registry.ts +16 -9
  63. package/src/runtime/foundation/service-authority-payload-codec.ts +0 -46
  64. package/src/runtime/foundation/service-stateful-registry.ts +10 -5
  65. package/src/runtime/foundation/service-stateful-runtime.ts +103 -181
  66. package/src/runtime/foundation/service-stateful-wire-codec.ts +0 -25
  67. package/src/runtime/foundation/service-topology-registry.ts +30 -20
  68. package/src/runtime/host/instance-activation-authority.ts +58 -125
  69. package/src/runtime/host/spot-address-transport.ts +1 -1
  70. package/src/runtime/host/stateful-authority-route-runtime.ts +22 -9
  71. package/src/runtime/locations/domain-store-contract.ts +5 -0
  72. package/src/runtime/locations/in-memory-authority-store.ts +42 -8
  73. package/src/runtime/locations/in-memory-location-store.ts +8 -0
  74. package/src/runtime/locations/internal-store-contracts.ts +1 -0
  75. package/src/runtime/locations/location-store-repository.ts +123 -64
  76. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
  77. package/src/runtime/spots/spot-serial-executor.ts +4 -1
  78. package/src/runtime/spots/spot-serial-turn-executor.ts +10 -0
  79. package/src/runtime/streams/index.ts +3 -1
  80. package/src/runtime/streams/protocol.ts +0 -13
  81. package/src/runtime/streams/session-context.ts +0 -32
  82. package/src/runtime/streams/session-serial-executor.ts +0 -8
  83. package/src/runtime/streams/stream-message-utils.ts +1 -27
  84. package/src/runtime/streams/stream-session-runtime.ts +54 -97
  85. package/dist/runtime/streams/session-requests.d.ts +0 -25
  86. package/dist/runtime/streams/session-requests.js +0 -101
  87. package/src/runtime/streams/session-requests.ts +0 -110
@@ -118,15 +118,6 @@ function decodeStreamHeader(header, flowEnabled = true) {
118
118
  const decoded = (0, stream_wire_1.decodeStreamWireHeader)(header, undefined, flowEnabled);
119
119
  const kind = decoded.kind;
120
120
  const flags = decoded.flags;
121
- const hasRequestSeq = (flags & stream_wire_1.ZlinkStreamHeaderFlags.HasRequestSeq) !== 0;
122
- const hasMetadata = (flags & stream_wire_1.ZlinkStreamHeaderFlags.HasMetadata) !== 0;
123
- const hasCorrelation = (flags & stream_wire_1.ZlinkStreamHeaderFlags.HasCorrelationId) !== 0;
124
- const hasFlow = (flags & stream_wire_1.ZlinkStreamHeaderFlags.HasFlowId) !== 0;
125
- const hasActorSlot = (flags & stream_wire_1.ZlinkStreamHeaderFlags.HasActorSlot) !== 0;
126
- if (kind === stream_wire_1.ZlinkStreamMessageKind.Control &&
127
- (hasCorrelation || hasRequestSeq || hasMetadata || hasFlow || hasActorSlot)) {
128
- throw new Error('Control packet must not contain a request sequence, metadata, correlation id, or flow id.');
129
- }
130
121
  const metadata = publicStreamMetadata(decoded.metadata);
131
122
  return {
132
123
  kind,
@@ -6,7 +6,6 @@ import type { ServiceRetiredBoundSessionRouteFence } from '../foundation/service
6
6
  import { type ZLinkSubmitResult } from '../messaging/submission-result';
7
7
  import { ZLinkStreamMessageKind, type ZLinkStreamFrameHeader, type ZLinkStreamReplyMessageKind } from './protocol';
8
8
  import { DefaultZLinkBoundSessionSendCall } from './session-calls';
9
- import { type ZLinkPendingSessionRequest } from './session-requests';
10
9
  export interface ZLinkSessionContextStream extends ZLinkStream {
11
10
  writeRaw(payload: Message, flags?: number): boolean;
12
11
  submitRaw(payload: Message): Promise<ZLinkSubmitResult>;
@@ -42,7 +41,6 @@ export declare class DefaultZLinkSessionContext implements ZLinkSessionContext {
42
41
  private readonly handlerRegistry;
43
42
  readonly handlers: ZLinkSessionHandlerRegistry;
44
43
  private readonly localActors;
45
- private readonly requests;
46
44
  private currentDispatchHeader;
47
45
  private currentReplyClaimed;
48
46
  private actorBindingReplacementHandler?;
@@ -73,8 +71,6 @@ export declare class DefaultZLinkSessionContext implements ZLinkSessionContext {
73
71
  /** True while the current request dispatch has already submitted its reply. */
74
72
  get replyClaimed(): boolean;
75
73
  claimReply(requestHeader: ZLinkStreamFrameHeader): void;
76
- startRequest(timeoutMs?: number): ZLinkPendingSessionRequest;
77
- tryCompleteResponse(header: ZLinkStreamFrameHeader, payload: Message): boolean;
78
74
  payloadForHeader(header: ZLinkStreamFrameHeader, payload: Message): Message;
79
75
  get boundActors(): readonly DefaultZLinkSessionActor[];
80
76
  findBoundActor(actorId: string): DefaultZLinkSessionActor | undefined;
@@ -12,7 +12,6 @@ const submission_result_1 = require("../messaging/submission-result");
12
12
  const protocol_1 = require("./protocol");
13
13
  const session_calls_1 = require("./session-calls");
14
14
  const session_local_actors_1 = require("./session-local-actors");
15
- const session_requests_1 = require("./session-requests");
16
15
  class DefaultZLinkSessionContext {
17
16
  runtime;
18
17
  stream;
@@ -22,7 +21,6 @@ class DefaultZLinkSessionContext {
22
21
  handlerRegistry;
23
22
  handlers;
24
23
  localActors = new session_local_actors_1.ZLinkSessionLocalActorBindings();
25
- requests = new session_requests_1.ZLinkSessionRequestTracker();
26
24
  currentDispatchHeader;
27
25
  currentReplyClaimed = false;
28
26
  actorBindingReplacementHandler;
@@ -137,21 +135,6 @@ class DefaultZLinkSessionContext {
137
135
  }
138
136
  this.currentReplyClaimed = true;
139
137
  }
140
- startRequest(timeoutMs) {
141
- return this.requests.start(timeoutMs);
142
- }
143
- tryCompleteResponse(header, payload) {
144
- if (header.requestSeq === undefined) {
145
- return false;
146
- }
147
- if (header.kind === protocol_1.ZLinkStreamMessageKind.Response) {
148
- return this.requests.complete(header.requestSeq, payload);
149
- }
150
- if (header.kind === protocol_1.ZLinkStreamMessageKind.Error) {
151
- return this.requests.fail(header.requestSeq, decodeStreamErrorPayload(payload));
152
- }
153
- return false;
154
- }
155
138
  payloadForHeader(header, payload) {
156
139
  if ((header.flags & protocol_1.ZLinkStreamHeaderFlags.PayloadCompressed) === 0) {
157
140
  return payload;
@@ -374,15 +357,6 @@ class DefaultZLinkBoundSessionFactory {
374
357
  }
375
358
  }
376
359
  exports.DefaultZLinkBoundSessionFactory = DefaultZLinkBoundSessionFactory;
377
- function decodeStreamErrorPayload(payload) {
378
- try {
379
- const value = JSON.parse(Buffer.from((0, protocol_1.messageToBytes)(payload)).toString());
380
- return new Error(typeof value.message === 'string' ? value.message : 'Stream request failed.');
381
- }
382
- catch {
383
- return new Error('Stream request failed.');
384
- }
385
- }
386
360
  function sameRetiredSession(left, right) {
387
361
  return (left.sessionOwnerNodeRid === right.sessionOwnerNodeRid &&
388
362
  left.sessionOwnerNodeGeneration === right.sessionOwnerNodeGeneration &&
@@ -4,7 +4,6 @@ export declare class ZLinkSessionSerialExecutor {
4
4
  private closed;
5
5
  executeApplication(work: () => Promise<void>, options?: Omit<ZLinkSerialWorkOptions, 'lane'>, onRejected?: (error?: unknown) => void): boolean;
6
6
  executeControl(work: () => Promise<void>, options?: Omit<ZLinkSerialWorkOptions, 'lane'>, onRejected?: (error?: unknown) => void): boolean;
7
- executeInfrastructure(work: () => Promise<void>, options?: Omit<ZLinkSerialWorkOptions, 'lane'>, onRejected?: (error?: unknown) => void): boolean;
8
7
  executeFinal(work: () => Promise<void>): Promise<void>;
9
8
  private submit;
10
9
  dispose(): Promise<void>;
@@ -13,9 +13,6 @@ class ZLinkSessionSerialExecutor {
13
13
  executeControl(work, options = {}, onRejected) {
14
14
  return this.submit(work, 'lifecycle', options, onRejected);
15
15
  }
16
- executeInfrastructure(work, options = {}, onRejected) {
17
- return this.submit(work, 'lifecycle', options, onRejected);
18
- }
19
16
  executeFinal(work) {
20
17
  if (this.closed) {
21
18
  return Promise.reject((0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RuntimeShutdown, 'Session execution queue is closed.'));
@@ -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.0",
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.0",
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> {
@@ -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
  : {