@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
@@ -1,8 +1,11 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { availableParallelism } from 'node:os';
2
3
  import { ZLINK_BACKEND_RECV_DONT_WAIT } from './runtime-values';
3
4
  import {
4
5
  ReadyDomain,
6
+ type ReadyBatch,
5
7
  type ReadyRecord,
8
+ type ReceiveBatch,
6
9
  type ReceiveRecord
7
10
  } from '../foundation/service-runtime-contracts';
8
11
  import type { ZLinkBackendMeshNode } from './contracts';
@@ -13,9 +16,11 @@ import type {
13
16
  } from '../application-jobs/contracts';
14
17
  import { runWithApplicationJobPermit } from '../application-jobs/application-job-queue-scope';
15
18
 
16
- const MESH_DISPATCH_TIMER_YIELD_BATCHES = 16;
17
- const MESH_DISPATCH_TIMER_YIELD_INTERVAL_MS = 2;
19
+ const MESH_DISPATCH_YIELD_RECORDS = 16;
20
+ const MESH_DISPATCH_YIELD_INTERVAL_MS = 2;
18
21
  const MESH_DISPATCH_LIFECYCLE_CLAIM_BUDGET = 4;
22
+ const MESH_DISPATCH_RECEIVE_CAPACITY = 64;
23
+ const MESH_DISPATCH_MAX_APPLICATION_WORKERS = Math.max(2, availableParallelism());
19
24
  // Captured while the runtime module is loaded, before any application owner
20
25
  // exists. A ready callback may run inside a Spot turn; the shared Mesh pump
21
26
  // must enter like an independent receive-loop task, without inheriting that
@@ -52,13 +57,15 @@ class ZLinkMeshDispatchFailure extends Error {
52
57
 
53
58
  export class ZLinkMeshDispatchPump {
54
59
  private pendingDomains: number = ReadyDomain.None;
55
- private scheduled = false;
56
60
  private infrastructureScheduled = false;
57
61
  private disposed = false;
58
- private drainPromise?: Promise<void>;
59
62
  private infrastructureDrainPromise?: Promise<void>;
60
63
  private readonly activeDrains = new Set<Promise<void>>();
64
+ private readonly idleApplicationWorkers = new Set<() => void>();
65
+ private applicationWorkerCount = 0;
61
66
  private readonly capacityStop = new AbortController();
67
+ private recordsSinceYield = 0;
68
+ private yieldStartedAtMs = 0;
62
69
 
63
70
  constructor(
64
71
  private readonly node: ZLinkBackendMeshNode,
@@ -67,12 +74,12 @@ export class ZLinkMeshDispatchPump {
67
74
  }
68
75
 
69
76
  start(): void {
77
+ if (this.applicationWorkerCount === 0) this.startApplicationWorker();
70
78
  this.node.setReadyHandler((domains) => {
71
79
  if (this.disposed) {
72
80
  return ReadyDomain.None;
73
81
  }
74
- this.pendingDomains |= domains;
75
- this.schedule();
82
+ this.markReady(domains);
76
83
  return domains;
77
84
  });
78
85
  }
@@ -84,43 +91,102 @@ export class ZLinkMeshDispatchPump {
84
91
  this.disposed = true;
85
92
  this.capacityStop.abort();
86
93
  this.pendingDomains = ReadyDomain.None;
94
+ for (const wake of this.idleApplicationWorkers) wake();
95
+ this.idleApplicationWorkers.clear();
87
96
  while (this.activeDrains.size > 0) {
88
97
  await Promise.all([...this.activeDrains]);
89
98
  }
90
99
  }
91
100
 
92
- private schedule(): void {
93
- if ((this.pendingDomains & ReadyDomain.Infrastructure) !== 0) {
94
- this.scheduleInfrastructure();
95
- }
96
- if ((this.pendingDomains & ReadyDomain.Application) === 0) {
101
+ private markReady(domains: number): void {
102
+ if (domains === ReadyDomain.None || this.disposed) return;
103
+ const previous = this.pendingDomains;
104
+ this.pendingDomains |= domains;
105
+ const startInfrastructure =
106
+ (domains & ReadyDomain.Infrastructure) !== 0
107
+ && (previous & ReadyDomain.Infrastructure) === 0;
108
+ const startApplication =
109
+ (domains & ReadyDomain.Application) !== 0
110
+ && (previous & ReadyDomain.Application) === 0;
111
+ if (startInfrastructure && startApplication) {
112
+ const startTurn = this.yieldIfNeeded() ?? Promise.resolve();
113
+ this.scheduleInfrastructure(startTurn);
97
114
  return;
98
115
  }
99
- if (this.scheduled) {
116
+ if (startInfrastructure) this.scheduleInfrastructure();
117
+ if (startApplication) {
118
+ if ((domains & ReadyDomain.Infrastructure) !== 0) {
119
+ setImmediate(() => this.wakeApplicationWorker());
120
+ } else {
121
+ this.wakeApplicationWorker();
122
+ }
123
+ }
124
+ }
125
+
126
+ private wakeApplicationWorker(): void {
127
+ if (this.disposed || (this.pendingDomains & ReadyDomain.Application) === 0) return;
128
+ const idle = this.idleApplicationWorkers.values().next().value as (() => void) | undefined;
129
+ if (idle !== undefined) {
130
+ this.idleApplicationWorkers.delete(idle);
131
+ this.pendingDomains &= ~ReadyDomain.Application;
132
+ idle();
100
133
  return;
101
134
  }
102
- this.scheduled = true;
103
- const drain = yieldToEventLoop()
104
- .then(() => detachedMeshDispatchScope(() => this.drain()))
105
- .catch((error) => {
106
- if (error instanceof ZLinkMeshDispatchFailure) {
107
- this.options.reportError?.(error.dispatchCause, error.context);
108
- return;
109
- }
110
- this.options.reportError?.(error);
135
+ if (this.applicationWorkerCount < MESH_DISPATCH_MAX_APPLICATION_WORKERS) {
136
+ this.startApplicationWorker();
137
+ }
138
+ }
139
+
140
+ private startApplicationWorker(): void {
141
+ this.applicationWorkerCount += 1;
142
+ let worker: Promise<void>;
143
+ worker = detachedMeshDispatchScope(() =>
144
+ runZLinkExecutionArea('application', () => this.runApplicationWorker()))
145
+ .finally(() => {
146
+ this.applicationWorkerCount -= 1;
147
+ this.activeDrains.delete(worker);
111
148
  });
112
- this.activeDrains.add(drain);
113
- this.drainPromise = drain;
114
- void drain.finally(() => {
115
- this.activeDrains.delete(drain);
116
- if (this.drainPromise === drain) {
117
- this.drainPromise = undefined;
118
- this.scheduled = false;
119
- if (!this.disposed && this.pendingDomains !== ReadyDomain.None) {
120
- this.schedule();
149
+ this.activeDrains.add(worker);
150
+ }
151
+
152
+ private async runApplicationWorker(): Promise<void> {
153
+ // A worker claims one owner at a time so a suspended handler cannot hold
154
+ // unrelated owners. Each owner may supply up to 64 pre-admitted records.
155
+ const readyBatch = this.node.createReadyBatch(1);
156
+ const receiveBatch = this.node.createReceiveBatch(
157
+ MESH_DISPATCH_RECEIVE_CAPACITY, this.options.partCapacity ?? 256
158
+ );
159
+ try {
160
+ for (;;) {
161
+ await this.waitForApplicationReady();
162
+ if (this.disposed) return;
163
+ try {
164
+ await this.drainDomain(ReadyDomain.Application, readyBatch, receiveBatch);
165
+ } catch (error) {
166
+ this.reportDispatchError(error);
121
167
  }
122
168
  }
123
- });
169
+ } finally {
170
+ receiveBatch.close();
171
+ readyBatch.close();
172
+ }
173
+ }
174
+
175
+ private waitForApplicationReady(): Promise<void> {
176
+ if (this.disposed) return Promise.resolve();
177
+ if ((this.pendingDomains & ReadyDomain.Application) !== 0) {
178
+ this.pendingDomains &= ~ReadyDomain.Application;
179
+ return Promise.resolve();
180
+ }
181
+ return new Promise(resolve => this.idleApplicationWorkers.add(resolve));
182
+ }
183
+
184
+ private reportDispatchError(error: unknown): void {
185
+ if (error instanceof ZLinkMeshDispatchFailure) {
186
+ this.options.reportError?.(error.dispatchCause, error.context);
187
+ return;
188
+ }
189
+ this.options.reportError?.(error);
124
190
  }
125
191
 
126
192
  /**
@@ -130,13 +196,14 @@ export class ZLinkMeshDispatchPump {
130
196
  * the infrastructure mailbox and prevent the relocation coordinator from
131
197
  * completing the drain that releases the application work.
132
198
  */
133
- private scheduleInfrastructure(): void {
199
+ private scheduleInfrastructure(startTurn?: Promise<void>): void {
134
200
  if (this.infrastructureScheduled) {
135
201
  return;
136
202
  }
137
203
  this.infrastructureScheduled = true;
138
- const drain = yieldToEventLoop()
139
- .then(() => detachedMeshDispatchScope(() => this.drainInfrastructure()))
204
+ const drain = (startTurn ?? this.yieldIfNeeded() ?? Promise.resolve())
205
+ .then(() => detachedMeshDispatchScope(() =>
206
+ runZLinkExecutionArea('infrastructure', () => this.drainInfrastructure())))
140
207
  .catch((error) => {
141
208
  if (error instanceof ZLinkMeshDispatchFailure) {
142
209
  this.options.reportError?.(error.dispatchCause, error.context);
@@ -158,44 +225,45 @@ export class ZLinkMeshDispatchPump {
158
225
  });
159
226
  }
160
227
 
161
- private async drain(): Promise<void> {
162
- while (!this.disposed) {
163
- if ((this.pendingDomains & ReadyDomain.Application) === 0) {
164
- return;
165
- }
166
- this.pendingDomains &= ~ReadyDomain.Application;
167
- await this.drainDomain(ReadyDomain.Application);
168
- }
169
- }
170
-
171
228
  private async drainInfrastructure(): Promise<void> {
172
- while (!this.disposed) {
173
- if ((this.pendingDomains & ReadyDomain.Infrastructure) === 0) {
174
- return;
175
- }
176
- this.pendingDomains &= ~ReadyDomain.Infrastructure;
177
- const lifecycleBudgetExhausted = await this.drainDomain(
178
- ReadyDomain.Infrastructure,
179
- MESH_DISPATCH_LIFECYCLE_CLAIM_BUDGET
180
- );
181
- if (lifecycleBudgetExhausted) {
182
- this.pendingDomains |= ReadyDomain.Infrastructure;
229
+ const readyBatch = this.node.createReadyBatch(
230
+ Math.min(this.options.readyCapacity ?? 32, MESH_DISPATCH_LIFECYCLE_CLAIM_BUDGET)
231
+ );
232
+ const receiveBatch = this.node.createReceiveBatch(
233
+ MESH_DISPATCH_RECEIVE_CAPACITY, this.options.partCapacity ?? 256
234
+ );
235
+ try {
236
+ while (!this.disposed) {
237
+ if ((this.pendingDomains & ReadyDomain.Infrastructure) === 0) {
238
+ return;
239
+ }
240
+ this.pendingDomains &= ~ReadyDomain.Infrastructure;
241
+ if ((this.pendingDomains & ReadyDomain.Application) !== 0) {
242
+ setImmediate(() => this.wakeApplicationWorker());
243
+ }
244
+ const lifecycleBudgetExhausted = await this.drainDomain(
245
+ ReadyDomain.Infrastructure,
246
+ readyBatch,
247
+ receiveBatch,
248
+ MESH_DISPATCH_LIFECYCLE_CLAIM_BUDGET
249
+ );
250
+ if (lifecycleBudgetExhausted) {
251
+ this.pendingDomains |= ReadyDomain.Infrastructure;
252
+ await yieldToEventLoop();
253
+ }
183
254
  }
255
+ } finally {
256
+ receiveBatch.close();
257
+ readyBatch.close();
184
258
  }
185
259
  }
186
260
 
187
- private async drainDomain(domain: number, claimBudget?: number): Promise<boolean> {
188
- const readyCapacity = domain === ReadyDomain.Application
189
- ? 1
190
- : claimBudget === undefined
191
- ? (this.options.readyCapacity ?? 32)
192
- : Math.min(this.options.readyCapacity ?? 32, claimBudget);
193
- const readyBatch = this.node.createReadyBatch(readyCapacity);
194
- // One record per claim receive keeps one ordinary-ingress permit paired
195
- // with exactly one dispatch turn. Terminal completion claims bypass it.
196
- const receiveBatch = this.node.createReceiveBatch(1, this.options.partCapacity ?? 256);
197
- let receiveBatchesSinceTimerYield = 0;
198
- let timerYieldStartedAtMs = this.nowMs();
261
+ private async drainDomain(
262
+ domain: number,
263
+ readyBatch: ReadyBatch,
264
+ receiveBatch: ReceiveBatch,
265
+ claimBudget?: number
266
+ ): Promise<boolean> {
199
267
  let claimsDrained = 0;
200
268
  try {
201
269
  for (;;) {
@@ -210,59 +278,46 @@ export class ZLinkMeshDispatchPump {
210
278
  return false;
211
279
  }
212
280
  if (domain === ReadyDomain.Application && drained.hasResidue) {
213
- this.pendingDomains |= ReadyDomain.Application;
281
+ this.markReady(ReadyDomain.Application);
214
282
  }
215
283
  for (let index = 0; index < drained.records.length; index += 1) {
216
284
  const claim = readyBatch.takeClaim(index);
217
285
  claimsDrained += 1;
286
+ const owner = drained.records[index];
287
+ // Raw ingress already attached one host permit to each record.
288
+ // A claim that reserves admission here may receive only that permit's record.
218
289
  try {
219
- receiveBatch.reset();
290
+ const capacity = owner.ordinaryIngressPreAdmitted === true
291
+ ? MESH_DISPATCH_RECEIVE_CAPACITY : 1;
220
292
  for (;;) {
221
- const owner = drained.records[index];
222
- const claimPermit = owner.terminalCompletion === true
293
+ let claimPermit = owner.terminalCompletion === true
223
294
  || owner.ordinaryIngressPreAdmitted === true
224
295
  || domain === ReadyDomain.Infrastructure
225
296
  ? undefined
226
297
  : await this.acquirePermit();
227
- if (claimPermit === undefined
228
- && this.disposed
229
- && owner.terminalCompletion !== true
230
- && owner.ordinaryIngressPreAdmitted !== true) return false;
231
- const received = claim.recvBatch(receiveBatch, ZLINK_BACKEND_RECV_DONT_WAIT);
232
- if (!received.ok) {
233
- claimPermit?.releaseAfterInternalProcessing();
234
- break;
235
- }
236
- if (received.records.length === 0) {
237
- claimPermit?.releaseAfterInternalProcessing();
238
- }
239
- for (const record of received.records) {
240
- const permit = record.applicationJobPermit ?? claimPermit;
241
- if (owner.ordinaryIngressPreAdmitted === true && permit === undefined) {
242
- record.releaseRetainedIngress?.();
243
- throw new Error('Pre-admitted raw ingress record lost its Application Job Queue permit.');
298
+ try {
299
+ if (this.capacityStop.signal.aborted) return false;
300
+ receiveBatch.reset(capacity);
301
+ const received = claim.recvBatch(receiveBatch, ZLINK_BACKEND_RECV_DONT_WAIT);
302
+ if (!received.ok) break;
303
+ // This owner keeps its claim while it drains. Other owners and
304
+ // infrastructure may progress if one of its handlers suspends.
305
+ if (domain === ReadyDomain.Infrastructure) {
306
+ this.infrastructureScheduled = false;
307
+ this.infrastructureDrainPromise = undefined;
308
+ if ((this.pendingDomains & ReadyDomain.Infrastructure) !== 0) {
309
+ this.scheduleInfrastructure();
310
+ }
244
311
  }
245
312
  try {
246
- // A handler may synchronously submit an operation whose
247
- // control/completion is owned by this same MeshNode (e.g. a
248
- // native Completion record fed back through this node's own
249
- // Infrastructure claim). The active lane's scheduling state
250
- // must be released here, or a nested re-entrant drain can
251
- // never start to deliver that completion, and the awaiting
252
- // handler deadlocks against its own claim (spec
253
- // 46-internal-dispatch-loop, receive/dispatch wake/ordering).
254
- this.scheduled = false;
255
- this.drainPromise = undefined;
256
- if (domain === ReadyDomain.Infrastructure) {
257
- this.infrastructureScheduled = false;
258
- this.infrastructureDrainPromise = undefined;
259
- }
260
- if (this.pendingDomains !== ReadyDomain.None) {
261
- this.schedule();
262
- }
263
- const dispatch = () => runZLinkExecutionArea(
264
- domain === ReadyDomain.Infrastructure ? 'infrastructure' : 'application',
265
- async () => {
313
+ for (const record of received.records) {
314
+ if (this.disposed) break;
315
+ const permit = record.applicationJobPermit ?? claimPermit;
316
+ if (owner.ordinaryIngressPreAdmitted === true && permit === undefined) {
317
+ throw new Error('Pre-admitted raw ingress record lost its Application Job Queue permit.');
318
+ }
319
+ this.recordsSinceYield += 1;
320
+ const dispatch = async () => {
266
321
  try {
267
322
  await this.options.dispatch(drained.records[index], record);
268
323
  } catch (error) {
@@ -272,55 +327,67 @@ export class ZLinkMeshDispatchPump {
272
327
  );
273
328
  }
274
329
  await record.onTerminalCompletion?.();
330
+ };
331
+ if (permit === undefined) {
332
+ await dispatch();
333
+ } else {
334
+ if (
335
+ domain === ReadyDomain.Application
336
+ && record.applicationJobPermit === undefined
337
+ ) permit.markApplicationQueued();
338
+ // The scope now owns this reservation, including failure
339
+ // and detached handler handoff. Only unused permits remain
340
+ // owned by the receive attempt's finally block.
341
+ if (permit === claimPermit) claimPermit = undefined;
342
+ await runWithApplicationJobPermit(permit, dispatch);
275
343
  }
276
- );
277
- if (permit === undefined) {
278
- await dispatch();
279
- } else {
280
- if (
281
- domain === ReadyDomain.Application
282
- && record.applicationJobPermit === undefined
283
- ) permit.markApplicationQueued();
284
- await runWithApplicationJobPermit(permit, dispatch);
344
+ const yieldTurn = this.yieldIfNeeded();
345
+ if (yieldTurn !== undefined) await yieldTurn;
285
346
  }
286
347
  } finally {
287
- for (const part of record.parts) {
288
- part.close();
348
+ // Batch ownership includes records whose dispatch never began,
349
+ // including on failure or shutdown in the middle of the batch.
350
+ for (const record of received.records) {
351
+ for (const part of record.parts) part.close();
352
+ record.releaseRetainedIngress?.();
289
353
  }
290
- record.releaseRetainedIngress?.();
291
354
  }
355
+ } finally {
356
+ claimPermit?.releaseAfterInternalProcessing();
292
357
  }
293
- // A continuously readable owner must not keep the timers phase
294
- // from running, but a timer turn for every single record adds a
295
- // Promise, closure, and timer allocation to the hot path.
296
- receiveBatchesSinceTimerYield += 1;
297
- if (
298
- receiveBatchesSinceTimerYield >= MESH_DISPATCH_TIMER_YIELD_BATCHES
299
- || this.nowMs() - timerYieldStartedAtMs >= MESH_DISPATCH_TIMER_YIELD_INTERVAL_MS
300
- ) {
301
- receiveBatchesSinceTimerYield = 0;
302
- await yieldToTimers();
303
- timerYieldStartedAtMs = this.nowMs();
304
- }
305
- receiveBatch.reset();
306
358
  }
307
359
  } finally {
308
360
  claim.release();
309
361
  }
310
362
  }
311
- await yieldToTimers();
312
- receiveBatchesSinceTimerYield = 0;
313
- timerYieldStartedAtMs = this.nowMs();
363
+ if (claimBudget !== undefined && claimsDrained >= claimBudget) {
364
+ this.wakeApplicationWorker();
365
+ return true;
366
+ }
367
+ // Empty ready claims also count as work, so they cannot monopolize
368
+ // the microtask queue without giving I/O and deadlines a turn.
369
+ this.recordsSinceYield += 1;
370
+ const yieldTurn = this.yieldIfNeeded();
371
+ if (yieldTurn !== undefined) await yieldTurn;
314
372
  if (!drained.hasResidue) {
315
373
  return false;
316
374
  }
317
375
  }
318
376
  } finally {
319
- receiveBatch.close();
320
- readyBatch.close();
377
+ readyBatch.reset();
321
378
  }
322
379
  }
323
380
 
381
+ private yieldIfNeeded(): Promise<void> | undefined {
382
+ const nowMs = this.nowMs();
383
+ if (this.recordsSinceYield < MESH_DISPATCH_YIELD_RECORDS
384
+ && nowMs - this.yieldStartedAtMs < MESH_DISPATCH_YIELD_INTERVAL_MS) return undefined;
385
+ this.recordsSinceYield = 0;
386
+ return yieldToTimers().then(() => {
387
+ this.yieldStartedAtMs = this.nowMs();
388
+ });
389
+ }
390
+
324
391
  private nowMs(): number {
325
392
  return this.options.monotonicNowMs?.() ?? performance.now();
326
393
  }
@@ -358,6 +425,7 @@ function yieldToEventLoop(): Promise<void> {
358
425
  return new Promise((resolve) => setImmediate(resolve));
359
426
  }
360
427
 
428
+
361
429
  function yieldToTimers(): Promise<void> {
362
430
  return new Promise((resolve) => setTimeout(resolve, 0));
363
431
  }
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  Context,
3
3
  CoreHwmBudgetSnapshot,
4
- Received,
5
4
  TopicMessage
6
5
  } from '@zlink-systems/zlink';
7
6
  import type { ZLinkCoreHwmOptions } from '../../../contracts/Configuration';
@@ -11,6 +10,7 @@ import type {
11
10
  ZLinkBackendDealerSocket,
12
11
  ZLinkBackendPublisherSocket,
13
12
  ZLinkBackendReadablePoller,
13
+ ZLinkBackendReceived,
14
14
  ZLinkBackendRouterSocket,
15
15
  ZLinkBackendSocket,
16
16
  ZLinkBackendSocketMonitor,
@@ -24,18 +24,15 @@ import type {
24
24
  import {
25
25
  closeWithBusyRetry,
26
26
  isContextTerminatedError,
27
- isPollerInterruptedError,
28
27
  zlink,
29
28
  type ZLinkBindingModule
30
29
  } from './node-backend-adapter-support';
31
30
  import { wrapMonitorSocket } from './node-monitor-backend-adapter';
32
- import { wrapSocket } from './node-socket-backend-adapter';
31
+ import { nodeEventLoopPollerOf, wrapSocket } from './node-socket-backend-adapter';
33
32
  import { ZLinkNodeMeshBackendAdapter } from './node-mesh-backend-adapter';
34
33
 
35
- export { isDisconnectRouteNotFoundError } from './node-socket-backend-adapter';
36
-
37
34
  export class ZLinkNodeBackendAdapterFactory implements ZLinkBackendAdapterFactory {
38
- createReceived(): Received {
35
+ createReceived(): ZLinkBackendReceived {
39
36
  return new zlink.Received();
40
37
  }
41
38
 
@@ -70,11 +67,17 @@ class ZLinkNodeChannelBackendAdapter implements ZLinkChannelBackendAdapter {
70
67
  }
71
68
 
72
69
  createDealerSocket(context: ZLinkBackendContext): ZLinkBackendDealerSocket {
73
- return wrapSocket(zlink.createDealerSocket(asNodeContext(context))) as unknown as ZLinkBackendDealerSocket;
70
+ return wrapSocket(
71
+ zlink.createDealerSocket(asNodeContext(context)),
72
+ true
73
+ ) as unknown as ZLinkBackendDealerSocket;
74
74
  }
75
75
 
76
76
  createRouterSocket(context: ZLinkBackendContext): ZLinkBackendRouterSocket {
77
- return wrapSocket(zlink.createRouterSocket(asNodeContext(context))) as unknown as ZLinkBackendRouterSocket;
77
+ return wrapSocket(
78
+ zlink.createRouterSocket(asNodeContext(context)),
79
+ true
80
+ ) as unknown as ZLinkBackendRouterSocket;
78
81
  }
79
82
 
80
83
  createPublisherSocket(context: ZLinkBackendContext): ZLinkBackendPublisherSocket {
@@ -82,7 +85,10 @@ class ZLinkNodeChannelBackendAdapter implements ZLinkChannelBackendAdapter {
82
85
  }
83
86
 
84
87
  createSubscriberSocket(context: ZLinkBackendContext): ZLinkBackendSubscriberSocket {
85
- return wrapSocket(zlink.createSubSocket(asNodeContext(context))) as unknown as ZLinkBackendSubscriberSocket;
88
+ return wrapSocket(
89
+ zlink.createSubSocket(asNodeContext(context)),
90
+ false
91
+ ) as unknown as ZLinkBackendSubscriberSocket;
86
92
  }
87
93
 
88
94
  createReadablePoller(socket: ZLinkBackendSubscriberSocket): ZLinkBackendReadablePoller {
@@ -94,7 +100,7 @@ class ZLinkNodeStreamBackendAdapter implements ZLinkStreamBackendAdapter {
94
100
  createStreamSocket(context: ZLinkBackendContext): ZLinkBackendStreamSocket {
95
101
  const socket = zlink.createStreamSocket(asNodeContext(context));
96
102
  socket.options.recvMode = zlink.StreamRecvMode.Packet;
97
- return wrapSocket(socket) as unknown as ZLinkBackendStreamSocket;
103
+ return wrapSocket(socket, true) as unknown as ZLinkBackendStreamSocket;
98
104
  }
99
105
 
100
106
  createStreamPacket(): ZLinkBackendStreamPacket {
@@ -167,38 +173,63 @@ function asNodeContext(context: ZLinkBackendContext): Context {
167
173
  function createNodeReadablePoller(
168
174
  socket: { readonly nativeInstance: unknown }
169
175
  ): ZLinkBackendReadablePoller {
170
- const poller = zlink.createPoller();
171
- const events = zlink.createPollEvents(1);
172
- try {
173
- poller.add(socket.nativeInstance as never, [zlink.PollEventFlag.PollIn], 0);
174
- } catch (error) {
175
- events.close();
176
- poller.close();
177
- throw error;
178
- }
176
+ const eventLoopPoller = nodeEventLoopPollerOf(socket);
179
177
  let disposed = false;
178
+ let readable = false;
179
+ let pending: {
180
+ readonly promise: Promise<boolean>;
181
+ readonly resolve: (readable: boolean) => void;
182
+ readonly signal?: AbortSignal;
183
+ readonly onAbort?: () => void;
184
+ } | undefined;
185
+
186
+ const settlePending = (value: boolean): void => {
187
+ const current = pending;
188
+ if (current === undefined) return;
189
+ pending = undefined;
190
+ if (current.signal !== undefined && current.onAbort !== undefined) {
191
+ current.signal.removeEventListener('abort', current.onAbort);
192
+ }
193
+ current.resolve(value);
194
+ };
195
+
196
+ eventLoopPoller.setReadableHandler(() => {
197
+ if (disposed) return;
198
+ readable = true;
199
+ settlePending(true);
200
+ });
201
+
180
202
  return {
181
- wait(timeoutMs: number): boolean {
182
- try {
183
- return poller.wait(events, timeoutMs) > 0
184
- && events.hasEvent(0, zlink.PollEventFlag.PollIn);
185
- } catch (error) {
186
- if (isPollerInterruptedError(error)) return false;
187
- throw error;
203
+ wait(_timeoutMs: number): boolean {
204
+ return !disposed && readable;
205
+ },
206
+ waitForReadable(signal?: AbortSignal): Promise<boolean> {
207
+ if (disposed || signal?.aborted === true) return Promise.resolve(false);
208
+ if (readable) return Promise.resolve(true);
209
+ if (pending !== undefined) return pending.promise;
210
+
211
+ let resolvePending!: (value: boolean) => void;
212
+ const promise = new Promise<boolean>((resolve) => {
213
+ resolvePending = resolve;
214
+ });
215
+ const onAbort = signal === undefined
216
+ ? undefined
217
+ : (): void => settlePending(false);
218
+ pending = { promise, resolve: resolvePending, signal, onAbort };
219
+ if (signal !== undefined && onAbort !== undefined) {
220
+ signal.addEventListener('abort', onAbort, { once: true });
188
221
  }
222
+ return promise;
223
+ },
224
+ markDrained(): void {
225
+ readable = false;
189
226
  },
190
227
  dispose(): void {
191
228
  if (disposed) return;
192
229
  disposed = true;
193
- try {
194
- poller.remove(socket.nativeInstance as never);
195
- } finally {
196
- try {
197
- events.close();
198
- } finally {
199
- poller.close();
200
- }
201
- }
230
+ readable = false;
231
+ settlePending(false);
232
+ eventLoopPoller.setReadableHandler(() => {});
202
233
  }
203
234
  };
204
235
  }
@@ -30,7 +30,7 @@ interface ZLinkBindingReplySubmitOperation {
30
30
 
31
31
  interface ZLinkBindingAsyncSendSubmitOperation {
32
32
  message(message: unknown): ZLinkBindingAsyncSendSubmitOperation;
33
- submit(): Promise<void>;
33
+ submit(): import('@zlink-systems/zlink').SendSubmission;
34
34
  submit_sync(): void;
35
35
  }
36
36
 
@@ -41,7 +41,7 @@ export interface ZLinkBindingRequestOperation {
41
41
  interface ZLinkBindingRequestSubmitOperation {
42
42
  message(message: unknown): ZLinkBindingRequestSubmitOperation;
43
43
  timeout(timeoutMs: number): ZLinkBindingRequestSubmitOperation;
44
- submit(): Promise<readonly unknown[]>;
44
+ submit(): import('@zlink-systems/zlink').RequestSubmission;
45
45
  }
46
46
 
47
47
  export function isBindingNotFound(error: unknown): boolean {
@@ -113,7 +113,7 @@ export async function submitBindingAsyncSend(
113
113
  current = current === undefined ? operation.message(nativePart) : current.message(nativePart);
114
114
  }
115
115
  current ??= operation.message(Buffer.alloc(0));
116
- await current.submit();
116
+ await current.submit().admitted;
117
117
  } catch (error) {
118
118
  throw translateBindingResultError(error);
119
119
  }
@@ -156,7 +156,7 @@ export async function submitBindingRequest(
156
156
  if (timeoutMs !== undefined) {
157
157
  current = current.timeout(timeoutMs);
158
158
  }
159
- return await current.submit();
159
+ return await current.submit().reply;
160
160
  } catch (error) {
161
161
  throw translateBindingResultError(error);
162
162
  }