@zlink-systems/framework 0.10.0 → 0.12.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 (125) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/contracts/Configuration/EndpointNotation.d.ts +5 -4
  3. package/dist/contracts/Configuration/EndpointNotation.js +5 -4
  4. package/dist/contracts/Configuration/InternalDefaults.d.ts +0 -1
  5. package/dist/contracts/Configuration/InternalDefaults.js +1 -2
  6. package/dist/contracts/Configuration/RegistrationNormalizers.js +1 -2
  7. package/dist/contracts/Configuration/RegistrationTypes.d.ts +2 -5
  8. package/dist/contracts/Configuration/RegistrationValidators.js +0 -1
  9. package/dist/contracts/Errors/ZLinkFrameworkException.d.ts +6 -7
  10. package/dist/contracts/Errors/ZLinkFrameworkException.js +6 -7
  11. package/dist/runtime/actors/actor-client.js +1 -1
  12. package/dist/runtime/actors/actor-mailbox.js +1 -5
  13. package/dist/runtime/backend/contracts/index.d.ts +2 -0
  14. package/dist/runtime/backend/mesh-completion-table.d.ts +1 -3
  15. package/dist/runtime/backend/mesh-completion-table.js +11 -15
  16. package/dist/runtime/backend/mesh-dispatch-pump.d.ts +11 -4
  17. package/dist/runtime/backend/mesh-dispatch-pump.js +201 -134
  18. package/dist/runtime/backend/node/node-backend-adapter-factory.d.ts +3 -4
  19. package/dist/runtime/backend/node/node-backend-adapter-factory.js +51 -37
  20. package/dist/runtime/backend/node/node-backend-adapter-support.d.ts +2 -2
  21. package/dist/runtime/backend/node/node-backend-adapter-support.js +2 -2
  22. package/dist/runtime/backend/node/node-dealer-channel-client-transport.js +2 -2
  23. package/dist/runtime/backend/node/node-event-loop-poller.d.ts +16 -0
  24. package/dist/runtime/backend/node/node-event-loop-poller.js +49 -0
  25. package/dist/runtime/backend/node/node-raw-binding-port.js +24 -41
  26. package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +8 -8
  27. package/dist/runtime/backend/node/node-raw-mesh-backend.js +100 -107
  28. package/dist/runtime/backend/node/node-socket-backend-adapter.d.ts +3 -2
  29. package/dist/runtime/backend/node/node-socket-backend-adapter.js +18 -67
  30. package/dist/runtime/backend/raw-binding-port.d.ts +1 -0
  31. package/dist/runtime/backend/runtime-values.d.ts +6 -1
  32. package/dist/runtime/backend/runtime-values.js +6 -0
  33. package/dist/runtime/channels/channel-dispatchers.js +2 -2
  34. package/dist/runtime/channels/channel-envelope.js +324 -121
  35. package/dist/runtime/channels/channel-multipart.d.ts +8 -1
  36. package/dist/runtime/channels/channel-outbound-operations.js +0 -2
  37. package/dist/runtime/channels/channel-receive-loops.d.ts +1 -3
  38. package/dist/runtime/channels/channel-receive-loops.js +25 -65
  39. package/dist/runtime/channels/channel-socket-registry.js +14 -1
  40. package/dist/runtime/channels/channel-transports.d.ts +2 -1
  41. package/dist/runtime/channels/channel-transports.js +12 -10
  42. package/dist/runtime/channels/client-server-location-runtime.js +15 -3
  43. package/dist/runtime/execution/serial-execution-queue.d.ts +5 -18
  44. package/dist/runtime/execution/serial-execution-queue.js +16 -80
  45. package/dist/runtime/foundation/event-loop-resources.d.ts +0 -3
  46. package/dist/runtime/foundation/event-loop-resources.js +2 -11
  47. package/dist/runtime/foundation/operation-registry.d.ts +2 -8
  48. package/dist/runtime/foundation/operation-registry.js +3 -21
  49. package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +19 -7
  50. package/dist/runtime/foundation/raw-service-mesh-runtime.js +56 -10
  51. package/dist/runtime/foundation/service-mailbox.d.ts +1 -7
  52. package/dist/runtime/foundation/service-mailbox.js +3 -2
  53. package/dist/runtime/foundation/service-runtime-contracts.d.ts +2 -1
  54. package/dist/runtime/foundation/service-stateful-registry.d.ts +1 -1
  55. package/dist/runtime/foundation/service-stateful-registry.js +2 -2
  56. package/dist/runtime/foundation/service-stateful-runtime.js +14 -14
  57. package/dist/runtime/framework-errors-internal.d.ts +4 -4
  58. package/dist/runtime/framework-errors-internal.js +8 -10
  59. package/dist/runtime/host/actor-placement-coordinator.js +1 -1
  60. package/dist/runtime/host/index.js +1 -1
  61. package/dist/runtime/host/service-relocation-host-runtime.js +0 -4
  62. package/dist/runtime/host/spot-address-transport.js +1 -1
  63. package/dist/runtime/host/user-spot-creation-coordinator.js +1 -1
  64. package/dist/runtime/locations/location-store-repository.d.ts +1 -0
  65. package/dist/runtime/locations/location-store-repository.js +64 -75
  66. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +0 -12
  67. package/dist/runtime/spots/spot-node-runtime-manager.js +6 -166
  68. package/dist/runtime/spots/spot-serial-turn-executor.js +1 -4
  69. package/dist/runtime/spots/spot-timer.d.ts +3 -0
  70. package/dist/runtime/spots/spot-timer.js +87 -18
  71. package/dist/runtime/streams/session-serial-executor.js +1 -3
  72. package/dist/runtime/streams/stream-session-runtime.d.ts +2 -2
  73. package/dist/runtime/streams/stream-session-runtime.js +13 -19
  74. package/dist/runtime/workers/index.d.ts +0 -1
  75. package/dist/runtime/workers/index.js +1 -9
  76. package/package.json +9 -3
  77. package/src/contracts/Configuration/EndpointNotation.ts +5 -4
  78. package/src/contracts/Configuration/InternalDefaults.ts +0 -1
  79. package/src/contracts/Configuration/RegistrationNormalizers.ts +2 -4
  80. package/src/contracts/Configuration/RegistrationTypes.ts +2 -5
  81. package/src/contracts/Configuration/RegistrationValidators.ts +0 -1
  82. package/src/contracts/Errors/ZLinkFrameworkException.ts +6 -7
  83. package/src/runtime/actors/actor-client.ts +1 -1
  84. package/src/runtime/actors/actor-mailbox.ts +1 -12
  85. package/src/runtime/backend/contracts/index.ts +2 -0
  86. package/src/runtime/backend/mesh-completion-table.ts +12 -23
  87. package/src/runtime/backend/mesh-dispatch-pump.ts +206 -138
  88. package/src/runtime/backend/node/node-backend-adapter-factory.ts +66 -35
  89. package/src/runtime/backend/node/node-backend-adapter-support.ts +4 -4
  90. package/src/runtime/backend/node/node-dealer-channel-client-transport.ts +2 -2
  91. package/src/runtime/backend/node/node-event-loop-poller.ts +55 -0
  92. package/src/runtime/backend/node/node-raw-binding-port.ts +30 -45
  93. package/src/runtime/backend/node/node-raw-mesh-backend.ts +97 -110
  94. package/src/runtime/backend/node/node-socket-backend-adapter.ts +23 -69
  95. package/src/runtime/backend/raw-binding-port.ts +1 -0
  96. package/src/runtime/backend/runtime-values.ts +13 -1
  97. package/src/runtime/channels/channel-dispatchers.ts +2 -2
  98. package/src/runtime/channels/channel-envelope.ts +379 -109
  99. package/src/runtime/channels/channel-multipart.ts +9 -1
  100. package/src/runtime/channels/channel-outbound-operations.ts +0 -1
  101. package/src/runtime/channels/channel-receive-loops.ts +24 -75
  102. package/src/runtime/channels/channel-socket-registry.ts +17 -1
  103. package/src/runtime/channels/channel-transports.ts +20 -10
  104. package/src/runtime/channels/client-server-location-runtime.ts +16 -2
  105. package/src/runtime/execution/serial-execution-queue.ts +16 -108
  106. package/src/runtime/foundation/event-loop-resources.ts +2 -11
  107. package/src/runtime/foundation/operation-registry.ts +2 -22
  108. package/src/runtime/foundation/raw-service-mesh-runtime.ts +74 -19
  109. package/src/runtime/foundation/service-mailbox.ts +3 -13
  110. package/src/runtime/foundation/service-runtime-contracts.ts +2 -1
  111. package/src/runtime/foundation/service-stateful-registry.ts +2 -2
  112. package/src/runtime/foundation/service-stateful-runtime.ts +14 -14
  113. package/src/runtime/framework-errors-internal.ts +8 -10
  114. package/src/runtime/host/actor-placement-coordinator.ts +1 -1
  115. package/src/runtime/host/index.ts +2 -1
  116. package/src/runtime/host/service-relocation-host-runtime.ts +0 -4
  117. package/src/runtime/host/spot-address-transport.ts +1 -1
  118. package/src/runtime/host/user-spot-creation-coordinator.ts +1 -1
  119. package/src/runtime/locations/location-store-repository.ts +118 -80
  120. package/src/runtime/spots/spot-node-runtime-manager.ts +13 -233
  121. package/src/runtime/spots/spot-serial-turn-executor.ts +1 -8
  122. package/src/runtime/spots/spot-timer.ts +109 -18
  123. package/src/runtime/streams/session-serial-executor.ts +1 -7
  124. package/src/runtime/streams/stream-session-runtime.ts +13 -17
  125. package/src/runtime/workers/index.ts +2 -17
@@ -54,7 +54,7 @@ export class ZLinkDealerChannelClientTransport implements ZLinkChannelClientTran
54
54
  true,
55
55
  metadata
56
56
  )
57
- ).submit());
57
+ ).submit().admitted);
58
58
  return { status: ZLinkSubmitStatus.Submitted };
59
59
  }
60
60
 
@@ -83,7 +83,7 @@ export class ZLinkDealerChannelClientTransport implements ZLinkChannelClientTran
83
83
  )
84
84
  ));
85
85
  if (timeoutMs !== undefined) operation.timeout(timeoutMs);
86
- const reply = await submitRequestOperation(operation, 'channel request');
86
+ const reply = await submitRequestOperation({ submit: () => operation.submit().reply }, 'channel request');
87
87
  try {
88
88
  return decodeChannelReply<TReply>(reply);
89
89
  } finally {
@@ -0,0 +1,55 @@
1
+ import type {
2
+ BaseSocket,
3
+ PollEventFlagValue,
4
+ Socket
5
+ } from '@zlink-systems/zlink';
6
+ import { zlink } from './node-backend-adapter-support';
7
+
8
+ /**
9
+ * Owns one binding public Poller and drives it from the socket's libuv
10
+ * readiness callback. PollCompletion is drained only by Poller.wait; the
11
+ * socket callback itself is only the event-loop wakeup.
12
+ */
13
+ export class ZLinkNodeEventLoopPoller {
14
+ private readonly poller = zlink.createPoller();
15
+ private readonly events = zlink.createPollEvents(1);
16
+ private readableHandler: () => void;
17
+ private disposed = false;
18
+
19
+ constructor(
20
+ socket: Socket,
21
+ pollCompletion: boolean,
22
+ readableHandler: () => void
23
+ ) {
24
+ this.readableHandler = readableHandler;
25
+ try {
26
+ const pollEvents: PollEventFlagValue[] = [zlink.PollEventFlag.PollIn];
27
+ if (pollCompletion) pollEvents.push(zlink.PollEventFlag.PollCompletion);
28
+ this.poller.add(socket as BaseSocket, pollEvents, 0);
29
+ socket.setReadableHandler(this.onReady);
30
+ } catch (error) {
31
+ this.events.close();
32
+ this.poller.close();
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ setReadableHandler(handler: () => void): void {
38
+ this.readableHandler = handler;
39
+ }
40
+
41
+ dispose(): void {
42
+ if (this.disposed) return;
43
+ this.disposed = true;
44
+ this.events.close();
45
+ this.poller.close();
46
+ }
47
+
48
+ private readonly onReady = (): void => {
49
+ if (this.disposed) return;
50
+ const ready = this.poller.wait(this.events, 0);
51
+ if (ready === 0 || this.events.hasEvent(0, zlink.PollEventFlag.PollIn)) {
52
+ this.readableHandler();
53
+ }
54
+ };
55
+ }
@@ -5,11 +5,9 @@ import {
5
5
  RoutingId as BindingRoutingId,
6
6
  createContext,
7
7
  createDealerSocket,
8
- createPollEvents,
9
- createPoller,
10
8
  createRouterSocket,
11
- PollEventFlag,
12
9
  ReceiveFlowState,
10
+ SubmitResult,
13
11
  type Context,
14
12
  type DealerSocket,
15
13
  type MonitorEvent,
@@ -24,8 +22,9 @@ import {
24
22
  type SendSubmitOperation,
25
23
  type Socket
26
24
  } from '@zlink-systems/zlink';
27
- import { isPollerInterruptedError, translateBindingResultError } from './node-backend-adapter-support';
25
+ import { translateBindingResultError } from './node-backend-adapter-support';
28
26
  import { isEndpointCloseIgnorableError } from './node-socket-backend-adapter';
27
+ import { ZLinkNodeEventLoopPoller } from './node-event-loop-poller';
29
28
  import type {
30
29
  ZLinkRawBindingPort,
31
30
  ZLinkRawDealerPort,
@@ -122,12 +121,20 @@ class NodeRawHostPort implements ZLinkRawHostPort {
122
121
  abstract class NodeRawSocketPort<TSocket extends Socket> implements ZLinkRawSocketPort {
123
122
  private readonly endpoints = new Set<string>();
124
123
  private readonly monitors = new Set<NodeRawMonitorPort>();
125
- private readonly readablePoller = createPoller();
126
- private readonly readableEvents = createPollEvents(1);
124
+ private readonly eventLoopPoller: ZLinkNodeEventLoopPoller;
127
125
  private closed = false;
128
126
 
129
127
  protected constructor(protected readonly socket: TSocket) {
130
- this.readablePoller.add(socket as never, [PollEventFlag.PollIn], 0);
128
+ this.eventLoopPoller = new ZLinkNodeEventLoopPoller(
129
+ socket,
130
+ true,
131
+ () => {}
132
+ );
133
+ }
134
+
135
+ setReadableHandler(handler: () => void): void {
136
+ this.requireOpen();
137
+ this.eventLoopPoller.setReadableHandler(handler);
131
138
  }
132
139
 
133
140
  bind(endpoint: string): void {
@@ -172,6 +179,11 @@ abstract class NodeRawSocketPort<TSocket extends Socket> implements ZLinkRawSock
172
179
  close(): void {
173
180
  if (this.closed) return;
174
181
  const failures: unknown[] = [];
182
+ try {
183
+ this.eventLoopPoller.dispose();
184
+ } catch (error) {
185
+ failures.push(error);
186
+ }
175
187
  for (const monitor of [...this.monitors].reverse()) {
176
188
  try {
177
189
  monitor.close();
@@ -192,21 +204,6 @@ abstract class NodeRawSocketPort<TSocket extends Socket> implements ZLinkRawSock
192
204
  }
193
205
  }
194
206
  this.endpoints.clear();
195
- try {
196
- this.readablePoller.remove(this.socket as never);
197
- } catch (error) {
198
- failures.push(error);
199
- }
200
- try {
201
- this.readableEvents.close();
202
- } catch (error) {
203
- failures.push(error);
204
- }
205
- try {
206
- this.readablePoller.close();
207
- } catch (error) {
208
- failures.push(error);
209
- }
210
207
  try {
211
208
  this.socket.close();
212
209
  } catch (error) {
@@ -222,24 +219,6 @@ abstract class NodeRawSocketPort<TSocket extends Socket> implements ZLinkRawSock
222
219
  protected requireOpen(): void {
223
220
  if (this.closed) throw new Error('Raw socket is closed.');
224
221
  }
225
-
226
- protected receiveRecord(
227
- dontWait: boolean
228
- ): ZLinkRawReceivedRecord | undefined {
229
- const timeoutMs = dontWait ? 0 : -1;
230
- try {
231
- if (
232
- this.readablePoller.wait(this.readableEvents, timeoutMs) <= 0
233
- || !this.readableEvents.hasEvent(0, PollEventFlag.PollIn)
234
- ) {
235
- return undefined;
236
- }
237
- } catch (error) {
238
- if (isPollerInterruptedError(error)) return undefined;
239
- throw error;
240
- }
241
- return receiveRecord(this.socket as never, true);
242
- }
243
222
  }
244
223
 
245
224
  class NodeRawRouterPort extends NodeRawSocketPort<RouterSocket> implements ZLinkRawRouterPort {
@@ -276,10 +255,13 @@ class NodeRawRouterPort extends NodeRawSocketPort<RouterSocket> implements ZLink
276
255
 
277
256
  async send(targetRid: string, parts: readonly Uint8Array[]): Promise<void> {
278
257
  this.requireOpen();
279
- await appendSendParts(
258
+ const submission = appendSendParts(
280
259
  this.socket.send(bindingRoutingId(targetRid)),
281
260
  parts
282
261
  ).submit();
262
+ if (submission.result === SubmitResult.Backpressured) {
263
+ await submission.admitted;
264
+ }
283
265
  }
284
266
 
285
267
  async request(
@@ -291,7 +273,7 @@ class NodeRawRouterPort extends NodeRawSocketPort<RouterSocket> implements ZLink
291
273
  try {
292
274
  const replies = await appendRequestParts(this.socket.request(bindingRoutingId(targetRid)), parts)
293
275
  .timeout(timeoutMs)
294
- .submit();
276
+ .submit().reply;
295
277
  return copyAndClose(replies);
296
278
  } catch (error) {
297
279
  throw translateBindingResultError(error);
@@ -332,18 +314,21 @@ class NodeRawDealerPort extends NodeRawSocketPort<DealerSocket> implements ZLink
332
314
 
333
315
  async send(parts: readonly Uint8Array[]): Promise<void> {
334
316
  this.requireOpen();
335
- await appendSendParts(this.socket.send(), parts).submit();
317
+ const submission = appendSendParts(this.socket.send(), parts).submit();
318
+ if (submission.result === SubmitResult.Backpressured) {
319
+ await submission.admitted;
320
+ }
336
321
  }
337
322
 
338
323
  async request(parts: readonly Uint8Array[], timeoutMs: number): Promise<readonly Buffer[]> {
339
324
  this.requireOpen();
340
- const replies = await appendRequestParts(this.socket.request(), parts).timeout(timeoutMs).submit();
325
+ const replies = await appendRequestParts(this.socket.request(), parts).timeout(timeoutMs).submit().reply;
341
326
  return copyAndClose(replies);
342
327
  }
343
328
 
344
329
  receive(dontWait = false): ZLinkRawReceivedRecord | undefined {
345
330
  this.requireOpen();
346
- return this.receiveRecord(dontWait);
331
+ return receiveRecord(this.socket, dontWait);
347
332
  }
348
333
  }
349
334
 
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { setImmediate as yieldToIO } from 'node:timers/promises';
2
3
  import { disconnectStreamPeer } from './node-socket-backend-adapter';
3
4
  import { ZLinkFrameworkException } from '../../../contracts';
4
5
  import { internalFrameworkWireReply } from '../../framework-errors-internal';
@@ -17,6 +18,8 @@ import {
17
18
  ZLinkBackendResultError,
18
19
  type ZLinkBackendMessageLike as MessageLike
19
20
  } from '../runtime-values';
21
+ import { ZLinkBufferMessage } from '../runtime-message';
22
+ import type { Message as FrameworkMessage } from '../../../contracts/Common/Message';
20
23
  import type {
21
24
  MeshOperationId,
22
25
  MeshPeerEntry,
@@ -113,12 +116,9 @@ const MULTIPART_PACKET_NAME = SERVICE_FRAMEWORK_MULTIPART_PACKET_NAME;
113
116
  const MULTIPART_CONTENT_TYPE = SERVICE_FRAMEWORK_MULTIPART_CONTENT_TYPE;
114
117
  const FATAL_UTF8 = new TextDecoder('utf-8', { fatal: true });
115
118
  const MAX_DRAIN_RECORDS = 64;
116
- // A timer is only a no-data backoff. Once the binding has reported readable
117
- // work, continue from the next event-loop turn so a full socket does not wait
118
- // for an arbitrary timer cadence between batches.
119
- const MESH_BACKEND_IDLE_POLL_INTERVAL_MS = 1;
120
- const MESH_RECEIVE_BATCH_BYTE_LIMIT = 4 * 1024 * 1024;
121
- const MESH_RECEIVE_BATCH_TIME_LIMIT_MS = 2;
119
+ // Preserve the existing monitor/admission/liveness cadence. Only the binding
120
+ // readable handler admits receive work; this timer never probes the socket.
121
+ const MESH_BACKEND_MAINTENANCE_INTERVAL_MS = 1;
122
122
  /**
123
123
  * Conservative Actor Join admission cap for relocation state chunks (spec 15
124
124
  * §4.2): a stable lower bound safe on any deployment, never lowered on
@@ -126,28 +126,6 @@ const MESH_RECEIVE_BATCH_TIME_LIMIT_MS = 2;
126
126
  */
127
127
  const ACTOR_JOIN_ADVERTISED_RECEIVE_CHUNK_LIMIT_BYTES = 32 * 1024;
128
128
 
129
- class ZLinkMeshReceiveBatchBudget {
130
- private readonly peerMessages = new Map<string, number>();
131
- private readonly peerBytes = new Map<string, number>();
132
- private startedAtMs = 0;
133
-
134
- reset(nowMs: number): void {
135
- this.peerMessages.clear();
136
- this.peerBytes.clear();
137
- this.startedAtMs = nowMs;
138
- }
139
-
140
- record(peer: string, byteCount: number, nowMs: number): boolean {
141
- const messages = (this.peerMessages.get(peer) ?? 0) + 1;
142
- const bytes = (this.peerBytes.get(peer) ?? 0) + byteCount;
143
- this.peerMessages.set(peer, messages);
144
- this.peerBytes.set(peer, bytes);
145
- return messages >= MAX_DRAIN_RECORDS
146
- || bytes >= MESH_RECEIVE_BATCH_BYTE_LIMIT
147
- || nowMs - this.startedAtMs >= MESH_RECEIVE_BATCH_TIME_LIMIT_MS;
148
- }
149
- }
150
-
151
129
  /**
152
130
  * M6A MeshNode backend. Stateful Spot/Actor entry points stay explicit until
153
131
  * their owning M6B runtime is connected; topology and node/channel dispatch do
@@ -172,7 +150,17 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
172
150
  private runtime?: RawServiceMeshRuntime;
173
151
  private stateful?: ServiceStatefulRuntime;
174
152
  private readyHandler?: (domains: number) => number;
175
- private pollTimer?: NodeJS.Timeout;
153
+ private maintenanceTimer?: NodeJS.Timeout;
154
+ private pumping = false;
155
+ private readable = false;
156
+ private readonly onReadable = (): void => {
157
+ this.readable = true;
158
+ if (!this.closed && !this.pumping) void this.pump();
159
+ };
160
+ private readonly onMaintenance = (): void => {
161
+ this.maintenanceTimer = undefined;
162
+ void this.pump();
163
+ };
176
164
  private nextPeerIntent = 1n;
177
165
  private closed = false;
178
166
  private objectRole: ServiceNodeDescriptor['objectRole'] = 'none';
@@ -195,20 +183,6 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
195
183
  record: import('../../foundation/service-stateful-wire-codec').ServiceMessageFollowRecord
196
184
  ) => void;
197
185
  private readonly peerDisconnectedHandlers = new Set<(endpoint: string) => void>();
198
- private readonly receiveBatchBudget = new ZLinkMeshReceiveBatchBudget();
199
- private observedPumpSourceRoutingId?: string;
200
- private observedPumpByteCount = 0;
201
- private readonly observePump = (sourceRoutingId: string, byteCount: number): void => {
202
- this.observedPumpSourceRoutingId = sourceRoutingId;
203
- this.observedPumpByteCount = byteCount;
204
- };
205
-
206
- private takePumpObservation(): { readonly sourceRoutingId: string; readonly byteCount: number } | undefined {
207
- const sourceRoutingId = this.observedPumpSourceRoutingId;
208
- if (sourceRoutingId === undefined) return undefined;
209
- return { sourceRoutingId, byteCount: this.observedPumpByteCount };
210
- }
211
-
212
186
  constructor(
213
187
  private readonly meshName: string,
214
188
  routingId: string | undefined,
@@ -414,7 +388,13 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
414
388
  }
415
389
  this.stateful.setMessageFollowHandler((record) => this.messageFollowHandler?.(record));
416
390
  this.runtime = runtime;
417
- this.schedulePoll();
391
+ try {
392
+ runtime.setReadableHandler(this.onReadable);
393
+ } catch (error) {
394
+ this.close();
395
+ throw error;
396
+ }
397
+ this.scheduleMaintenance();
418
398
  }
419
399
 
420
400
  setMessageFollowHandler(handler: (
@@ -446,8 +426,8 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
446
426
  close(): void {
447
427
  if (this.closed) return;
448
428
  this.closed = true;
449
- if (this.pollTimer !== undefined) clearTimeout(this.pollTimer);
450
- this.pollTimer = undefined;
429
+ if (this.maintenanceTimer !== undefined) clearTimeout(this.maintenanceTimer);
430
+ this.maintenanceTimer = undefined;
451
431
  this.stateful?.close();
452
432
  this.stateful = undefined;
453
433
  this.runtime?.close();
@@ -614,7 +594,7 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
614
594
  ): Promise<SubmitResultValue> {
615
595
  return await this.requireRuntime().sendToNode(
616
596
  String(targetRid),
617
- encodeMultipart(parts)
597
+ encodeMultipartApplicationFrame(parts)
618
598
  ) ? SubmitResult.Ok : SubmitResult.NotConnected;
619
599
  }
620
600
 
@@ -657,7 +637,7 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
657
637
  ): MeshOperationId {
658
638
  const pending = this.requireRuntime().requestToNode(
659
639
  String(targetRid),
660
- encodeMultipart(parts),
640
+ encodeMultipartApplicationFrame(parts),
661
641
  options?.timeoutMs ?? 30_000
662
642
  );
663
643
  return this.observeCompletion(pending.id, OperationKind.NodeRequest, pending.promise);
@@ -669,7 +649,7 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
669
649
  ): Promise<SubmitResultValue> {
670
650
  return await this.requireRuntime().sendToChannel(
671
651
  channelName,
672
- encodeMultipart(parts)
652
+ encodeMultipartApplicationFrame(parts)
673
653
  ) ? SubmitResult.Ok : SubmitResult.NotConnected;
674
654
  }
675
655
 
@@ -680,7 +660,7 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
680
660
  ): MeshOperationId {
681
661
  const pending = this.requireRuntime().requestToChannel(
682
662
  channelName,
683
- encodeMultipart(parts),
663
+ encodeMultipartApplicationFrame(parts),
684
664
  options?.timeoutMs ?? 30_000
685
665
  );
686
666
  if (pending === undefined) {
@@ -1501,49 +1481,39 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
1501
1481
  return result;
1502
1482
  }
1503
1483
 
1504
- private schedulePoll(delayMs = MESH_BACKEND_IDLE_POLL_INTERVAL_MS): void {
1505
- if (this.closed || this.pollTimer !== undefined) return;
1506
- this.pollTimer = setTimeout(async () => {
1507
- this.pollTimer = undefined;
1508
- try {
1509
- const received = await this.poll();
1510
- // A zero-delay timer yields to I/O and other ready work first without
1511
- // inserting the idle polling interval between readable batches.
1512
- delayMs = received ? 0 : MESH_BACKEND_IDLE_POLL_INTERVAL_MS;
1513
- } finally {
1514
- if (!this.closed) this.schedulePoll(delayMs);
1515
- }
1516
- }, delayMs);
1484
+ private scheduleMaintenance(): void {
1485
+ if (this.closed || this.maintenanceTimer !== undefined) return;
1486
+ this.maintenanceTimer = setTimeout(
1487
+ this.onMaintenance,
1488
+ MESH_BACKEND_MAINTENANCE_INTERVAL_MS
1489
+ );
1517
1490
  }
1518
1491
 
1519
- private async poll(): Promise<boolean> {
1520
- const runtime = this.runtime;
1521
- if (runtime === undefined) return false;
1522
- let received = false;
1523
- await runtime.drainMonitorEvents();
1524
- this.receiveBatchBudget.reset(performance.now());
1525
- for (;;) {
1526
- this.observedPumpSourceRoutingId = undefined;
1527
- const result = await runtime.pumpOne(performance.now(), this.observePump);
1528
- if (result === 'noData') break;
1529
- received = true;
1530
- if (result === 'application') this.readyHandler?.(ReadyDomain.Application);
1531
- const observation = this.takePumpObservation();
1532
- // Core ROUTER advances its fair-queue cursor after each complete
1533
- // multipart message, so the next poll resumes from the following pipe.
1534
- if (
1535
- observation !== undefined
1536
- && this.receiveBatchBudget.record(
1537
- observation.sourceRoutingId,
1538
- observation.byteCount,
1539
- performance.now()
1540
- )
1541
- ) break;
1492
+ private async pump(): Promise<void> {
1493
+ if (this.isClosed() || this.pumping) return;
1494
+ this.pumping = true;
1495
+ if (this.maintenanceTimer !== undefined) clearTimeout(this.maintenanceTimer);
1496
+ this.maintenanceTimer = undefined;
1497
+ try {
1498
+ do {
1499
+ const receiveReady = this.readable;
1500
+ this.readable = false;
1501
+ const more = await this.requireRuntime().pumpBatch(receiveReady);
1502
+ if (more) {
1503
+ // Yield only after actual progress, retaining readiness across the
1504
+ // existing batch limits until receive reports no data.
1505
+ this.readable = true;
1506
+ await yieldToIO();
1507
+ }
1508
+ } while (!this.closed && this.readable);
1509
+ } finally {
1510
+ this.pumping = false;
1511
+ this.scheduleMaintenance();
1542
1512
  }
1543
- await runtime.announceExpectedPeers();
1544
- await runtime.tickLiveness();
1545
- this.notifyReady();
1546
- return received;
1513
+ }
1514
+
1515
+ private isClosed(): boolean {
1516
+ return this.closed;
1547
1517
  }
1548
1518
 
1549
1519
  private notifyReady(): void {
@@ -1654,16 +1624,7 @@ export class ZLinkNodeRawMeshBackend implements ZLinkBackendMeshNode {
1654
1624
  const owner = readyOwner(claim.owner, this.routingId, this.stateful, domain);
1655
1625
  batch.push(
1656
1626
  owner,
1657
- new MailboxClaim(runtime, claim, () => {
1658
- // A receive batch may intentionally consume fewer records than the
1659
- // mailbox claim. Releasing the claim re-queues the remainder, so
1660
- // expose that newly ready work to the dispatch pump.
1661
- if (runtime.mailbox.pendingMessages(domain) > 0) {
1662
- this.readyHandler?.(
1663
- domain === 'infrastructure' ? ReadyDomain.Infrastructure : ReadyDomain.Application
1664
- );
1665
- }
1666
- })
1627
+ new MailboxClaim(runtime, claim)
1667
1628
  );
1668
1629
  }
1669
1630
  }
@@ -2013,7 +1974,7 @@ class RawStreamSessionService implements StreamSessionService {
2013
1974
  submit = submit.message(parts[index]!);
2014
1975
  }
2015
1976
  try {
2016
- await submit.submit();
1977
+ await submit.submit().admitted;
2017
1978
  return true;
2018
1979
  } catch (error) {
2019
1980
  if (!(error instanceof SubmitError)) throw error;
@@ -2087,16 +2048,28 @@ class RawReadyBatch implements ReadyBatch {
2087
2048
  }
2088
2049
 
2089
2050
  class RawReceiveBatch implements ReceiveBatch {
2051
+ private receiveCapacity: number;
2052
+
2090
2053
  constructor(
2091
- readonly messageCapacity: number,
2054
+ private readonly capacity: number,
2092
2055
  readonly partCapacity: number
2093
2056
  ) {
2094
- if ([messageCapacity, partCapacity].some(value => !Number.isInteger(value) || value < 1)) {
2057
+ if ([capacity, partCapacity].some(value => !Number.isInteger(value) || value < 1)) {
2095
2058
  throw new RangeError('Receive batch capacities must be positive.');
2096
2059
  }
2060
+ this.receiveCapacity = capacity;
2097
2061
  }
2098
2062
 
2099
- reset(): void {}
2063
+ get messageCapacity(): number {
2064
+ return this.receiveCapacity;
2065
+ }
2066
+
2067
+ reset(messageCapacity = this.capacity): void {
2068
+ if (!Number.isInteger(messageCapacity) || messageCapacity < 1 || messageCapacity > this.capacity) {
2069
+ throw new RangeError('Receive limit must be within the batch capacity.');
2070
+ }
2071
+ this.receiveCapacity = messageCapacity;
2072
+ }
2100
2073
  close(): void {}
2101
2074
  }
2102
2075
 
@@ -2107,8 +2080,7 @@ class MailboxClaim implements RawClaim {
2107
2080
 
2108
2081
  constructor(
2109
2082
  private readonly runtime: RawServiceMeshRuntime,
2110
- private readonly claim: ServiceMailboxClaim,
2111
- private readonly onRelease?: () => void
2083
+ private readonly claim: ServiceMailboxClaim
2112
2084
  ) {}
2113
2085
 
2114
2086
  recvBatch(batch: ReceiveBatch) {
@@ -2130,6 +2102,12 @@ class MailboxClaim implements RawClaim {
2130
2102
  }
2131
2103
  this.runtime.mailbox.releaseClaimedPayload(record);
2132
2104
  this.remaining = this.claim.records.slice(index + 1);
2105
+ // Nothing was transferred to the receive caller when decoding throws.
2106
+ // Earlier records therefore still belong to this failed batch.
2107
+ for (const decoded of records) {
2108
+ for (const part of decoded.parts) part.close();
2109
+ decoded.releaseRetainedIngress?.();
2110
+ }
2133
2111
  throw error;
2134
2112
  }
2135
2113
  const nextParts = decoded.parts.length;
@@ -2152,7 +2130,6 @@ class MailboxClaim implements RawClaim {
2152
2130
  if (this.released) return;
2153
2131
  this.released = true;
2154
2132
  this.runtime.mailbox.release(this.claim, this.remaining);
2155
- this.onRelease?.();
2156
2133
  }
2157
2134
  }
2158
2135
 
@@ -2297,7 +2274,7 @@ function decodeMultipartRecord(
2297
2274
  parts: decodeMultipart(application.payload),
2298
2275
  reply(parts) {
2299
2276
  if (record.correlation === undefined) return SubmitResult.InvalidState;
2300
- runtime.reply(record, encodeMultipart(parts));
2277
+ runtime.reply(record, encodeMultipartApplicationFrame(parts));
2301
2278
  return SubmitResult.Ok;
2302
2279
  },
2303
2280
  replyActorJoin: () => SubmitResult.NotSupported
@@ -2585,9 +2562,19 @@ function decodeApplicationEnvelope(frame: Uint8Array) {
2585
2562
  return { packetName, contentType, payload: bytes.subarray(offset) };
2586
2563
  }
2587
2564
 
2588
- function decodeMultipart(payload: Uint8Array): Message[] {
2565
+ function decodeMultipart(payload: Uint8Array): FrameworkMessage[] {
2589
2566
  const buffers = decodeMultipartBuffers(payload);
2590
- return buffers.map(part => Message.from(part));
2567
+ const parts = new Array<FrameworkMessage>(buffers.length);
2568
+ let materialized = 0;
2569
+ try {
2570
+ for (; materialized < buffers.length; materialized += 1) {
2571
+ parts[materialized] = ZLinkBufferMessage.fromOwned(buffers[materialized]!);
2572
+ }
2573
+ return parts;
2574
+ } catch (error) {
2575
+ for (let index = 0; index < materialized; index += 1) parts[index]!.close();
2576
+ throw error;
2577
+ }
2591
2578
  }
2592
2579
 
2593
2580
  function decodeMultipartBuffers(payload: Uint8Array): Buffer[] {