@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
@@ -17,6 +17,7 @@ const DEFAULT_RELOCATION_CALLBACK_CONCURRENCY = 8;
17
17
 
18
18
  export interface ServiceRelocationSealedWork {
19
19
  readonly boundSessionState: Uint8Array;
20
+ readonly sessionBinding?: ServiceRelocationParticipant['sessionBinding'];
20
21
  readonly queuedMessages: readonly ServiceRelocationQueuedMessage[];
21
22
  readonly timers: readonly ServiceRelocationTimer[];
22
23
  }
@@ -185,6 +186,7 @@ export class ServiceRelocationObjectCaptureOwner {
185
186
  authorityOwnerGeneration: unit.authorityOwnerGeneration,
186
187
  applicationState: Buffer.from(await unit.captureApplicationState(signal)),
187
188
  boundSessionState: Buffer.from(work.boundSessionState),
189
+ ...(work.sessionBinding === undefined ? {} : { sessionBinding: work.sessionBinding }),
188
190
  queuedMessages: work.queuedMessages.map((message) => ({
189
191
  sequence: message.sequence,
190
192
  payload: Buffer.from(message.payload)
@@ -39,6 +39,7 @@ export interface ServiceRelocationParticipant {
39
39
  readonly applicationStatePresent?: boolean;
40
40
  readonly applicationState: Uint8Array;
41
41
  readonly boundSessionState: Uint8Array;
42
+ readonly sessionBinding?: GeneratedRelocationEnvelopeV1['sessionBindings'][number];
42
43
  readonly queuedMessages: readonly ServiceRelocationQueuedMessage[];
43
44
  readonly timers: readonly ServiceRelocationTimer[];
44
45
  }
@@ -726,6 +727,11 @@ export function encodeServiceRelocationEnvelope(
726
727
  'timer due time'
727
728
  )
728
729
  })),
730
+ sessionBindings: participants.flatMap((participant, index) =>
731
+ participant.sessionBinding === undefined
732
+ ? []
733
+ : [{ ...participant.sessionBinding, participantId: BigInt(index + 1) }]
734
+ ),
729
735
  pendingTimerTicks: pending.map((value) => ({
730
736
  participantId: value.participantId,
731
737
  order: value.order,
@@ -765,11 +771,22 @@ export function decodeServiceRelocationEnvelope(
765
771
  const aggregateId = uuid(generated.relocationHigh, generated.relocationLow);
766
772
  if (aggregateId === undefined) throw new TypeError('Invalid relocation identity.');
767
773
  positiveBigInt(aggregateGeneration, 'aggregate generation');
774
+ const bindings = new Map(
775
+ generated.sessionBindings.map((binding) => [binding.participantId, binding])
776
+ );
777
+ for (const id of bindings.keys()) {
778
+ if (id > BigInt(generated.applicationStates.length))
779
+ throw new TypeError('Session binding participant is outside its inventory.');
780
+ }
768
781
  const participants = generated.applicationStates.map((state, index) => {
769
782
  if (state.participantId !== BigInt(index + 1)) {
770
783
  throw new TypeError('Application state participant order is invalid.');
771
784
  }
772
- return wireParticipant(state.participantId, state.hasState, Buffer.from(state.payload));
785
+ const binding = bindings.get(state.participantId);
786
+ return {
787
+ ...wireParticipant(state.participantId, state.hasState, Buffer.from(state.payload)),
788
+ ...(binding === undefined ? {} : { sessionBinding: binding })
789
+ };
773
790
  });
774
791
  const byId = new Map(participants.map((value) => [value.participantId!, value]));
775
792
  for (const work of generated.savedWork) {
@@ -422,7 +422,12 @@ export interface ServiceSpot {
422
422
  requestToChannel(
423
423
  channelName: string,
424
424
  parts: MessageLike | readonly MessageLike[],
425
- options?: { flags?: number; timeoutMs?: number; applicationMetadata?: Buffer }
425
+ options?: {
426
+ flags?: number;
427
+ timeoutMs?: number;
428
+ applicationMetadata?: Buffer;
429
+ onAdmitted?: () => void;
430
+ }
426
431
  ): MeshOperationId;
427
432
  sendToSpot(
428
433
  targetNodeRid: unknown,
@@ -445,6 +450,7 @@ export interface ServiceSpot {
445
450
  timeoutMs?: number;
446
451
  applicationMetadata?: Buffer;
447
452
  routeFence?: ServiceDirectSpotRouteFence;
453
+ onAdmitted?: () => void;
448
454
  entrySpot?: boolean;
449
455
  }
450
456
  ): MeshOperationId;
@@ -10,7 +10,7 @@ import {
10
10
  ZLinkDispatchMessageKind,
11
11
  ZLinkRuntimeMessageFlowOutcome as ZLinkMessageFlowOutcome
12
12
  } from '../../contracts/Dispatch/ZLinkDispatchOptions';
13
- import { awaitWithAbort } from '../abort';
13
+ import { requestDurableService } from './durable-service-request';
14
14
  import { shouldCompactBackingArray } from '../admission';
15
15
  import { RequestResult, SubmitResult, isZLinkBackendResultError } from '../backend/runtime-values';
16
16
  import type { ZLinkDispatchErrorReporter } from '../channels/dispatch-error-reporter';
@@ -112,7 +112,6 @@ import {
112
112
  encodeApplicationPayload,
113
113
  type ServiceApplicationPayload
114
114
  } from './service-wire-m6a-codec';
115
- const STATEFUL_OPERATION_RETRY_TICK_MS = 20;
116
115
 
117
116
  const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
118
117
 
@@ -454,7 +453,7 @@ export class ServiceStatefulRuntime {
454
453
  );
455
454
  this.registry = new ServiceStatefulRegistry(nodeRid, nodeGeneration);
456
455
  this.registry.createEntrySpot(nodeRid);
457
- raw.setServiceIngress((record) => this.ingress(record));
456
+ raw.setServiceIngress((record) => this.ingress(record), acceptsStatefulIngress);
458
457
  }
459
458
 
460
459
  setMailboxDropHandler(
@@ -1326,7 +1325,8 @@ export class ServiceStatefulRuntime {
1326
1325
  sourceSpotId: string,
1327
1326
  requested: ServiceDirectSpotRouteFence,
1328
1327
  payload: ServiceApplicationPayload,
1329
- timeoutMs: number
1328
+ timeoutMs: number,
1329
+ onAdmitted?: () => void
1330
1330
  ): ServiceStatefulPendingOperation {
1331
1331
  const pending = this.operations.register(timeoutMs);
1332
1332
  const target = this.acceptSpotAuthority(requested);
@@ -1346,7 +1346,9 @@ export class ServiceStatefulRuntime {
1346
1346
  target.targetNodeRid,
1347
1347
  [header, encodeApplicationPayload(payload)],
1348
1348
  timeoutMs,
1349
- 'spotRequest'
1349
+ 'spotRequest',
1350
+ undefined,
1351
+ onAdmitted
1350
1352
  );
1351
1353
  return pending;
1352
1354
  }
@@ -1384,7 +1386,8 @@ export class ServiceStatefulRuntime {
1384
1386
  payload: ServiceApplicationPayload,
1385
1387
  timeoutMs: number,
1386
1388
  sourceActor?: ServiceActorRef,
1387
- boundSession?: { readonly sessionRid: string; readonly bindingGeneration: bigint }
1389
+ boundSession?: { readonly sessionRid: string; readonly bindingGeneration: bigint },
1390
+ onAdmitted?: () => void
1388
1391
  ): ServiceStatefulPendingOperation {
1389
1392
  const pending = this.operations.register(timeoutMs);
1390
1393
  const route = this.tryActorFence(target);
@@ -1413,7 +1416,9 @@ export class ServiceStatefulRuntime {
1413
1416
  target.nodeRid,
1414
1417
  [header, encodeApplicationPayload(payload)],
1415
1418
  timeoutMs,
1416
- 'actorRequest'
1419
+ 'actorRequest',
1420
+ undefined,
1421
+ onAdmitted
1417
1422
  );
1418
1423
  return pending;
1419
1424
  }
@@ -1499,7 +1504,8 @@ export class ServiceStatefulRuntime {
1499
1504
  timeoutMs: number,
1500
1505
  sourceSpotId?: string,
1501
1506
  metadataFrame?: Uint8Array,
1502
- instanceIntent = false
1507
+ instanceIntent = false,
1508
+ onAdmitted?: () => void
1503
1509
  ): ServiceStatefulPendingOperation {
1504
1510
  const pending = this.operations.register(timeoutMs);
1505
1511
  this.submitRequest(
@@ -1523,7 +1529,9 @@ export class ServiceStatefulRuntime {
1523
1529
  metadataFrame
1524
1530
  ),
1525
1531
  timeoutMs,
1526
- 'instanceSpotRequest'
1532
+ 'instanceSpotRequest',
1533
+ undefined,
1534
+ onAdmitted
1527
1535
  );
1528
1536
  return pending;
1529
1537
  }
@@ -1551,7 +1559,8 @@ export class ServiceStatefulRuntime {
1551
1559
  payloadFrame: Buffer,
1552
1560
  deadlineUnixMs: bigint,
1553
1561
  sourceSpotId?: string,
1554
- metadataFrame?: Uint8Array
1562
+ metadataFrame?: Uint8Array,
1563
+ onAdmitted?: () => void
1555
1564
  ): ServiceStatefulPendingOperation {
1556
1565
  const timeoutMs = remainingDeadlineMs(deadlineUnixMs);
1557
1566
  const pending = this.operations.register(timeoutMs);
@@ -1576,7 +1585,9 @@ export class ServiceStatefulRuntime {
1576
1585
  metadataFrame
1577
1586
  ),
1578
1587
  timeoutMs,
1579
- 'instanceSpotRequest'
1588
+ 'instanceSpotRequest',
1589
+ undefined,
1590
+ onAdmitted
1580
1591
  );
1581
1592
  return pending;
1582
1593
  }
@@ -2029,29 +2040,6 @@ export class ServiceStatefulRuntime {
2029
2040
  private async ingress(
2030
2041
  record: RawServiceIngressRecord
2031
2042
  ): Promise<RawServicePumpResult | undefined> {
2032
- // These frozen relocation controls overlap the legacy stateful command
2033
- // number range (30-34). They are bare infrastructure records and must
2034
- // fall through to RawServiceMeshRuntime's relocation ingress instead of
2035
- // being decoded as stateful actor/spot work.
2036
- if (
2037
- record.command === M6bServiceWireCommand.relocationReady ||
2038
- record.command === M6bServiceWireCommand.relocationData ||
2039
- record.command === M6bServiceWireCommand.replyRelay ||
2040
- record.command === M6bServiceWireCommand.relocationCutover
2041
- ) {
2042
- return undefined;
2043
- }
2044
- if (
2045
- record.command < M6bServiceWireCommand.spotSend ||
2046
- (record.command > M6bServiceWireCommand.instanceSpot &&
2047
- record.command !== M6bServiceWireCommand.userSpotCreate &&
2048
- record.command !== M6bServiceWireCommand.userSpotClose &&
2049
- record.command !== M6bServiceWireCommand.actorCreate &&
2050
- record.command !== M6bServiceWireCommand.messageFollow &&
2051
- record.command !== M6bServiceWireCommand.boundSessionReplaced)
2052
- ) {
2053
- return undefined;
2054
- }
2055
2043
  let decoded: ServiceStatefulWireRecord;
2056
2044
  let canonicalActorJoin: ActorJoin28 | undefined;
2057
2045
  try {
@@ -2371,6 +2359,7 @@ export class ServiceStatefulRuntime {
2371
2359
  localReply,
2372
2360
  metadataFrame
2373
2361
  ).finally(() => ownedIngress.applicationJobOwner?.close());
2362
+ ingress.onAdmitted?.();
2374
2363
  return 'infrastructure';
2375
2364
  }
2376
2365
  if (
@@ -2384,6 +2373,7 @@ export class ServiceStatefulRuntime {
2384
2373
  payloadFrame,
2385
2374
  metadataFrame
2386
2375
  ).finally(() => ownedIngress.applicationJobOwner?.close());
2376
+ ingress.onAdmitted?.();
2387
2377
  return 'infrastructure';
2388
2378
  }
2389
2379
  const spot = this.requireInstanceActivation(ingress, record);
@@ -3821,7 +3811,10 @@ export class ServiceStatefulRuntime {
3821
3811
  stateful,
3822
3812
  applicationJob
3823
3813
  });
3824
- if (accepted) return domain;
3814
+ if (accepted) {
3815
+ ingress.onAdmitted?.();
3816
+ return domain;
3817
+ }
3825
3818
  applicationJob.close();
3826
3819
  if (stateful.reply !== undefined) {
3827
3820
  const result = failure(
@@ -4212,64 +4205,15 @@ export class ServiceStatefulRuntime {
4212
4205
  operationKind: 'userSpotCreate' | 'userSpotClose' | 'actorCreate' | 'streamBind' | 'actorJoin',
4213
4206
  deadlineMs: number
4214
4207
  ): Promise<readonly Buffer[]> {
4215
- const stop = new AbortController();
4216
- const unobserve = this.raw.observePeerConnectionIntentRemoved((nodeRoutingId) => {
4217
- if (nodeRoutingId === targetNodeRid) {
4218
- stop.abort(
4219
- createInternalFrameworkException(
4220
- ZLinkFrameworkInternalErrorKind.RouteNotConnected,
4221
- `${operationKind} target node lifecycle ended.`,
4222
- true
4223
- )
4224
- );
4225
- }
4226
- });
4227
- let wasAdmitted = false;
4228
- try {
4229
- // Only durable lifecycle operations replay these frozen frames. Received
4230
- // envelopes leave this loop before decoding, including failure terminals.
4231
- for (;;) {
4232
- this.requireOpen();
4233
- stop.signal.throwIfAborted();
4234
- const remainingMs = Math.ceil(deadlineMs - performance.now());
4235
- if (remainingMs <= 0) throw durableOperationExhausted(operationKind, wasAdmitted);
4236
- try {
4237
- return await awaitWithAbort(
4238
- targetNodeRid === this.nodeRid
4239
- ? this.requestLocalInfrastructure(parts[0]!, correlation, remainingMs)
4240
- : this.raw.requestService(targetNodeRid, parts, remainingMs),
4241
- stop.signal
4242
- );
4243
- } catch (error) {
4244
- stop.signal.throwIfAborted();
4245
- if (!durableRequestCanReplay(error)) throw error;
4246
- if (durableRequestWasAdmitted(error)) wasAdmitted = true;
4247
- const retryDelayMs = Math.min(
4248
- STATEFUL_OPERATION_RETRY_TICK_MS,
4249
- deadlineMs - performance.now()
4250
- );
4251
- if (retryDelayMs <= 0) {
4252
- throw durableOperationExhausted(operationKind, wasAdmitted, error);
4253
- }
4254
- let timer: ReturnType<typeof setTimeout> | undefined;
4255
- try {
4256
- await awaitWithAbort(
4257
- new Promise<void>((resolve) => {
4258
- timer = setTimeout(resolve, retryDelayMs);
4259
- }),
4260
- stop.signal
4261
- );
4262
- } finally {
4263
- if (timer !== undefined) clearTimeout(timer);
4264
- }
4265
- }
4266
- }
4267
- } catch (error) {
4268
- stop.signal.throwIfAborted();
4269
- throw error;
4270
- } finally {
4271
- unobserve();
4208
+ this.requireOpen();
4209
+ if (targetNodeRid === this.nodeRid) {
4210
+ return this.requestLocalInfrastructure(
4211
+ parts[0]!,
4212
+ correlation,
4213
+ Math.ceil(deadlineMs - performance.now())
4214
+ );
4272
4215
  }
4216
+ return requestDurableService(this.raw, targetNodeRid, parts, deadlineMs, operationKind);
4273
4217
  }
4274
4218
 
4275
4219
  private requestLocalInfrastructure(
@@ -4393,7 +4337,8 @@ export class ServiceStatefulRuntime {
4393
4337
  | 'streamBind'
4394
4338
  | 'streamUnbind'
4395
4339
  | 'instanceSpotRequest',
4396
- actor?: ServiceActorRef
4340
+ actor?: ServiceActorRef,
4341
+ onAdmitted?: () => void
4397
4342
  ): void {
4398
4343
  if (targetNodeRid === this.nodeRid) {
4399
4344
  const deadlineUnixMs = BigInt(Date.now() + Math.max(0, timeoutMs));
@@ -4411,6 +4356,7 @@ export class ServiceStatefulRuntime {
4411
4356
  flags: parts[0]![SERVICE_WIRE_FLAGS_OFFSET]!,
4412
4357
  sourceRoutingId: this.nodeRid,
4413
4358
  requestSequence: pending.id,
4359
+ onAdmitted,
4414
4360
  parts,
4415
4361
  applicationJobOwner
4416
4362
  };
@@ -4429,7 +4375,7 @@ export class ServiceStatefulRuntime {
4429
4375
  })().catch((error) => this.operations.reply(pending.id, failure(error)));
4430
4376
  return;
4431
4377
  }
4432
- void this.raw.requestService(targetNodeRid, parts, timeoutMs).then(
4378
+ void this.raw.requestService(targetNodeRid, parts, timeoutMs, onAdmitted).then(
4433
4379
  (reply) => this.completeRemoteReply(pending, targetNodeRid, operationKind, actor, reply),
4434
4380
  (error) => this.operations.fail(pending.id, error)
4435
4381
  );
@@ -5414,37 +5360,6 @@ function operationReplayExpired(replayExpiresAtMs: number): boolean {
5414
5360
  return performance.now() > replayExpiresAtMs;
5415
5361
  }
5416
5362
 
5417
- function durableRequestWasAdmitted(error: unknown): boolean {
5418
- // Request NotConnected includes Core handover of an admitted request. The
5419
- // submit/request phase, rather than the result number, owns this distinction.
5420
- return isZLinkBackendResultError(error) && error.operation === 'request';
5421
- }
5422
-
5423
- function durableOperationExhausted(
5424
- operationKind: 'userSpotCreate' | 'userSpotClose' | 'actorCreate' | 'streamBind' | 'actorJoin',
5425
- wasAdmitted: boolean,
5426
- cause?: unknown
5427
- ): ZLinkFrameworkException {
5428
- return createInternalFrameworkException(
5429
- wasAdmitted
5430
- ? ZLinkFrameworkInternalErrorKind.DeadlineExceeded
5431
- : ZLinkFrameworkInternalErrorKind.RouteNotConnected,
5432
- `${operationKind} exhausted its end-to-end deadline.`,
5433
- true,
5434
- cause
5435
- );
5436
- }
5437
-
5438
- function durableRequestCanReplay(error: unknown): boolean {
5439
- if (!isZLinkBackendResultError(error)) return false;
5440
- return error.operation === 'request'
5441
- ? error.result === RequestResult.NotConnected || error.result === RequestResult.TimedOut
5442
- : error.result === SubmitResult.NotConnected ||
5443
- error.result === SubmitResult.NotFound ||
5444
- error.result === SubmitResult.NotAdmitted ||
5445
- error.result === SubmitResult.Backpressured;
5446
- }
5447
-
5448
5363
  function remainingDeadlineMs(deadlineUnixMs: bigint): number {
5449
5364
  const remainingMs = deadlineUnixMs - BigInt(Date.now());
5450
5365
  if (remainingMs <= 0n) {
@@ -5937,3 +5852,30 @@ export function statefulMailboxData(
5937
5852
  ): ServiceStatefulMailboxData | undefined {
5938
5853
  return record.stateful as ServiceStatefulMailboxData | undefined;
5939
5854
  }
5855
+
5856
+ function acceptsStatefulIngress(command: number): boolean {
5857
+ // These frozen relocation controls overlap the legacy stateful command
5858
+ // number range (30-34). They are bare infrastructure records and must
5859
+ // fall through to RawServiceMeshRuntime's relocation ingress instead of
5860
+ // being decoded as stateful actor/spot work.
5861
+ if (
5862
+ command === M6bServiceWireCommand.relocationReady ||
5863
+ command === M6bServiceWireCommand.relocationData ||
5864
+ command === M6bServiceWireCommand.replyRelay ||
5865
+ command === M6bServiceWireCommand.relocationCutover
5866
+ ) {
5867
+ return false;
5868
+ }
5869
+ if (
5870
+ command < M6bServiceWireCommand.spotSend ||
5871
+ (command > M6bServiceWireCommand.instanceSpot &&
5872
+ command !== M6bServiceWireCommand.userSpotCreate &&
5873
+ command !== M6bServiceWireCommand.userSpotClose &&
5874
+ command !== M6bServiceWireCommand.actorCreate &&
5875
+ command !== M6bServiceWireCommand.messageFollow &&
5876
+ command !== M6bServiceWireCommand.boundSessionReplaced)
5877
+ ) {
5878
+ return false;
5879
+ }
5880
+ return true;
5881
+ }
@@ -189,11 +189,6 @@ export interface ServiceMaintenanceRelocationPrepare extends ServiceWireRelocati
189
189
  /** CRC-32C (Castagnoli) over the fully assembled payload bytes. */
190
190
  readonly payloadChecksumCrc32c: number;
191
191
  readonly applicationVersion: bigint;
192
- /** NODE-INTERNAL; serialized as extra frames after canonical command 40. */
193
- readonly nodeInternalBoundSessions?: readonly {
194
- readonly participantKey: string;
195
- readonly payload: Uint8Array;
196
- }[];
197
192
  }
198
193
 
199
194
  export interface ServiceMaintenanceRelocationReady extends ServiceWireRelocationBase {
@@ -8,7 +8,10 @@ import {
8
8
  } from '../../contracts/Configuration/RegistrationBuilderPolicy';
9
9
  import { descriptorConnectionNotRequired } from './route-mesh-connection-policy';
10
10
  import { SmoothWeightedSelection } from './service-weighted-selection';
11
- import { SERVICE_WIRE_REQUIRED_CAPABILITY } from './service-wire-constants.generated';
11
+ import {
12
+ SERVICE_WIRE_REQUIRED_CAPABILITY,
13
+ ServiceWireCommand
14
+ } from './service-wire-constants.generated';
12
15
 
13
16
  export type ServiceNodeState =
14
17
  'preparing' | 'serving' | 'retiring' | 'draining' | 'stopped' | 'error';
@@ -40,8 +43,14 @@ export interface ServiceNodeDescriptor {
40
43
  readonly pendingCapacityUsed: number;
41
44
  }
42
45
 
43
- export interface AdmittedServicePeer {
46
+ interface ServiceRouteAdmission {
44
47
  readonly descriptor: ServiceNodeDescriptor;
48
+ readonly descriptorBytes?: Uint8Array;
49
+ readonly connectionId?: string | bigint;
50
+ readonly remoteAdmitReceived?: boolean;
51
+ }
52
+
53
+ export interface AdmittedServicePeer extends ServiceRouteAdmission {
45
54
  // The Core selected-route generation this admission belongs to (Core ROUTER
46
55
  // §10.1), as an opaque token compared only for equality.
47
56
  readonly connectionId: string | bigint;
@@ -69,7 +78,7 @@ interface ServiceSelectionCacheEntry {
69
78
  export class ServiceTopologyRegistry {
70
79
  private local: ServiceNodeDescriptor;
71
80
  private readonly peersByRid = new Map<string, AdmittedServicePeer>();
72
- private readonly notRequiredByRid = new Map<string, ServiceNodeDescriptor>();
81
+ private readonly notRequiredByRid = new Map<string, ServiceRouteAdmission>();
73
82
  private readonly knownByRid = new Map<string, ServiceNodeDescriptor>();
74
83
  private readonly selections = new Map<string, ServiceSelectionCacheEntry>();
75
84
 
@@ -96,7 +105,7 @@ export class ServiceTopologyRegistry {
96
105
  }
97
106
  this.local = cloneDescriptor(descriptor);
98
107
  for (const [nodeRoutingId, remote] of this.notRequiredByRid) {
99
- if (!descriptorConnectionNotRequired(this.local, remote)) {
108
+ if (!descriptorConnectionNotRequired(this.local, remote.descriptor)) {
100
109
  this.notRequiredByRid.delete(nodeRoutingId);
101
110
  }
102
111
  }
@@ -107,7 +116,9 @@ export class ServiceTopologyRegistry {
107
116
  descriptor: ServiceNodeDescriptor,
108
117
  connectionId: string | bigint,
109
118
  expected?: ServicePeerAdmissionExpectation,
110
- admitLiveness?: () => ServiceLivenessConnection
119
+ admitLiveness?: () => ServiceLivenessConnection,
120
+ command: number = ServiceWireCommand.hello,
121
+ descriptorBytes?: Uint8Array
111
122
  ): PeerAdmissionResult {
112
123
  try {
113
124
  validateDescriptor(descriptor);
@@ -127,37 +138,53 @@ export class ServiceTopologyRegistry {
127
138
  ) {
128
139
  return 'invalidDescriptor';
129
140
  }
141
+ const current = this.peersByRid.get(descriptor.nodeRoutingId);
142
+ const previous = current ?? this.notRequiredByRid.get(descriptor.nodeRoutingId);
143
+ const receivedAdmit =
144
+ command === ServiceWireCommand.admit ||
145
+ (previous?.connectionId === connectionId && previous.remoteAdmitReceived === true);
146
+ if (
147
+ previous !== undefined &&
148
+ previous.descriptor.lifecycleGeneration === descriptor.lifecycleGeneration
149
+ ) {
150
+ const existing = previous.descriptor;
151
+ if (
152
+ descriptor.descriptorRevision < existing.descriptorRevision ||
153
+ (descriptor.descriptorRevision === existing.descriptorRevision &&
154
+ !(previous.descriptorBytes !== undefined && descriptorBytes !== undefined
155
+ ? Buffer.compare(previous.descriptorBytes, descriptorBytes) === 0
156
+ : sameServiceNodeDescriptor(existing, descriptor))) ||
157
+ (descriptor.descriptorRevision > existing.descriptorRevision &&
158
+ command !== ServiceWireCommand.update)
159
+ )
160
+ return 'staleDescriptor';
161
+ if (
162
+ descriptor.descriptorRevision > existing.descriptorRevision &&
163
+ !sameImmutableDescriptor(existing, descriptor)
164
+ )
165
+ return 'invalidDescriptor';
166
+ } else if (command === ServiceWireCommand.update) {
167
+ return 'staleDescriptor';
168
+ }
169
+ const admission: ServiceRouteAdmission = {
170
+ descriptor: cloneDescriptor(descriptor),
171
+ ...(descriptorBytes === undefined ? {} : { descriptorBytes: Buffer.from(descriptorBytes) }),
172
+ connectionId,
173
+ remoteAdmitReceived: receivedAdmit
174
+ };
130
175
  if (descriptorConnectionNotRequired(this.local, descriptor)) {
131
176
  this.peersByRid.delete(descriptor.nodeRoutingId);
132
177
  this.remember(descriptor);
133
- this.notRequiredByRid.set(descriptor.nodeRoutingId, cloneDescriptor(descriptor));
178
+ this.notRequiredByRid.set(descriptor.nodeRoutingId, admission);
134
179
  this.rebuildSelections();
135
180
  return 'notRequired';
136
181
  }
137
182
  this.notRequiredByRid.delete(descriptor.nodeRoutingId);
138
183
 
139
- const current = this.peersByRid.get(descriptor.nodeRoutingId);
140
- if (
141
- current !== undefined &&
142
- current.descriptor.lifecycleGeneration === descriptor.lifecycleGeneration &&
143
- descriptor.descriptorRevision > current.descriptor.descriptorRevision &&
144
- !sameImmutableDescriptor(current.descriptor, descriptor)
145
- ) {
146
- return 'invalidDescriptor';
147
- }
148
- if (
149
- current !== undefined &&
150
- current.descriptor.lifecycleGeneration === descriptor.lifecycleGeneration &&
151
- (current.descriptor.descriptorRevision > descriptor.descriptorRevision ||
152
- (current.descriptor.descriptorRevision === descriptor.descriptorRevision &&
153
- !sameServiceNodeDescriptor(current.descriptor, descriptor)))
154
- ) {
155
- return 'staleDescriptor';
156
- }
157
184
  // Core selects the one route per RID (Core ROUTER §10.1); an admission on
158
185
  // that route replaces the admission of a route it replaced.
159
186
  this.peersByRid.set(descriptor.nodeRoutingId, {
160
- descriptor: cloneDescriptor(descriptor),
187
+ ...admission,
161
188
  connectionId,
162
189
  liveness: admitLiveness?.() ?? current?.liveness
163
190
  });
@@ -175,10 +202,18 @@ export class ServiceTopologyRegistry {
175
202
  }
176
203
 
177
204
  peer(nodeRoutingId: string): AdmittedServicePeer | undefined {
178
- const peer = this.peersByRid.get(nodeRoutingId);
205
+ const peer = this.admittedPeer(nodeRoutingId);
179
206
  return peer === undefined ? undefined : clonePeer(peer);
180
207
  }
181
208
 
209
+ routeAdmission(nodeRoutingId: string): ServiceRouteAdmission | undefined {
210
+ return this.peersByRid.get(nodeRoutingId) ?? this.notRequiredByRid.get(nodeRoutingId);
211
+ }
212
+
213
+ admittedPeer(nodeRoutingId: string): AdmittedServicePeer | undefined {
214
+ return this.peersByRid.get(nodeRoutingId);
215
+ }
216
+
182
217
  peers(): readonly AdmittedServicePeer[] {
183
218
  return this.peersCore();
184
219
  }
@@ -193,12 +228,14 @@ export class ServiceTopologyRegistry {
193
228
 
194
229
  notRequiredPeers(): readonly ServiceNodeDescriptor[] {
195
230
  return [...this.notRequiredByRid.values()]
196
- .sort((left, right) => compareOrdinal(left.nodeRoutingId, right.nodeRoutingId))
197
- .map(cloneDescriptor);
231
+ .sort((left, right) =>
232
+ compareOrdinal(left.descriptor.nodeRoutingId, right.descriptor.nodeRoutingId)
233
+ )
234
+ .map((peer) => cloneDescriptor(peer.descriptor));
198
235
  }
199
236
 
200
237
  replaceDiscoveredNotRequired(descriptors: readonly ServiceNodeDescriptor[]): void {
201
- const next = new Map<string, ServiceNodeDescriptor>();
238
+ const next = new Map<string, ServiceRouteAdmission>();
202
239
  for (const descriptor of descriptors) {
203
240
  validateDescriptor(descriptor);
204
241
  if (
@@ -206,13 +243,16 @@ export class ServiceTopologyRegistry {
206
243
  descriptor.nodeRoutingId !== this.local.nodeRoutingId &&
207
244
  descriptorConnectionNotRequired(this.local, descriptor)
208
245
  ) {
209
- next.set(descriptor.nodeRoutingId, cloneDescriptor(descriptor));
246
+ next.set(descriptor.nodeRoutingId, {
247
+ ...this.notRequiredByRid.get(descriptor.nodeRoutingId),
248
+ descriptor: cloneDescriptor(descriptor)
249
+ });
210
250
  }
211
251
  }
212
252
  this.notRequiredByRid.clear();
213
- for (const [nodeRoutingId, descriptor] of next) {
214
- this.notRequiredByRid.set(nodeRoutingId, descriptor);
215
- this.remember(descriptor);
253
+ for (const [nodeRoutingId, admission] of next) {
254
+ this.notRequiredByRid.set(nodeRoutingId, admission);
255
+ this.remember(admission.descriptor);
216
256
  }
217
257
  this.rebuildSelections();
218
258
  }
@@ -222,12 +262,27 @@ export class ServiceTopologyRegistry {
222
262
  }
223
263
 
224
264
  knownDescriptor(nodeRoutingId: string): ServiceNodeDescriptor | undefined {
225
- const admitted = this.peersByRid.get(nodeRoutingId)?.descriptor;
226
- const descriptor =
227
- admitted ?? this.notRequiredByRid.get(nodeRoutingId) ?? this.knownByRid.get(nodeRoutingId);
265
+ const descriptor = this.knownPeerDescriptor(nodeRoutingId);
228
266
  return descriptor === undefined ? undefined : cloneDescriptor(descriptor);
229
267
  }
230
268
 
269
+ isObjectClientNodeDirectTarget(nodeRoutingId: string): boolean {
270
+ const descriptor =
271
+ nodeRoutingId === this.local.nodeRoutingId
272
+ ? this.local
273
+ : this.knownPeerDescriptor(nodeRoutingId);
274
+ return descriptor?.objectRole === 'client';
275
+ }
276
+
277
+ private knownPeerDescriptor(nodeRoutingId: string): ServiceNodeDescriptor | undefined {
278
+ const admitted = this.peersByRid.get(nodeRoutingId)?.descriptor;
279
+ return (
280
+ admitted ??
281
+ this.notRequiredByRid.get(nodeRoutingId)?.descriptor ??
282
+ this.knownByRid.get(nodeRoutingId)
283
+ );
284
+ }
285
+
231
286
  hasKnownChannelTarget(channelName: string): boolean {
232
287
  requireText(channelName, 'channelName');
233
288
  return [...this.knownByRid.values()].some(