@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
@@ -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[] {
@@ -1,3 +1,4 @@
1
+ import type { Socket } from '@zlink-systems/zlink';
1
2
  import type { ZLinkBackendObject } from '../contracts';
2
3
  import {
3
4
  closeWithBusyRetry,
@@ -17,14 +18,14 @@ import {
17
18
  type ZLinkBindingReplyOperation,
18
19
  type ZLinkBindingAsyncSendOperation
19
20
  } from './node-backend-adapter-support';
21
+ import { ZLinkNodeEventLoopPoller } from './node-event-loop-poller';
20
22
 
23
+ const eventLoopPollers = new WeakMap<object, ZLinkNodeEventLoopPoller>();
21
24
 
22
25
  export function wrapSocket<T extends { close(): void }>(
23
- nativeInstance: T
26
+ nativeInstance: T,
27
+ pollCompletion?: boolean
24
28
  ): T & ZLinkBackendObject {
25
- const boundEndpoints = new Set<string>();
26
- const connectedEndpoints = new Set<string>();
27
- const peerRoutingIds = new Set<unknown>();
28
29
  const socket = nativeInstance as T & {
29
30
  options?: {
30
31
  probe?: boolean;
@@ -40,32 +41,35 @@ export function wrapSocket<T extends { close(): void }>(
40
41
  const hasRoutedPeer = hasRequest
41
42
  && typeof (nativeInstance as { reply?: unknown }).reply === 'function';
42
43
  const hasStream = typeof (nativeInstance as { recvPacket?: unknown }).recvPacket === 'function';
44
+ const eventLoopPoller = pollCompletion === undefined
45
+ ? undefined
46
+ : new ZLinkNodeEventLoopPoller(
47
+ nativeInstance as unknown as Socket,
48
+ pollCompletion,
49
+ () => {}
50
+ );
43
51
  const adapter = {
44
52
  nativeInstance,
45
53
  async dispose(): Promise<void> {
54
+ eventLoopPoller?.dispose();
46
55
  disableSocketLinger(nativeInstance);
47
- closeSocketRoutes(nativeInstance, peerRoutingIds);
48
- closeSocketEndpoints(nativeInstance, boundEndpoints, connectedEndpoints);
49
56
  await closeWithBusyRetry(nativeInstance);
50
57
  },
51
58
  close(): void {
59
+ eventLoopPoller?.dispose();
52
60
  nativeInstance.close();
53
61
  },
54
62
  bind(endpoint: string): void {
55
63
  (nativeInstance as T & { bind(endpoint: string): void }).bind(endpoint);
56
- boundEndpoints.add(endpoint);
57
64
  },
58
65
  unbind(endpoint: string): void {
59
66
  (nativeInstance as T & { unbind(endpoint: string): void }).unbind(endpoint);
60
- boundEndpoints.delete(endpoint);
61
67
  },
62
68
  connect(endpoint: string): void {
63
69
  (nativeInstance as T & { connect(endpoint: string): void }).connect(endpoint);
64
- connectedEndpoints.add(endpoint);
65
70
  },
66
71
  disconnect(endpoint: string): void {
67
72
  (nativeInstance as T & { disconnect(endpoint: string): void }).disconnect(endpoint);
68
- connectedEndpoints.delete(endpoint);
69
73
  },
70
74
  setChannelName(channelName: string): void {
71
75
  const setChannelName = (nativeInstance as T & { setChannelName?: (value: string) => void }).setChannelName;
@@ -133,7 +137,6 @@ export function wrapSocket<T extends { close(): void }>(
133
137
  send(...args: unknown[]): unknown {
134
138
  if (hasStream) {
135
139
  const [routingId, payload] = args as [unknown, unknown];
136
- peerRoutingIds.add(routingId);
137
140
  const operation = (nativeInstance as T & {
138
141
  send(routingId: unknown): ZLinkBindingAsyncSendOperation;
139
142
  }).send(toNativeRoutingId(routingId));
@@ -142,7 +145,6 @@ export function wrapSocket<T extends { close(): void }>(
142
145
  }
143
146
  if (hasRoutedPeer) {
144
147
  const [routingId, payload] = args as [unknown, unknown];
145
- peerRoutingIds.add(routingId);
146
148
  return submitBindingAsyncSend(
147
149
  (nativeInstance as T & { send(routingId: unknown): ZLinkBindingAsyncSendOperation })
148
150
  .send(toNativeRoutingId(routingId)),
@@ -167,7 +169,6 @@ export function wrapSocket<T extends { close(): void }>(
167
169
  },
168
170
  submit(routingId: unknown, payload: unknown, _timeoutMs?: number): Promise<void> {
169
171
  if (!hasStream) throw new TypeError('Async stream send requires a STREAM socket.');
170
- peerRoutingIds.add(routingId);
171
172
  return submitBindingAsyncSend(
172
173
  (nativeInstance as T & { send(routingId: unknown): ZLinkBindingAsyncSendOperation })
173
174
  .send(toNativeRoutingId(routingId)),
@@ -177,7 +178,6 @@ export function wrapSocket<T extends { close(): void }>(
177
178
  request(...args: unknown[]): unknown {
178
179
  if (hasRoutedPeer) {
179
180
  const [routingId, payload, timeoutMs] = args as [unknown, unknown, number | undefined];
180
- peerRoutingIds.add(routingId);
181
181
  return submitBindingRequest(
182
182
  (nativeInstance as T & { request(routingId: unknown): ZLinkBindingRequestOperation })
183
183
  .request(toNativeRoutingId(routingId)),
@@ -197,7 +197,6 @@ export function wrapSocket<T extends { close(): void }>(
197
197
  },
198
198
  reply(...args: unknown[]): unknown {
199
199
  const [routingId, replyToken, payload] = args as [unknown, unknown, unknown];
200
- peerRoutingIds.add(routingId);
201
200
  const operation = (nativeInstance as T & {
202
201
  reply(routingId: unknown, replyToken: unknown): ZLinkBindingReplyOperation;
203
202
  }).reply(toNativeRoutingId(routingId), replyToken);
@@ -287,9 +286,18 @@ export function wrapSocket<T extends { close(): void }>(
287
286
  return false;
288
287
  }
289
288
  };
289
+ if (eventLoopPoller !== undefined) eventLoopPollers.set(adapter, eventLoopPoller);
290
290
  return adapter as unknown as T & ZLinkBackendObject;
291
291
  }
292
292
 
293
+ export function nodeEventLoopPollerOf(socket: object): ZLinkNodeEventLoopPoller {
294
+ const poller = eventLoopPollers.get(socket);
295
+ if (poller === undefined) {
296
+ throw new TypeError('Node backend socket does not own an event-loop poller.');
297
+ }
298
+ return poller;
299
+ }
300
+
293
301
  export function disconnectStreamPeer(
294
302
  socket: { disconnectRid(routingId: unknown): void },
295
303
  routingId: unknown
@@ -310,60 +318,6 @@ function requireSocketOptions<TOptions>(socket: { readonly options?: TOptions })
310
318
  return socket.options;
311
319
  }
312
320
 
313
- function closeSocketRoutes(target: unknown, peerRoutingIds: Set<unknown>): void {
314
- if (peerRoutingIds.size === 0 || !hasDisconnectRid(target)) {
315
- return;
316
- }
317
- for (const routingId of peerRoutingIds) {
318
- try {
319
- target.disconnectRid(toNativeRoutingId(routingId));
320
- } catch (error) {
321
- if (!isDisconnectRouteNotFoundError(error)) {
322
- throw error;
323
- }
324
- }
325
- peerRoutingIds.delete(routingId);
326
- }
327
- }
328
-
329
- export function isDisconnectRouteNotFoundError(error: unknown): boolean {
330
- return isBindingNotFound(error) || isContextTerminatedError(error) || (
331
- error instanceof Error && 'code' in error &&
332
- ((error as { code: unknown }).code === zlink.ConnectResult.NotFound ||
333
- (error as { code: unknown }).code === zlink.ConnectResult.Busy ||
334
- ((error as { code: unknown }).code === zlink.ConnectResult.InternalError &&
335
- /current state/i.test(error.message)))
336
- );
337
- }
338
-
339
- function hasDisconnectRid(target: unknown): target is { disconnectRid(routingId: unknown): void } {
340
- return target !== null && typeof target === 'object' && 'disconnectRid' in target &&
341
- typeof (target as { disconnectRid: unknown }).disconnectRid === 'function';
342
- }
343
-
344
- function closeSocketEndpoints(target: unknown, boundEndpoints: Set<string>, connectedEndpoints: Set<string>): void {
345
- for (const endpoint of connectedEndpoints) {
346
- try {
347
- (target as { disconnect(endpoint: string): void }).disconnect(endpoint);
348
- } catch (error) {
349
- if (!isEndpointCloseIgnorableError(error)) {
350
- throw error;
351
- }
352
- }
353
- connectedEndpoints.delete(endpoint);
354
- }
355
- for (const endpoint of boundEndpoints) {
356
- try {
357
- (target as { unbind(endpoint: string): void }).unbind(endpoint);
358
- } catch (error) {
359
- if (!isEndpointCloseIgnorableError(error)) {
360
- throw error;
361
- }
362
- }
363
- boundEndpoints.delete(endpoint);
364
- }
365
- }
366
-
367
321
  export function isEndpointCloseIgnorableError(error: unknown): boolean {
368
322
  return isContextTerminatedError(error) || (
369
323
  error instanceof Error && 'code' in error &&
@@ -21,6 +21,7 @@ export interface ZLinkRawMonitorRecord {
21
21
  }
22
22
 
23
23
  export interface ZLinkRawSocketPort {
24
+ setReadableHandler(handler: () => void): void;
24
25
  bind(endpoint: string): void;
25
26
  unbind(endpoint: string): void;
26
27
  connect(endpoint: string): void;
@@ -75,9 +75,15 @@ export function isBackendRequestTimeoutError(error: unknown): boolean {
75
75
  && error.result === RequestResult.TimedOut;
76
76
  }
77
77
  export type ZLinkBackendMessageLike = Message | Buffer | Uint8Array | string;
78
+ /** Binding의 SendSubmission과 구조적으로 호환된다. binding 타입을 import 하지 않는다. */
79
+ export interface ZLinkBackendSendSubmission {
80
+ readonly admitted: Promise<void>;
81
+ }
82
+
78
83
  export interface ZLinkBackendSendSubmitBuilder {
79
84
  message(message: ZLinkBackendMessageLike): ZLinkBackendSendSubmitBuilder;
80
- submit(): Promise<void>;
85
+ // 제출 스냅샷을 그대로 돌려준다. admission을 기다리려면 .admitted를 await 한다.
86
+ submit(): ZLinkBackendSendSubmission;
81
87
  }
82
88
 
83
89
  export interface ZLinkBackendSendBuilder {
@@ -115,7 +115,7 @@ export class ZLinkChannelRequestDispatcher {
115
115
  if (received.send === undefined) {
116
116
  throw new ZLinkConfigurationException('Routed SPOT packet is missing a local SPOT delivery context.');
117
117
  }
118
- await appendParts(received.send(), received.parts).submit();
118
+ await appendParts(received.send(), received.parts).submit().admitted;
119
119
  return true;
120
120
  }
121
121
  if (received.parts.length === 0 || received.parts[0].data().length === 0) {
@@ -523,7 +523,7 @@ export class ZLinkRoutePacketDispatcher {
523
523
  if (received.send === undefined) {
524
524
  throw new ZLinkConfigurationException('Routed SPOT packet is missing a local SPOT delivery context.');
525
525
  }
526
- return appendParts(received.send(), received.parts).submit();
526
+ return appendParts(received.send(), received.parts).submit().admitted;
527
527
  }
528
528
  return false;
529
529
  }