@zlink-systems/framework 0.25.0 → 0.26.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 (215) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/contracts/Channels/IZLinkFanoutClient.d.ts +0 -6
  3. package/dist/contracts/RouteMesh/RuntimeTopology.d.ts +8 -0
  4. package/dist/internal.d.ts +1 -0
  5. package/dist/internal.js +1 -0
  6. package/dist/runtime/activation-admission.d.ts +29 -0
  7. package/dist/runtime/activation-admission.js +61 -0
  8. package/dist/runtime/actors/actor-context.js +6 -6
  9. package/dist/runtime/actors/actor-handoff.d.ts +9 -3
  10. package/dist/runtime/actors/actor-handoff.js +28 -17
  11. package/dist/runtime/actors/actor-local-native-join.js +2 -18
  12. package/dist/runtime/actors/actor-remote-joiner.d.ts +2 -2
  13. package/dist/runtime/actors/actor-remote-joiner.js +6 -8
  14. package/dist/runtime/actors/actor-runtime-contracts.d.ts +4 -2
  15. package/dist/runtime/actors/actor-source-transfer.d.ts +5 -5
  16. package/dist/runtime/actors/deferred-join-completion.d.ts +14 -0
  17. package/dist/runtime/actors/deferred-join-completion.js +20 -0
  18. package/dist/runtime/actors/index.d.ts +1 -1
  19. package/dist/runtime/actors/index.js +15 -5
  20. package/dist/runtime/admission.d.ts +5 -1
  21. package/dist/runtime/admission.js +4 -0
  22. package/dist/runtime/backend/contracts/index.d.ts +2 -10
  23. package/dist/runtime/backend/mesh-actor-session-node-adapter.js +2 -9
  24. package/dist/runtime/backend/node/node-backend-adapter-factory.d.ts +1 -2
  25. package/dist/runtime/backend/node/node-backend-adapter-factory.js +4 -18
  26. package/dist/runtime/backend/node/node-backend-adapter-support.d.ts +6 -3
  27. package/dist/runtime/backend/node/node-backend-adapter-support.js +12 -28
  28. package/dist/runtime/backend/node/node-event-loop-poller.d.ts +1 -1
  29. package/dist/runtime/backend/node/node-event-loop-poller.js +8 -2
  30. package/dist/runtime/backend/node/node-raw-binding-port.d.ts +1 -1
  31. package/dist/runtime/backend/node/node-raw-binding-port.js +11 -3
  32. package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +12 -9
  33. package/dist/runtime/backend/node/node-raw-mesh-backend.js +61 -29
  34. package/dist/runtime/backend/node/node-socket-backend-adapter.js +1 -1
  35. package/dist/runtime/backend/raw-binding-port.d.ts +12 -0
  36. package/dist/runtime/channels/channel-clients.d.ts +0 -1
  37. package/dist/runtime/channels/channel-clients.js +0 -8
  38. package/dist/runtime/channels/channel-outbound-operations.js +2 -10
  39. package/dist/runtime/channels/channel-runtime-manager.d.ts +2 -5
  40. package/dist/runtime/channels/channel-runtime-manager.js +1 -12
  41. package/dist/runtime/channels/channel-socket-registry.d.ts +17 -4
  42. package/dist/runtime/channels/channel-socket-registry.js +49 -62
  43. package/dist/runtime/channels/channel-transports.d.ts +0 -3
  44. package/dist/runtime/channels/fanout-location-runtime.js +3 -9
  45. package/dist/runtime/execution/index.d.ts +18 -8
  46. package/dist/runtime/execution/index.js +55 -29
  47. package/dist/runtime/execution/serial-execution-queue.d.ts +5 -6
  48. package/dist/runtime/execution/serial-execution-queue.js +49 -15
  49. package/dist/runtime/foundation/listener-records.d.ts +11 -0
  50. package/dist/runtime/foundation/listener-records.js +20 -0
  51. package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +10 -21
  52. package/dist/runtime/foundation/raw-service-mesh-runtime.js +66 -392
  53. package/dist/runtime/foundation/service-authority-payload-codec.d.ts +1 -0
  54. package/dist/runtime/foundation/service-authority-payload-codec.js +8 -0
  55. package/dist/runtime/foundation/service-maintenance-runtime.js +23 -2
  56. package/dist/runtime/foundation/service-relocation-coordinator.d.ts +9 -0
  57. package/dist/runtime/foundation/service-relocation-coordinator.js +14 -1
  58. package/dist/runtime/foundation/service-relocation-object-owner.d.ts +8 -0
  59. package/dist/runtime/foundation/service-relocation-object-owner.js +16 -4
  60. package/dist/runtime/foundation/service-session-binding-ingress-port.d.ts +6 -13
  61. package/dist/runtime/foundation/service-stateful-runtime.d.ts +10 -7
  62. package/dist/runtime/foundation/service-stateful-runtime.js +100 -60
  63. package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +9 -4
  64. package/dist/runtime/foundation/service-stateful-wire-codec.js +153 -90
  65. package/dist/runtime/foundation/service-topology-registry.d.ts +1 -2
  66. package/dist/runtime/foundation/service-topology-registry.js +6 -30
  67. package/dist/runtime/host/actor-transfer-runtime.d.ts +17 -15
  68. package/dist/runtime/host/actor-transfer-runtime.js +73 -86
  69. package/dist/runtime/host/index.d.ts +6 -2
  70. package/dist/runtime/host/index.js +63 -37
  71. package/dist/runtime/host/remote-bound-session-relay.js +4 -3
  72. package/dist/runtime/host/route-mesh-runtime.d.ts +2 -0
  73. package/dist/runtime/host/route-mesh-runtime.js +7 -5
  74. package/dist/runtime/host/service-relocation-host-runtime.d.ts +50 -8
  75. package/dist/runtime/host/service-relocation-host-runtime.js +343 -270
  76. package/dist/runtime/host/spot-runtime-options-factory.d.ts +7 -4
  77. package/dist/runtime/host/spot-runtime-options-factory.js +4 -8
  78. package/dist/runtime/host/stateful-authority-route-runtime.js +6 -2
  79. package/dist/runtime/host/user-spot-creation-coordinator.d.ts +11 -3
  80. package/dist/runtime/host/user-spot-creation-coordinator.js +85 -87
  81. package/dist/runtime/locations/in-memory-provider-location-store.js +8 -6
  82. package/dist/runtime/locations/lifecycle.d.ts +1 -1
  83. package/dist/runtime/locations/lifecycle.js +2 -2
  84. package/dist/runtime/locations/resolvers.js +7 -2
  85. package/dist/runtime/locations/spot-location-claims.d.ts +2 -2
  86. package/dist/runtime/locations/spot-location-claims.js +3 -27
  87. package/dist/runtime/protocol/service_wire_codec.generated.d.ts +1 -0
  88. package/dist/runtime/protocol/service_wire_codec.generated.js +5 -1
  89. package/dist/runtime/protocol/service_wire_pilot_codec.generated.d.ts +1 -0
  90. package/dist/runtime/protocol/service_wire_pilot_codec.generated.js +3 -2
  91. package/dist/runtime/spots/formal-remote-actor-admission-registry.d.ts +3 -3
  92. package/dist/runtime/spots/formal-remote-actor-admission-registry.js +4 -4
  93. package/dist/runtime/spots/formal-remote-actor-transfer-registry.d.ts +3 -3
  94. package/dist/runtime/spots/index.d.ts +25 -19
  95. package/dist/runtime/spots/index.js +207 -205
  96. package/dist/runtime/spots/spot-activation-registry.d.ts +4 -2
  97. package/dist/runtime/spots/spot-activation-registry.js +34 -31
  98. package/dist/runtime/spots/spot-activation-state.d.ts +2 -11
  99. package/dist/runtime/spots/spot-activation-state.js +4 -17
  100. package/dist/runtime/spots/spot-activation.d.ts +8 -6
  101. package/dist/runtime/spots/spot-activation.js +54 -49
  102. package/dist/runtime/spots/spot-actor-admission-coordinator.d.ts +1 -1
  103. package/dist/runtime/spots/spot-actor-admission-coordinator.js +1 -1
  104. package/dist/runtime/spots/spot-actor-join-dispatch.d.ts +2 -2
  105. package/dist/runtime/spots/spot-actor-join-dispatch.js +3 -5
  106. package/dist/runtime/spots/spot-actor-lifecycle-drain.js +2 -2
  107. package/dist/runtime/spots/spot-actor-membership.d.ts +4 -0
  108. package/dist/runtime/spots/spot-actor-membership.js +26 -12
  109. package/dist/runtime/spots/spot-entry-activation.d.ts +1 -2
  110. package/dist/runtime/spots/spot-entry-activation.js +0 -1
  111. package/dist/runtime/spots/spot-manager-public.js +1 -1
  112. package/dist/runtime/spots/spot-native-actor-join-admission.d.ts +1 -1
  113. package/dist/runtime/spots/spot-native-actor-join-admission.js +2 -2
  114. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +5 -5
  115. package/dist/runtime/spots/spot-node-runtime-manager.js +7 -11
  116. package/dist/runtime/spots/spot-outbound.js +1 -29
  117. package/dist/runtime/spots/spot-routed-actor-admission.d.ts +1 -0
  118. package/dist/runtime/spots/spot-routed-actor-admission.js +10 -0
  119. package/dist/runtime/spots/spot-routed-frame-dispatch.d.ts +2 -6
  120. package/dist/runtime/spots/spot-routed-frame-dispatch.js +2 -70
  121. package/dist/runtime/spots/spot-runtime-ports.d.ts +3 -5
  122. package/dist/runtime/spots/spot-serial-executor.d.ts +5 -0
  123. package/dist/runtime/spots/spot-serial-executor.js +8 -1
  124. package/dist/runtime/spots/spot-serial-turn-executor.d.ts +3 -0
  125. package/dist/runtime/spots/spot-serial-turn-executor.js +8 -4
  126. package/dist/runtime/streams/actor-session-binding-registry.d.ts +13 -15
  127. package/dist/runtime/streams/actor-session-binding-registry.js +19 -171
  128. package/dist/runtime/streams/actor-session-binding-runtime-owner.d.ts +3 -4
  129. package/dist/runtime/streams/bound-session-service.js +1 -18
  130. package/dist/runtime/streams/index.d.ts +2 -0
  131. package/dist/runtime/streams/index.js +19 -19
  132. package/dist/runtime/streams/session-serial-executor.js +2 -2
  133. package/package.json +3 -3
  134. package/src/contracts/Channels/IZLinkFanoutClient.ts +0 -7
  135. package/src/contracts/RouteMesh/RuntimeTopology.ts +10 -0
  136. package/src/internal.ts +1 -0
  137. package/src/runtime/activation-admission.ts +78 -0
  138. package/src/runtime/actors/actor-context.ts +6 -6
  139. package/src/runtime/actors/actor-handoff.ts +31 -23
  140. package/src/runtime/actors/actor-local-native-join.ts +3 -42
  141. package/src/runtime/actors/actor-remote-joiner.ts +6 -25
  142. package/src/runtime/actors/actor-runtime-contracts.ts +4 -10
  143. package/src/runtime/actors/actor-source-transfer.ts +5 -18
  144. package/src/runtime/actors/deferred-join-completion.ts +40 -0
  145. package/src/runtime/actors/index.ts +22 -9
  146. package/src/runtime/admission.ts +5 -1
  147. package/src/runtime/backend/contracts/index.ts +2 -13
  148. package/src/runtime/backend/mesh-actor-session-node-adapter.ts +2 -10
  149. package/src/runtime/backend/node/node-backend-adapter-factory.ts +5 -25
  150. package/src/runtime/backend/node/node-backend-adapter-support.ts +9 -29
  151. package/src/runtime/backend/node/node-event-loop-poller.ts +14 -2
  152. package/src/runtime/backend/node/node-raw-binding-port.ts +17 -3
  153. package/src/runtime/backend/node/node-raw-mesh-backend.ts +65 -32
  154. package/src/runtime/backend/node/node-socket-backend-adapter.ts +2 -2
  155. package/src/runtime/backend/raw-binding-port.ts +13 -0
  156. package/src/runtime/channels/channel-clients.ts +0 -11
  157. package/src/runtime/channels/channel-outbound-operations.ts +3 -11
  158. package/src/runtime/channels/channel-runtime-manager.ts +4 -15
  159. package/src/runtime/channels/channel-socket-registry.ts +69 -71
  160. package/src/runtime/channels/channel-transports.ts +0 -3
  161. package/src/runtime/channels/fanout-location-runtime.ts +4 -16
  162. package/src/runtime/execution/index.ts +68 -30
  163. package/src/runtime/execution/serial-execution-queue.ts +52 -20
  164. package/src/runtime/foundation/listener-records.ts +21 -0
  165. package/src/runtime/foundation/raw-service-mesh-runtime.ts +66 -466
  166. package/src/runtime/foundation/service-authority-payload-codec.ts +10 -0
  167. package/src/runtime/foundation/service-maintenance-runtime.ts +30 -9
  168. package/src/runtime/foundation/service-relocation-coordinator.ts +13 -0
  169. package/src/runtime/foundation/service-relocation-object-owner.ts +18 -5
  170. package/src/runtime/foundation/service-session-binding-ingress-port.ts +6 -14
  171. package/src/runtime/foundation/service-stateful-runtime.ts +151 -89
  172. package/src/runtime/foundation/service-stateful-wire-codec.ts +203 -121
  173. package/src/runtime/foundation/service-topology-registry.ts +8 -48
  174. package/src/runtime/host/actor-transfer-runtime.ts +94 -166
  175. package/src/runtime/host/index.ts +119 -54
  176. package/src/runtime/host/remote-bound-session-relay.ts +4 -3
  177. package/src/runtime/host/route-mesh-runtime.ts +9 -5
  178. package/src/runtime/host/service-relocation-host-runtime.ts +495 -376
  179. package/src/runtime/host/spot-runtime-options-factory.ts +24 -13
  180. package/src/runtime/host/stateful-authority-route-runtime.ts +8 -4
  181. package/src/runtime/host/user-spot-creation-coordinator.ts +147 -142
  182. package/src/runtime/locations/in-memory-provider-location-store.ts +11 -7
  183. package/src/runtime/locations/lifecycle.ts +3 -2
  184. package/src/runtime/locations/resolvers.ts +19 -13
  185. package/src/runtime/locations/spot-location-claims.ts +5 -34
  186. package/src/runtime/protocol/service_wire_codec.generated.ts +9 -2
  187. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +4 -4
  188. package/src/runtime/spots/formal-remote-actor-admission-registry.ts +6 -6
  189. package/src/runtime/spots/formal-remote-actor-transfer-registry.ts +3 -3
  190. package/src/runtime/spots/index.ts +341 -295
  191. package/src/runtime/spots/spot-activation-registry.ts +41 -31
  192. package/src/runtime/spots/spot-activation-state.ts +4 -16
  193. package/src/runtime/spots/spot-activation.ts +102 -80
  194. package/src/runtime/spots/spot-actor-admission-coordinator.ts +2 -2
  195. package/src/runtime/spots/spot-actor-join-dispatch.ts +6 -7
  196. package/src/runtime/spots/spot-actor-lifecycle-drain.ts +20 -18
  197. package/src/runtime/spots/spot-actor-membership.ts +81 -17
  198. package/src/runtime/spots/spot-entry-activation.ts +0 -3
  199. package/src/runtime/spots/spot-manager-public.ts +8 -4
  200. package/src/runtime/spots/spot-native-actor-join-admission.ts +2 -2
  201. package/src/runtime/spots/spot-node-runtime-manager.ts +12 -18
  202. package/src/runtime/spots/spot-outbound.ts +4 -47
  203. package/src/runtime/spots/spot-routed-actor-admission.ts +20 -0
  204. package/src/runtime/spots/spot-routed-frame-dispatch.ts +6 -76
  205. package/src/runtime/spots/spot-runtime-ports.ts +6 -17
  206. package/src/runtime/spots/spot-serial-executor.ts +9 -1
  207. package/src/runtime/spots/spot-serial-turn-executor.ts +10 -4
  208. package/src/runtime/streams/actor-session-binding-registry.ts +20 -308
  209. package/src/runtime/streams/actor-session-binding-runtime-owner.ts +2 -12
  210. package/src/runtime/streams/bound-session-service.ts +2 -36
  211. package/src/runtime/streams/index.ts +29 -27
  212. package/src/runtime/streams/session-serial-executor.ts +2 -2
  213. package/dist/runtime/actors/deferred-join-accepted-journal.d.ts +0 -47
  214. package/dist/runtime/actors/deferred-join-accepted-journal.js +0 -992
  215. package/src/runtime/actors/deferred-join-accepted-journal.ts +0 -1346
@@ -1,3 +1,4 @@
1
+ import { ZLinkListenerRecords } from '../foundation/listener-records';
1
2
  import {
2
3
  ZLinkFrameworkRuntimeState,
3
4
  type RoutingId,
@@ -85,7 +86,6 @@ interface ClientServerPhysicalConnection {
85
86
  readonly aliases: Set<string>;
86
87
  readonly callbacksByAlias: Map<string, ZLinkClientServerConnectionCallbacks>;
87
88
  physicalConnectionId: symbol;
88
- terminationRequested: boolean;
89
89
  readyConnectionId?: string;
90
90
  admittedDescriptor?: ClientServerDiscoveryDescriptor;
91
91
  nextProbeAt?: number;
@@ -170,14 +170,19 @@ export class ZLinkChannelSocketRegistry {
170
170
  >();
171
171
  private readonly fanoutTopologyHandlers = new Map<string, Set<() => void>>();
172
172
 
173
+ private readonly listenerRecords: ZLinkListenerRecords;
174
+
173
175
  constructor(
174
176
  private readonly registration: ZLinkFrameworkRegistration,
175
177
  private readonly adapter: ZLinkChannelBackendAdapter,
176
178
  private readonly context: ZLinkBackendContext,
177
179
  private readonly monitoringAdapter?: ZLinkMonitoringBackendAdapter,
178
180
  private readonly oneWayFailureSink?: (error: unknown) => void,
179
- private readonly applicationJobQueue?: ApplicationJobQueue
180
- ) {}
181
+ private readonly applicationJobQueue?: ApplicationJobQueue,
182
+ listenerRecords?: ZLinkListenerRecords
183
+ ) {
184
+ this.listenerRecords = listenerRecords ?? new ZLinkListenerRecords();
185
+ }
181
186
 
182
187
  async dispose(): Promise<void> {
183
188
  const clientServerConnections = [...new Set(this.clientServerConnections.values())];
@@ -306,14 +311,18 @@ export class ZLinkChannelSocketRegistry {
306
311
  });
307
312
  }
308
313
  this.channelRouters.set(channelName, router);
314
+ const endpoint = advertisedEndpoint(
315
+ router.lastEndpoint ?? channel.server.bind,
316
+ channel.server.advertiseHost
317
+ );
318
+ this.recordListener('clientServer', channelName, endpoint);
309
319
  if (!this.clientServerServerDescriptors.has(channelName)) {
310
- const boundEndpoint = router.lastEndpoint ?? channel.server.bind;
311
320
  this.clientServerServerDescriptors.set(channelName, {
312
321
  channelName,
313
322
  serverRid: identity.serverRid,
314
323
  lifecycleGeneration: identity.lifecycleGeneration,
315
324
  descriptorRevision: 1n,
316
- endpoint: advertisedEndpoint(boundEndpoint, channel.server.advertiseHost),
325
+ endpoint,
317
326
  weight: publicWeight,
318
327
  state: ZLinkFrameworkRuntimeState.Serving,
319
328
  securityIdentity: 'default',
@@ -326,22 +335,21 @@ export class ZLinkChannelSocketRegistry {
326
335
  }
327
336
 
328
337
  clientServerServerIdentity(channelName: string): ZLinkClientServerServerSocketIdentity {
329
- const router = this.channelRouter(channelName);
338
+ this.channelRouter(channelName);
330
339
  const identity = this.clientServerIdentities.get(channelName);
331
- const channel = this.registration.channels.get(channelName);
332
- if (identity === undefined || channel?.server === undefined) {
340
+ const endpoint = this.listenerRecords.endpoint('clientServer', channelName);
341
+ if (identity === undefined || endpoint === undefined) {
333
342
  throw new ZLinkConfigurationException(`Channel server '${channelName}' is not registered.`);
334
343
  }
335
- const boundEndpoint = router.lastEndpoint ?? channel.server.bind;
336
- if (boundEndpoint === undefined || boundEndpoint.length === 0) {
337
- throw new ZLinkConfigurationException(
338
- `Channel server '${channelName}' did not report its bound endpoint.`
339
- );
340
- }
341
- return {
342
- ...identity,
343
- endpoint: advertisedEndpoint(boundEndpoint, channel.server.advertiseHost)
344
- };
344
+ return { ...identity, endpoint };
345
+ }
346
+
347
+ fanoutPublisherEndpoint(channelName: string): string | undefined {
348
+ return this.listenerRecords.endpoint('fanout', channelName);
349
+ }
350
+
351
+ private recordListener(kind: 'clientServer' | 'fanout', name: string, endpoint: string): void {
352
+ this.listenerRecords.record(kind, name, endpoint);
345
353
  }
346
354
 
347
355
  clientServerServerSocket(channelName: string): ZLinkBackendRouterSocket {
@@ -417,8 +425,7 @@ export class ZLinkChannelSocketRegistry {
417
425
  monitor,
418
426
  aliases: new Set([connectionId]),
419
427
  callbacksByAlias: new Map([[connectionId, callbacks]]),
420
- physicalConnectionId: Symbol(connectionId),
421
- terminationRequested: false
428
+ physicalConnectionId: Symbol(connectionId)
422
429
  };
423
430
  this.clientServerConnections.set(connectionId, connection);
424
431
  this.ensureClientServerLivenessTimer();
@@ -436,7 +443,7 @@ export class ZLinkChannelSocketRegistry {
436
443
  const routingId = event.routingId === undefined ? undefined : String(event.routingId);
437
444
  if (event.nativeEvent === ZLinkSocketNativeEventType.ConnectionReady) {
438
445
  // This single-endpoint DEALER also receives its disconnected ready-count snapshot.
439
- if (event.value === 0n || connection.terminationRequested) return;
446
+ if (event.value === 0n) return;
440
447
  connection.physicalConnectionId = Symbol(connectionId);
441
448
  connection.admissionAttempt = undefined;
442
449
  for (const currentCallbacks of [...connection.callbacksByAlias.values()]) {
@@ -451,26 +458,9 @@ export class ZLinkChannelSocketRegistry {
451
458
  event.nativeEvent === ZLinkSocketNativeEventType.HandshakeFailedProtocol ||
452
459
  event.nativeEvent === ZLinkSocketNativeEventType.HandshakeFailedAuth
453
460
  ) {
454
- if (
455
- connection.terminationRequested &&
456
- (event.remoteAddr !== connection.endpoint ||
457
- (event.nativeEvent !== ZLinkSocketNativeEventType.Disconnected &&
458
- event.nativeEvent !== ZLinkSocketNativeEventType.Closed))
459
- )
460
- return;
461
- connection.physicalConnectionId = Symbol(connectionId);
462
- connection.admissionAttempt = undefined;
463
- if (connection.readyConnectionId !== undefined) {
464
- this.removeReadyConnection(connection.readyConnectionId);
465
- }
466
- for (const currentCallbacks of [...connection.callbacksByAlias.values()]) {
467
- currentCallbacks.onTerminated(routingId, event.remoteAddr);
468
- }
469
- if (connection.terminationRequested) {
470
- connection.terminationRequested = false;
471
- // Explicit disconnect removes intent; restore it once after this endpoint closes.
472
- connection.dealer.connect(connection.endpoint);
473
- }
461
+ // Core owns the endpoint reconnect (transport liveness §6); the
462
+ // connect intent stays and the next READY starts a new admission.
463
+ this.endClientServerAdmission(connectionId, connection, routingId, event.remoteAddr);
474
464
  }
475
465
  });
476
466
  dealer.connect(endpoint);
@@ -790,10 +780,6 @@ export class ZLinkChannelSocketRegistry {
790
780
  );
791
781
  }
792
782
 
793
- hasKnownClientServerTargets(channelName: string): boolean {
794
- return this.clientServerDiscovery.clientServerDescriptors(channelName).length > 0;
795
- }
796
-
797
783
  fanoutActiveTargets(channelName: string) {
798
784
  return this.clientServerDiscovery.fanoutEndpoints(channelName);
799
785
  }
@@ -929,7 +915,7 @@ export class ZLinkChannelSocketRegistry {
929
915
  this.drainClientServerControl(connectionId, connection);
930
916
  if (connection.deadlineAt === undefined) continue;
931
917
  if (nowMs >= connection.deadlineAt) {
932
- this.requestClientServerEndpointTermination(connectionId, connection);
918
+ this.restartClientServerAdmission(connectionId, connection);
933
919
  continue;
934
920
  }
935
921
  if (connection.nextProbeAt === undefined || nowMs < connection.nextProbeAt) continue;
@@ -1298,7 +1284,6 @@ export class ZLinkChannelSocketRegistry {
1298
1284
  connectionId: string,
1299
1285
  connection: ClientServerPhysicalConnection
1300
1286
  ): void {
1301
- if (connection.terminationRequested) return;
1302
1287
  for (;;) {
1303
1288
  if (!connection.readablePoller.wait(0)) return;
1304
1289
  const received = connection.dealer.recv(1);
@@ -1331,7 +1316,7 @@ export class ZLinkChannelSocketRegistry {
1331
1316
  }
1332
1317
  this.applyClientServerDescriptorUpdate(connectionId, connection, record.admission);
1333
1318
  } catch (error) {
1334
- this.requestClientServerEndpointTermination(connectionId, connection);
1319
+ this.restartClientServerAdmission(connectionId, connection);
1335
1320
  this.oneWayFailureSink?.(error);
1336
1321
  return;
1337
1322
  } finally {
@@ -1340,19 +1325,39 @@ export class ZLinkChannelSocketRegistry {
1340
1325
  }
1341
1326
  }
1342
1327
 
1343
- private requestClientServerEndpointTermination(
1328
+ /**
1329
+ * Ends the current logical admission of this connection: later replies of
1330
+ * the fenced attempt no longer apply and its ready target is withdrawn.
1331
+ */
1332
+ private endClientServerAdmission(
1344
1333
  connectionId: string,
1345
- connection: ClientServerPhysicalConnection
1334
+ connection: ClientServerPhysicalConnection,
1335
+ routingId: string | undefined,
1336
+ endpoint: string
1346
1337
  ): void {
1347
- if (connection.terminationRequested) return;
1348
- connection.terminationRequested = true;
1349
1338
  connection.physicalConnectionId = Symbol(connectionId);
1350
1339
  connection.admissionAttempt = undefined;
1351
- this.removeReadyConnection(connectionId);
1352
- try {
1353
- connection.dealer.disconnect(connection.endpoint);
1354
- } catch (error) {
1355
- this.oneWayFailureSink?.(error);
1340
+ if (connection.readyConnectionId !== undefined) {
1341
+ this.removeReadyConnection(connection.readyConnectionId);
1342
+ }
1343
+ for (const callbacks of [...connection.callbacksByAlias.values()]) {
1344
+ callbacks.onTerminated(routingId, endpoint);
1345
+ }
1346
+ }
1347
+
1348
+ /**
1349
+ * A peer deadline or an invalid pushed control ends only the current logical
1350
+ * admission. The connect intent stays with Core, which owns the endpoint
1351
+ * reconnect (transport liveness §6); the service handshake starts again on
1352
+ * the existing admission path.
1353
+ */
1354
+ private restartClientServerAdmission(
1355
+ connectionId: string,
1356
+ connection: ClientServerPhysicalConnection
1357
+ ): void {
1358
+ this.endClientServerAdmission(connectionId, connection, undefined, connection.endpoint);
1359
+ for (const callbacks of [...connection.callbacksByAlias.values()]) {
1360
+ callbacks.onTransportReady('', connection.endpoint);
1356
1361
  }
1357
1362
  }
1358
1363
 
@@ -1612,6 +1617,13 @@ export class ZLinkChannelSocketRegistry {
1612
1617
  applyFanoutPublisherSocketOptions(publisher, channel);
1613
1618
  publisher.bind(channel.publisher.bind);
1614
1619
  this.publishers.set(channelName, publisher);
1620
+ const endpoint = buildAdvertisedEndpoint(
1621
+ publisher.lastEndpoint ?? channel.publisher.bind,
1622
+ channel.publisher.advertiseHost
1623
+ );
1624
+ if (endpoint !== undefined && endpoint.length > 0) {
1625
+ this.recordListener('fanout', channelName, endpoint);
1626
+ }
1615
1627
  this.fanoutPublisherNextBeacon.set(
1616
1628
  channelName,
1617
1629
  performance.now() + CLIENT_SERVER_PROBE_INTERVAL_MS
@@ -1620,20 +1632,6 @@ export class ZLinkChannelSocketRegistry {
1620
1632
  return publisher;
1621
1633
  }
1622
1634
 
1623
- fanoutPublisherEndpoint(channelName: string): string | undefined {
1624
- const channel = this.registration.channels.get(channelName);
1625
- const publisher = this.publishers.get(channelName);
1626
- const boundEndpoint = publisher?.lastEndpoint;
1627
- if (
1628
- channel?.publisher === undefined ||
1629
- boundEndpoint === undefined ||
1630
- boundEndpoint.length === 0
1631
- ) {
1632
- return undefined;
1633
- }
1634
- return advertisedEndpoint(boundEndpoint, channel.publisher.advertiseHost);
1635
- }
1636
-
1637
1635
  routeRouter(routerChannelId: string): ZLinkBackendRouterSocket {
1638
1636
  const existing = this.routeRouters.get(routerChannelId);
1639
1637
  if (existing !== undefined) {
@@ -15,7 +15,6 @@ import {
15
15
  import type { ZLinkBackendMeshNode, ZLinkBackendSpot } from '../backend/contracts';
16
16
  import type { ZLinkFrameworkInternalErrorKind as ZLinkFrameworkInternalErrorKindType } from '../framework-errors-internal';
17
17
  import { ZLinkFrameworkException } from '../../contracts';
18
- import type { ZLinkFanoutListenerStatus } from '../../contracts';
19
18
  import type { Message } from '../../contracts/Common/Message';
20
19
  import { ZLinkSubmitStatus, type ZLinkSubmitResult } from '../messaging/submission-result';
21
20
  import { ZLinkConfigurationException } from '../configuration';
@@ -73,7 +72,6 @@ export interface ZLinkChannelClientTransport {
73
72
  signal?: AbortSignal,
74
73
  metadata?: ReadonlyMap<string, string>
75
74
  ): ZLinkSubmitResult | Promise<ZLinkSubmitResult>;
76
- getFanoutListenerStatus?(channelName: string): ZLinkFanoutListenerStatus;
77
75
  }
78
76
 
79
77
  export type ZLinkChannelClientTransportSource =
@@ -187,7 +185,6 @@ interface ZLinkChannelTransportRuntime {
187
185
  signal?: AbortSignal,
188
186
  metadata?: ReadonlyMap<string, string>
189
187
  ): Promise<ZLinkSubmitResult>;
190
- getFanoutListenerStatus(channelName: string): ZLinkFanoutListenerStatus;
191
188
  canRouteChannel(routerChannelId: string): boolean;
192
189
  canRoutePacketChannel(routerChannelId: string): boolean;
193
190
  routeSubmit(
@@ -12,7 +12,7 @@ import {
12
12
  type ZLinkLocationOptionOverrides
13
13
  } from '../../contracts/Locations/Options';
14
14
  import type { ZLinkFrameworkRegistration } from '../configuration';
15
- import { buildAdvertisedEndpoint, ZLinkConfigurationException } from '../configuration';
15
+ import { ZLinkConfigurationException } from '../configuration';
16
16
  import type { ZLinkLocationRuntime, ZLinkLocationRuntimeStores } from '../locations';
17
17
  import type { ZLinkBackendSubscriberSocket } from '../backend/contracts';
18
18
  import { ZLinkChannelSocketRegistry } from './channel-socket-registry';
@@ -142,11 +142,9 @@ export class ZLinkFanoutLocationRuntime {
142
142
  const owner = this.requireOwnerToken();
143
143
  for (const [channelName, channel] of this.registration.channels) {
144
144
  if (channel.publisher === undefined) continue;
145
- const publisher = this.sockets.publisher(channelName);
146
- const endpoint = advertisedEndpoint(
147
- publisher.lastEndpoint ?? channel.publisher.bind ?? '',
148
- channel.publisher.advertiseHost
149
- );
145
+ // Opening the publisher binds it and fixes its advertised endpoint.
146
+ this.sockets.publisher(channelName);
147
+ const endpoint = this.sockets.fanoutPublisherEndpoint(channelName) ?? '';
150
148
  if (endpoint.length === 0) {
151
149
  throw new ZLinkConfigurationException(
152
150
  `Fanout publisher '${channelName}' did not report a bound endpoint.`
@@ -412,16 +410,6 @@ export class ZLinkFanoutLocationRuntime {
412
410
  }
413
411
  }
414
412
 
415
- function advertisedEndpoint(boundEndpoint: string, advertiseHost: string | undefined): string {
416
- const result = buildAdvertisedEndpoint(boundEndpoint, advertiseHost);
417
- if (result === undefined) {
418
- throw new ZLinkConfigurationException(
419
- `Fanout publisher bound endpoint '${boundEndpoint}' cannot be advertised.`
420
- );
421
- }
422
- return result;
423
- }
424
-
425
413
  function fanoutConnectionId(descriptor: ZLinkFanoutPublisherDescriptor): string {
426
414
  return (
427
415
  `${descriptor.channelName}\0${String(descriptor.publisherRid)}\0` +
@@ -66,17 +66,15 @@ export class ZLinkSpotSerialTurn {
66
66
  readonly yieldAllowed: boolean
67
67
  ) {}
68
68
 
69
- bindExecutionClaim(claim: ZLinkExecutionBarrierClaim | undefined): void {
70
- if (this.executionClaim !== undefined) {
71
- throw new Error('ZLink Spot serial turn already owns an execution claim.');
72
- }
73
- this.executionClaim = claim;
74
- }
75
-
76
69
  get isSuspended(): boolean {
77
70
  return this.suspendSignaled;
78
71
  }
79
72
 
73
+ /** False when a seal that does not drain yielded turns won before this resume. */
74
+ resumeExecutionClaim(): boolean {
75
+ return this.executionClaim?.resume() ?? true;
76
+ }
77
+
80
78
  releaseBoundExecutionClaim(): void {
81
79
  this.executionClaim?.release();
82
80
  this.executionClaim = undefined;
@@ -96,6 +94,14 @@ export class ZLinkSpotSerialTurn {
96
94
  this.owner = owner;
97
95
  }
98
96
 
97
+ /** Settles when the whole turn ends by any path, including yielded continuations. */
98
+ get completion(): Promise<void> {
99
+ return (this.owner ?? Promise.resolve()).then(
100
+ () => undefined,
101
+ () => undefined
102
+ );
103
+ }
104
+
99
105
  blockFrameworkOperations(): void {
100
106
  this.frameworkOperationsBlocked = true;
101
107
  }
@@ -150,7 +156,7 @@ export class ZLinkSpotSerialTurn {
150
156
  return;
151
157
  }
152
158
  this.suspendSignaled = true;
153
- this.releaseBoundExecutionClaim();
159
+ this.executionClaim?.suspend();
154
160
  this.suspendedResolve?.();
155
161
  }
156
162
 
@@ -235,10 +241,19 @@ export function isCurrentZLinkSpotSerialTurn(executor: ZLinkSpotSerialTurnExecut
235
241
 
236
242
  export interface ZLinkExecutionBarrierSeal {
237
243
  readonly generation: bigint;
244
+ /**
245
+ * A Spot Close seal waits for yielded turns accepted before it and lets them
246
+ * resume (Spot address messaging §7 step 2); a relocation seal does not, and
247
+ * their continuation fails (execution gate §4).
248
+ */
249
+ readonly drainsYieldedTurns: boolean;
238
250
  }
239
251
 
240
252
  export interface ZLinkExecutionBarrierClaim {
241
253
  release(): void;
254
+ /** The turn yielded; it no longer runs until resume() admits it again. */
255
+ suspend(): void;
256
+ resume(): boolean;
242
257
  }
243
258
 
244
259
  interface ZLinkExecutionBarrierWaiter {
@@ -254,11 +269,16 @@ interface ZLinkExecutionBarrierWaiter {
254
269
  export class ZLinkExecutionBarrier {
255
270
  private generation = 0n;
256
271
  private activeClaims = 0;
272
+ private suspendedClaims = 0;
257
273
  private currentSeal: ZLinkExecutionBarrierSeal | undefined;
258
274
  private readonly admissionWaiters: ZLinkExecutionBarrierWaiter[] = [];
259
275
  private readonly quiescenceWaiters = new Set<() => void>();
260
276
  private committed = false;
261
277
 
278
+ get isSealed(): boolean {
279
+ return this.currentSeal !== undefined || this.committed;
280
+ }
281
+
262
282
  async enter(): Promise<ZLinkExecutionBarrierClaim> {
263
283
  if (this.committed) {
264
284
  throw createInternalFrameworkException(
@@ -274,19 +294,7 @@ export class ZLinkExecutionBarrier {
274
294
  return this.createClaim();
275
295
  }
276
296
 
277
- /**
278
- * Re-enters a previously yielded application turn only while admission is
279
- * open. A continuation must fail instead of waiting behind a relocation
280
- * seal that already won the turn-boundary race.
281
- */
282
- tryEnter(): ZLinkExecutionBarrierClaim | undefined {
283
- if (this.committed || this.currentSeal !== undefined) {
284
- return undefined;
285
- }
286
- return this.createClaim();
287
- }
288
-
289
- seal(): ZLinkExecutionBarrierSeal {
297
+ seal(drainsYieldedTurns = false): ZLinkExecutionBarrierSeal {
290
298
  if (this.committed) {
291
299
  throw createInternalFrameworkException(
292
300
  ZLinkFrameworkInternalErrorKind.SpotMoving,
@@ -296,17 +304,19 @@ export class ZLinkExecutionBarrier {
296
304
  if (this.currentSeal !== undefined) {
297
305
  throw new Error('ZLink execution barrier is already sealed.');
298
306
  }
299
- const seal = Object.freeze({ generation: ++this.generation });
307
+ const seal = Object.freeze({ generation: ++this.generation, drainsYieldedTurns });
300
308
  this.currentSeal = seal;
301
309
  return seal;
302
310
  }
303
311
 
304
312
  async waitForQuiescence(seal: ZLinkExecutionBarrierSeal, signal?: AbortSignal): Promise<void> {
305
313
  this.requireCurrent(seal);
306
- if (this.activeClaims === 0) return;
314
+ if (this.isQuiescent(seal)) return;
307
315
  if (signal?.aborted === true) throw signal.reason;
308
316
  await new Promise<void>((resolve, reject) => {
309
317
  const complete = () => {
318
+ if (!this.isQuiescent(seal)) return;
319
+ this.quiescenceWaiters.delete(complete);
310
320
  signal?.removeEventListener('abort', abort);
311
321
  resolve();
312
322
  };
@@ -345,18 +355,46 @@ export class ZLinkExecutionBarrier {
345
355
  return this.currentSeal?.generation === seal.generation;
346
356
  }
347
357
 
358
+ private isQuiescent(seal: ZLinkExecutionBarrierSeal): boolean {
359
+ return this.activeClaims === 0 && (!seal.drainsYieldedTurns || this.suspendedClaims === 0);
360
+ }
361
+
362
+ private notifyQuiescence(): void {
363
+ for (const waiter of [...this.quiescenceWaiters]) waiter();
364
+ }
365
+
348
366
  private createClaim(): ZLinkExecutionBarrierClaim {
349
367
  this.activeClaims++;
350
- let released = false;
368
+ let state: 'active' | 'suspended' | 'released' = 'active';
369
+ const release = () => {
370
+ if (state === 'released') return;
371
+ if (state === 'active') this.activeClaims--;
372
+ else this.suspendedClaims--;
373
+ state = 'released';
374
+ this.notifyQuiescence();
375
+ };
351
376
  return {
352
- release: () => {
353
- if (released) return;
354
- released = true;
377
+ release,
378
+ suspend: () => {
379
+ if (state !== 'active') return;
380
+ state = 'suspended';
355
381
  this.activeClaims--;
356
- if (this.activeClaims === 0) {
357
- for (const waiter of this.quiescenceWaiters) waiter();
358
- this.quiescenceWaiters.clear();
382
+ this.suspendedClaims++;
383
+ this.notifyQuiescence();
384
+ },
385
+ resume: () => {
386
+ if (state !== 'suspended') return state === 'active';
387
+ if (
388
+ this.committed ||
389
+ (this.currentSeal !== undefined && !this.currentSeal.drainsYieldedTurns)
390
+ ) {
391
+ release();
392
+ return false;
359
393
  }
394
+ state = 'active';
395
+ this.suspendedClaims--;
396
+ this.activeClaims++;
397
+ return true;
360
398
  }
361
399
  };
362
400
  }
@@ -38,6 +38,7 @@ export interface ZLinkSerialWorkPreparation {
38
38
 
39
39
  type SerialWorkRecord<T> = Omit<ZLinkSerialWorkRecord<T>, 'acceptedSequence'> & {
40
40
  acceptedSequence: bigint;
41
+ readonly operationEnvelope: boolean;
41
42
  readonly preparation?: ZLinkSerialWorkPreparation;
42
43
  preparationState: 'idle' | 'pending' | 'canceling' | 'ready' | 'failed';
43
44
  preparationController?: AbortController;
@@ -55,6 +56,7 @@ interface ZLinkSerialAdmissionLane {
55
56
  export class ZLinkSerialExecutionQueue {
56
57
  private readonly application: ZLinkSerialAdmissionLane;
57
58
  private readonly lifecycle: ZLinkSerialAdmissionLane;
59
+ private activeLifecycle?: ZLinkSerialWorkRecord<unknown>;
58
60
  private readonly ownerTimeBudget: number;
59
61
  private readonly lifecycleBurstLimit: number;
60
62
  private nextAcceptedSequence = 1n;
@@ -138,23 +140,17 @@ export class ZLinkSerialExecutionQueue {
138
140
  return this.admit(operation, options, context);
139
141
  }
140
142
 
141
- snapshot(): {
142
- readonly applicationMessages: number;
143
- readonly applicationBytes: number;
144
- readonly lifecycleMessages: number;
145
- readonly lifecycleBytes: number;
146
- } {
147
- return {
148
- applicationMessages: this.application.records.length,
149
- applicationBytes: 0,
150
- lifecycleMessages: this.lifecycle.records.length,
151
- lifecycleBytes: 0
152
- };
143
+ /** Holds lifecycle FIFO through an operation result while its application turns can run. */
144
+ submitLifecycleOperation<T>(operation: () => Promise<T> | T): Promise<T> {
145
+ return this.admit(operation, { lane: 'lifecycle' }, undefined, undefined, true);
153
146
  }
154
147
 
155
148
  get hasPendingWork(): boolean {
156
149
  return (
157
- this.draining || this.application.records.length > 0 || this.lifecycle.records.length > 0
150
+ this.draining ||
151
+ this.activeLifecycle !== undefined ||
152
+ this.application.records.length > 0 ||
153
+ this.lifecycle.records.length > 0
158
154
  );
159
155
  }
160
156
 
@@ -165,15 +161,21 @@ export class ZLinkSerialExecutionQueue {
165
161
 
166
162
  /** Stops new submissions while allowing the accepted FIFO to finish. */
167
163
  close(): Promise<void> {
168
- this.closed = true;
164
+ this.closeAdmission();
169
165
  return this.whenIdle();
170
166
  }
171
167
 
168
+ /** Stops admission within the lifecycle operation that owns this queue's close. */
169
+ closeAdmission(): void {
170
+ this.closed = true;
171
+ }
172
+
172
173
  private admit<T>(
173
174
  operation: () => Promise<T> | T,
174
175
  options: ZLinkSerialWorkOptions,
175
176
  context?: unknown,
176
- preparation?: ZLinkSerialWorkPreparation
177
+ preparation?: ZLinkSerialWorkPreparation,
178
+ operationEnvelope = false
177
179
  ): Promise<T> {
178
180
  if (this.closed) throw new Error('The serial execution queue is closed.');
179
181
  const lane = options.lane ?? 'application';
@@ -189,6 +191,14 @@ export class ZLinkSerialExecutionQueue {
189
191
  const release = (): void => {
190
192
  if (released) return;
191
193
  released = true;
194
+ if (this.activeLifecycle === record) {
195
+ this.activeLifecycle = undefined;
196
+ if (this.application.records.length > 0 || this.lifecycle.records.length > 0) {
197
+ this.scheduleDrain();
198
+ } else if (!this.draining) {
199
+ this.resolveIdleWaiters();
200
+ }
201
+ }
192
202
  };
193
203
  const resolve = (value: T): void => {
194
204
  if (settled) return;
@@ -204,6 +214,7 @@ export class ZLinkSerialExecutionQueue {
204
214
  acceptedSequence: 0n,
205
215
  lane,
206
216
  operation,
217
+ operationEnvelope,
207
218
  context,
208
219
  preparation,
209
220
  preparationState: preparation === undefined ? 'ready' : 'idle',
@@ -275,7 +286,25 @@ export class ZLinkSerialExecutionQueue {
275
286
  }
276
287
  this.claimStartedAt ??= performance.now();
277
288
  try {
278
- await this.executeRecord(record);
289
+ if (record.operationEnvelope) {
290
+ try {
291
+ const outcome = record.operation();
292
+ void Promise.resolve(outcome).then(
293
+ (value) => {
294
+ record.resolve(value);
295
+ record.release();
296
+ },
297
+ (error) => {
298
+ record.reject(error);
299
+ record.release();
300
+ }
301
+ );
302
+ } catch (error) {
303
+ record.fail(error);
304
+ }
305
+ } else {
306
+ await this.executeRecord(record);
307
+ }
279
308
  } catch (error) {
280
309
  record.fail(error);
281
310
  } finally {
@@ -291,11 +320,12 @@ export class ZLinkSerialExecutionQueue {
291
320
  this.claimStartedAt = undefined;
292
321
  if (
293
322
  !waitingForPreparation &&
294
- (this.application.records.length > 0 || this.lifecycle.records.length > 0)
323
+ (this.application.records.length > 0 ||
324
+ (this.activeLifecycle === undefined && this.lifecycle.records.length > 0))
295
325
  ) {
296
326
  this.scheduleDrain();
297
327
  } else {
298
- if (this.application.records.length === 0 && this.lifecycle.records.length === 0) {
328
+ if (!this.hasPendingWork) {
299
329
  this.resolveIdleWaiters();
300
330
  }
301
331
  }
@@ -309,7 +339,7 @@ export class ZLinkSerialExecutionQueue {
309
339
  }
310
340
  | undefined {
311
341
  const applicationReady = this.application.records.length > 0;
312
- const lifecycleReady = this.lifecycle.records.length > 0;
342
+ const lifecycleReady = this.activeLifecycle === undefined && this.lifecycle.records.length > 0;
313
343
  if (!applicationReady && !lifecycleReady) return undefined;
314
344
  if (!applicationReady) {
315
345
  return { lane: 'lifecycle', record: this.lifecycle.records[0]! };
@@ -339,7 +369,9 @@ export class ZLinkSerialExecutionQueue {
339
369
  if (this.application.records.length > 0 && this.lifecycleStreak >= this.lifecycleBurstLimit) {
340
370
  this.lifecycleDebt = true;
341
371
  }
342
- return this.lifecycle.records.shift()!;
372
+ const record = this.lifecycle.records.shift()!;
373
+ this.activeLifecycle = record;
374
+ return record;
343
375
  }
344
376
 
345
377
  private takeApplication(): ZLinkSerialWorkRecord<unknown> {
@@ -0,0 +1,21 @@
1
+ import type { ZLinkListenerKind } from '../../contracts/RouteMesh/RuntimeTopology';
2
+
3
+ /**
4
+ * Bound listener records of one runtime generation (spec 04 §3.1). A listener writes its
5
+ * record when it finishes binding. The listener status query reads only this object.
6
+ */
7
+ export class ZLinkListenerRecords {
8
+ private readonly endpoints = new Map<string, string>();
9
+
10
+ record(kind: ZLinkListenerKind, name: string, endpoint: string): void {
11
+ this.endpoints.set(`${kind}:${name}`, endpoint);
12
+ }
13
+
14
+ clear(): void {
15
+ this.endpoints.clear();
16
+ }
17
+
18
+ endpoint(kind: ZLinkListenerKind, name: string): string | undefined {
19
+ return this.endpoints.get(`${kind}:${name}`);
20
+ }
21
+ }