@zlink-systems/framework 0.11.0 → 0.13.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 (119) 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 +5 -1
  32. package/dist/runtime/channels/channel-dispatchers.js +2 -2
  33. package/dist/runtime/channels/channel-envelope.js +324 -121
  34. package/dist/runtime/channels/channel-multipart.d.ts +8 -1
  35. package/dist/runtime/channels/channel-outbound-operations.js +0 -2
  36. package/dist/runtime/channels/channel-receive-loops.d.ts +1 -3
  37. package/dist/runtime/channels/channel-receive-loops.js +25 -65
  38. package/dist/runtime/channels/channel-socket-registry.js +3 -1
  39. package/dist/runtime/channels/channel-transports.d.ts +2 -1
  40. package/dist/runtime/channels/channel-transports.js +12 -10
  41. package/dist/runtime/execution/serial-execution-queue.d.ts +5 -18
  42. package/dist/runtime/execution/serial-execution-queue.js +16 -80
  43. package/dist/runtime/foundation/event-loop-resources.d.ts +0 -3
  44. package/dist/runtime/foundation/event-loop-resources.js +2 -11
  45. package/dist/runtime/foundation/operation-registry.d.ts +2 -8
  46. package/dist/runtime/foundation/operation-registry.js +3 -21
  47. package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +19 -7
  48. package/dist/runtime/foundation/raw-service-mesh-runtime.js +56 -10
  49. package/dist/runtime/foundation/service-mailbox.d.ts +1 -7
  50. package/dist/runtime/foundation/service-mailbox.js +3 -2
  51. package/dist/runtime/foundation/service-runtime-contracts.d.ts +2 -1
  52. package/dist/runtime/foundation/service-stateful-registry.d.ts +1 -1
  53. package/dist/runtime/foundation/service-stateful-registry.js +2 -2
  54. package/dist/runtime/foundation/service-stateful-runtime.js +14 -14
  55. package/dist/runtime/framework-errors-internal.d.ts +4 -4
  56. package/dist/runtime/framework-errors-internal.js +8 -10
  57. package/dist/runtime/host/actor-placement-coordinator.js +1 -1
  58. package/dist/runtime/host/index.js +1 -1
  59. package/dist/runtime/host/service-relocation-host-runtime.js +0 -4
  60. package/dist/runtime/host/spot-address-transport.js +1 -1
  61. package/dist/runtime/host/user-spot-creation-coordinator.js +1 -1
  62. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +0 -12
  63. package/dist/runtime/spots/spot-node-runtime-manager.js +6 -166
  64. package/dist/runtime/spots/spot-serial-turn-executor.js +1 -4
  65. package/dist/runtime/spots/spot-timer.d.ts +3 -0
  66. package/dist/runtime/spots/spot-timer.js +87 -18
  67. package/dist/runtime/streams/session-serial-executor.js +1 -3
  68. package/dist/runtime/streams/stream-session-runtime.d.ts +2 -2
  69. package/dist/runtime/streams/stream-session-runtime.js +13 -19
  70. package/dist/runtime/workers/index.d.ts +0 -1
  71. package/dist/runtime/workers/index.js +1 -9
  72. package/package.json +3 -3
  73. package/src/contracts/Configuration/EndpointNotation.ts +5 -4
  74. package/src/contracts/Configuration/InternalDefaults.ts +0 -1
  75. package/src/contracts/Configuration/RegistrationNormalizers.ts +2 -4
  76. package/src/contracts/Configuration/RegistrationTypes.ts +2 -5
  77. package/src/contracts/Configuration/RegistrationValidators.ts +0 -1
  78. package/src/contracts/Errors/ZLinkFrameworkException.ts +6 -7
  79. package/src/runtime/actors/actor-client.ts +1 -1
  80. package/src/runtime/actors/actor-mailbox.ts +1 -12
  81. package/src/runtime/backend/contracts/index.ts +2 -0
  82. package/src/runtime/backend/mesh-completion-table.ts +12 -23
  83. package/src/runtime/backend/mesh-dispatch-pump.ts +206 -138
  84. package/src/runtime/backend/node/node-backend-adapter-factory.ts +66 -35
  85. package/src/runtime/backend/node/node-backend-adapter-support.ts +4 -4
  86. package/src/runtime/backend/node/node-dealer-channel-client-transport.ts +2 -2
  87. package/src/runtime/backend/node/node-event-loop-poller.ts +55 -0
  88. package/src/runtime/backend/node/node-raw-binding-port.ts +30 -45
  89. package/src/runtime/backend/node/node-raw-mesh-backend.ts +97 -110
  90. package/src/runtime/backend/node/node-socket-backend-adapter.ts +23 -69
  91. package/src/runtime/backend/raw-binding-port.ts +1 -0
  92. package/src/runtime/backend/runtime-values.ts +7 -1
  93. package/src/runtime/channels/channel-dispatchers.ts +2 -2
  94. package/src/runtime/channels/channel-envelope.ts +379 -109
  95. package/src/runtime/channels/channel-multipart.ts +9 -1
  96. package/src/runtime/channels/channel-outbound-operations.ts +0 -1
  97. package/src/runtime/channels/channel-receive-loops.ts +24 -75
  98. package/src/runtime/channels/channel-socket-registry.ts +4 -1
  99. package/src/runtime/channels/channel-transports.ts +20 -10
  100. package/src/runtime/execution/serial-execution-queue.ts +16 -108
  101. package/src/runtime/foundation/event-loop-resources.ts +2 -11
  102. package/src/runtime/foundation/operation-registry.ts +2 -22
  103. package/src/runtime/foundation/raw-service-mesh-runtime.ts +74 -19
  104. package/src/runtime/foundation/service-mailbox.ts +3 -13
  105. package/src/runtime/foundation/service-runtime-contracts.ts +2 -1
  106. package/src/runtime/foundation/service-stateful-registry.ts +2 -2
  107. package/src/runtime/foundation/service-stateful-runtime.ts +14 -14
  108. package/src/runtime/framework-errors-internal.ts +8 -10
  109. package/src/runtime/host/actor-placement-coordinator.ts +1 -1
  110. package/src/runtime/host/index.ts +2 -1
  111. package/src/runtime/host/service-relocation-host-runtime.ts +0 -4
  112. package/src/runtime/host/spot-address-transport.ts +1 -1
  113. package/src/runtime/host/user-spot-creation-coordinator.ts +1 -1
  114. package/src/runtime/spots/spot-node-runtime-manager.ts +13 -233
  115. package/src/runtime/spots/spot-serial-turn-executor.ts +1 -8
  116. package/src/runtime/spots/spot-timer.ts +109 -18
  117. package/src/runtime/streams/session-serial-executor.ts +1 -7
  118. package/src/runtime/streams/stream-session-runtime.ts +13 -17
  119. package/src/runtime/workers/index.ts +2 -17
@@ -104,16 +104,13 @@ export interface ZLinkLocationRegistration {
104
104
  /**
105
105
  * CPU worker-thread pool settings. I/O workers do not consume this pool.
106
106
  * `minThreads` keeps a warm baseline, `maxThreads` bounds active CPU jobs,
107
- * `idleTimeoutMs` reclaims workers above the baseline, and `maxQueueLength`
108
- * bounds queued jobs. The registration normalizer supplies defaults when a
109
- * JavaScript caller omits a field: `0`, `max(2, availableParallelism())`,
110
- * `30000`, and `1024`, respectively.
107
+ * `idleTimeoutMs` reclaims workers above the baseline. The registration
108
+ * normalizer supplies defaults when a JavaScript caller omits a field.
111
109
  */
112
110
  export interface ZLinkWorkerOptions {
113
111
  readonly minThreads: number;
114
112
  readonly maxThreads: number;
115
113
  readonly idleTimeoutMs: number;
116
- readonly maxQueueLength: number;
117
114
  }
118
115
 
119
116
  export interface ZLinkCodecSerializerRegistration {
@@ -97,7 +97,6 @@ function validateWorkerOptions(worker: ZLinkWorkerOptions | undefined): void {
97
97
  requireNonNegativeInteger('Worker minThreads', worker.minThreads);
98
98
  requirePositiveInteger('Worker maxThreads', worker.maxThreads);
99
99
  requireNonNegativeInteger('Worker idleTimeoutMs', worker.idleTimeoutMs);
100
- requirePositiveInteger('Worker maxQueueLength', worker.maxQueueLength);
101
100
  if (worker.maxThreads < worker.minThreads) {
102
101
  throw new ZLinkConfigurationException(
103
102
  'Worker maxThreads must be greater than or equal to minThreads.'
@@ -6,13 +6,12 @@ export enum ZLinkFrameworkErrorKind {
6
6
  NotConfigured = 3,
7
7
  Rejected = 4,
8
8
  Unavailable = 5,
9
- CapacityExceeded = 6,
10
- DeadlineExceeded = 7,
11
- ShuttingDown = 8,
12
- ProtocolError = 9,
13
- InvalidOperation = 10,
14
- DataLost = 11,
15
- InternalFailure = 12
9
+ DeadlineExceeded = 6,
10
+ ShuttingDown = 7,
11
+ ProtocolError = 8,
12
+ InvalidOperation = 9,
13
+ DataLost = 10,
14
+ InternalFailure = 11
16
15
  }
17
16
 
18
17
  export class ZLinkFrameworkException extends Error {
@@ -916,7 +916,7 @@ function submitted(): ZLinkSubmitResult {
916
916
  // the reply comes from a remote target, so a fine framework failure code
917
917
  // refines the coarse terminal, and a terminal-only conflict/busy is the
918
918
  // target's owner/queue state (retryable stale/Unavailable), never a
919
- // source-owned CapacityExceeded. Backpressure never appears on this reply
919
+ // source-owned queue exhaustion. Backpressure never appears on this reply
920
920
  // path. Internal kinds are preserved so the stale-actor re-resolve retry
921
921
  // (isStaleActorError) keeps working.
922
922
  function actorFailureCodeKind(
@@ -5,10 +5,6 @@ import {
5
5
  type ZLinkSerialWorkOptions,
6
6
  type ZLinkSerialWorkRecord
7
7
  } from '../execution/serial-execution-queue';
8
- import {
9
- ZLinkFrameworkInternalErrorKind,
10
- createInternalFrameworkException
11
- } from '../framework-errors-internal';
12
8
  import { runZLinkActorExecution } from './actor-execution-context';
13
9
  import {
14
10
  bindApplicationJobPermit,
@@ -25,14 +21,7 @@ export class ZLinkActorSerialExecutor {
25
21
  ) {
26
22
  this.scheduler = new ZLinkSerialExecutionQueue(
27
23
  (record) => this.runRecord(record),
28
- {
29
- ...options,
30
- capacityError: options?.capacityError ?? (() =>
31
- createInternalFrameworkException(
32
- ZLinkFrameworkInternalErrorKind.WorkerQueueFull,
33
- 'Actor execution queue capacity was exceeded.'
34
- ))
35
- }
24
+ options
36
25
  );
37
26
  }
38
27
 
@@ -578,6 +578,8 @@ export interface ZLinkBackendSubscriberSocket extends ZLinkBackendConnectableSoc
578
578
 
579
579
  export interface ZLinkBackendReadablePoller {
580
580
  wait(timeoutMs: number): boolean;
581
+ waitForReadable(signal?: AbortSignal): Promise<boolean>;
582
+ markDrained(): void;
581
583
  dispose(): void;
582
584
  }
583
585
 
@@ -1,16 +1,11 @@
1
1
  import { ZLinkBufferMessage as Message } from './runtime-message';
2
+ import type { Message as ZLinkMessage } from '../../contracts/Common/Message';
2
3
  import type {
3
4
  MeshOperationId,
4
5
  ReceiveKindData,
5
6
  ReceiveRecord
6
7
  } from '../foundation/service-runtime-contracts';
7
8
  import { operationIdentityKey } from '../foundation/operation-identity';
8
- import {
9
- ZLinkFrameworkErrorKind,
10
- ZLinkFrameworkException
11
- } from '../../contracts';
12
-
13
- export const ZLINK_MESH_COMPLETION_CAPACITY = 4_096;
14
9
 
15
10
  export interface ZLinkMeshCompletionDiagnostic {
16
11
  readonly kind: 'unknownOrLate';
@@ -36,14 +31,7 @@ export class ZLinkMeshCompletionTable {
36
31
  private readonly pending = new Map<string, PendingCompletion>();
37
32
  private disposed = false;
38
33
 
39
- constructor(
40
- private readonly maxPendingOperations = ZLINK_MESH_COMPLETION_CAPACITY,
41
- private readonly onDiagnostic?: (diagnostic: ZLinkMeshCompletionDiagnostic) => void
42
- ) {
43
- if (!Number.isSafeInteger(maxPendingOperations) || maxPendingOperations <= 0) {
44
- throw new RangeError('maxPendingOperations must be a positive safe integer.');
45
- }
46
- }
34
+ constructor(private readonly onDiagnostic?: (diagnostic: ZLinkMeshCompletionDiagnostic) => void) {}
47
35
 
48
36
  /**
49
37
  * Submits and registers without yielding control to the event loop. Mesh
@@ -63,12 +51,6 @@ export class ZLinkMeshCompletionTable {
63
51
  signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
64
52
  );
65
53
  }
66
- if (this.pending.size >= this.maxPendingOperations) {
67
- return Promise.reject(new ZLinkFrameworkException(
68
- ZLinkFrameworkErrorKind.CapacityExceeded,
69
- `Mesh completion capacity ${this.maxPendingOperations} is exhausted.`
70
- ));
71
- }
72
54
  const operationId = operation();
73
55
  // The backend submission callback can synchronously re-enter `dispose()`.
74
56
  // TypeScript's control-flow analysis cannot observe mutation through it.
@@ -117,7 +99,7 @@ export class ZLinkMeshCompletionTable {
117
99
  this.pending.delete(key);
118
100
  pending.removeAbort?.();
119
101
  try {
120
- pending.resolve(copyCompletion(record));
102
+ pending.resolve(retainCompletion(record));
121
103
  } catch (error) {
122
104
  pending.reject(error);
123
105
  }
@@ -144,16 +126,23 @@ export class ZLinkMeshCompletionTable {
144
126
 
145
127
  }
146
128
 
147
- function copyCompletion(record: ReceiveRecord): ZLinkMeshCompletion {
129
+ function retainCompletion(record: ReceiveRecord): ZLinkMeshCompletion {
148
130
  return {
149
131
  terminalResult: record.terminalResult,
150
132
  failureErrno: record.failureErrno,
151
133
  operationKind: record.operationKind,
152
134
  kindData: record.kindData,
153
- parts: record.parts.map((part) => Message.fromOwned(Buffer.from(part.data())))
135
+ parts: record.parts.map(retainCompletionPart)
154
136
  };
155
137
  }
156
138
 
139
+ function retainCompletionPart(part: ZLinkMessage): Message {
140
+ // A runtime message already owns a managed Buffer and close() is a no-op.
141
+ // Binding-native messages can expose native storage through data(), which
142
+ // becomes invalid when their owner closes; materialize that one boundary copy.
143
+ return part instanceof Message ? part : Message.fromOwned(Buffer.from(part.data()));
144
+ }
145
+
157
146
  export function closeMeshCompletion(completion: ZLinkMeshCompletion): void {
158
147
  closeParts(completion.parts);
159
148
  }
@@ -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
  }