@zlink-systems/framework 0.29.0 → 0.29.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 (42) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/runtime/actors/actor-runtime-contracts.d.ts +1 -0
  3. package/dist/runtime/actors/index.d.ts +2 -0
  4. package/dist/runtime/actors/index.js +21 -6
  5. package/dist/runtime/foundation/service-stateful-runtime.js +27 -19
  6. package/dist/runtime/handlers/handler-instance-scope.js +1 -1
  7. package/dist/runtime/host/actor-placement-coordinator.d.ts +2 -1
  8. package/dist/runtime/host/actor-placement-coordinator.js +2 -1
  9. package/dist/runtime/host/actor-runtime-options-factory.d.ts +2 -1
  10. package/dist/runtime/host/actor-runtime-options-factory.js +1 -0
  11. package/dist/runtime/host/index.d.ts +1 -1
  12. package/dist/runtime/host/index.js +3 -2
  13. package/dist/runtime/locations/domain-store-contract.d.ts +2 -1
  14. package/dist/runtime/locations/in-memory-authority-store.d.ts +1 -0
  15. package/dist/runtime/locations/in-memory-authority-store.js +32 -11
  16. package/dist/runtime/locations/in-memory-location-store.d.ts +1 -1
  17. package/dist/runtime/locations/in-memory-location-store.js +12 -10
  18. package/dist/runtime/locations/internal-location-contracts.d.ts +5 -0
  19. package/dist/runtime/locations/location-store-repository.d.ts +1 -0
  20. package/dist/runtime/locations/location-store-repository.js +54 -21
  21. package/dist/runtime/spots/spot-entry-activation.d.ts +1 -0
  22. package/dist/runtime/spots/spot-entry-activation.js +3 -0
  23. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +1 -0
  24. package/dist/runtime/spots/spot-node-runtime-manager.js +6 -0
  25. package/dist/runtime/spots/spot-serial-executor.d.ts +1 -0
  26. package/dist/runtime/spots/spot-serial-executor.js +10 -1
  27. package/package.json +2 -2
  28. package/src/runtime/actors/actor-runtime-contracts.ts +4 -0
  29. package/src/runtime/actors/index.ts +77 -42
  30. package/src/runtime/foundation/service-stateful-runtime.ts +49 -43
  31. package/src/runtime/handlers/handler-instance-scope.ts +2 -1
  32. package/src/runtime/host/actor-placement-coordinator.ts +8 -2
  33. package/src/runtime/host/actor-runtime-options-factory.ts +6 -0
  34. package/src/runtime/host/index.ts +6 -2
  35. package/src/runtime/locations/domain-store-contract.ts +1 -1
  36. package/src/runtime/locations/in-memory-authority-store.ts +47 -10
  37. package/src/runtime/locations/in-memory-location-store.ts +14 -10
  38. package/src/runtime/locations/internal-location-contracts.ts +6 -0
  39. package/src/runtime/locations/location-store-repository.ts +82 -28
  40. package/src/runtime/spots/spot-entry-activation.ts +4 -0
  41. package/src/runtime/spots/spot-node-runtime-manager.ts +7 -0
  42. package/src/runtime/spots/spot-serial-executor.ts +10 -4
@@ -89,6 +89,7 @@ export declare class ZLinkEntrySpotActivation {
89
89
  */
90
90
  commitServiceActorJoin(actor: ZLinkActor, handoffBacklog?: readonly ZLinkActorHandoffPacket[], signal?: AbortSignal): Promise<void>;
91
91
  executeActor<T>(actorId: string, operation: () => Promise<T>): Promise<T>;
92
+ terminateActor(actorId: string, terminal: () => Promise<void>): Promise<void>;
92
93
  /**
93
94
  * Entry Spot membership does not have an application admission callback.
94
95
  * The shared core round-trip therefore accepts a valid returning Actor and
@@ -220,6 +220,9 @@ class ZLinkEntrySpotActivation {
220
220
  executeActor(actorId, operation) {
221
221
  return this.spotSerialExecutor.executeActor(actorId, operation);
222
222
  }
223
+ terminateActor(actorId, terminal) {
224
+ return this.spotSerialExecutor.terminateActor(actorId, terminal);
225
+ }
223
226
  /**
224
227
  * Entry Spot membership does not have an application admission callback.
225
228
  * The shared core round-trip therefore accepts a valid returning Actor and
@@ -104,6 +104,7 @@ export declare class ZLinkSpotNodeRuntimeManager {
104
104
  get primaryMeshCompletions(): ZLinkMeshCompletionTable | undefined;
105
105
  notifyEntrySpotActorCreated(nodeRid: RoutingId, actor: ZLinkActor, createRequest: ZLinkMessage, signal?: AbortSignal): Promise<import('../../contracts').ZLinkActorCreateResponse | undefined>;
106
106
  notifyPrimaryEntrySpotActorJoined(actor: ZLinkActor, signal?: AbortSignal): Promise<void>;
107
+ terminateActorActivation(actor: ZLinkActor, terminal: () => Promise<void>): Promise<void>;
107
108
  notifyPrimaryEntrySpotActorLeft(actor: ZLinkActor, signal?: AbortSignal, actorRef?: ActorRef, membershipEpoch?: bigint): Promise<void>;
108
109
  notifyPrimaryEntrySpotActorDisconnected(actor: ZLinkActor, signal?: AbortSignal): Promise<void>;
109
110
  notifyEntrySpotActorDisconnected(meshName: string, actor: ZLinkActor, signal?: AbortSignal): Promise<void>;
@@ -542,6 +542,12 @@ class ZLinkSpotNodeRuntimeManager {
542
542
  const activation = this.primaryEntryActivation();
543
543
  return activation?.notifyJoinActor(actor, signal) ?? Promise.resolve();
544
544
  }
545
+ terminateActorActivation(actor, terminal) {
546
+ const activation = this.entryActivations.get(actor.context.meshName);
547
+ return activation === undefined
548
+ ? terminal()
549
+ : activation.terminateActor(actor.context.actorId, terminal);
550
+ }
545
551
  notifyPrimaryEntrySpotActorLeft(actor, signal, actorRef, membershipEpoch) {
546
552
  const activation = this.primaryEntryActivation();
547
553
  return (activation?.notifyLeaveActor(actor, signal, actorRef, membershipEpoch) ?? Promise.resolve());
@@ -26,6 +26,7 @@ export declare class ZLinkSpotSerialExecutor {
26
26
  executeTimer<T>(timerName: string, operation: () => Promise<T> | T, workOptions?: ZLinkSerialWorkOptions): Promise<T>;
27
27
  isTimerExecuting(timerName: string): boolean;
28
28
  executeLifecycle<T>(operation: () => Promise<T> | T, workOptions?: ZLinkSerialWorkOptions): Promise<T>;
29
+ terminateActor(actorId: string, terminal: () => Promise<void>): Promise<void>;
29
30
  /**
30
31
  * Makes all child queues unreachable and completes them in one synchronous
31
32
  * ownership turn. Already accepted work remains drainable.
@@ -42,7 +42,8 @@ class ZLinkSpotSerialExecutor {
42
42
  return this.spotSerial.execute(operation, workOptions);
43
43
  }
44
44
  executeActor(actorId, operation, workOptions) {
45
- return this.actorExecutor(actorId).execute(() => operation(this.actorSerial(actorId)), workOptions);
45
+ const serial = this.actorSerial(actorId);
46
+ return this.actorExecutor(actorId).execute(() => operation(serial), workOptions);
46
47
  }
47
48
  /** @internal Restores a durable Actor FIFO prefix without starting it. */
48
49
  admitActorDurablePrefix(actorId, records) {
@@ -65,6 +66,14 @@ class ZLinkSpotSerialExecutor {
65
66
  executeLifecycle(operation, workOptions) {
66
67
  return this.spotSerial.execute(operation, { ...workOptions, lane: 'lifecycle' });
67
68
  }
69
+ async terminateActor(actorId, terminal) {
70
+ const executor = this.actorExecutor(actorId);
71
+ await executor.close();
72
+ await terminal();
73
+ if (this.actorExecutors.get(actorId) === executor)
74
+ this.actorExecutors.delete(actorId);
75
+ this.actorSerials.delete(actorId);
76
+ }
68
77
  /**
69
78
  * Makes all child queues unreachable and completes them in one synchronous
70
79
  * ownership turn. Already accepted work remains drainable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zlink-systems/framework",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@opentelemetry/api": "^1.9.1",
21
21
  "@opentelemetry/api-logs": "^0.221.0",
22
- "@zlink-systems/stream-wire": "0.29.0",
22
+ "@zlink-systems/stream-wire": "0.29.1",
23
23
  "@zlink-systems/zlink": "1.17.0"
24
24
  }
25
25
  }
@@ -49,6 +49,10 @@ export interface ZLinkActorManagerOptions {
49
49
  signal?: AbortSignal
50
50
  ) => Promise<ZLinkActorCreateResponse | undefined>;
51
51
  readonly actorDestroyedCleanup?: (actorId: string) => void;
52
+ readonly terminateActorActivation?: (
53
+ actor: ZLinkActor,
54
+ terminal: () => Promise<void>
55
+ ) => Promise<void>;
52
56
  readonly publishActorAuthority?: (
53
57
  actorType: string,
54
58
  actorRef: ActorRef,
@@ -29,6 +29,7 @@ import {
29
29
  wireReplyFailureException
30
30
  } from '../framework-errors-internal';
31
31
  import { disposeLifecycleHandlers } from '../handlers/handler-instance-scope';
32
+ import { currentZLinkActorExecution } from './actor-execution-context';
32
33
  import { encodeFrameworkPayloadMessage } from '../messaging/payload-codec';
33
34
  import { ZLinkActorCreationCoordinator, type ZLinkActorCreateRequest } from './actor-creation';
34
35
  import type { ZLinkActorManagerOptions } from './actor-runtime-contracts';
@@ -217,34 +218,43 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
217
218
  const completions = this.options.nativeActorCompletionTableProvider?.();
218
219
  const entryNodeRid = state.entryNodeRid ?? current.nodeRid;
219
220
  if (node === undefined || completions === undefined) return false;
220
- const destroyTask = state.getOrStartDestroy(entryNodeRid, async (nativeRef) => {
221
- if (nativeRef !== undefined) {
222
- const completion = await completions.submit(() => node.destroyActor(nativeRef, 0), signal);
223
- try {
224
- if (completion.terminalResult !== 0 || completion.failureErrno !== 0) {
225
- // Classify the (terminal, fine) pair via the shared translator
226
- // (spec 32-framework-error-model:83-118, 99-108) instead of
227
- // collapsing every destroy failure to NotFound; genuine
228
- // route-not-found fine codes still yield NotFound through the
229
- // shared table.
230
- throw wireReplyFailureException(
231
- completion.terminalResult,
232
- completion.failureErrno,
233
- `Actor '${actor.actorId}' destroy failed with result '${completion.terminalResult}' and errno '${completion.failureErrno}'.`
234
- );
221
+ const destroyTask = state.getOrStartDestroy(entryNodeRid, (nativeRef) =>
222
+ this.terminateActor(state, async () => {
223
+ if (nativeRef !== undefined) {
224
+ const completion = await completions.submit(
225
+ () => node.destroyActor(nativeRef, 0),
226
+ signal
227
+ );
228
+ try {
229
+ if (completion.terminalResult !== 0 || completion.failureErrno !== 0) {
230
+ // Classify the (terminal, fine) pair via the shared translator
231
+ // (spec 32-framework-error-model:83-118, 99-108) instead of
232
+ // collapsing every destroy failure to NotFound; genuine
233
+ // route-not-found fine codes still yield NotFound through the
234
+ // shared table.
235
+ throw wireReplyFailureException(
236
+ completion.terminalResult,
237
+ completion.failureErrno,
238
+ `Actor '${actor.actorId}' destroy failed with result '${completion.terminalResult}' and errno '${completion.failureErrno}'.`
239
+ );
240
+ }
241
+ } finally {
242
+ closeMeshCompletion(completion);
235
243
  }
236
- } finally {
237
- closeMeshCompletion(completion);
244
+ state.markNativeActorDestroyed(nativeRef);
238
245
  }
239
- state.markNativeActorDestroyed(nativeRef);
240
- }
241
- if (state.actorType !== undefined && state.ownsLocation) {
242
- await this.options.locationLifecycle?.releaseActor(state.actorType, actor.actorId, current);
243
- }
244
- await this.finalizeDestroyedActor(actor.actorId, state);
245
- });
246
+ if (state.actorType !== undefined && state.ownsLocation) {
247
+ await this.options.locationLifecycle?.releaseActor(
248
+ state.actorType,
249
+ actor.actorId,
250
+ current
251
+ );
252
+ }
253
+ await this.finalizeDestroyedActor(actor.actorId, state);
254
+ })
255
+ );
246
256
  try {
247
- await destroyTask;
257
+ await this.awaitActorDestroy(state, destroyTask);
248
258
  } catch (error) {
249
259
  state.clearFailedDestroy(destroyTask);
250
260
  throw error;
@@ -548,6 +558,29 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
548
558
  this.options.metrics?.change(METRIC_NAMES.ActorCount, -1);
549
559
  }
550
560
  }
561
+
562
+ private terminateActor(
563
+ state: ZLinkActorRuntimeState,
564
+ terminal: () => Promise<void>
565
+ ): Promise<void> {
566
+ const actor = state.actor;
567
+ const completion =
568
+ actor === undefined || this.options.terminateActorActivation === undefined
569
+ ? terminal()
570
+ : this.options.terminateActorActivation(actor, terminal);
571
+ return completion;
572
+ }
573
+
574
+ private awaitActorDestroy(
575
+ state: ZLinkActorRuntimeState,
576
+ completion: Promise<void>
577
+ ): Promise<void> {
578
+ if (currentZLinkActorExecution()?.actorId === state.actorId) {
579
+ this.detachedTaskRunner.runDetached('Actor terminal cleanup', () => completion);
580
+ return Promise.resolve();
581
+ }
582
+ return completion;
583
+ }
551
584
  async getOrCreateActor(
552
585
  actorId: string,
553
586
  actorType: string,
@@ -648,25 +681,27 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
648
681
  `Actor '${actor.context.actorId}' must leave its current SPOT before destroy.`
649
682
  );
650
683
  }
651
- const destroyTask = state.getOrStartDestroy(entryNodeRid, async (actorRef) => {
652
- const destroyedActorRef =
653
- actorRef === undefined ? undefined : toFrameworkActorRef(actorRef, state.meshName ?? '');
654
- if (actorRef !== undefined) {
655
- await node.destroyActor(actorRef, 0, destroySignal);
656
- state.markNativeActorDestroyed(actorRef);
657
- }
658
- if (state.actorType !== undefined && state.ownsLocation) {
659
- await this.options.locationLifecycle?.releaseActor(
660
- state.actorType,
661
- actor.context.actorId,
662
- destroyedActorRef
663
- );
664
- }
665
- await this.finalizeDestroyedActor(actor.context.actorId, state);
666
- });
684
+ const destroyTask = state.getOrStartDestroy(entryNodeRid, (actorRef) =>
685
+ this.terminateActor(state, async () => {
686
+ const destroyedActorRef =
687
+ actorRef === undefined ? undefined : toFrameworkActorRef(actorRef, state.meshName ?? '');
688
+ if (actorRef !== undefined) {
689
+ await node.destroyActor(actorRef, 0, destroySignal);
690
+ state.markNativeActorDestroyed(actorRef);
691
+ }
692
+ if (state.actorType !== undefined && state.ownsLocation) {
693
+ await this.options.locationLifecycle?.releaseActor(
694
+ state.actorType,
695
+ actor.context.actorId,
696
+ destroyedActorRef
697
+ );
698
+ }
699
+ await this.finalizeDestroyedActor(actor.context.actorId, state);
700
+ })
701
+ );
667
702
 
668
703
  try {
669
- await destroyTask;
704
+ await this.awaitActorDestroy(state, destroyTask);
670
705
  } catch (error) {
671
706
  state.clearFailedDestroy(destroyTask);
672
707
  throw error;
@@ -432,6 +432,7 @@ export class ServiceStatefulRuntime {
432
432
  Promise<{
433
433
  readonly spot: Pick<ServiceSpotState, 'ref'>;
434
434
  readonly route: ServiceInstanceRouteFence;
435
+ readonly stableType: string;
435
436
  }>
436
437
  >();
437
438
  private closed = false;
@@ -1559,7 +1560,7 @@ export class ServiceStatefulRuntime {
1559
1560
  this.nodeRid,
1560
1561
  sourceSpotId,
1561
1562
  'request',
1562
- { high: this.nodeGeneration, low: pending.id },
1563
+ { high: ZLINK_NATIVE_CORRELATION_OPERATION_NAMESPACE, low: pending.id },
1563
1564
  deadlineUnixMs,
1564
1565
  pending.id,
1565
1566
  metadataFrame !== undefined
@@ -2520,27 +2521,24 @@ export class ServiceStatefulRuntime {
2520
2521
  ): Promise<void> {
2521
2522
  try {
2522
2523
  this.validateInstanceIngress(ingress, record);
2523
- const activation = await this.activateMissingInstanceAsync(
2524
- record,
2525
- payloadFrame,
2526
- metadataFrame
2527
- );
2528
- if (this.closed) return;
2529
- const terminal = this.activationTerminalCompletion(record.target, activation.route);
2530
- const admitted = this.enqueueActivatedInstanceSpot(
2531
- ingress,
2532
- record,
2533
- payloadFrame,
2534
- activation.spot,
2535
- localReply,
2536
- terminal.onTerminalCompletion,
2537
- metadataFrame,
2538
- terminal.onHandlerTurnStarted,
2539
- this.instanceApplicationTarget(record, activation.route.objectGeneration)
2540
- );
2541
- if (admitted !== 'application') {
2542
- throw new Error('Activated Instance message was not admitted to the local queue.');
2543
- }
2524
+ await this.activateMissingInstanceAsync(record, payloadFrame, metadataFrame, (activation) => {
2525
+ if (this.closed) return;
2526
+ const terminal = this.activationTerminalCompletion(record.target, activation.route);
2527
+ const admitted = this.enqueueActivatedInstanceSpot(
2528
+ ingress,
2529
+ record,
2530
+ payloadFrame,
2531
+ activation.spot,
2532
+ localReply,
2533
+ terminal.onTerminalCompletion,
2534
+ metadataFrame,
2535
+ terminal.onHandlerTurnStarted,
2536
+ this.instanceApplicationTarget(record, activation.route.objectGeneration)
2537
+ );
2538
+ if (admitted !== 'application') {
2539
+ throw new Error('Activated Instance message was not admitted to the local queue.');
2540
+ }
2541
+ });
2544
2542
  } catch (error) {
2545
2543
  this.finishMissingInstanceActivationFailure(ingress, record, error, localReply);
2546
2544
  }
@@ -2949,15 +2947,35 @@ export class ServiceStatefulRuntime {
2949
2947
  { readonly kind: 'instanceSpot'; readonly activation: 'missing' }
2950
2948
  >,
2951
2949
  payloadFrame: Buffer,
2952
- metadataFrame?: Buffer
2953
- ): Promise<{
2954
- readonly spot: Pick<ServiceSpotState, 'ref'>;
2955
- readonly route: ServiceInstanceRouteFence;
2956
- }> {
2957
- const key = instanceActivationKey(record);
2950
+ metadataFrame: Buffer | undefined,
2951
+ admit: (activation: {
2952
+ readonly spot: Pick<ServiceSpotState, 'ref'>;
2953
+ readonly route: ServiceInstanceRouteFence;
2954
+ }) => void
2955
+ ): Promise<unknown> {
2956
+ const key = record.target.targetSpotId;
2958
2957
  const pending = this.pendingInstanceActivations.get(key);
2959
- if (pending !== undefined) return pending;
2960
- const activation = this.runMissingInstanceActivation(record, payloadFrame, metadataFrame);
2958
+ const ready =
2959
+ pending ??
2960
+ this.runMissingInstanceActivation(record, payloadFrame, metadataFrame).then((activation) => ({
2961
+ ...activation,
2962
+ stableType: record.target.stableType
2963
+ }));
2964
+ const operation = ready.then((value) => {
2965
+ if (value.stableType !== record.target.stableType) {
2966
+ throw new ZLinkFrameworkException(
2967
+ ZLinkFrameworkErrorKind.TypeMismatch,
2968
+ 'Instance Spot type does not match.'
2969
+ );
2970
+ }
2971
+ admit(value);
2972
+ return value;
2973
+ });
2974
+ // The caller owns operation failure. Keep the activation available for later participants.
2975
+ const activation = operation.then(
2976
+ (value) => value,
2977
+ () => ready
2978
+ );
2961
2979
  this.pendingInstanceActivations.set(key, activation);
2962
2980
  const clear = () => {
2963
2981
  if (this.pendingInstanceActivations.get(key) === activation) {
@@ -2965,7 +2983,7 @@ export class ServiceStatefulRuntime {
2965
2983
  }
2966
2984
  };
2967
2985
  void activation.then(clear, clear);
2968
- return activation;
2986
+ return operation;
2969
2987
  }
2970
2988
 
2971
2989
  private async runMissingInstanceActivation(
@@ -5817,18 +5835,6 @@ function instanceOperationKey(
5817
5835
  );
5818
5836
  }
5819
5837
 
5820
- function instanceActivationKey(
5821
- record: Extract<
5822
- ServiceStatefulWireRecord,
5823
- { readonly kind: 'instanceSpot'; readonly activation: 'missing' }
5824
- >
5825
- ): string {
5826
- return (
5827
- `${record.target.targetNodeRid}\0${record.target.targetNodeGeneration}\0` +
5828
- `${record.target.targetSpotId}\0${record.target.stableType}`
5829
- );
5830
- }
5831
-
5832
5838
  function subscriptionKey(channelName: string, topicFilter: string): string {
5833
5839
  if (channelName.length === 0 || topicFilter.length === 0) {
5834
5840
  throw new TypeError('Channel name and topic filter must be non-empty.');
@@ -212,7 +212,8 @@ class LifecycleHandlerInstanceScope {
212
212
  }
213
213
 
214
214
  async dispose(detachedTaskRunner: ZLinkDetachedTaskRunner): Promise<void> {
215
- const disposeFromActiveInvocation = activeLifecycleScope.getStore() === this;
215
+ const disposeFromActiveInvocation =
216
+ this.activeInvocations > 0 && activeLifecycleScope.getStore() === this;
216
217
  const disposal = this.beginDisposeCore();
217
218
  if (disposal.deferred !== undefined) {
218
219
  startOutsideLifecycleInvocation(() => {
@@ -11,7 +11,8 @@ import type {
11
11
  ZLinkAuthoritySnapshot,
12
12
  ZLinkCreationOperationIdentity,
13
13
  ZLinkLocationOwnerToken,
14
- ZLinkObjectReserveResult
14
+ ZLinkObjectReserveResult,
15
+ ZLinkObjectReserveRequest
15
16
  } from '../locations/internal-location-contracts';
16
17
  import type {
17
18
  ZLinkAuthorityStore,
@@ -57,6 +58,10 @@ export interface ZLinkActorPlacementTarget {
57
58
  }
58
59
 
59
60
  export interface ZLinkActorPlacementCoordinatorOptions {
61
+ readonly actorRelocationPolicy?: (
62
+ meshName: string,
63
+ stableType: string
64
+ ) => ZLinkObjectReserveRequest['actorRelocationPolicy'];
60
65
  readonly store: ZLinkObjectCreationStore & ZLinkAuthorityStore;
61
66
  readonly target: (
62
67
  meshName: string | undefined,
@@ -130,7 +135,8 @@ export class ZLinkActorPlacementCoordinator {
130
135
  },
131
136
  target: reservationTarget(target),
132
137
  creatingPayload,
133
- capacity: { actors: 1, spots: 0 }
138
+ capacity: { actors: 1, spots: 0 },
139
+ actorRelocationPolicy: this.options.actorRelocationPolicy?.(target.meshName, stableType)
134
140
  },
135
141
  deadline.signal,
136
142
  BigInt(deadlineUnixMs)
@@ -34,6 +34,10 @@ import type { ZLinkRemoteActorPacketTarget } from '../actors';
34
34
  import type { ZLinkActorJoinRelocation } from '../actors/actor-join-relocation';
35
35
 
36
36
  export interface ZLinkActorRuntimeOptionsFactoryOptions {
37
+ readonly terminateActorActivation: (
38
+ actor: ZLinkActor,
39
+ terminal: () => Promise<void>
40
+ ) => Promise<void>;
37
41
  readonly registration: ZLinkFrameworkRegistration;
38
42
  /** Message flow tracer, so a failed deferred Join records its cause on the flow. */
39
43
  readonly messageFlow?: () =>
@@ -101,6 +105,7 @@ export class ZLinkActorRuntimeOptionsFactory {
101
105
  | 'actorRefResolver'
102
106
  | 'actorCreatedNotifier'
103
107
  | 'actorDestroyedCleanup'
108
+ | 'terminateActorActivation'
104
109
  | 'publishActorAuthority'
105
110
  | 'locationLifecycle'
106
111
  | 'boundSessionFactory'
@@ -111,6 +116,7 @@ export class ZLinkActorRuntimeOptionsFactory {
111
116
  > {
112
117
  const actorTransferRegistry = this.options.actorTransferRegistry;
113
118
  return {
119
+ terminateActorActivation: this.options.terminateActorActivation,
114
120
  actorMeshNameProvider: this.options.actorMeshName,
115
121
  actorLeaveSpot: (meshName, spotId, actor, signal) => {
116
122
  const spotManager = this.options.spotManager();
@@ -2016,6 +2016,7 @@ export class ZLinkFrameworkRuntimeHost
2016
2016
  | 'actorRefResolver'
2017
2017
  | 'actorCreatedNotifier'
2018
2018
  | 'actorDestroyedCleanup'
2019
+ | 'terminateActorActivation'
2019
2020
  | 'locationLifecycle'
2020
2021
  | 'boundSessionFactory'
2021
2022
  | 'shutdownSignal'
@@ -2139,6 +2140,9 @@ export class ZLinkFrameworkRuntimeHost
2139
2140
  }
2140
2141
  this.actorPlacement ??= new ZLinkActorPlacementCoordinator({
2141
2142
  store: locationStore,
2143
+ actorRelocationPolicy: (meshName, stableType) =>
2144
+ this.options.registration.spotNodes.get(meshName)?.actorFactoryRegistrations?.[stableType]
2145
+ ?.relocation.kind,
2142
2146
  remoteCreate: (meshName, targetNodeRid, request, timeoutMs) => {
2143
2147
  const node = this.spotNodeRuntime?.meshNode(meshName);
2144
2148
  if (node === undefined) {
@@ -2194,14 +2198,12 @@ export class ZLinkFrameworkRuntimeHost
2194
2198
  const capability = descriptor.objectCapabilities.find(
2195
2199
  (candidate) => candidate.objectKind === 'actor' && candidate.stableType === stableType
2196
2200
  );
2197
- const actors = descriptor.populationCapacity.actors;
2198
2201
  return (
2199
2202
  descriptor.state === ZLinkFrameworkRuntimeState.Serving &&
2200
2203
  descriptor.objectRole === 'server' &&
2201
2204
  descriptor.placementWeight > 0 &&
2202
2205
  descriptor.entrySpotId !== undefined &&
2203
2206
  excludedNodeRids?.has(String(descriptor.rid)) !== true &&
2204
- (actors.limit === 0 || actors.active + actors.reserved < actors.limit) &&
2205
2207
  capability !== undefined
2206
2208
  );
2207
2209
  }
@@ -2592,6 +2594,8 @@ export class ZLinkFrameworkRuntimeHost
2592
2594
 
2593
2595
  private actorRuntimeOptionsFactory(): ZLinkActorRuntimeOptionsFactory {
2594
2596
  return new ZLinkActorRuntimeOptionsFactory({
2597
+ terminateActorActivation: (actor, terminal) =>
2598
+ this.spotNodeRuntime?.terminateActorActivation(actor, terminal) ?? terminal(),
2595
2599
  registration: this.options.registration,
2596
2600
  messageFlow: () => this.createDispatchErrorReporter(this.runtimeOrPreStartErrorSink).flow,
2597
2601
  routeTransport: this.routeTransport,
@@ -37,9 +37,9 @@ import type {
37
37
  ZLinkObjectCommitResult,
38
38
  ZLinkObjectCreationCompleteRequest,
39
39
  ZLinkObjectCreationCompleteResult,
40
- ZLinkObjectReserveRequest,
41
40
  ZLinkObjectReserveResult
42
41
  } from '../../contracts/Locations/Authority';
42
+ import type { ZLinkObjectReserveRequest } from './internal-location-contracts';
43
43
 
44
44
  /**
45
45
  * Framework-private domain repository. Provider implementations never
@@ -7,6 +7,7 @@ import { UINT64_MAX } from '@zlink-systems/stream-wire';
7
7
  import { ZLINK_MAX_ROUTING_ID_BYTES } from '../../contracts/Common/CoreTypes';
8
8
  import { ZLINK_PROVIDER_MAX_PAGE_SIZE } from '../../contracts/Locations/Stores';
9
9
  import { randomUUID } from 'node:crypto';
10
+ import { ZLinkFrameworkException, ZLinkFrameworkErrorKind } from '../../contracts';
10
11
  import { SHA256_DIGEST_BYTES } from '../foundation/actor-join-recovery-codec';
11
12
  import type {
12
13
  ZLinkAggregateAbortResult,
@@ -41,13 +42,16 @@ import type {
41
42
  } from './internal-location-contracts';
42
43
  import { encodeAuthorityKey } from './authority-key-codec';
43
44
  import { creationTerminalPreimage } from './opaque-record-key';
45
+ import { ServiceRelocationAuthorityPayloadCodec } from '../foundation/service-relocation-runtime';
44
46
 
45
47
  const MAX_GENERATION = 0x7fff_ffff_ffff_ffffn;
46
48
 
47
49
  const CREATION_TERMINAL_RETENTION_MS = 5 * 60 * 1000;
48
50
  const MAX_U64 = UINT64_MAX;
51
+ const relocationAuthorityCodec = new ServiceRelocationAuthorityPayloadCodec();
49
52
 
50
53
  export interface ZLinkInMemoryAuthorityValidation {
54
+ isOwnerLive(owner: ZLinkLocationOwnerToken): boolean;
51
55
  isTargetLive(
52
56
  descriptor: ZLinkMeshNodeDescriptorKey,
53
57
  lifecycleGeneration: bigint,
@@ -287,14 +291,42 @@ export class ZLinkInMemoryAuthorityStore {
287
291
 
288
292
  private tryReleaseEndedReservation(
289
293
  key: ZLinkAuthorityKey,
290
- expectedStoreVersion: string
294
+ expectedStoreVersion: string,
295
+ request?: ZLinkObjectReserveRequest
291
296
  ): boolean {
292
297
  const current = this.rows.get(key.value);
293
298
  if (
294
299
  current === undefined ||
295
300
  current.snapshot.storeVersion.value !== expectedStoreVersion ||
296
- current.snapshot.allocation.state !== 'reserved' ||
297
- current.creation === undefined ||
301
+ (current.snapshot.allocation.state === 'active'
302
+ ? request?.key.kind !== 'actor'
303
+ : current.creation === undefined)
304
+ )
305
+ return false;
306
+ if (current.snapshot.allocation.state === 'active') {
307
+ for (const aggregate of this.aggregates.values()) {
308
+ if (
309
+ aggregate.state === 'prepared' &&
310
+ aggregate.request.participants.some(
311
+ (participant) => participant.authorityKey.value === key.value
312
+ )
313
+ )
314
+ return false;
315
+ }
316
+ if (relocationAuthorityCodec.read(current.snapshot.payload) !== undefined) return false;
317
+ if (
318
+ this.validation.isOwnerLive({
319
+ ownerId: current.snapshot.ownerId,
320
+ leaseGeneration: current.snapshot.ownerLeaseGeneration
321
+ })
322
+ )
323
+ return false;
324
+ if (request?.actorRelocationPolicy !== 'disabled')
325
+ throw new ZLinkFrameworkException(
326
+ ZLinkFrameworkErrorKind.Unavailable,
327
+ 'Actor owner lease is unavailable.'
328
+ );
329
+ } else if (
298
330
  this.validation.isTargetLive(
299
331
  current.snapshot.allocation.descriptor,
300
332
  current.snapshot.allocation.descriptorLifecycleGeneration,
@@ -305,9 +337,14 @@ export class ZLinkInMemoryAuthorityStore {
305
337
  )
306
338
  )
307
339
  return false;
308
- this.adjustCapacity(this.pendingCapacity, current.snapshot.allocation, -1);
340
+ this.adjustCapacity(
341
+ current.snapshot.allocation.state === 'active' ? this.activeCapacity : this.pendingCapacity,
342
+ current.snapshot.allocation,
343
+ -1
344
+ );
309
345
  this.rows.delete(key.value);
310
- this.creationTerminals.set(current.creation.reservationId, 'aborted');
346
+ if (current.snapshot.allocation.state === 'reserved' && current.creation !== undefined)
347
+ this.creationTerminals.set(current.creation.reservationId, 'aborted');
311
348
  this.scanRevision++;
312
349
  return true;
313
350
  }
@@ -330,16 +367,16 @@ export class ZLinkInMemoryAuthorityStore {
330
367
  ) {
331
368
  return { kind: 'typeMismatch', current: this.snapshot(current.snapshot) };
332
369
  }
333
- if (current.snapshot.allocation.state === 'active') {
334
- return { kind: 'alreadyExists', current: this.snapshot(current.snapshot) };
335
- }
336
370
  if (
337
371
  !this.tryReleaseEndedReservation(
338
372
  encodeAuthorityKey(request.key.kind, request.key.globalId),
339
- current.snapshot.storeVersion.value
373
+ current.snapshot.storeVersion.value,
374
+ request
340
375
  )
341
376
  )
342
- return { kind: 'conflict', current: this.read(key) };
377
+ return current.snapshot.allocation.state === 'active'
378
+ ? { kind: 'alreadyExists', current: this.snapshot(current.snapshot) }
379
+ : { kind: 'conflict', current: this.read(key) };
343
380
  }
344
381
  const target = creationTarget(request);
345
382
  if (