@zlink-systems/framework 0.24.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
@@ -181,10 +181,6 @@ class ZLinkHostServiceRelocationRuntime {
181
181
  this.targetReadyFailures.clear();
182
182
  this.targetReadyResponses.clear();
183
183
  for (const window of this.sourceCutoverWindows.values()) {
184
- if (window.windowTimer !== undefined)
185
- clearTimeout(window.windowTimer);
186
- if (window.retryTimer !== undefined)
187
- clearTimeout(window.retryTimer);
188
184
  if (window.followTimer !== undefined)
189
185
  clearTimeout(window.followTimer);
190
186
  }
@@ -302,19 +298,44 @@ class ZLinkHostServiceRelocationRuntime {
302
298
  publishState: () => undefined,
303
299
  forceStop: () => undefined
304
300
  });
301
+ const started = [];
305
302
  for (const unit of work) {
306
303
  maintenance.enqueue({
307
304
  id: unit.id,
308
305
  ready: () => true,
309
- relocate: unit.relocate
306
+ relocate: (relocationSignal) => {
307
+ const run = unit.relocate(relocationSignal);
308
+ started.push(run);
309
+ return run;
310
+ }
310
311
  });
311
312
  }
312
313
  const operationSignal = signal ?? new AbortController().signal;
313
314
  const deadlineMs = signal === undefined ? 30_000 : 24 * 60 * 60 * 1000;
314
- const result = await raceAbort(maintenance.start('retire', deadlineMs, stopStartingSignal, operationSignal), operationSignal);
315
- if (result.state !== 'completed') {
316
- throw result.terminalError ?? new Error(`Host relocation ended in '${result.state}'.`);
315
+ let failure;
316
+ try {
317
+ const result = await raceAbort(maintenance.start('retire', deadlineMs, stopStartingSignal, operationSignal), operationSignal);
318
+ if (result.state === 'completed')
319
+ return;
320
+ failure = result.terminalError ?? new Error(`Host relocation ended in '${result.state}'.`);
317
321
  }
322
+ catch (error) {
323
+ failure = error;
324
+ }
325
+ // The host result follows the settled authority of every started unit
326
+ // (spec 30 §13): a unit completes only after its target commit is
327
+ // confirmed, so a failure beside a completed unit leaves units on both
328
+ // sides.
329
+ const settled = await Promise.allSettled(started);
330
+ const targetSide = settled.filter((value) => value.status === 'fulfilled' || value.reason instanceof service_relocation_coordinator_1.ServiceRelocationPostCommitError).length;
331
+ const authorityError = settled.find((value) => value.status === 'rejected' && value.reason instanceof service_relocation_coordinator_1.ServiceRelocationAuthorityError);
332
+ if (authorityError !== undefined)
333
+ throw authorityError.reason;
334
+ // A unit that never started also stays on the source.
335
+ if (targetSide > 0 && targetSide < work.length) {
336
+ throw new service_relocation_coordinator_1.ServiceRelocationAuthorityError('Relocation units settled on both the source and the target.', { cause: failure });
337
+ }
338
+ throw failure;
318
339
  }
319
340
  async tryHandleControl(meshName, record, signal) {
320
341
  if (record.parts.length === 0)
@@ -827,6 +848,9 @@ class ZLinkHostServiceRelocationRuntime {
827
848
  }
828
849
  }
829
850
  async relocateSpotAggregate(meshName, activation, kind, actorStates, target, targetApplicationVersion, signal) {
851
+ await activation.serial.executeLifecycleOperation(() => this.relocateSpotAggregateCore(meshName, activation, kind, actorStates, target, targetApplicationVersion, signal));
852
+ }
853
+ async relocateSpotAggregateCore(meshName, activation, kind, actorStates, target, targetApplicationVersion, signal) {
830
854
  const store = this.requireLocationStore();
831
855
  const spotKey = (0, authority_key_codec_1.encodeAuthorityKey)(kind, String(activation.spotId));
832
856
  const spotAuthority = await requireAuthority(store, spotKey, signal);
@@ -866,12 +890,12 @@ class ZLinkHostServiceRelocationRuntime {
866
890
  objectGeneration: spotAuthority.objectGeneration,
867
891
  authorityOwnerGeneration: spotAuthority.authorityOwnerGeneration,
868
892
  seal: async (captureSignal) => {
869
- interruptionStartedAt = performance.now();
870
- spotMessageFollowSeal = this.sealSpotMessageFollow(meshName, spotAuthority, activation);
871
- // captureRelocation seals synchronously before its first await. Invoke
872
- // it in the same event-loop turn as the wire ingress seal so no
873
- // accepted direct message can enter between the two boundaries.
874
- const spotCaptureOperation = activation.captureRelocation(captureSignal);
893
+ const { spotCaptureOperation } = await this.afterSpotCloseDecision(meshName, activation.spotId, () => {
894
+ interruptionStartedAt = performance.now();
895
+ spotMessageFollowSeal = this.sealSpotMessageFollow(meshName, spotAuthority, activation);
896
+ // Both seals run in the same event-loop turn after the Close decision.
897
+ return { spotCaptureOperation: activation.captureRelocation(captureSignal) };
898
+ });
875
899
  const preparedSessions = Promise.all(actorStates.map(async (state) => ({
876
900
  state,
877
901
  actor: state.actor,
@@ -986,7 +1010,9 @@ class ZLinkHostServiceRelocationRuntime {
986
1010
  }
987
1011
  catch (error) {
988
1012
  outcome = 'failed';
989
- if (readinessBoundaryConsumed && !(error instanceof service_relocation_coordinator_1.ServiceRelocationPostCommitError)) {
1013
+ if (readinessBoundaryConsumed &&
1014
+ !(error instanceof service_relocation_coordinator_1.ServiceRelocationPostCommitError) &&
1015
+ !(error instanceof service_relocation_coordinator_1.ServiceRelocationAuthorityError)) {
990
1016
  await activation
991
1017
  .completeConsumedRelocationBoundary(contracts_1.ZLinkSpotRelocationReadyOutcome.Continued)
992
1018
  .catch(() => undefined);
@@ -1058,6 +1084,9 @@ class ZLinkHostServiceRelocationRuntime {
1058
1084
  }
1059
1085
  }
1060
1086
  async relocatePerActorSpotShell(meshName, activation, target, targetApplicationVersion, signal) {
1087
+ await activation.serial.executeLifecycleOperation(() => this.relocatePerActorSpotShellCore(meshName, activation, target, targetApplicationVersion, signal));
1088
+ }
1089
+ async relocatePerActorSpotShellCore(meshName, activation, target, targetApplicationVersion, signal) {
1061
1090
  const spotKey = (0, authority_key_codec_1.encodeAuthorityKey)('user_spot', String(activation.spotId));
1062
1091
  const spotAuthority = await requireAuthority(this.requireLocationStore(), spotKey, signal);
1063
1092
  const spotRegistration = this.spotRegistration(meshName, 'user_spot', spotAuthority.allocation.stableType);
@@ -1080,9 +1109,12 @@ class ZLinkHostServiceRelocationRuntime {
1080
1109
  objectGeneration: spotAuthority.objectGeneration,
1081
1110
  authorityOwnerGeneration: spotAuthority.authorityOwnerGeneration,
1082
1111
  seal: async (captureSignal) => {
1083
- interruptionStartedAt = performance.now();
1084
- spotMessageFollowSeal = this.sealSpotMessageFollow(meshName, spotAuthority, activation);
1085
- spotCapture = await activation.captureRelocation(captureSignal);
1112
+ const { spotCaptureOperation } = await this.afterSpotCloseDecision(meshName, activation.spotId, () => {
1113
+ interruptionStartedAt = performance.now();
1114
+ spotMessageFollowSeal = this.sealSpotMessageFollow(meshName, spotAuthority, activation);
1115
+ return { spotCaptureOperation: activation.captureRelocation(captureSignal) };
1116
+ });
1117
+ spotCapture = await spotCaptureOperation;
1086
1118
  return {
1087
1119
  boundSessionState: Buffer.alloc(0),
1088
1120
  queuedMessages: [],
@@ -1191,7 +1223,10 @@ class ZLinkHostServiceRelocationRuntime {
1191
1223
  abortSeal: async () => {
1192
1224
  if (standalone && ownSession !== undefined)
1193
1225
  await ownSession.prepared.rollback();
1194
- }
1226
+ },
1227
+ discardSeal: (reason) => (standalone
1228
+ ? ownSession
1229
+ : sessions.find((value) => value.state === state))?.prepared.discard(reason)
1195
1230
  };
1196
1231
  }
1197
1232
  reserveRelocationId() {
@@ -1264,10 +1299,13 @@ class ZLinkHostServiceRelocationRuntime {
1264
1299
  // configured limit unchanged (spec 15 §4.2).
1265
1300
  const chunkLimitBytes = (0, relocation_direct_transfer_1.effectiveActorJoinChunkLimitBytes)(limits.relocationPayloadChunkLimitBytes, advertisedReceiveChunkLimitBytes);
1266
1301
  const plan = (0, relocation_direct_transfer_1.planRelocationChunks)(encoded, chunkLimitBytes);
1267
- const convergenceDeadlineAtMs = signal === undefined
1268
- ? performance.now() + RELOCATION_OPERATION_RETENTION_MS
1269
- : controlDeadlineAtMs;
1302
+ // The Restore absolute deadline is this unit's relocation deadline: the
1303
+ // caller's signal, or the control deadline without one. From it the
1304
+ // source settles the authority with its Preserve fence (Location runtime
1305
+ // §10). The target keeps no deadline of its own.
1306
+ const restoreDeadlineReached = () => signal?.aborted === true || performance.now() >= controlDeadlineAtMs;
1270
1307
  let readyReceived = false;
1308
+ let settlement;
1271
1309
  let sourceCommitted = false;
1272
1310
  try {
1273
1311
  const prepare = {
@@ -1343,9 +1381,9 @@ class ZLinkHostServiceRelocationRuntime {
1343
1381
  boundaryRecordCount: BigInt(boundaryBatch.length),
1344
1382
  boundaryChecksumCrc32c: (0, service_relocation_runtime_1.crc32c)(Buffer.concat(boundaryBatch.map((value) => value.canonicalBytes)))
1345
1383
  });
1346
- // The one-way cutover submit reaches a terminal result exactly once;
1347
- // success and failure both end source dispatch permanently. The batch
1348
- // copy stays for the retransmission window (spec 28 §4.4).
1384
+ // The cutover submit result is not the authority result. The memory
1385
+ // copy of the boundary batch and cutover stays until the authority
1386
+ // settlement below (spec 28 §4.4).
1349
1387
  let submitFailure;
1350
1388
  try {
1351
1389
  for (const record of boundaryBatch) {
@@ -1357,11 +1395,30 @@ class ZLinkHostServiceRelocationRuntime {
1357
1395
  submitFailure = error;
1358
1396
  console.warn('[zlink.runtime.relocation.cutover_submit_failed]', relayAuthorityId, error);
1359
1397
  }
1360
- this.beginSourceCutoverWindow(meshName, target.rid, relayAuthorityId, [...boundaryBatch.map((value) => value.frame), cutoverFrame], submitFailure !== undefined, limits);
1361
- await this.waitForTargetAuthorities(captured.envelope, targetFence, convergenceDeadlineAtMs, signal);
1362
- relocationDebug('coordinator.target_authorities_observed', {
1363
- aggregateId: captured.envelope.aggregateId
1398
+ const window = this.beginSourceCutoverWindow(meshName, target.rid, relayAuthorityId, [...boundaryBatch.map((value) => value.frame), cutoverFrame], submitFailure !== undefined, limits);
1399
+ try {
1400
+ settlement = await this.settleSourceAuthority(captured.envelope, primary, targetFence, restoreDeadlineReached, window);
1401
+ }
1402
+ finally {
1403
+ this.closeSourceCutoverWindow(relayAuthorityId, window);
1404
+ }
1405
+ relocationDebug('coordinator.authority_settled', {
1406
+ aggregateId: captured.envelope.aggregateId,
1407
+ settlement
1364
1408
  });
1409
+ if (settlement === 'source') {
1410
+ // Preserve won: the captured queue, timers and held records return to
1411
+ // the source queue in their accepted order, and dispatch reopens.
1412
+ await captured.abortSource();
1413
+ throw new Error(`Relocation '${relayAuthorityId}' ended by the source Preserve fence; the source kept the authority.`);
1414
+ }
1415
+ if (settlement === 'lost') {
1416
+ // Expired owner: dispatch and Store changes stop for good and every
1417
+ // pending request without a terminal gets one Unavailable.
1418
+ const unavailable = (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RouteNotConnected, `Relocation '${relayAuthorityId}' source owner lease ended before its authority settled.`, true);
1419
+ await captured.discardSource(unavailable);
1420
+ throw new service_relocation_coordinator_1.ServiceRelocationAuthorityError(unavailable.message, { cause: unavailable });
1421
+ }
1365
1422
  for (const session of sessions)
1366
1423
  session.prepared.setReplayResults([]);
1367
1424
  await captured.commitSource();
@@ -1369,26 +1426,18 @@ class ZLinkHostServiceRelocationRuntime {
1369
1426
  await this.options.reconcileStatefulAuthorityRoutes?.(signal);
1370
1427
  }
1371
1428
  catch (error) {
1372
- if (!readyReceived)
1429
+ if (settlement !== 'target')
1373
1430
  throw error;
1374
- try {
1375
- await this.waitForTargetAuthorities(captured.envelope, targetFence, convergenceDeadlineAtMs);
1376
- if (!sourceCommitted) {
1377
- for (const session of sessions)
1378
- session.prepared.setReplayResults([]);
1379
- await captured.commitSource();
1380
- sourceCommitted = true;
1381
- }
1382
- await this.options.reconcileStatefulAuthorityRoutes?.();
1383
- const authority = await requireAuthority(this.requireLocationStore(), { value: primaryKey(captured.envelope) }, undefined);
1384
- throw new service_relocation_coordinator_1.ServiceRelocationPostCommitError(authority, { id: `${captured.envelope.aggregateId}:${captured.envelope.aggregateGeneration}` }, error);
1385
- }
1386
- catch (convergenceError) {
1387
- if (convergenceError instanceof service_relocation_coordinator_1.ServiceRelocationPostCommitError) {
1388
- throw convergenceError;
1389
- }
1390
- throw new AggregateError([error, convergenceError], 'Relocation became irreversible after Ready and target convergence failed.');
1431
+ // Target commit is confirmed; later source cleanup failures cannot move
1432
+ // the authority back (spec 28 §9).
1433
+ if (!sourceCommitted) {
1434
+ for (const session of sessions)
1435
+ session.prepared.setReplayResults([]);
1436
+ await captured.commitSource().catch(() => undefined);
1391
1437
  }
1438
+ await this.options.reconcileStatefulAuthorityRoutes?.().catch(() => undefined);
1439
+ const authority = await requireAuthority(this.requireLocationStore(), { value: primaryKey(captured.envelope) }, undefined);
1440
+ throw new service_relocation_coordinator_1.ServiceRelocationPostCommitError(authority, { id: `${captured.envelope.aggregateId}:${captured.envelope.aggregateGeneration}` }, error);
1392
1441
  }
1393
1442
  finally {
1394
1443
  if (!readyReceived) {
@@ -1461,12 +1510,11 @@ class ZLinkHostServiceRelocationRuntime {
1461
1510
  return budget;
1462
1511
  }
1463
1512
  /**
1464
- * Starts the source retransmission window after the cutover submit
1465
- * terminal. The boundary batch and cutover copies live in framework memory
1466
- * (not charged to the in-flight budget) and are cleaned exactly once when
1467
- * the window ends. While the window is open a failed submit retries the
1468
- * whole batch on the (re-established) connection. S4 — the Message Follow
1469
- * route removal point — closes the unit for SafeToShutdown.
1513
+ * Starts the source cutover window after the cutover submit terminal. The
1514
+ * boundary batch and cutover copies live in framework memory (not charged
1515
+ * to the in-flight budget) until the authority settlement closes the
1516
+ * window. S4 — the Message Follow route removal point — closes the unit for
1517
+ * SafeToShutdown.
1470
1518
  */
1471
1519
  beginSourceCutoverWindow(meshName, targetNodeRid, relocationKey, frames, initialSubmitFailed, limits) {
1472
1520
  const submittedAt = performance.now();
@@ -1474,54 +1522,134 @@ class ZLinkHostServiceRelocationRuntime {
1474
1522
  meshName,
1475
1523
  targetNodeRid,
1476
1524
  frames,
1525
+ resendPending: initialSubmitFailed,
1477
1526
  cutoverSubmittedAtMs: submittedAt,
1478
1527
  windowClosed: false,
1479
1528
  followExpired: false
1480
1529
  };
1481
1530
  this.sourceCutoverWindows.set(relocationKey, window);
1482
- const settle = () => {
1483
- if (window.windowClosed && window.followExpired) {
1484
- this.sourceCutoverWindows.delete(relocationKey);
1485
- }
1486
- };
1487
- window.windowTimer = setTimeout(() => {
1488
- window.windowClosed = true;
1489
- window.frames = undefined;
1490
- if (window.retryTimer !== undefined)
1491
- clearTimeout(window.retryTimer);
1492
- settle();
1493
- }, limits.relocationCutoverWaitTimeoutMs);
1494
- window.windowTimer.unref();
1495
1531
  window.followTimer = setTimeout(() => {
1496
1532
  window.followExpired = true;
1497
1533
  // S1→S4 route convergence: the span the source must keep its Message
1498
1534
  // Follow route (spec 30 §7.1).
1499
1535
  this.options.metrics?.duration('zlink.relocation.route_convergence', Math.max(0, performance.now() - submittedAt) / 1000, { mesh_name: meshName });
1500
- settle();
1536
+ this.releaseSourceCutoverWindow(relocationKey, window);
1501
1537
  }, limits.messageFollowDurationMs);
1502
1538
  window.followTimer.unref();
1503
- if (initialSubmitFailed)
1504
- this.scheduleCutoverRetransmit(window);
1539
+ return window;
1540
+ }
1541
+ /** The authority settlement ended: the retained batch copy is released. */
1542
+ closeSourceCutoverWindow(relocationKey, window) {
1543
+ window.windowClosed = true;
1544
+ window.frames = undefined;
1545
+ this.releaseSourceCutoverWindow(relocationKey, window);
1546
+ }
1547
+ releaseSourceCutoverWindow(relocationKey, window) {
1548
+ if (window.windowClosed &&
1549
+ window.followExpired &&
1550
+ this.sourceCutoverWindows.get(relocationKey) === window) {
1551
+ this.sourceCutoverWindows.delete(relocationKey);
1552
+ }
1505
1553
  }
1506
- scheduleCutoverRetransmit(window) {
1507
- if (window.windowClosed || window.frames === undefined)
1554
+ /**
1555
+ * Sends the whole boundary batch and cutover again after a failed submit
1556
+ * or an observed loss of the target connection (spec 28 §4.4). The target
1557
+ * replaces its partial span with the whole batch.
1558
+ */
1559
+ async resendSourceCutover(window) {
1560
+ const frames = window.frames;
1561
+ if (frames === undefined)
1508
1562
  return;
1509
- window.retryTimer = setTimeout(() => {
1510
- void (async () => {
1511
- const frames = window.frames;
1512
- if (window.windowClosed || frames === undefined)
1513
- return;
1514
- try {
1515
- for (const frame of frames) {
1516
- await this.submitControlFrame(window.meshName, window.targetNodeRid, frame, 'cutover retransmission');
1517
- }
1518
- }
1519
- catch {
1520
- this.scheduleCutoverRetransmit(window);
1521
- }
1522
- })();
1523
- }, 250);
1524
- window.retryTimer.unref();
1563
+ const peers = this.requireMeshNode(window.meshName).peers();
1564
+ const peer = peers.find((value) => value.routingId !== null && String(value.routingId) === String(window.targetNodeRid));
1565
+ // Backend peer state 3 is Ready.
1566
+ if (peer !== undefined && peer.state !== 3) {
1567
+ window.resendPending = true;
1568
+ return;
1569
+ }
1570
+ if (!window.resendPending)
1571
+ return;
1572
+ try {
1573
+ for (const frame of frames) {
1574
+ await this.submitControlFrame(window.meshName, window.targetNodeRid, frame, 'cutover retransmission');
1575
+ }
1576
+ window.resendPending = false;
1577
+ }
1578
+ catch {
1579
+ // The next settlement round retries the whole batch.
1580
+ }
1581
+ }
1582
+ /**
1583
+ * The one source authority settlement after Ready (Location runtime
1584
+ * §6.1·§10, spec 28 §4.4). It reads the Store until the target commit or
1585
+ * the Restore deadline. From the Restore deadline it runs the source
1586
+ * Preserve fence: a CAS on the StoreVersion expected by target NewOwner
1587
+ * excludes a late target commit. `target`: the
1588
+ * target committed. `source`: Preserve fenced the target out while the
1589
+ * source owner lease was valid. `lost`: the source owner lease ended first
1590
+ * or another authority owns the object. An uncertain Store or lease result
1591
+ * keeps the work retained and repeats the same decision.
1592
+ */
1593
+ async settleSourceAuthority(envelope, primary, target, restoreDeadlineReached, window) {
1594
+ const owner = { ownerId: primary.ownerId, leaseGeneration: primary.ownerLeaseGeneration };
1595
+ const store = this.requireLocationStore();
1596
+ for (;;) {
1597
+ if (this.disposed)
1598
+ throw new Error('Relocation runtime stopped.');
1599
+ await this.resendSourceCutover(window);
1600
+ const observed = await this.readSourceSettlement(envelope, primary, target).catch(() => undefined);
1601
+ if (observed?.kind === 'target')
1602
+ return 'target';
1603
+ if (observed?.kind === 'other')
1604
+ return 'lost';
1605
+ if (restoreDeadlineReached() && observed?.kind === 'source') {
1606
+ // An unreadable lease is an uncertain result, not an expiry.
1607
+ if (await this.exactSourceLeaseExpired(owner).catch(() => false))
1608
+ return 'lost';
1609
+ const publication = this.codec.read(observed.current.payload);
1610
+ const preserved = await store
1611
+ .compareExchangeAuthority({ value: primaryKey(envelope) }, observed.current.storeVersion, {
1612
+ kind: 'put',
1613
+ generationTransition: 'preserve',
1614
+ payload: publication === undefined
1615
+ ? observed.current.payload
1616
+ : this.codec.clear(observed.current.payload, publication.reference)
1617
+ })
1618
+ .catch(() => undefined);
1619
+ if (preserved?.kind === 'stored')
1620
+ return 'source';
1621
+ }
1622
+ // The caller awaits this settlement, so the retry timer keeps the loop alive.
1623
+ await new Promise((resolve) => setTimeout(resolve, 10));
1624
+ }
1625
+ }
1626
+ async readSourceSettlement(envelope, primary, target) {
1627
+ const primaryAuthorityKey = primaryKey(envelope);
1628
+ let committed = 0;
1629
+ let primaryCurrent;
1630
+ for (const participant of envelope.participants) {
1631
+ const current = await this.requireLocationStore().readAuthority({
1632
+ value: participant.key
1633
+ });
1634
+ if (current.kind !== 'snapshot')
1635
+ return { kind: 'other' };
1636
+ if (current.objectGeneration === participant.objectGeneration &&
1637
+ current.ownerId === target.ownerId &&
1638
+ current.ownerLeaseGeneration === target.ownerLeaseGeneration &&
1639
+ current.authorityOwnerGeneration > participant.authorityOwnerGeneration &&
1640
+ String(current.allocation.descriptor.rid) === target.nodeRid &&
1641
+ current.allocation.descriptorLifecycleGeneration === target.nodeGeneration) {
1642
+ committed += 1;
1643
+ }
1644
+ if (participant.key === primaryAuthorityKey)
1645
+ primaryCurrent = current;
1646
+ }
1647
+ if (committed === envelope.participants.length)
1648
+ return { kind: 'target' };
1649
+ if (primaryCurrent !== undefined && isSourceOwner(primaryCurrent, primary)) {
1650
+ return { kind: 'source', current: primaryCurrent };
1651
+ }
1652
+ return { kind: 'other' };
1525
1653
  }
1526
1654
  /**
1527
1655
  * True when every relocation this source started reached its Message
@@ -1611,6 +1739,7 @@ class ZLinkHostServiceRelocationRuntime {
1611
1739
  const envelope = (0, service_relocation_runtime_1.decodeServiceRelocationEnvelope)(payload, request.targetAttemptGeneration);
1612
1740
  const inventoryEnvelope = attachPrepareBoundSessions(await this.resolveTargetParticipantInventory(request, envelope, signal), request.nodeInternalBoundSessions);
1613
1741
  validatePrepareEnvelope(request, inventoryEnvelope);
1742
+ const releaseActivation = (await this.options.activationAdmission?.acquire(meshName, signal)) ?? (() => undefined);
1614
1743
  let materialized;
1615
1744
  let reservation;
1616
1745
  try {
@@ -1620,13 +1749,12 @@ class ZLinkHostServiceRelocationRuntime {
1620
1749
  // rejects unrelated authority CAS until commit/abort.
1621
1750
  materialized = await this.materializeTargetEnvelope(meshName, inventoryEnvelope, signal);
1622
1751
  await materialized.owner.restoreSavedWork(materialized.staging, signal);
1623
- reservation = await this.reserveTargetForPrepare(meshName, request, inventoryEnvelope, materialized.target, signal);
1752
+ reservation = await this.reserveTargetForPrepare(meshName, request, inventoryEnvelope, signal);
1624
1753
  const offer = {
1625
1754
  prepare: request,
1626
1755
  prepareFingerprint: fingerprint,
1627
1756
  authenticatedSourceNodeRid: String(sourceNodeRid),
1628
1757
  envelope: inventoryEnvelope,
1629
- restoreDeadlineAtMs: performance.now() + RELOCATION_OPERATION_RETENTION_MS,
1630
1758
  reservation
1631
1759
  };
1632
1760
  const stage = {
@@ -1635,11 +1763,13 @@ class ZLinkHostServiceRelocationRuntime {
1635
1763
  phase: 'ready',
1636
1764
  lane: Promise.resolve(),
1637
1765
  cutoverReceived: false,
1638
- boundaryRelay: []
1766
+ boundaryRelay: [],
1767
+ releaseActivation
1639
1768
  };
1640
1769
  this.targetStages.set(stagingId, stage);
1641
1770
  }
1642
1771
  catch (error) {
1772
+ releaseActivation();
1643
1773
  if (reservation !== undefined) {
1644
1774
  await this.abortTargetReservation(reservation, signal).catch(() => undefined);
1645
1775
  }
@@ -1650,12 +1780,25 @@ class ZLinkHostServiceRelocationRuntime {
1650
1780
  }
1651
1781
  return relocationReady(request);
1652
1782
  }
1783
+ /**
1784
+ * The cutover wait of a Ready stage (spec 28 §4.4) is a Warning only: it
1785
+ * records `cutover_timeout` and never submits the target CAS or opens
1786
+ * dispatch. From then on the target settlement reads the Location Store so
1787
+ * a source Preserve fence or a lost target lease ends the staging; only a
1788
+ * verified cutover lets that settlement submit the CAS.
1789
+ */
1653
1790
  armTargetCutoverFallback(meshName, stagingId) {
1654
1791
  const stage = this.targetStages.get(stagingId);
1655
1792
  if (stage === undefined || stage.fallback !== undefined || stage.finalize !== undefined)
1656
1793
  return;
1657
1794
  stage.fallback = setTimeout(() => {
1658
- void this.beginTargetFinalize(meshName, stagingId, stage, true).catch((error) => console.error('[zlink.runtime.relocation.location_update_failed]', error));
1795
+ if (stage.finalize !== undefined)
1796
+ return;
1797
+ console.warn('[zlink.runtime.relocation.cutover_timeout]', stagingId);
1798
+ this.options.metrics?.count('zlink.relocation.cutover_timeout', 1, {
1799
+ mesh_name: meshName
1800
+ });
1801
+ void this.beginTargetFinalize(meshName, stagingId, stage).catch((error) => console.error('[zlink.runtime.relocation.location_update_failed]', error));
1659
1802
  }, this.relocationLimits().relocationCutoverWaitTimeoutMs);
1660
1803
  stage.fallback.unref();
1661
1804
  }
@@ -1675,20 +1818,28 @@ class ZLinkHostServiceRelocationRuntime {
1675
1818
  throw new Error(`Relocation staging '${stagingId}' is missing.`);
1676
1819
  }
1677
1820
  validateTargetOneWayControl(stage, request, sourceNodeRid);
1821
+ // A verified cutover closes the boundary batch. A later record or cutover
1822
+ // is a duplicate retransmission and never changes the staged batch.
1823
+ if (stage.cutoverReceived || (stage.phase !== 'ready' && stage.phase !== 'finalizing')) {
1824
+ if (request.kind === 'cutover') {
1825
+ console.warn('[zlink.runtime.relocation.late_cutover]', stagingId);
1826
+ }
1827
+ return;
1828
+ }
1678
1829
  if (request.kind === 'data') {
1679
1830
  stage.lane = stage.lane.then(() => this.stageBoundaryRelay(stage, request));
1680
1831
  await stage.lane;
1681
1832
  return;
1682
1833
  }
1683
- if (stage.cutoverReceived || stage.phase !== 'ready') {
1684
- console.warn('[zlink.runtime.relocation.late_cutover]', stagingId);
1685
- return;
1686
- }
1687
1834
  this.reconcileBoundaryRelay(stage, request, stagingId);
1688
1835
  stage.cutoverReceived = true;
1836
+ // A settlement started by the cutover wait Warning is still reading the
1837
+ // Store; it submits the CAS on its next round now that cutover verified.
1838
+ if (stage.finalize !== undefined)
1839
+ return;
1689
1840
  if (stage.fallback !== undefined)
1690
1841
  clearTimeout(stage.fallback);
1691
- await this.beginTargetFinalize(meshName, stagingId, stage, false, signal);
1842
+ await this.beginTargetFinalize(meshName, stagingId, stage, signal);
1692
1843
  }
1693
1844
  /** Validates one boundary relay record and buffers it until cutover applies it. */
1694
1845
  async stageBoundaryRelay(stage, request) {
@@ -1707,14 +1858,13 @@ class ZLinkHostServiceRelocationRuntime {
1707
1858
  }
1708
1859
  const expectedCount = Number(request.boundaryRecordCount);
1709
1860
  const staged = stage.boundaryRelay;
1710
- const checksumOf = (records) => (0, service_relocation_runtime_1.crc32c)(Buffer.concat(records.map((value) => value.frozenRecord.canonicalBytes)));
1711
- if (staged.length === expectedCount && checksumOf(staged) === request.boundaryChecksumCrc32c) {
1712
- return;
1713
- }
1714
- if (staged.length > expectedCount) {
1715
- const suffix = staged.slice(staged.length - expectedCount);
1716
- if (checksumOf(suffix) === request.boundaryChecksumCrc32c) {
1717
- console.warn('[zlink.runtime.relocation.boundary_batch_replaced]', stagingId);
1861
+ // The batch is the last `boundaryRecordCount` records: the source sends
1862
+ // the whole batch right before this cutover on the ordered connection,
1863
+ // so a retransmission replaces an earlier partial span as a whole.
1864
+ if (staged.length >= expectedCount) {
1865
+ const batch = staged.slice(staged.length - expectedCount);
1866
+ if ((0, service_relocation_runtime_1.crc32c)(Buffer.concat(batch.map((value) => value.frozenRecord.canonicalBytes))) ===
1867
+ request.boundaryChecksumCrc32c) {
1718
1868
  staged.splice(0, staged.length - expectedCount);
1719
1869
  return;
1720
1870
  }
@@ -1724,18 +1874,14 @@ class ZLinkHostServiceRelocationRuntime {
1724
1874
  throw new Error(`Relocation cutover boundary confirmation mismatch for '${stagingId}' ` +
1725
1875
  `(staged=${staged.length} declared=${expectedCount}).`);
1726
1876
  }
1727
- beginTargetFinalize(meshName, stagingId, stage, fallback, signal) {
1877
+ beginTargetFinalize(meshName, stagingId, stage, signal) {
1728
1878
  if (stage.finalize !== undefined)
1729
1879
  return stage.finalize;
1730
- if (fallback) {
1731
- console.warn('[zlink.runtime.relocation.cutover_timeout]', stagingId);
1732
- this.options.metrics?.count('zlink.relocation.cutover_timeout', 1, {
1733
- mesh_name: meshName
1734
- });
1735
- }
1880
+ // Records staged before this point finish first. The settlement is not
1881
+ // put on the lane, so a cutover that arrives while it waits for the
1882
+ // authority result is still reconciled.
1736
1883
  const finalize = stage.lane.then(() => this.finalizeTargetStage(meshName, stagingId, stage, signal));
1737
1884
  stage.finalize = finalize;
1738
- stage.lane = finalize;
1739
1885
  return finalize;
1740
1886
  }
1741
1887
  async finalizeTargetStage(meshName, stagingId, stage, signal) {
@@ -1746,14 +1892,15 @@ class ZLinkHostServiceRelocationRuntime {
1746
1892
  let authority;
1747
1893
  let resumeStartedAt;
1748
1894
  try {
1749
- // The boundary relay span is applied to the temporary queue before the
1750
- // owner CAS so the merged order is saved work, pre-boundary relay, then
1751
- // post-boundary temporary records (spec 28 §4.6).
1752
- for (const relay of stage.boundaryRelay.splice(0)) {
1753
- await this.applyRelocationData(stage, relay, signal);
1754
- }
1755
1895
  authority = await this.commitTargetReservation(stage, stage.offer.reservation, signal);
1896
+ if (authority === undefined) {
1897
+ // Source Preserve, a definitive conflict or the target lease ended
1898
+ // this staging before any target commit (Location runtime §10).
1899
+ console.warn('[zlink.runtime.relocation.target_staging_discarded]', stagingId);
1900
+ throw new Error(`Relocation '${stagingId}' target staging lost the authority settlement.`);
1901
+ }
1756
1902
  authorityCommitted = true;
1903
+ stage.releaseActivation();
1757
1904
  resumeStartedAt = performance.now();
1758
1905
  stage.phase = 'committed';
1759
1906
  await stage.owner.normalize(stage.staging, authority, signal);
@@ -1768,13 +1915,10 @@ class ZLinkHostServiceRelocationRuntime {
1768
1915
  }
1769
1916
  }
1770
1917
  await this.finalizeActorJoinProfiles(meshName, stage, signal);
1771
- // Actor Join completion may advance its accepted-completion journal in
1772
- // the canonical authority slot. Try to clear that final publication
1773
- // before admission becomes externally visible so a later Join cannot
1774
- // reserve against the previous root. A bounded CAS conflict must not
1775
- // roll back already committed target ownership or strand its staged
1776
- // object: the next reservation recognizes the retained journal root and
1777
- // continues its existing cleanup/retry path.
1918
+ // Clear the final relocation publication before admission becomes
1919
+ // externally visible so a later Join cannot reserve against the
1920
+ // previous root. A bounded CAS conflict must not roll back already
1921
+ // committed target ownership or strand its staged object.
1778
1922
  try {
1779
1923
  await this.clearTargetRelocationPublication(stage, authority, signal);
1780
1924
  }
@@ -1907,58 +2051,23 @@ class ZLinkHostServiceRelocationRuntime {
1907
2051
  throw new Error(`Relocation ${kind} was not accepted by RouteMesh.`);
1908
2052
  }
1909
2053
  }
1910
- async waitForTargetAuthorities(envelope, target, deadlineAtMs, signal) {
1911
- for (;;) {
1912
- signal?.throwIfAborted();
1913
- let exact = true;
1914
- for (const participant of envelope.participants) {
1915
- const current = await this.requireLocationStore().readAuthority({ value: participant.key }, signal);
1916
- if (current.kind !== 'snapshot' ||
1917
- current.objectGeneration !== participant.objectGeneration ||
1918
- current.ownerId !== target.ownerId ||
1919
- current.ownerLeaseGeneration !== target.ownerLeaseGeneration ||
1920
- current.authorityOwnerGeneration <= participant.authorityOwnerGeneration ||
1921
- String(current.allocation.descriptor.rid) !== target.nodeRid ||
1922
- current.allocation.descriptorLifecycleGeneration !== target.nodeGeneration) {
1923
- exact = false;
1924
- break;
1925
- }
1926
- }
1927
- if (exact)
1928
- return;
1929
- if (performance.now() >= deadlineAtMs) {
1930
- throw new Error('Relocation target owner was not confirmed before its original deadline.');
1931
- }
1932
- await new Promise((resolve) => setTimeout(resolve, 10));
1933
- }
1934
- }
1935
2054
  async clearTargetRelocationPublication(stage, primary, signal) {
1936
2055
  await this.releaseParticipantAuthorities(stage.staging.envelope, stage.staging.primaryAuthorityKey.value, signal);
1937
- // The deferred-Join journal advances its completion cursor with its
1938
- // own authority CAS while the target finalizes, so the initially
1939
- // captured storeVersion can be stale by the time the relocation
1940
- // wrapper is cleared. Re-read and retry on conflict instead of turning
1941
- // a completed relocation into a post-commit failure (spec 28 —
1942
- // retryable store conflicts converge on the same CAS and the same
1943
- // relocation identity until the outcome is determined).
1944
- let current = primary;
1945
- for (let attempt = 0; attempt < 16; attempt += 1) {
1946
- const publication = this.codec.read(current.payload);
1947
- if (publication === undefined)
1948
- return;
1949
- const result = await this.requireLocationStore().compareExchangeAuthority(stage.staging.primaryAuthorityKey, current.storeVersion, {
1950
- kind: 'put',
1951
- generationTransition: 'preserve',
1952
- payload: this.codec.clear(current.payload, publication.reference)
1953
- }, signal);
1954
- if (result.kind === 'stored')
1955
- return;
1956
- const read = await this.requireLocationStore().readAuthority(stage.staging.primaryAuthorityKey, signal);
1957
- if (read.kind !== 'snapshot')
1958
- break;
1959
- current = read;
2056
+ // The target owner is the only writer of the committed primary
2057
+ // authority between its commit and this clear, so one CAS against the
2058
+ // committed snapshot decides. A conflict means another owner changed the
2059
+ // row; the caller records it and leaves the committed ownership intact.
2060
+ const publication = this.codec.read(primary.payload);
2061
+ if (publication === undefined)
2062
+ return;
2063
+ const result = await this.requireLocationStore().compareExchangeAuthority(stage.staging.primaryAuthorityKey, primary.storeVersion, {
2064
+ kind: 'put',
2065
+ generationTransition: 'preserve',
2066
+ payload: this.codec.clear(primary.payload, publication.reference)
2067
+ }, signal);
2068
+ if (result.kind !== 'stored') {
2069
+ throw new Error('Relocation target authority normalization CAS failed.');
1960
2070
  }
1961
- throw new Error('Relocation target authority normalization CAS failed.');
1962
2071
  }
1963
2072
  async handleReplyRelay(meshName, relay, sourceNodeRid, signal) {
1964
2073
  if (sourceNodeRid === null) {
@@ -2234,39 +2343,9 @@ class ZLinkHostServiceRelocationRuntime {
2234
2343
  }
2235
2344
  return authorities;
2236
2345
  }
2237
- async reserveTargetForPrepare(meshName, prepare, envelope, targetPort, signal) {
2346
+ async reserveTargetForPrepare(meshName, prepare, envelope, signal) {
2238
2347
  const authorities = await this.readTargetParticipantAuthorities(envelope, signal);
2239
2348
  const publication = relocationPublication(prepare, envelope);
2240
- // Carries the exact reference + CRC of a deferred-Join publication the
2241
- // target reservation must keep connected through the CAS (spec 28 —
2242
- // clearing/replacing it here would strand the journal's own
2243
- // prepared->committed transition with nothing left for it to find).
2244
- const retainedDeferredJoinRoots = new Map();
2245
- for (const participant of envelope.participants) {
2246
- const staged = targetPort.deferredJoinRoot(participant.key);
2247
- if (staged !== undefined) {
2248
- retainedDeferredJoinRoots.set(participant.key, {
2249
- reference: staged.reference.value,
2250
- checksumCrc32c: staged.checksumCrc32c
2251
- });
2252
- continue;
2253
- }
2254
- const current = this.codec.read(authorities.get(participant.key).payload);
2255
- if (participant.objectKind === 'actor' &&
2256
- current?.canonical === true &&
2257
- (await this.options.actorTransfer.isDeferredJoinAcceptedRootPublication(current.reference, current.checksumCrc32c, {
2258
- authorityKey: participant.key,
2259
- objectKind: 'actor',
2260
- objectGeneration: participant.objectGeneration,
2261
- aggregateId: current.aggregateId,
2262
- aggregateGeneration: current.aggregateGeneration
2263
- }, signal))) {
2264
- retainedDeferredJoinRoots.set(participant.key, {
2265
- reference: current.reference,
2266
- checksumCrc32c: current.checksumCrc32c
2267
- });
2268
- }
2269
- }
2270
2349
  const participants = envelope.participants.map((participant) => {
2271
2350
  const expected = authorities.get(participant.key);
2272
2351
  const membership = actorMembershipTarget(envelope, participant.key);
@@ -2278,19 +2357,11 @@ class ZLinkHostServiceRelocationRuntime {
2278
2357
  entrySpotId === undefined) {
2279
2358
  throw new Error(`Relocation target RouteMesh '${meshName}' has no Entry Spot.`);
2280
2359
  }
2281
- const retainedDeferredJoinRoot = retainedDeferredJoinRoots.get(participant.key);
2282
- const participantPublication = retainedDeferredJoinRoot === undefined
2283
- ? publication
2284
- : {
2285
- ...publication,
2286
- reference: retainedDeferredJoinRoot.reference,
2287
- checksumCrc32c: retainedDeferredJoinRoot.checksumCrc32c
2288
- };
2289
2360
  return {
2290
2361
  key: { value: participant.key },
2291
2362
  expected,
2292
2363
  ownerTransition: 'newOwner',
2293
- authorityPayload: this.authorityPayloadForPublication(expected.payload, participantPublication, {
2364
+ authorityPayload: this.authorityPayloadForPublication(expected.payload, publication, {
2294
2365
  owner: {
2295
2366
  ownerId: prepare.target.ownerId,
2296
2367
  leaseGeneration: prepare.target.ownerLeaseGeneration
@@ -2313,7 +2384,7 @@ class ZLinkHostServiceRelocationRuntime {
2313
2384
  ...membership,
2314
2385
  actorSpotKind: contracts_1.ZLinkSpotKind.User
2315
2386
  })
2316
- }, retainedDeferredJoinRoot !== undefined),
2387
+ }),
2317
2388
  membershipMutation: encodeMembershipMutation(envelope.memberships, participant.key)
2318
2389
  };
2319
2390
  });
@@ -2332,19 +2403,13 @@ class ZLinkHostServiceRelocationRuntime {
2332
2403
  prepared: await new service_relocation_aggregate_committer_1.ServiceRelocationAggregateCommitter(this.requireLocationStore()).prepare(plan, signal)
2333
2404
  };
2334
2405
  }
2335
- authorityPayloadForPublication(payload, publication, target, replacesDeferredJoinRoot = false) {
2406
+ authorityPayloadForPublication(payload, publication, target) {
2336
2407
  const existing = this.codec.read(payload);
2337
2408
  const canonicalIdentity = (0, service_relocation_runtime_1.serviceRelocationAuthoritySlotIdentity)(payload);
2338
- const replacesCanonicalJournal = replacesDeferredJoinRoot && existing?.canonical === true;
2339
2409
  if (canonicalIdentity !== undefined &&
2340
2410
  (existing === undefined ||
2341
- (existing.canonical === true &&
2342
- (existing.aggregateId === publication.aggregateId || replacesCanonicalJournal)))) {
2343
- const published = existing === undefined
2344
- ? this.codec.publish(payload, publication)
2345
- : replacesCanonicalJournal && existing.aggregateId !== publication.aggregateId
2346
- ? this.codec.publish(this.codec.clear(payload, existing.reference), publication)
2347
- : payload;
2411
+ (existing.canonical === true && existing.aggregateId === publication.aggregateId))) {
2412
+ const published = existing === undefined ? this.codec.publish(payload, publication) : payload;
2348
2413
  if (target === undefined)
2349
2414
  return published;
2350
2415
  return (0, service_relocation_runtime_1.projectServiceRelocationAuthorityTargetReady)((0, service_relocation_runtime_1.replaceServiceRelocationAuthorityApplicationPayload)(published, rewriteAuthorityApplicationRoute(published, target)), {
@@ -2394,6 +2459,7 @@ class ZLinkHostServiceRelocationRuntime {
2394
2459
  async abortTargetStage(stage, signal) {
2395
2460
  if (stage.fallback !== undefined)
2396
2461
  clearTimeout(stage.fallback);
2462
+ stage.releaseActivation();
2397
2463
  const failures = [];
2398
2464
  try {
2399
2465
  await this.abortTargetReservation(stage.offer.reservation, signal);
@@ -2417,19 +2483,39 @@ class ZLinkHostServiceRelocationRuntime {
2417
2483
  async commitTargetReservation(stage, reservation, signal) {
2418
2484
  return await this.commitTargetAggregate(stage, reservation.prepared, signal);
2419
2485
  }
2486
+ /**
2487
+ * The one target authority settlement (Location runtime §10, spec 28
2488
+ * §4.4-§4.5). Only a verified cutover lets the target submit its NewOwner
2489
+ * CAS; a transient or indeterminate result resubmits the same RelocationId
2490
+ * and expected StoreVersion while the target owner lease is valid (no
2491
+ * separate timeout, no fixed count). Before a verified cutover the target
2492
+ * only reads. It returns the committed authority, or undefined when a
2493
+ * definitive Conflict excludes this commit after reconciliation, a source
2494
+ * Preserve fence moved the source row, or the target lease ended; the
2495
+ * caller then discards the staging.
2496
+ */
2420
2497
  async commitTargetAggregate(stage, prepared, signal) {
2421
- let firstError;
2498
+ const target = stage.offer.prepare.target;
2422
2499
  for (;;) {
2423
2500
  signal?.throwIfAborted();
2424
- let result;
2425
- try {
2426
- result = await this.requireLocationStore().commitAggregate(prepared.fence, signal);
2427
- }
2428
- catch (error) {
2429
- firstError ??= error;
2430
- }
2431
- if (result?.kind === 'stale' || result?.kind === 'generationExhausted') {
2432
- throw new Error(`location_update_failed: relocation aggregate commit returned ${result.kind}.`, { cause: firstError });
2501
+ if (this.disposed)
2502
+ throw new Error('Relocation runtime stopped.');
2503
+ let conflict = false;
2504
+ if (stage.cutoverReceived) {
2505
+ // The boundary relay span is applied to the temporary queue before
2506
+ // the owner CAS so the merged order is saved work, pre-boundary relay,
2507
+ // then post-boundary temporary records (spec 28 §4.6).
2508
+ for (const relay of stage.boundaryRelay.splice(0)) {
2509
+ await this.applyRelocationData(stage, relay, signal);
2510
+ }
2511
+ // A thrown provider call has an unknown result; the read below decides.
2512
+ const result = await this.requireLocationStore()
2513
+ .commitAggregate(prepared.fence, signal)
2514
+ .catch(() => undefined);
2515
+ if (result?.kind === 'generationExhausted') {
2516
+ throw new Error('location_update_failed: relocation aggregate commit exhausted.');
2517
+ }
2518
+ conflict = result?.kind === 'stale';
2433
2519
  }
2434
2520
  const observed = await this.readAggregateForCommitRetry(prepared, signal);
2435
2521
  if (observed.kind === 'committed') {
@@ -2439,12 +2525,13 @@ class ZLinkHostServiceRelocationRuntime {
2439
2525
  }
2440
2526
  return primary;
2441
2527
  }
2442
- if (observed.kind === 'stale') {
2443
- throw new Error('location_update_failed: relocation aggregate source authority became stale.', {
2444
- cause: firstError
2445
- });
2528
+ if (observed.kind === 'stale' || (conflict && observed.kind === 'source'))
2529
+ return undefined;
2530
+ // An unreadable lease is an uncertain result, not an expiry.
2531
+ if (await this.exactSourceLeaseExpired({ ownerId: target.ownerId, leaseGeneration: target.ownerLeaseGeneration }, signal).catch(() => false)) {
2532
+ return undefined;
2446
2533
  }
2447
- await waitForLocationRetry(stage.offer.restoreDeadlineAtMs, signal, firstError);
2534
+ await waitForRelocationRetry(25, signal);
2448
2535
  }
2449
2536
  }
2450
2537
  async readAggregateForCommitRetry(prepared, signal) {
@@ -2708,6 +2795,17 @@ class ZLinkHostServiceRelocationRuntime {
2708
2795
  throw new Error('Host relocation requires the Spot manager.');
2709
2796
  return value;
2710
2797
  }
2798
+ afterSpotCloseDecision(meshName, spotId, seal) {
2799
+ const manager = this.requireSpotManager();
2800
+ const sealAfterDecision = () => {
2801
+ if (manager.isSpotClosing(meshName, spotId)) {
2802
+ throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.SpotMoving, `Spot '${String(spotId)}' is closing.`, true);
2803
+ }
2804
+ return seal();
2805
+ };
2806
+ const decision = manager.pendingSpotCloseDecision(meshName, spotId);
2807
+ return decision === undefined ? sealAfterDecision() : decision.then(sealAfterDecision);
2808
+ }
2711
2809
  requireActorManager() {
2712
2810
  const value = this.options.actorManager();
2713
2811
  if (value === undefined)
@@ -2802,7 +2900,6 @@ class LocalTargetPort {
2802
2900
  options;
2803
2901
  meshName;
2804
2902
  envelope;
2805
- deferredJoinRoots = new Map();
2806
2903
  constructor(options, meshName, envelope) {
2807
2904
  this.options = options;
2808
2905
  this.meshName = meshName;
@@ -2955,10 +3052,7 @@ class LocalTargetPort {
2955
3052
  if (recovery.request.actorId !== hidden.actor.context.actorId) {
2956
3053
  throw new Error('Canonical Actor Join recovery names a different staged Actor.');
2957
3054
  }
2958
- const deferred = await this.requireSpotManager().restoreCanonicalActorJoinRecovery(recovery, undefined, (0, service_relocation_runtime_1.inventoryDigest)(this.envelope.participants, this.envelope.memberships));
2959
- if (deferred !== undefined) {
2960
- this.deferredJoinRoots.set(hidden.authorityKey, deferred);
2961
- }
3055
+ await this.requireSpotManager().restoreCanonicalActorJoinRecovery(recovery);
2962
3056
  return;
2963
3057
  }
2964
3058
  const packet = decodeQueuedHandoffPacket(message);
@@ -2967,9 +3061,6 @@ class LocalTargetPort {
2967
3061
  throw new Error('Relocated Actor state is not staged.');
2968
3062
  hidden.replayPackets.push(packet);
2969
3063
  }
2970
- deferredJoinRoot(authorityKey) {
2971
- return this.deferredJoinRoots.get(authorityKey);
2972
- }
2973
3064
  async openAdmission(hidden) {
2974
3065
  if (hidden.actor === undefined)
2975
3066
  return;
@@ -3324,8 +3415,11 @@ function sameCoordinator(left, right) {
3324
3415
  left.expectedAuthorityStoreVersion === right.expectedAuthorityStoreVersion);
3325
3416
  }
3326
3417
  function isExactSourceAuthority(current, expected) {
3327
- return (current.storeVersion.value === expected.storeVersion.value &&
3328
- current.objectGeneration === expected.objectGeneration &&
3418
+ return (current.storeVersion.value === expected.storeVersion.value && isSourceOwner(current, expected));
3419
+ }
3420
+ /** The same source tenure owns the row, at any StoreVersion. */
3421
+ function isSourceOwner(current, expected) {
3422
+ return (current.objectGeneration === expected.objectGeneration &&
3329
3423
  current.authorityOwnerGeneration === expected.authorityOwnerGeneration &&
3330
3424
  current.ownerId === expected.ownerId &&
3331
3425
  current.ownerLeaseGeneration === expected.ownerLeaseGeneration &&
@@ -3345,26 +3439,6 @@ function isExactCommittedTarget(current, expected, target, payload) {
3345
3439
  current.allocation.descriptorLifecycleGeneration === target.nodeGeneration &&
3346
3440
  Buffer.from(current.payload).equals(Buffer.from(payload)));
3347
3441
  }
3348
- async function waitForLocationRetry(deadlineAtMs, signal, cause) {
3349
- signal?.throwIfAborted();
3350
- if (performance.now() >= deadlineAtMs) {
3351
- throw new Error('location_update_failed: relocation Restore validity expired before exact target owner confirmation.', { cause });
3352
- }
3353
- await new Promise((resolve, reject) => {
3354
- let timer;
3355
- const aborted = () => {
3356
- if (timer !== undefined)
3357
- clearTimeout(timer);
3358
- reject(signal?.reason);
3359
- };
3360
- signal?.addEventListener('abort', aborted, { once: true });
3361
- timer = setTimeout(() => {
3362
- signal?.removeEventListener('abort', aborted);
3363
- resolve();
3364
- }, Math.min(25, Math.max(1, deadlineAtMs - performance.now())));
3365
- timer.unref();
3366
- });
3367
- }
3368
3442
  async function waitForRelocationRetry(delayMs, signal) {
3369
3443
  signal?.throwIfAborted();
3370
3444
  await new Promise((resolve, reject) => {
@@ -3379,7 +3453,6 @@ async function waitForRelocationRetry(delayMs, signal) {
3379
3453
  signal?.removeEventListener('abort', aborted);
3380
3454
  resolve();
3381
3455
  }, delayMs);
3382
- timer.unref();
3383
3456
  });
3384
3457
  }
3385
3458
  function sameRequestSource(left, right) {