@zlink-systems/framework 0.30.0 → 0.30.1

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 (245) hide show
  1. package/LICENSE +150 -68
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/contracts/Actors/ZLinkActorClient.d.ts +2 -1
  4. package/dist/contracts/Channels/Calls.d.ts +2 -1
  5. package/dist/contracts/Common/ZLinkMessageMetadata.js +11 -5
  6. package/dist/contracts/Handlers/Attributes.d.ts +5 -13
  7. package/dist/contracts/Handlers/Attributes.js +7 -11
  8. package/dist/contracts/Handlers/JsonContract.d.ts +2 -46
  9. package/dist/contracts/Handlers/JsonContract.js +4 -153
  10. package/dist/contracts/Spots/Contracts.d.ts +2 -1
  11. package/dist/nest-integration.d.ts +1 -1
  12. package/dist/nest-integration.js +2 -1
  13. package/dist/runtime/actors/actor-client.d.ts +1 -1
  14. package/dist/runtime/actors/actor-client.js +14 -12
  15. package/dist/runtime/actors/actor-handoff.d.ts +1 -1
  16. package/dist/runtime/actors/actor-handoff.js +7 -4
  17. package/dist/runtime/actors/actor-join-relocation.d.ts +1 -0
  18. package/dist/runtime/actors/actor-local-native-join.js +9 -12
  19. package/dist/runtime/actors/actor-remote-wire.js +15 -0
  20. package/dist/runtime/actors/actor-routed-join-transport.d.ts +6 -2
  21. package/dist/runtime/actors/actor-routed-json-request.d.ts +2 -0
  22. package/dist/runtime/actors/index.d.ts +2 -1
  23. package/dist/runtime/actors/index.js +18 -6
  24. package/dist/runtime/actors/spot-actor-dispatch.js +2 -2
  25. package/dist/runtime/admission.d.ts +1 -1
  26. package/dist/runtime/admission.js +15 -5
  27. package/dist/runtime/application-jobs/application-ingress-record-owner.d.ts +6 -8
  28. package/dist/runtime/application-jobs/application-ingress-record-owner.js +39 -22
  29. package/dist/runtime/application-jobs/application-job-queue-scope.d.ts +2 -1
  30. package/dist/runtime/application-jobs/application-job-queue-scope.js +37 -22
  31. package/dist/runtime/application-jobs/contracts.d.ts +10 -1
  32. package/dist/runtime/backend/contracts/index.d.ts +18 -9
  33. package/dist/runtime/backend/mesh-completion-table.d.ts +9 -1
  34. package/dist/runtime/backend/mesh-completion-table.js +149 -49
  35. package/dist/runtime/backend/mesh-dispatch-pump.d.ts +3 -0
  36. package/dist/runtime/backend/mesh-dispatch-pump.js +32 -11
  37. package/dist/runtime/backend/node/node-backend-adapter-support.d.ts +2 -2
  38. package/dist/runtime/backend/node/node-backend-adapter-support.js +16 -9
  39. package/dist/runtime/backend/node/node-dealer-channel-client-transport.d.ts +1 -1
  40. package/dist/runtime/backend/node/node-dealer-channel-client-transport.js +5 -2
  41. package/dist/runtime/backend/node/node-raw-binding-port.js +42 -22
  42. package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +13 -5
  43. package/dist/runtime/backend/node/node-raw-mesh-backend.js +69 -51
  44. package/dist/runtime/backend/node/node-socket-backend-adapter.js +6 -6
  45. package/dist/runtime/backend/raw-binding-port.d.ts +2 -2
  46. package/dist/runtime/backend/runtime-message.d.ts +1 -0
  47. package/dist/runtime/backend/runtime-message.js +5 -0
  48. package/dist/runtime/backend/runtime-values.d.ts +1 -0
  49. package/dist/runtime/backend/runtime-values.js +2 -1
  50. package/dist/runtime/channels/channel-clients.d.ts +1 -0
  51. package/dist/runtime/channels/channel-clients.js +58 -21
  52. package/dist/runtime/channels/channel-dispatch-pipeline.d.ts +3 -2
  53. package/dist/runtime/channels/channel-dispatch-pipeline.js +20 -11
  54. package/dist/runtime/channels/channel-dispatch-services.d.ts +1 -0
  55. package/dist/runtime/channels/channel-dispatch-services.js +45 -0
  56. package/dist/runtime/channels/channel-dispatchers.d.ts +1 -1
  57. package/dist/runtime/channels/channel-dispatchers.js +27 -26
  58. package/dist/runtime/channels/channel-envelope-inspection.d.ts +1 -2
  59. package/dist/runtime/channels/channel-envelope-inspection.js +1 -16
  60. package/dist/runtime/channels/channel-envelope.d.ts +1 -0
  61. package/dist/runtime/channels/channel-envelope.js +83 -8
  62. package/dist/runtime/channels/channel-outbound-operations.d.ts +2 -2
  63. package/dist/runtime/channels/channel-outbound-operations.js +8 -8
  64. package/dist/runtime/channels/channel-runtime-manager.d.ts +6 -6
  65. package/dist/runtime/channels/channel-runtime-manager.js +12 -12
  66. package/dist/runtime/channels/channel-transports.d.ts +18 -15
  67. package/dist/runtime/channels/channel-transports.js +48 -31
  68. package/dist/runtime/channels/local-spot-route-transport.d.ts +1 -1
  69. package/dist/runtime/channels/local-spot-route-transport.js +2 -1
  70. package/dist/runtime/channels/router-socket-spot-route-transport.d.ts +2 -2
  71. package/dist/runtime/channels/router-socket-spot-route-transport.js +4 -4
  72. package/dist/runtime/channels/source-spot-router.d.ts +2 -2
  73. package/dist/runtime/channels/source-spot-router.js +4 -4
  74. package/dist/runtime/channels/spot-node-route-transport.d.ts +2 -2
  75. package/dist/runtime/channels/spot-node-route-transport.js +4 -4
  76. package/dist/runtime/channels/spot-route-bridge-transport.d.ts +2 -2
  77. package/dist/runtime/channels/spot-route-bridge-transport.js +4 -4
  78. package/dist/runtime/channels/spot-route-dispatch-strategy.d.ts +5 -4
  79. package/dist/runtime/channels/spot-route-dispatch-strategy.js +18 -18
  80. package/dist/runtime/execution/serial-execution-queue.d.ts +1 -0
  81. package/dist/runtime/execution/serial-execution-queue.js +1 -0
  82. package/dist/runtime/foundation/actor-join-recovery-codec.js +2 -2
  83. package/dist/runtime/foundation/durable-service-request.d.ts +14 -0
  84. package/dist/runtime/foundation/durable-service-request.js +98 -0
  85. package/dist/runtime/foundation/operation-identity.d.ts +1 -0
  86. package/dist/runtime/foundation/operation-identity.js +5 -1
  87. package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +22 -5
  88. package/dist/runtime/foundation/raw-service-mesh-runtime.js +356 -232
  89. package/dist/runtime/foundation/service-relocation-object-owner.d.ts +1 -0
  90. package/dist/runtime/foundation/service-relocation-object-owner.js +1 -0
  91. package/dist/runtime/foundation/service-relocation-runtime.d.ts +2 -0
  92. package/dist/runtime/foundation/service-relocation-runtime.js +13 -1
  93. package/dist/runtime/foundation/service-runtime-contracts.d.ts +2 -0
  94. package/dist/runtime/foundation/service-stateful-runtime.d.ts +4 -4
  95. package/dist/runtime/foundation/service-stateful-runtime.js +44 -103
  96. package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +0 -5
  97. package/dist/runtime/foundation/service-topology-registry.d.ts +12 -2
  98. package/dist/runtime/foundation/service-topology-registry.js +61 -27
  99. package/dist/runtime/foundation/service-wire-m6a-codec.d.ts +3 -3
  100. package/dist/runtime/foundation/service-wire-m6a-codec.js +36 -38
  101. package/dist/runtime/handlers/handler-instance-scope.d.ts +8 -0
  102. package/dist/runtime/handlers/handler-instance-scope.js +54 -11
  103. package/dist/runtime/host/actor-runtime-options-factory.js +2 -2
  104. package/dist/runtime/host/application-job-queue.d.ts +19 -7
  105. package/dist/runtime/host/application-job-queue.js +150 -42
  106. package/dist/runtime/host/entry-actor-runtime.d.ts +1 -0
  107. package/dist/runtime/host/entry-actor-runtime.js +3 -0
  108. package/dist/runtime/host/index.d.ts +7 -1
  109. package/dist/runtime/host/index.js +142 -118
  110. package/dist/runtime/host/remote-bound-session-relay.js +2 -18
  111. package/dist/runtime/host/route-mesh-runtime.js +1 -1
  112. package/dist/runtime/host/service-relocation-control.d.ts +1 -0
  113. package/dist/runtime/host/service-relocation-control.js +10 -4
  114. package/dist/runtime/host/service-relocation-host-runtime.d.ts +6 -8
  115. package/dist/runtime/host/service-relocation-host-runtime.js +213 -241
  116. package/dist/runtime/host/spot-address-transport.js +3 -2
  117. package/dist/runtime/host/spot-runtime-options-factory.js +1 -0
  118. package/dist/runtime/locations/actor-location-claims.d.ts +2 -2
  119. package/dist/runtime/locations/actor-location-claims.js +34 -19
  120. package/dist/runtime/locations/lifecycle.d.ts +1 -2
  121. package/dist/runtime/locations/lifecycle.js +2 -5
  122. package/dist/runtime/locations/runtime.d.ts +1 -1
  123. package/dist/runtime/locations/runtime.js +24 -23
  124. package/dist/runtime/messaging/framework-json-v1.d.ts +4 -6
  125. package/dist/runtime/messaging/framework-json-v1.js +38 -294
  126. package/dist/runtime/messaging/packet-name.d.ts +1 -1
  127. package/dist/runtime/messaging/packet-name.js +8 -24
  128. package/dist/runtime/messaging/request-result.d.ts +2 -0
  129. package/dist/runtime/messaging/request-result.js +54 -0
  130. package/dist/runtime/messaging/submission-result.d.ts +1 -0
  131. package/dist/runtime/messaging/submission-result.js +5 -2
  132. package/dist/runtime/protocol/service_wire_codec.generated.d.ts +15 -0
  133. package/dist/runtime/protocol/service_wire_codec.generated.js +144 -0
  134. package/dist/runtime/protocol/service_wire_pilot_codec.generated.d.ts +9 -0
  135. package/dist/runtime/protocol/service_wire_pilot_codec.generated.js +14 -1
  136. package/dist/runtime/spots/formal-remote-actor-admission-registry.d.ts +7 -2
  137. package/dist/runtime/spots/formal-remote-actor-admission-registry.js +33 -19
  138. package/dist/runtime/spots/index.d.ts +3 -0
  139. package/dist/runtime/spots/index.js +9 -2
  140. package/dist/runtime/spots/spot-activation.d.ts +1 -0
  141. package/dist/runtime/spots/spot-activation.js +17 -8
  142. package/dist/runtime/spots/spot-entry-activation.js +6 -2
  143. package/dist/runtime/spots/spot-handler-registry.d.ts +3 -1
  144. package/dist/runtime/spots/spot-handler-registry.js +18 -8
  145. package/dist/runtime/spots/spot-node-autoconnect.js +4 -3
  146. package/dist/runtime/spots/spot-outbound.d.ts +3 -0
  147. package/dist/runtime/spots/spot-outbound.js +24 -15
  148. package/dist/runtime/spots/spot-routed-spot-packet-dispatch.d.ts +1 -0
  149. package/dist/runtime/spots/spot-routed-spot-packet-dispatch.js +1 -1
  150. package/dist/runtime/spots/spot-runtime-ports.d.ts +1 -0
  151. package/dist/runtime/spots/spot-subscription-dispatch.js +0 -3
  152. package/dist/runtime/spots/spot-timer.js +1 -0
  153. package/dist/runtime/streams/stream-session-runtime.d.ts +1 -1
  154. package/dist/runtime/streams/stream-session-runtime.js +13 -27
  155. package/package.json +2 -2
  156. package/src/contracts/Actors/ZLinkActorClient.ts +2 -1
  157. package/src/contracts/Channels/Calls.ts +2 -1
  158. package/src/contracts/Common/ZLinkMessageMetadata.ts +13 -8
  159. package/src/contracts/Handlers/Attributes.ts +14 -21
  160. package/src/contracts/Handlers/JsonContract.ts +8 -216
  161. package/src/contracts/Spots/Contracts.ts +2 -1
  162. package/src/nest-integration.ts +1 -0
  163. package/src/runtime/actors/actor-client.ts +40 -16
  164. package/src/runtime/actors/actor-handoff.ts +9 -4
  165. package/src/runtime/actors/actor-join-relocation.ts +1 -0
  166. package/src/runtime/actors/actor-local-native-join.ts +17 -26
  167. package/src/runtime/actors/actor-remote-wire.ts +17 -0
  168. package/src/runtime/actors/actor-routed-join-transport.ts +14 -4
  169. package/src/runtime/actors/actor-routed-json-request.ts +10 -2
  170. package/src/runtime/actors/index.ts +19 -5
  171. package/src/runtime/actors/spot-actor-dispatch.ts +4 -2
  172. package/src/runtime/admission.ts +18 -5
  173. package/src/runtime/application-jobs/application-ingress-record-owner.ts +41 -25
  174. package/src/runtime/application-jobs/application-job-queue-scope.ts +51 -22
  175. package/src/runtime/application-jobs/contracts.ts +14 -1
  176. package/src/runtime/backend/contracts/index.ts +53 -14
  177. package/src/runtime/backend/mesh-completion-table.ts +156 -57
  178. package/src/runtime/backend/mesh-dispatch-pump.ts +40 -10
  179. package/src/runtime/backend/node/node-backend-adapter-support.ts +17 -8
  180. package/src/runtime/backend/node/node-dealer-channel-client-transport.ts +12 -2
  181. package/src/runtime/backend/node/node-raw-binding-port.ts +57 -30
  182. package/src/runtime/backend/node/node-raw-mesh-backend.ts +128 -65
  183. package/src/runtime/backend/node/node-socket-backend-adapter.ts +21 -6
  184. package/src/runtime/backend/raw-binding-port.ts +7 -2
  185. package/src/runtime/backend/runtime-message.ts +6 -0
  186. package/src/runtime/backend/runtime-values.ts +1 -0
  187. package/src/runtime/channels/channel-clients.ts +90 -41
  188. package/src/runtime/channels/channel-dispatch-pipeline.ts +69 -34
  189. package/src/runtime/channels/channel-dispatch-services.ts +69 -1
  190. package/src/runtime/channels/channel-dispatchers.ts +29 -27
  191. package/src/runtime/channels/channel-envelope-inspection.ts +2 -16
  192. package/src/runtime/channels/channel-envelope.ts +133 -8
  193. package/src/runtime/channels/channel-outbound-operations.ts +22 -8
  194. package/src/runtime/channels/channel-runtime-manager.ts +29 -12
  195. package/src/runtime/channels/channel-transports.ts +128 -60
  196. package/src/runtime/channels/local-spot-route-transport.ts +3 -1
  197. package/src/runtime/channels/router-socket-spot-route-transport.ts +13 -4
  198. package/src/runtime/channels/source-spot-router.ts +8 -4
  199. package/src/runtime/channels/spot-node-route-transport.ts +8 -4
  200. package/src/runtime/channels/spot-route-bridge-transport.ts +6 -4
  201. package/src/runtime/channels/spot-route-dispatch-strategy.ts +47 -18
  202. package/src/runtime/execution/serial-execution-queue.ts +2 -0
  203. package/src/runtime/foundation/actor-join-recovery-codec.ts +2 -2
  204. package/src/runtime/foundation/durable-service-request.ts +137 -0
  205. package/src/runtime/foundation/operation-identity.ts +5 -1
  206. package/src/runtime/foundation/raw-service-mesh-runtime.ts +484 -293
  207. package/src/runtime/foundation/service-relocation-object-owner.ts +2 -0
  208. package/src/runtime/foundation/service-relocation-runtime.ts +18 -1
  209. package/src/runtime/foundation/service-runtime-contracts.ts +7 -1
  210. package/src/runtime/foundation/service-stateful-runtime.ts +67 -125
  211. package/src/runtime/foundation/service-stateful-wire-codec.ts +0 -5
  212. package/src/runtime/foundation/service-topology-registry.ts +91 -36
  213. package/src/runtime/foundation/service-wire-m6a-codec.ts +57 -38
  214. package/src/runtime/handlers/handler-instance-scope.ts +67 -16
  215. package/src/runtime/host/actor-runtime-options-factory.ts +13 -2
  216. package/src/runtime/host/application-job-queue.ts +175 -41
  217. package/src/runtime/host/entry-actor-runtime.ts +4 -0
  218. package/src/runtime/host/index.ts +201 -156
  219. package/src/runtime/host/remote-bound-session-relay.ts +2 -23
  220. package/src/runtime/host/route-mesh-runtime.ts +1 -1
  221. package/src/runtime/host/service-relocation-control.ts +11 -5
  222. package/src/runtime/host/service-relocation-host-runtime.ts +315 -291
  223. package/src/runtime/host/spot-address-transport.ts +4 -2
  224. package/src/runtime/host/spot-runtime-options-factory.ts +2 -0
  225. package/src/runtime/locations/actor-location-claims.ts +52 -30
  226. package/src/runtime/locations/lifecycle.ts +8 -26
  227. package/src/runtime/locations/runtime.ts +28 -24
  228. package/src/runtime/messaging/framework-json-v1.ts +5 -325
  229. package/src/runtime/messaging/packet-name.ts +5 -25
  230. package/src/runtime/messaging/request-result.ts +58 -0
  231. package/src/runtime/messaging/submission-result.ts +5 -1
  232. package/src/runtime/protocol/service_wire_codec.generated.ts +131 -1
  233. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +4 -4
  234. package/src/runtime/spots/formal-remote-actor-admission-registry.ts +43 -23
  235. package/src/runtime/spots/index.ts +14 -2
  236. package/src/runtime/spots/spot-activation.ts +28 -10
  237. package/src/runtime/spots/spot-entry-activation.ts +9 -2
  238. package/src/runtime/spots/spot-handler-registry.ts +41 -8
  239. package/src/runtime/spots/spot-node-autoconnect.ts +6 -2
  240. package/src/runtime/spots/spot-outbound.ts +36 -16
  241. package/src/runtime/spots/spot-routed-spot-packet-dispatch.ts +2 -1
  242. package/src/runtime/spots/spot-runtime-ports.ts +1 -0
  243. package/src/runtime/spots/spot-subscription-dispatch.ts +0 -3
  244. package/src/runtime/spots/spot-timer.ts +5 -1
  245. package/src/runtime/streams/stream-session-runtime.ts +12 -29
@@ -2,6 +2,7 @@ import { constants as osConstants } from 'node:os';
2
2
  import { ApplicationIngressRecordOwner } from '../application-jobs/application-ingress-record-owner';
3
3
  import type {
4
4
  ApplicationJobPermitPort,
5
+ ApplicationJobReceiveRotationPort,
5
6
  ApplicationJobQueuePort
6
7
  } from '../application-jobs/contracts';
7
8
  import type {
@@ -13,11 +14,11 @@ import type {
13
14
  import { submitToRequestResult } from '../messaging/submission-result';
14
15
  import { RequestResult, SubmitResult, isZLinkBackendResultError } from '../backend/runtime-values';
15
16
  import { enumWireRejectReason } from '../protocol/service_wire_codec.generated';
17
+ import { SERVICE_WIRE_PREFIX_SIZE } from './service-wire-binary-primitives';
16
18
  import { OperationRegistry, type PendingOperation } from './operation-registry';
17
19
  import { ServiceLivenessRegistry, type ServiceLivenessTick } from './service-liveness-registry';
18
20
  import { ServiceMailbox, type ServiceMailboxRecord } from './service-mailbox';
19
21
  import {
20
- sameServiceNodeDescriptor,
21
22
  ServiceTopologyRegistry,
22
23
  validateDescriptor,
23
24
  type AdmittedServicePeer,
@@ -84,6 +85,7 @@ type RawServiceChannelTargetSelection =
84
85
  type ServiceApplicationPayloadInput = ServiceApplicationPayload | Buffer;
85
86
 
86
87
  export interface RawServiceIngressRecord {
88
+ readonly onAdmitted?: () => void;
87
89
  readonly command: number;
88
90
  readonly flags: number;
89
91
  readonly sourceRoutingId: string;
@@ -129,6 +131,7 @@ export interface RawServiceMeshRuntimeOptions {
129
131
  // Framework error code 13 (RequestTargetNotFound) is encoded as 14 on a
130
132
  // RequestResult.NotFound reply. Boundary transport results keep failureCode 0.
131
133
  const RAW_MESH_RECEIVE_RECORD_BUDGET = 64;
134
+ const NODE_SEND_HEADER = encodeNodeSendHeader();
132
135
  const RAW_MESH_RECEIVE_BYTE_BUDGET = 4 * 1024 * 1024;
133
136
  export const RAW_MESH_RECEIVE_TIME_BUDGET_MS = 2;
134
137
 
@@ -140,6 +143,14 @@ const livenessCodec = createServiceWireCodec({
140
143
  commands: M6aServiceWireCommand
141
144
  });
142
145
 
146
+ interface ControlSendState {
147
+ task?: Promise<boolean>;
148
+ next?: () => boolean | Promise<boolean>;
149
+ current?: () => boolean;
150
+ // Admission owns the first HELLO until ADMIT or route termination.
151
+ hello?: Uint8Array;
152
+ }
153
+
143
154
  /**
144
155
  * RouteMesh M6A runtime built only on the public raw binding package.
145
156
  * It owns protocol, admission, mailbox, request and liveness state.
@@ -158,11 +169,11 @@ export class RawServiceMeshRuntime {
158
169
  readonly endpoint?: string;
159
170
  readonly securityIdentity?: string;
160
171
  readonly lifecycleGeneration?: bigint;
161
- helloSubmittedGeneration?: bigint;
162
172
  }
163
173
  >();
164
174
  private readonly endpointOnlyPeers = new Set<string>();
165
175
  private readonly peerConnectionIntentRemoved = new Set<(nodeRoutingId: string) => void>();
176
+ private readonly peerRouteChanged = new Set<(nodeRoutingId: string) => void>();
166
177
  /**
167
178
  * The last observed Core selected route of each RID (Core ROUTER §10.1):
168
179
  * RID -> route generation. Only `observeSelectedRoutes` replaces it.
@@ -170,18 +181,13 @@ export class RawServiceMeshRuntime {
170
181
  private selectedRoutes = new Map<string, bigint>();
171
182
  private readonly bindingPort: ZLinkRawBindingPort;
172
183
  private readonly applicationJobQueue: ApplicationJobQueuePort;
184
+ private readonly nodeMailboxOwner: string;
173
185
  private readonly peerAdmissionSealed?: () => boolean;
174
186
  private readonly applicationJobStop = new AbortController();
175
187
  private receiveAcquisition?: Promise<ApplicationJobPermitPort>;
176
188
  private receivePermit?: ApplicationJobPermitPort;
177
- private readonly controlSends = new Map<
178
- string,
179
- {
180
- readonly task: Promise<boolean>;
181
- next?: () => boolean | Promise<boolean>;
182
- current?: () => boolean;
183
- }
184
- >();
189
+ private receiveRotation?: ApplicationJobReceiveRotationPort;
190
+ private readonly controlSends = new Map<string, ControlSendState>();
185
191
  private infrastructureFailure?: { readonly error: unknown };
186
192
  private readonly onReceiveReady?: () => void;
187
193
  private readonly infrastructureTaskRunner?: RawServiceMeshRuntimeOptions['infrastructureTaskRunner'];
@@ -195,6 +201,7 @@ export class RawServiceMeshRuntime {
195
201
  private router?: ZLinkRawRouterPort;
196
202
  private nextCorrelation = 1n;
197
203
  private serviceIngress?: RawServiceIngressHandler;
204
+ private serviceIngressAccepts?: (command: number) => boolean;
198
205
  private closed = false;
199
206
 
200
207
  constructor(options: RawServiceMeshRuntimeOptions) {
@@ -202,6 +209,7 @@ export class RawServiceMeshRuntime {
202
209
  this.infrastructureTaskRunner = options.infrastructureTaskRunner;
203
210
  this.operations = new OperationRegistry(undefined, options.onPendingOperationsChanged);
204
211
  this.descriptor = options.descriptor;
212
+ this.nodeMailboxOwner = `node:${options.descriptor.nodeRoutingId}`;
205
213
  this.topology = new ServiceTopologyRegistry(options.descriptor);
206
214
  this.mailbox = new ServiceMailbox(options.onMailboxReady);
207
215
  this.liveness = new ServiceLivenessRegistry(options.probeIntervalMs, options.peerTimeoutMs);
@@ -380,6 +388,36 @@ export class RawServiceMeshRuntime {
380
388
  };
381
389
  }
382
390
 
391
+ observePeerRoute(
392
+ nodeRoutingId: string
393
+ ): import('./durable-service-request').PeerRouteObservation {
394
+ const selected = this.selectedRoutes.get(nodeRoutingId);
395
+ const admitted = this.topology.admittedPeer(nodeRoutingId)?.liveness?.connectionId;
396
+ return {
397
+ generation: selected ?? (typeof admitted === 'bigint' ? admitted : undefined),
398
+ ready: admitted !== undefined && (selected === undefined || admitted === selected)
399
+ };
400
+ }
401
+
402
+ get requestStopSignal(): AbortSignal {
403
+ return this.applicationJobStop.signal;
404
+ }
405
+
406
+ observePeerRouteChanged(listener: (nodeRoutingId: string) => void): () => void {
407
+ this.peerRouteChanged.add(listener);
408
+ return () => {
409
+ this.peerRouteChanged.delete(listener);
410
+ };
411
+ }
412
+
413
+ hasSelectedPeerRoute(nodeRoutingId: string): boolean {
414
+ return this.selectedRoutes.has(nodeRoutingId);
415
+ }
416
+
417
+ private notifyPeerRouteChanged(nodeRoutingId: string): void {
418
+ for (const listener of this.peerRouteChanged) listener(nodeRoutingId);
419
+ }
420
+
383
421
  private notifyPeerConnectionIntentRemoved(nodeRoutingId: string): void {
384
422
  // Only the logical owner's removal publishes lifecycle termination.
385
423
  // A physical disconnect alone retains the connection expectation.
@@ -389,33 +427,36 @@ export class RawServiceMeshRuntime {
389
427
  }
390
428
 
391
429
  announcePeer(nodeRoutingId: string): Promise<boolean> {
392
- const expected = this.expectedPeers.get(nodeRoutingId);
393
430
  const generation = this.selectedRoutes.get(nodeRoutingId);
394
- if (
395
- expected === undefined ||
396
- generation === undefined ||
397
- expected.helloSubmittedGeneration === generation ||
398
- this.peerAdmissionSealed?.() === true
399
- )
431
+ if (generation === undefined) return Promise.resolve(false);
432
+ const current = () => {
433
+ const admission = this.topology.routeAdmission(nodeRoutingId);
434
+ return (
435
+ this.selectedRoutes.get(nodeRoutingId) === generation &&
436
+ !(
437
+ admission?.connectionId === routeConnectionId(generation) &&
438
+ admission.remoteAdmitReceived === true
439
+ ) &&
440
+ this.peerAdmissionSealed?.() !== true
441
+ );
442
+ };
443
+ const key = controlSendKey(nodeRoutingId, M6aServiceWireCommand.hello);
444
+ if (!current()) {
445
+ this.controlSends.delete(key);
400
446
  return Promise.resolve(false);
401
- const current = () =>
402
- this.expectedPeers.get(nodeRoutingId) === expected &&
403
- this.selectedRoutes.get(nodeRoutingId) === generation;
447
+ }
448
+ const existing = this.controlSends.get(key);
449
+ const hello =
450
+ existing?.current?.() === true
451
+ ? existing.hello!
452
+ : encodeRouteMeshAdmission(M6aServiceWireCommand.hello, this.topology.localDescriptor());
404
453
  return Promise.resolve(
405
454
  this.startControlSend(
406
455
  nodeRoutingId,
407
456
  M6aServiceWireCommand.hello,
408
- () =>
409
- this.submitControl(
410
- nodeRoutingId,
411
- [
412
- encodeRouteMeshAdmission(M6aServiceWireCommand.hello, this.topology.localDescriptor())
413
- ],
414
- () => {
415
- if (current()) expected.helloSubmittedGeneration = generation;
416
- }
417
- ),
418
- current
457
+ () => this.submitControl(nodeRoutingId, [hello]),
458
+ current,
459
+ hello
419
460
  )
420
461
  );
421
462
  }
@@ -423,7 +464,7 @@ export class RawServiceMeshRuntime {
423
464
  isPeerRouteReady(nodeRoutingId: string, lifecycleGeneration?: bigint): boolean {
424
465
  // Admission is bound to the observed selected route: the route observer
425
466
  // removes the peer when that route ends or is replaced.
426
- const peer = this.topology.peer(nodeRoutingId);
467
+ const peer = this.topology.admittedPeer(nodeRoutingId);
427
468
  return (
428
469
  peer !== undefined &&
429
470
  (lifecycleGeneration === undefined ||
@@ -470,12 +511,18 @@ export class RawServiceMeshRuntime {
470
511
  // placement and channel selectors therefore need the new mutable fields
471
512
  // on that same connection before the next request is selected; waiting for
472
513
  // a reconnect would leave a race where a drained node is selected again.
514
+ await this.publishLocalDescriptor(
515
+ this.topology.peers().map((peer) => peer.descriptor.nodeRoutingId)
516
+ );
517
+ }
518
+
519
+ private async publishLocalDescriptor(targets: readonly string[]): Promise<void> {
473
520
  const update = encodeRouteMeshAdmission(
474
521
  M6aServiceWireCommand.update,
475
522
  this.topology.localDescriptor()
476
523
  );
477
- for (const peer of this.topology.peers()) {
478
- await this.send(peer.descriptor.nodeRoutingId, [update]);
524
+ for (const target of targets) {
525
+ await this.sendService(target, [update]);
479
526
  }
480
527
  }
481
528
 
@@ -484,18 +531,14 @@ export class RawServiceMeshRuntime {
484
531
  }
485
532
 
486
533
  isObjectClientNodeDirectTarget(nodeRoutingId: string): boolean {
487
- const descriptor =
488
- nodeRoutingId === this.descriptor.nodeRoutingId
489
- ? this.topology.localDescriptor()
490
- : this.topology.knownDescriptor(nodeRoutingId);
491
- return descriptor?.objectRole === 'client';
534
+ return this.topology.isObjectClientNodeDirectTarget(nodeRoutingId);
492
535
  }
493
536
 
494
- async sendToNode(
537
+ sendToNode(
495
538
  targetNodeRoutingId: string,
496
539
  payload: ServiceApplicationPayloadInput
497
- ): Promise<boolean> {
498
- return this.send(targetNodeRoutingId, [encodeNodeSendHeader(), this.applicationFrame(payload)]);
540
+ ): boolean | Promise<boolean> {
541
+ return this.send(targetNodeRoutingId, [NODE_SEND_HEADER, this.applicationFrame(payload)]);
499
542
  }
500
543
 
501
544
  async sendToChannel(
@@ -517,15 +560,17 @@ export class RawServiceMeshRuntime {
517
560
  requestToNode(
518
561
  targetNodeRoutingId: string,
519
562
  payload: ServiceApplicationPayloadInput,
520
- timeoutMs: number
563
+ timeoutMs: number,
564
+ onAdmitted?: () => void
521
565
  ): PendingOperation<RawServiceRequestResult> {
522
- return this.requestToTarget(targetNodeRoutingId, payload, timeoutMs);
566
+ return this.requestToTarget(targetNodeRoutingId, payload, timeoutMs, undefined, onAdmitted);
523
567
  }
524
568
 
525
569
  requestToChannel(
526
570
  channelName: string,
527
571
  payload: ServiceApplicationPayloadInput,
528
- timeoutMs: number
572
+ timeoutMs: number,
573
+ onAdmitted?: () => void
529
574
  ): PendingOperation<RawServiceRequestResult> {
530
575
  const selection = this.selectChannelTarget(channelName);
531
576
  if (selection.kind !== 'selected') {
@@ -537,7 +582,8 @@ export class RawServiceMeshRuntime {
537
582
  selection.peer.descriptor.nodeRoutingId,
538
583
  payload,
539
584
  timeoutMs,
540
- channelName
585
+ channelName,
586
+ onAdmitted
541
587
  );
542
588
  }
543
589
 
@@ -562,11 +608,15 @@ export class RawServiceMeshRuntime {
562
608
  };
563
609
  }
564
610
 
565
- setServiceIngress(handler: RawServiceIngressHandler): void {
611
+ setServiceIngress(
612
+ handler: RawServiceIngressHandler,
613
+ accepts?: (command: number) => boolean
614
+ ): void {
566
615
  if (this.serviceIngress !== undefined && this.serviceIngress !== handler) {
567
616
  throw new Error('Raw service ingress is already registered.');
568
617
  }
569
618
  this.serviceIngress = handler;
619
+ this.serviceIngressAccepts = accepts;
570
620
  }
571
621
 
572
622
  async reserveLocalIngress(signal?: AbortSignal): Promise<ApplicationIngressRecordOwner> {
@@ -579,20 +629,27 @@ export class RawServiceMeshRuntime {
579
629
  }
580
630
 
581
631
  async sendService(targetNodeRoutingId: string, parts: readonly Uint8Array[]): Promise<boolean> {
582
- return this.send(targetNodeRoutingId, parts);
632
+ try {
633
+ await this.requireStarted().send(targetNodeRoutingId, parts);
634
+ return true;
635
+ } catch (error) {
636
+ return this.sendFailure(error);
637
+ }
583
638
  }
584
639
 
585
640
  requestService(
586
641
  targetNodeRoutingId: string,
587
642
  parts: readonly Uint8Array[],
588
- timeoutMs: number
643
+ timeoutMs: number,
644
+ onAdmitted?: () => void
589
645
  ): Promise<readonly Buffer[]> {
590
- const admission = this.topology.peer(targetNodeRoutingId)?.liveness;
646
+ const admission = this.topology.admittedPeer(targetNodeRoutingId)?.liveness;
591
647
  return this.requireStarted().request(
592
648
  targetNodeRoutingId,
593
649
  parts,
594
650
  timeoutMs,
595
- admission?.recordReceived
651
+ admission?.recordReceived,
652
+ onAdmitted
596
653
  );
597
654
  }
598
655
 
@@ -668,59 +725,71 @@ export class RawServiceMeshRuntime {
668
725
  const observe: RawServicePumpObserver = (_source, byteCount) => {
669
726
  bytes += byteCount;
670
727
  };
671
- while (receiveReady && messages < RAW_MESH_RECEIVE_RECORD_BUDGET && !this.closed) {
672
- const result = await this.receiveOne(performance.now(), observe);
673
- if (result === 'noData') {
674
- receiveReady = false;
675
- break;
728
+ const rotation = (this.receiveRotation ??= this.applicationJobQueue.createReceiveRotation(
729
+ RAW_MESH_RECEIVE_RECORD_BUDGET,
730
+ 'remote'
731
+ ));
732
+ if (receiveReady) rotation.reserve();
733
+ try {
734
+ while (receiveReady && messages < RAW_MESH_RECEIVE_RECORD_BUDGET && !this.closed) {
735
+ const received = this.receiveOne(performance.now(), observe, rotation);
736
+ const result = received instanceof Promise ? await received : received;
737
+ if (result === 'noData') {
738
+ receiveReady = false;
739
+ break;
740
+ }
741
+ messages += 1;
742
+ // Core owns the per-peer fair-queue cursor. The Framework limit bounds
743
+ // the entire round, so changing peers does not renew its byte/count budget.
744
+ if (
745
+ bytes >= RAW_MESH_RECEIVE_BYTE_BUDGET ||
746
+ performance.now() - startedAtMs >= RAW_MESH_RECEIVE_TIME_BUDGET_MS
747
+ )
748
+ break;
676
749
  }
677
- messages += 1;
678
- // Core owns the per-peer fair-queue cursor. The Framework limit bounds
679
- // the entire round, so changing peers does not renew its byte/count budget.
680
- if (
681
- bytes >= RAW_MESH_RECEIVE_BYTE_BUDGET ||
682
- performance.now() - startedAtMs >= RAW_MESH_RECEIVE_TIME_BUDGET_MS
683
- )
684
- break;
750
+ } finally {
751
+ rotation.releaseUnused();
685
752
  }
686
753
  if (!this.closed) {
687
- for (const nodeRoutingId of this.expectedPeers.keys()) {
688
- if (this.topology.peer(nodeRoutingId) === undefined) await this.announcePeer(nodeRoutingId);
689
- }
690
754
  this.tickLiveness();
691
755
  }
692
756
  return receiveReady && messages > 0;
693
757
  }
694
758
 
695
- private async receiveOne(
759
+ private receiveOne(
696
760
  nowMs: number,
697
- observe?: RawServicePumpObserver
698
- ): Promise<RawServicePumpResult> {
761
+ observe?: RawServicePumpObserver,
762
+ rotation?: ApplicationJobReceiveRotationPort
763
+ ): RawServicePumpResult | Promise<RawServicePumpResult> {
699
764
  const router = this.requireStarted();
700
765
  this.observeInfrastructureFailure();
701
- if (this.receivePermit === undefined && this.receiveAcquisition === undefined) {
702
- let resolveAcquisition!: (permit: ApplicationJobPermitPort) => void;
703
- let rejectAcquisition!: (error: unknown) => void;
704
- const acquisition = new Promise<ApplicationJobPermitPort>((resolve, reject) => {
705
- resolveAcquisition = resolve;
706
- rejectAcquisition = reject;
707
- });
708
- const permit = this.applicationJobQueue.acquireOrResume(
766
+ let permit = this.receivePermit;
767
+ this.receivePermit = undefined;
768
+ permit ??= rotation?.take();
769
+ if (permit === undefined && this.receiveAcquisition === undefined) {
770
+ let resolveAcquisition: ((permit: ApplicationJobPermitPort) => void) | undefined;
771
+ let rejectAcquisition: ((error: unknown) => void) | undefined;
772
+ permit = this.applicationJobQueue.acquireOrResume(
709
773
  (permit) => {
710
774
  this.receiveAcquisition = undefined;
711
- if (this.closed) permit.releaseAfterInternalProcessing();
712
- else {
775
+ if (this.closed) {
776
+ permit.releaseAfterInternalProcessing();
777
+ permit.releaseStorage?.();
778
+ } else {
713
779
  this.receivePermit = permit;
714
780
  this.onReceiveReady?.();
715
781
  }
716
- resolveAcquisition(permit);
782
+ resolveAcquisition?.(permit);
717
783
  },
718
- rejectAcquisition,
784
+ (error) => rejectAcquisition?.(error),
719
785
  this.applicationJobStop.signal,
720
786
  'remote'
721
787
  );
722
- if (permit !== undefined) this.receivePermit = permit;
723
- else {
788
+ if (permit === undefined) {
789
+ const acquisition = new Promise<ApplicationJobPermitPort>((resolve, reject) => {
790
+ resolveAcquisition = resolve;
791
+ rejectAcquisition = reject;
792
+ });
724
793
  this.receiveAcquisition = acquisition;
725
794
  acquisition.catch((error) => {
726
795
  this.receiveAcquisition = undefined;
@@ -728,11 +797,10 @@ export class RawServiceMeshRuntime {
728
797
  });
729
798
  }
730
799
  }
731
- const permit = this.receivePermit;
732
- this.receivePermit = undefined;
733
800
  if (permit === undefined) return 'noData';
734
801
  if (this.closed) {
735
802
  permit.releaseAfterInternalProcessing();
803
+ permit.releaseStorage?.();
736
804
  return 'noData';
737
805
  }
738
806
  let received: ZLinkRawReceivedRecord | undefined;
@@ -740,10 +808,12 @@ export class RawServiceMeshRuntime {
740
808
  received = router.receive(true);
741
809
  } catch (error) {
742
810
  permit.releaseAfterInternalProcessing();
811
+ permit.releaseStorage?.();
743
812
  throw error;
744
813
  }
745
814
  if (received === undefined) {
746
815
  permit.releaseAfterInternalProcessing();
816
+ permit.releaseStorage?.();
747
817
  return 'noData';
748
818
  }
749
819
  const applicationJobOwner = ApplicationIngressRecordOwner.create(
@@ -751,18 +821,40 @@ export class RawServiceMeshRuntime {
751
821
  permit,
752
822
  received
753
823
  );
824
+ let result: RawServicePumpResult | Promise<RawServicePumpResult>;
825
+ try {
826
+ result = this.processReceived(received, nowMs, applicationJobOwner);
827
+ } catch (error) {
828
+ applicationJobOwner.close();
829
+ throw error;
830
+ }
831
+ return result instanceof Promise
832
+ ? result.then(
833
+ (value) => this.finishReceive(received, applicationJobOwner, observe, value),
834
+ (error) => {
835
+ applicationJobOwner.close();
836
+ throw error;
837
+ }
838
+ )
839
+ : this.finishReceive(received, applicationJobOwner, observe, result);
840
+ }
841
+
842
+ private finishReceive(
843
+ received: ZLinkRawReceivedRecord,
844
+ owner: ApplicationIngressRecordOwner,
845
+ observe: RawServicePumpObserver | undefined,
846
+ result: RawServicePumpResult
847
+ ): RawServicePumpResult {
754
848
  try {
755
- const result = await this.processReceived(received, nowMs, applicationJobOwner);
756
- if (result === 'protocolError') {
757
- this.reportProtocolError(received);
849
+ if (result === 'protocolError') this.reportProtocolError(received);
850
+ if (observe !== undefined) {
851
+ let bytes = 0;
852
+ for (const part of received.parts) bytes += part.byteLength;
853
+ observe(received.sourceRid, bytes);
758
854
  }
759
- observe?.(
760
- received.sourceRid,
761
- received.parts.reduce((sum, part) => sum + part.byteLength, 0)
762
- );
763
855
  return result;
764
856
  } finally {
765
- applicationJobOwner.close();
857
+ owner.close();
766
858
  }
767
859
  }
768
860
 
@@ -808,22 +900,15 @@ export class RawServiceMeshRuntime {
808
900
  }
809
901
  }
810
902
 
811
- private async processReceived(
903
+ private processReceived(
812
904
  received: ZLinkRawReceivedRecord,
813
905
  nowMs: number,
814
906
  applicationJobOwner: ApplicationIngressRecordOwner
815
- ): Promise<RawServicePumpResult> {
816
- if (
817
- received.parts.length === 0 ||
818
- (received.parts.length === 1 && received.parts[0]!.byteLength === 0)
819
- ) {
820
- if (this.peerAdmissionSealed?.() === true) return 'dropped';
821
- return (await this.sendControl(received.sourceRid, [
822
- encodeRouteMeshAdmission(M6aServiceWireCommand.hello, this.topology.localDescriptor())
823
- ]))
824
- ? 'infrastructure'
825
- : 'dropped';
826
- }
907
+ ): RawServicePumpResult | Promise<RawServicePumpResult> {
908
+ const peer = this.topology.admittedPeer(received.sourceRid);
909
+ const currentSource =
910
+ peer?.liveness !== undefined && peer.liveness.connectionId === received.routeGeneration;
911
+ if (received.parts.length === 0) return currentSource ? 'protocolError' : 'dropped';
827
912
  try {
828
913
  const header = decodeHeader(received.parts[0]!);
829
914
  if (header.command === M6aServiceWireCommand.hello && this.peerAdmissionSealed?.() === true) {
@@ -831,107 +916,186 @@ export class RawServiceMeshRuntime {
831
916
  }
832
917
  if (
833
918
  header.command === M6aServiceWireCommand.hello ||
834
- header.command === M6aServiceWireCommand.admit ||
835
- header.command === M6aServiceWireCommand.update
919
+ header.command === M6aServiceWireCommand.admit
836
920
  ) {
837
- if (received.parts.length !== 1) return 'protocolError';
838
- const descriptor = decodeRouteMeshAdmission(
839
- received.parts[0]!,
840
- header.command,
841
- received.sourceRid
921
+ return this.processAdmission(received, header, nowMs).catch((error) =>
922
+ this.receiveProtocolFailure(error)
842
923
  );
843
- const expected = this.expectedPeers.get(received.sourceRid);
844
- if (
845
- expected !== undefined &&
846
- (expected.meshName !== descriptor.meshName ||
847
- expected.nodeRoutingId !== descriptor.nodeRoutingId)
848
- ) {
849
- await this.sendControl(received.sourceRid, [
850
- encodeReject(enumWireRejectReason('identityMismatch'))
851
- ]);
852
- return 'infrastructure';
853
- }
854
- const result = this.admitPeer(descriptor, received.routeGeneration, nowMs, expected);
855
- if (result !== 'admitted' && result !== 'notRequired') {
856
- const detail =
857
- result === 'invalidDescriptor'
858
- ? describeInvalidAdmissionDescriptor(
859
- descriptor,
860
- expected,
861
- this.topology.peer(descriptor.nodeRoutingId)?.descriptor
862
- )
863
- : undefined;
864
- console.warn(
865
- `RouteMesh admission rejected peer '${received.sourceRid}' as ${result}` +
866
- (detail === undefined ? '.' : ` (${detail}).`)
867
- );
868
- await this.sendControl(received.sourceRid, [encodeReject(admissionReason(result))]);
869
- return 'infrastructure';
870
- }
871
- if (result === 'admitted') this.selectBilateralConnection(descriptor);
872
- if (header.command === M6aServiceWireCommand.hello) {
873
- // Hello supplies only the sender's descriptor. Even NotRequired
874
- // peers must receive ours before the intent owner closes transport;
875
- // submitting a response does not guarantee delivery before close.
876
- await this.sendControl(received.sourceRid, [
877
- encodeRouteMeshAdmission(M6aServiceWireCommand.admit, this.topology.localDescriptor())
878
- ]);
879
- } else if (result === 'notRequired') {
880
- this.retireNotRequiredExpectedPeer(received.sourceRid, descriptor.advertisedEndpoint);
881
- }
882
- return 'infrastructure';
883
924
  }
884
925
  if (header.command === M6aServiceWireCommand.reject) {
885
926
  if (received.parts.length !== 1) return 'protocolError';
886
927
  decodeReject(received.parts[0]!);
887
928
  return 'infrastructure';
888
929
  }
889
- const peer = this.topology.peer(received.sourceRid);
890
- if (peer === undefined) return 'protocolError';
891
- if (peer.liveness !== undefined && peer.liveness.connectionId === received.routeGeneration)
892
- this.liveness.recordReceived(peer.liveness, nowMs);
930
+ if (!currentSource) return 'dropped';
931
+ this.liveness.recordReceived(peer.liveness, nowMs);
932
+ if (header.command === M6aServiceWireCommand.update) {
933
+ return this.processAdmission(received, header, nowMs).catch((error) =>
934
+ this.receiveProtocolFailure(error)
935
+ );
936
+ }
893
937
  if (
894
938
  header.command === M6aServiceWireCommand.livenessProbe ||
895
939
  header.command === M6aServiceWireCommand.livenessAck
896
940
  ) {
897
- if (received.parts.length !== 1) return 'protocolError';
898
- const record = livenessCodec.decodeLivenessRecord(received.parts[0]!);
899
- if (record.command === M6aServiceWireCommand.livenessProbe) {
900
- const ack = this.liveness.acknowledgeProbe(
901
- received.sourceRid,
902
- received.routeGeneration,
903
- record.probeId
904
- );
905
- if (ack === undefined) return 'protocolError';
906
- const sent = await this.sendControl(received.sourceRid, [
907
- livenessCodec.encodeLivenessRecord({
908
- command: M6aServiceWireCommand.livenessAck,
909
- probeId: record.probeId
910
- })
911
- ]);
912
- if (!sent) return 'dropped';
913
- } else {
914
- this.liveness.acknowledge(
915
- received.sourceRid,
916
- received.routeGeneration,
917
- record.probeId,
918
- nowMs
919
- );
920
- }
921
- return 'infrastructure';
941
+ return this.processLiveness(received, nowMs).catch((error) =>
942
+ this.receiveProtocolFailure(error)
943
+ );
922
944
  }
923
- const stateful = await this.serviceIngress?.({
924
- command: header.command,
925
- flags: header.flags,
926
- sourceRoutingId: received.sourceRid,
927
- sourceNodeGeneration: peer.descriptor.lifecycleGeneration,
928
- sourceRoute: received.sourceRoute,
929
- ...(received.reply === undefined ? {} : { reply: received.reply }),
930
- ...(received.requestSeq === undefined ? {} : { requestSequence: received.requestSeq }),
931
- parts: received.parts,
932
- applicationJobOwner
933
- });
945
+ const stateful =
946
+ this.serviceIngress !== undefined && this.serviceIngressAccepts?.(header.command) !== false
947
+ ? this.serviceIngress({
948
+ command: header.command,
949
+ flags: header.flags,
950
+ sourceRoutingId: received.sourceRid,
951
+ sourceNodeGeneration: peer.descriptor.lifecycleGeneration,
952
+ sourceRoute: received.sourceRoute,
953
+ ...(received.reply === undefined ? {} : { reply: received.reply }),
954
+ ...(received.requestSeq === undefined
955
+ ? {}
956
+ : { requestSequence: received.requestSeq }),
957
+ parts: received.parts,
958
+ applicationJobOwner
959
+ })
960
+ : undefined;
961
+ if (stateful instanceof Promise)
962
+ return stateful.then(
963
+ (result) =>
964
+ result ?? this.processApplicationReceived(received, header, applicationJobOwner),
965
+ (error) => this.receiveProtocolFailure(error)
966
+ );
934
967
  if (stateful !== undefined) return stateful;
968
+ return this.processApplicationReceived(received, header, applicationJobOwner);
969
+ } catch (error) {
970
+ if (!currentSource && error instanceof ServiceWireProtocolError) return 'dropped';
971
+ return this.receiveProtocolFailure(error);
972
+ }
973
+ }
974
+
975
+ private receiveProtocolFailure(error: unknown): RawServicePumpResult {
976
+ if (error instanceof ServiceWireProtocolError) return 'protocolError';
977
+ throw error;
978
+ }
979
+
980
+ private async processAdmission(
981
+ received: ZLinkRawReceivedRecord,
982
+ header: ReturnType<typeof decodeHeader>,
983
+ nowMs: number
984
+ ): Promise<RawServicePumpResult> {
985
+ if (received.parts.length !== 1) return 'protocolError';
986
+ const descriptor = decodeRouteMeshAdmission(
987
+ received.parts[0]!,
988
+ header.command,
989
+ received.sourceRid
990
+ );
991
+ const expected = this.expectedPeers.get(received.sourceRid);
992
+ if (
993
+ expected !== undefined &&
994
+ (expected.meshName !== descriptor.meshName ||
995
+ expected.nodeRoutingId !== descriptor.nodeRoutingId)
996
+ ) {
997
+ await this.sendControl(received.sourceRid, [
998
+ encodeReject(enumWireRejectReason('identityMismatch'))
999
+ ]);
1000
+ return 'infrastructure';
1001
+ }
1002
+ const helloKey = controlSendKey(received.sourceRid, M6aServiceWireCommand.hello);
1003
+ const exchange = this.controlSends.get(helloKey);
1004
+ const result = this.admitPeer(
1005
+ descriptor,
1006
+ received.routeGeneration,
1007
+ nowMs,
1008
+ expected,
1009
+ header.command,
1010
+ received.parts[0]!.subarray(SERVICE_WIRE_PREFIX_SIZE)
1011
+ );
1012
+ if (result !== 'admitted' && result !== 'notRequired') {
1013
+ const detail =
1014
+ result === 'invalidDescriptor'
1015
+ ? describeInvalidAdmissionDescriptor(
1016
+ descriptor,
1017
+ expected,
1018
+ this.topology.peer(descriptor.nodeRoutingId)?.descriptor
1019
+ )
1020
+ : undefined;
1021
+ console.warn(
1022
+ `RouteMesh admission rejected peer '${received.sourceRid}' as ${result}` +
1023
+ (detail === undefined ? '.' : ` (${detail}).`)
1024
+ );
1025
+ await this.sendControl(received.sourceRid, [encodeReject(admissionReason(result))]);
1026
+ return 'infrastructure';
1027
+ }
1028
+ if (result === 'admitted') this.selectBilateralConnection(descriptor);
1029
+ if (header.command === M6aServiceWireCommand.hello) {
1030
+ // Hello supplies only the sender's descriptor. Even NotRequired
1031
+ // peers must receive ours before the intent owner closes transport;
1032
+ // submitting a response does not guarantee delivery before close.
1033
+ const local =
1034
+ exchange?.current?.() === true && exchange.hello !== undefined
1035
+ ? decodeRouteMeshAdmission(
1036
+ exchange.hello,
1037
+ M6aServiceWireCommand.hello,
1038
+ this.descriptor.nodeRoutingId
1039
+ )
1040
+ : this.topology.localDescriptor();
1041
+ await this.sendControl(received.sourceRid, [
1042
+ encodeRouteMeshAdmission(M6aServiceWireCommand.admit, local)
1043
+ ]);
1044
+ } else if (header.command === M6aServiceWireCommand.admit) {
1045
+ this.controlSends.delete(helloKey);
1046
+ if (result === 'notRequired') {
1047
+ this.retireNotRequiredExpectedPeer(received.sourceRid, descriptor.advertisedEndpoint);
1048
+ } else if (
1049
+ exchange?.hello !== undefined &&
1050
+ decodeRouteMeshAdmission(
1051
+ exchange.hello,
1052
+ M6aServiceWireCommand.hello,
1053
+ this.descriptor.nodeRoutingId
1054
+ ).descriptorRevision < this.descriptor.descriptorRevision
1055
+ ) {
1056
+ await this.publishLocalDescriptor([received.sourceRid]);
1057
+ }
1058
+ }
1059
+ return 'infrastructure';
1060
+ }
1061
+
1062
+ private async processLiveness(
1063
+ received: ZLinkRawReceivedRecord,
1064
+ nowMs: number
1065
+ ): Promise<RawServicePumpResult> {
1066
+ if (received.parts.length !== 1) return 'protocolError';
1067
+ const record = livenessCodec.decodeLivenessRecord(received.parts[0]!);
1068
+ if (record.command === M6aServiceWireCommand.livenessProbe) {
1069
+ const ack = this.liveness.acknowledgeProbe(
1070
+ received.sourceRid,
1071
+ received.routeGeneration,
1072
+ record.probeId
1073
+ );
1074
+ if (ack === undefined) return 'protocolError';
1075
+ const sent = await this.sendControl(received.sourceRid, [
1076
+ livenessCodec.encodeLivenessRecord({
1077
+ command: M6aServiceWireCommand.livenessAck,
1078
+ probeId: record.probeId
1079
+ })
1080
+ ]);
1081
+ if (!sent) return 'dropped';
1082
+ } else {
1083
+ this.liveness.acknowledge(
1084
+ received.sourceRid,
1085
+ received.routeGeneration,
1086
+ record.probeId,
1087
+ nowMs
1088
+ );
1089
+ }
1090
+ return 'infrastructure';
1091
+ }
1092
+
1093
+ private processApplicationReceived(
1094
+ received: ZLinkRawReceivedRecord,
1095
+ header: ReturnType<typeof decodeHeader>,
1096
+ applicationJobOwner: ApplicationIngressRecordOwner
1097
+ ): RawServicePumpResult | Promise<RawServicePumpResult> {
1098
+ try {
935
1099
  if (
936
1100
  header.flags === 0 &&
937
1101
  // A bare infrastructure control is normally exactly one frame, but a
@@ -949,7 +1113,7 @@ export class RawServiceMeshRuntime {
949
1113
  isBareInfrastructureControl(header.command)
950
1114
  ) {
951
1115
  const accepted = this.mailbox.tryEnqueue({
952
- owner: `node:${this.descriptor.nodeRoutingId}`,
1116
+ owner: this.nodeMailboxOwner,
953
1117
  domain: 'infrastructure',
954
1118
  parts: received.parts,
955
1119
  sourceRoutingId: received.sourceRid,
@@ -974,50 +1138,60 @@ export class RawServiceMeshRuntime {
974
1138
  let owner: string;
975
1139
  let correlation: bigint | undefined;
976
1140
  if (header.command === M6aServiceWireCommand.nodeSend) {
977
- owner = `node:${this.descriptor.nodeRoutingId}`;
1141
+ owner = this.nodeMailboxOwner;
978
1142
  } else if (header.command === M6aServiceWireCommand.nodeRequest) {
979
- owner = `node:${this.descriptor.nodeRoutingId}`;
980
- correlation = decodeNodeRequestHeader(received.parts[0]!);
1143
+ owner = this.nodeMailboxOwner;
1144
+ correlation = decodeNodeRequestHeader(received.parts[0]!, header);
981
1145
  } else if (header.command === M6aServiceWireCommand.channelSend) {
982
- owner = `channel:${decodeChannelSendHeader(received.parts[0]!)}`;
1146
+ owner = `channel:${decodeChannelSendHeader(received.parts[0]!, header)}`;
983
1147
  } else {
984
- const channel = decodeChannelRequestHeader(received.parts[0]!);
1148
+ const channel = decodeChannelRequestHeader(received.parts[0]!, header);
985
1149
  owner = `channel:${channel.channelName}`;
986
1150
  correlation = channel.correlation;
987
1151
  }
988
- const applicationJob = await applicationJobOwner.acquire('application');
989
- const accepted = this.mailbox.tryEnqueue({
990
- owner,
991
- domain: 'application',
992
- parts: received.parts,
993
- sourceRoutingId: received.sourceRid,
994
- sourceRoute: received.sourceRoute,
995
- ...(received.reply === undefined ? {} : { reply: received.reply }),
996
- requestSequence: received.requestSeq,
997
- ...(correlation === undefined ? {} : { correlation }),
998
- applicationJob
999
- });
1000
- if (accepted) return 'application';
1001
- applicationJob.close();
1002
- if (correlation !== undefined && received.requestSeq !== undefined) {
1003
- this.replyService(
1004
- {
1005
- sourceRoutingId: received.sourceRid,
1006
- sourceRoute: received.sourceRoute,
1007
- requestSequence: received.requestSeq,
1008
- ...(received.reply === undefined ? {} : { reply: received.reply })
1009
- },
1010
- [encodeReplyHeader(correlation, RequestResult.NotConnected, 0)]
1011
- );
1012
- return 'infrastructure';
1013
- }
1014
- return 'dropped';
1152
+ const job = applicationJobOwner.acquire('application');
1153
+ return job instanceof Promise
1154
+ ? job.then((permit) => this.enqueueOrdinaryRecord(received, owner, correlation, permit))
1155
+ : this.enqueueOrdinaryRecord(received, owner, correlation, job);
1015
1156
  } catch (error) {
1016
- if (error instanceof ServiceWireProtocolError) return 'protocolError';
1017
- throw error;
1157
+ return this.receiveProtocolFailure(error);
1018
1158
  }
1019
1159
  }
1020
1160
 
1161
+ private enqueueOrdinaryRecord(
1162
+ received: ZLinkRawReceivedRecord,
1163
+ owner: string,
1164
+ correlation: bigint | undefined,
1165
+ applicationJob: import('../application-jobs/application-ingress-record-owner').ApplicationJobRecordLease
1166
+ ): RawServicePumpResult {
1167
+ const accepted = this.mailbox.tryEnqueue({
1168
+ owner,
1169
+ domain: 'application',
1170
+ parts: received.parts,
1171
+ sourceRoutingId: received.sourceRid,
1172
+ sourceRoute: received.sourceRoute,
1173
+ ...(received.reply === undefined ? {} : { reply: received.reply }),
1174
+ requestSequence: received.requestSeq,
1175
+ ...(correlation === undefined ? {} : { correlation }),
1176
+ applicationJob
1177
+ });
1178
+ if (accepted) return 'application';
1179
+ applicationJob.close();
1180
+ if (correlation !== undefined && received.requestSeq !== undefined) {
1181
+ this.replyService(
1182
+ {
1183
+ sourceRoutingId: received.sourceRid,
1184
+ sourceRoute: received.sourceRoute,
1185
+ requestSequence: received.requestSeq,
1186
+ ...(received.reply === undefined ? {} : { reply: received.reply })
1187
+ },
1188
+ [encodeReplyHeader(correlation, RequestResult.NotConnected, 0)]
1189
+ );
1190
+ return 'infrastructure';
1191
+ }
1192
+ return 'dropped';
1193
+ }
1194
+
1021
1195
  expireOperations(nowMs: number, turnDeadlineMs: number): number {
1022
1196
  return this.operations.expire(nowMs, turnDeadlineMs);
1023
1197
  }
@@ -1029,6 +1203,7 @@ export class RawServiceMeshRuntime {
1029
1203
  tickLiveness(nowMs = performance.now()): ServiceLivenessTick {
1030
1204
  const result = this.liveness.tick(nowMs);
1031
1205
  this.requireStarted();
1206
+ for (const nodeRoutingId of this.selectedRoutes.keys()) void this.announcePeer(nodeRoutingId);
1032
1207
  for (const probe of result.probes) {
1033
1208
  const parts = [
1034
1209
  livenessCodec.encodeLivenessRecord({
@@ -1065,9 +1240,8 @@ export class RawServiceMeshRuntime {
1065
1240
  let changes = 0;
1066
1241
  for (const [nodeRoutingId, generation] of this.selectedRoutes) {
1067
1242
  if (observed.get(nodeRoutingId) === generation) continue;
1243
+ this.controlSends.delete(controlSendKey(nodeRoutingId, M6aServiceWireCommand.hello));
1068
1244
  changes++;
1069
- const expected = this.expectedPeers.get(nodeRoutingId);
1070
- if (expected !== undefined) delete expected.helloSubmittedGeneration;
1071
1245
  const peer = this.topology.peer(nodeRoutingId);
1072
1246
  if (peer?.liveness?.connectionId === generation) {
1073
1247
  this.removePeer(peer);
@@ -1080,6 +1254,7 @@ export class RawServiceMeshRuntime {
1080
1254
  selected.push(nodeRoutingId);
1081
1255
  }
1082
1256
  this.selectedRoutes = observed;
1257
+ for (const nodeRoutingId of selected) this.notifyPeerRouteChanged(nodeRoutingId);
1083
1258
  // A new selected route admits only through a new handshake.
1084
1259
  for (const nodeRoutingId of selected) await this.announcePeer(nodeRoutingId);
1085
1260
  return changes;
@@ -1092,8 +1267,10 @@ export class RawServiceMeshRuntime {
1092
1267
  new Error('Raw service runtime application job admission stopped.')
1093
1268
  );
1094
1269
  this.receivePermit?.releaseAfterInternalProcessing();
1270
+ this.receivePermit?.releaseStorage?.();
1095
1271
  this.receivePermit = undefined;
1096
1272
  for (const send of this.controlSends.values()) delete send.next;
1273
+ this.controlSends.clear();
1097
1274
  this.mailbox.close();
1098
1275
  this.operations.close('Raw service runtime closed.');
1099
1276
  this.serviceIngress = undefined;
@@ -1111,7 +1288,8 @@ export class RawServiceMeshRuntime {
1111
1288
  targetNodeRoutingId: string,
1112
1289
  payload: ServiceApplicationPayloadInput,
1113
1290
  timeoutMs: number,
1114
- channelName?: string
1291
+ channelName?: string,
1292
+ onAdmitted?: () => void
1115
1293
  ): PendingOperation<RawServiceRequestResult> {
1116
1294
  const correlation = this.nextCorrelation++;
1117
1295
  const header =
@@ -1182,6 +1360,8 @@ export class RawServiceMeshRuntime {
1182
1360
  terminalResult: RequestResult.NotConnected,
1183
1361
  failureCode: 0
1184
1362
  });
1363
+ } else {
1364
+ onAdmitted?.();
1185
1365
  }
1186
1366
  } finally {
1187
1367
  applicationJobOwner.close();
@@ -1189,7 +1369,7 @@ export class RawServiceMeshRuntime {
1189
1369
  })().catch((error) => this.operations.fail(pending.id, error));
1190
1370
  return pending;
1191
1371
  }
1192
- const selectedPeer = this.topology.peer(selectedTargetNodeRoutingId);
1372
+ const selectedPeer = this.topology.admittedPeer(selectedTargetNodeRoutingId);
1193
1373
  if (selectedPeer === undefined) {
1194
1374
  // Logical target selection is the requester's decision: an RID that is
1195
1375
  // not an admitted peer is not a target. Whether an admitted target's
@@ -1207,7 +1387,8 @@ export class RawServiceMeshRuntime {
1207
1387
  selectedTargetNodeRoutingId,
1208
1388
  parts,
1209
1389
  timeoutMs,
1210
- selectedPeer.liveness?.recordReceived
1390
+ selectedPeer.liveness?.recordReceived,
1391
+ onAdmitted
1211
1392
  );
1212
1393
  } catch (error) {
1213
1394
  this.operations.fail(pending.id, error);
@@ -1274,33 +1455,37 @@ export class RawServiceMeshRuntime {
1274
1455
  readonly endpoint?: string;
1275
1456
  readonly securityIdentity?: string;
1276
1457
  readonly lifecycleGeneration?: bigint;
1277
- }
1458
+ },
1459
+ command: number = M6aServiceWireCommand.hello,
1460
+ descriptorBytes?: Uint8Array
1278
1461
  ): PeerAdmissionResult {
1279
1462
  const previous = this.topology.peer(descriptor.nodeRoutingId);
1280
- if (
1281
- previous !== undefined &&
1282
- previous.descriptor.lifecycleGeneration === descriptor.lifecycleGeneration &&
1283
- (descriptor.descriptorRevision < previous.descriptor.descriptorRevision ||
1284
- (descriptor.descriptorRevision === previous.descriptor.descriptorRevision &&
1285
- !sameServiceNodeDescriptor(previous.descriptor, descriptor)))
1286
- ) {
1463
+ const topologyConnectionId =
1464
+ typeof connectionId === 'bigint' ? routeConnectionId(connectionId) : connectionId;
1465
+ const result = this.topology.admit(
1466
+ descriptor,
1467
+ topologyConnectionId,
1468
+ expected,
1469
+ () => {
1470
+ const admission = this.liveness.admit(descriptor.nodeRoutingId, connectionId, nowMs);
1471
+ this.liveness.recordReceived(admission, nowMs);
1472
+ return admission;
1473
+ },
1474
+ command,
1475
+ descriptorBytes
1476
+ );
1477
+ if (result === 'staleDescriptor') {
1287
1478
  throw new ServiceWireProtocolError(
1288
1479
  `RouteMesh peer '${descriptor.nodeRoutingId}' descriptor revision is stale or conflicting.`
1289
1480
  );
1290
1481
  }
1291
- const topologyConnectionId =
1292
- typeof connectionId === 'bigint' ? `route:${connectionId}` : connectionId;
1293
- const result = this.topology.admit(descriptor, topologyConnectionId, expected, () => {
1294
- const admission = this.liveness.admit(descriptor.nodeRoutingId, connectionId, nowMs);
1295
- this.liveness.recordReceived(admission, nowMs);
1296
- return admission;
1297
- });
1298
1482
  if (result === 'notRequired' && previous !== undefined) {
1299
1483
  this.liveness.disconnect(
1300
1484
  descriptor.nodeRoutingId,
1301
1485
  previous.liveness?.connectionId ?? previous.connectionId
1302
1486
  );
1303
1487
  }
1488
+ if (result === 'admitted') this.notifyPeerRouteChanged(descriptor.nodeRoutingId);
1304
1489
  return result;
1305
1490
  }
1306
1491
 
@@ -1361,26 +1546,16 @@ export class RawServiceMeshRuntime {
1361
1546
  );
1362
1547
  }
1363
1548
 
1364
- private submitControl(
1365
- target: string,
1366
- parts: readonly Uint8Array[],
1367
- accepted?: () => void
1368
- ): boolean | Promise<boolean> {
1549
+ private submitControl(target: string, parts: readonly Uint8Array[]): boolean | Promise<boolean> {
1369
1550
  let submission;
1370
1551
  try {
1371
1552
  submission = this.requireStarted().submitSend(target, parts);
1372
1553
  } catch (error) {
1373
1554
  return this.sendFailure(error);
1374
1555
  }
1375
- if (submission.result === SubmitResult.Ok) {
1376
- accepted?.();
1377
- return true;
1378
- }
1556
+ if (submission.result === SubmitResult.Ok) return true;
1379
1557
  return submission.admitted.then(
1380
- () => {
1381
- accepted?.();
1382
- return true;
1383
- },
1558
+ () => true,
1384
1559
  (error) => this.sendFailure(error)
1385
1560
  );
1386
1561
  }
@@ -1389,12 +1564,14 @@ export class RawServiceMeshRuntime {
1389
1564
  target: string,
1390
1565
  command: number,
1391
1566
  attempt: () => boolean | Promise<boolean>,
1392
- current?: () => boolean
1567
+ current?: () => boolean,
1568
+ hello?: Uint8Array
1393
1569
  ): boolean {
1394
- const key = JSON.stringify([target, command]);
1570
+ const key = controlSendKey(target, command);
1395
1571
  const existing = this.controlSends.get(key);
1396
1572
  if (
1397
1573
  existing !== undefined &&
1574
+ existing.task !== undefined &&
1398
1575
  (command !== M6aServiceWireCommand.hello || existing.current?.() === true)
1399
1576
  ) {
1400
1577
  if (command !== M6aServiceWireCommand.hello) {
@@ -1404,34 +1581,37 @@ export class RawServiceMeshRuntime {
1404
1581
  return true;
1405
1582
  }
1406
1583
  const first = attempt();
1407
- if (typeof first === 'boolean') return first;
1584
+ if (typeof first === 'boolean') {
1585
+ if (hello !== undefined) this.controlSends.set(key, { current, hello });
1586
+ return first;
1587
+ }
1408
1588
  const owner = this;
1409
- const state: {
1410
- task: Promise<boolean>;
1411
- next?: () => boolean | Promise<boolean>;
1412
- current?: () => boolean;
1413
- } = {
1414
- current,
1415
- task: first.then(function drain(accepted): boolean | Promise<boolean> {
1416
- if (!owner.closed && state.next !== undefined) {
1417
- const next = state.next;
1418
- delete state.next;
1419
- return Promise.resolve(next()).then(drain);
1420
- }
1421
- if (owner.controlSends.get(key) === state) owner.controlSends.delete(key);
1422
- return accepted;
1423
- })
1589
+ const state: ControlSendState = { current, hello };
1590
+ const completeSend = () => {
1591
+ if (owner.controlSends.get(key) !== state) return;
1592
+ if (hello !== undefined && current?.() === true) delete state.task;
1593
+ else owner.controlSends.delete(key);
1424
1594
  };
1595
+ const task = first.then(function drain(accepted): boolean | Promise<boolean> {
1596
+ if (!owner.closed && state.next !== undefined) {
1597
+ const next = state.next;
1598
+ delete state.next;
1599
+ return Promise.resolve(next()).then(drain);
1600
+ }
1601
+ completeSend();
1602
+ return accepted;
1603
+ });
1604
+ state.task = task;
1425
1605
  this.controlSends.set(key, state);
1426
- state.task.then(
1606
+ task.then(
1427
1607
  () => {},
1428
1608
  (error) => {
1429
- if (this.controlSends.get(key) === state) this.controlSends.delete(key);
1609
+ completeSend();
1430
1610
  if (this.infrastructureTaskRunner === undefined) this.reportInfrastructureFailure(error);
1431
1611
  }
1432
1612
  );
1433
1613
  this.infrastructureTaskRunner?.runDetached('RouteMesh control SEND', async () => {
1434
- await state.task;
1614
+ await task;
1435
1615
  });
1436
1616
  return true;
1437
1617
  }
@@ -1455,17 +1635,20 @@ export class RawServiceMeshRuntime {
1455
1635
  if (failure !== undefined) throw failure.error;
1456
1636
  }
1457
1637
 
1458
- private send(targetNodeRoutingId: string, parts: readonly Uint8Array[]): Promise<boolean> {
1459
- return this.requireStarted()
1460
- .send(targetNodeRoutingId, parts)
1461
- .then(
1462
- () => {
1463
- return true;
1464
- },
1465
- (error) => {
1466
- return this.sendFailure(error);
1467
- }
1638
+ private send(
1639
+ targetNodeRoutingId: string,
1640
+ parts: readonly Uint8Array[]
1641
+ ): boolean | Promise<boolean> {
1642
+ try {
1643
+ const submission = this.requireStarted().submitSend(targetNodeRoutingId, parts);
1644
+ if (submission.result === SubmitResult.Ok) return true;
1645
+ return submission.admitted.then(
1646
+ () => true,
1647
+ (error) => this.sendFailure(error)
1468
1648
  );
1649
+ } catch (error) {
1650
+ return this.sendFailure(error);
1651
+ }
1469
1652
  }
1470
1653
 
1471
1654
  private sendFailure(error: unknown): false {
@@ -1478,6 +1661,14 @@ export class RawServiceMeshRuntime {
1478
1661
  }
1479
1662
  }
1480
1663
 
1664
+ function controlSendKey(target: string, command: number): string {
1665
+ return JSON.stringify([target, command]);
1666
+ }
1667
+
1668
+ function routeConnectionId(generation: bigint): string {
1669
+ return `route:${generation}`;
1670
+ }
1671
+
1481
1672
  /**
1482
1673
  * Relocation and bound-Session controls are service-wire records themselves,
1483
1674
  * not application NodeSend(16) envelopes. Keep this list closed: a bare