@zlink-systems/framework 0.32.0 → 0.32.2

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 (37) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/runtime/actors/actor-context.js +1 -1
  3. package/dist/runtime/actors/actor-creation.js +1 -1
  4. package/dist/runtime/actors/actor-runtime-state.d.ts +2 -1
  5. package/dist/runtime/actors/actor-runtime-state.js +24 -8
  6. package/dist/runtime/actors/index.d.ts +3 -3
  7. package/dist/runtime/actors/index.js +19 -32
  8. package/dist/runtime/host/entry-actor-runtime.d.ts +3 -9
  9. package/dist/runtime/host/entry-actor-runtime.js +2 -10
  10. package/dist/runtime/host/spot-runtime-options-factory.js +2 -1
  11. package/dist/runtime/locations/actor-location-claims.js +9 -10
  12. package/dist/runtime/spots/index.d.ts +4 -4
  13. package/dist/runtime/spots/index.js +24 -0
  14. package/dist/runtime/spots/spot-activation.d.ts +2 -0
  15. package/dist/runtime/spots/spot-actor-admission-coordinator.d.ts +1 -1
  16. package/dist/runtime/spots/spot-actor-admission-coordinator.js +2 -1
  17. package/dist/runtime/spots/spot-actor-ingress-admission.d.ts +11 -7
  18. package/dist/runtime/spots/spot-actor-ingress-admission.js +14 -6
  19. package/dist/runtime/spots/spot-actor-packet-dispatch.d.ts +3 -3
  20. package/dist/runtime/spots/spot-actor-packet-dispatch.js +2 -2
  21. package/dist/runtime/spots/spot-entry-activation.js +1 -1
  22. package/dist/runtime/spots/spot-runtime-ports.d.ts +2 -4
  23. package/package.json +2 -2
  24. package/src/runtime/actors/actor-context.ts +1 -1
  25. package/src/runtime/actors/actor-creation.ts +1 -1
  26. package/src/runtime/actors/actor-runtime-state.ts +27 -9
  27. package/src/runtime/actors/index.ts +30 -35
  28. package/src/runtime/host/entry-actor-runtime.ts +4 -34
  29. package/src/runtime/host/spot-runtime-options-factory.ts +2 -1
  30. package/src/runtime/locations/actor-location-claims.ts +14 -14
  31. package/src/runtime/spots/index.ts +34 -4
  32. package/src/runtime/spots/spot-activation.ts +2 -0
  33. package/src/runtime/spots/spot-actor-admission-coordinator.ts +3 -1
  34. package/src/runtime/spots/spot-actor-ingress-admission.ts +29 -11
  35. package/src/runtime/spots/spot-actor-packet-dispatch.ts +5 -5
  36. package/src/runtime/spots/spot-entry-activation.ts +1 -11
  37. package/src/runtime/spots/spot-runtime-ports.ts +2 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zlink-systems/framework",
3
- "version": "0.32.0",
3
+ "version": "0.32.2",
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.32.0",
22
+ "@zlink-systems/stream-wire": "0.32.2",
23
23
  "@zlink-systems/zlink": "1.23.0"
24
24
  }
25
25
  }
@@ -97,7 +97,7 @@ export class DefaultZLinkActorContext implements ZLinkActorContext {
97
97
  }
98
98
 
99
99
  get objectGeneration(): bigint {
100
- return this.state.nativeActorRef?.generation ?? 1n;
100
+ return this.state.currentActorRef().generation;
101
101
  }
102
102
 
103
103
  get spotId(): SpotId | undefined {
@@ -211,7 +211,7 @@ export class ZLinkActorCreationCoordinator {
211
211
  await this.options.locationLifecycle?.setActorRef(
212
212
  actorType,
213
213
  actorId,
214
- { actorId, objectGeneration: 1n, meshName, nodeRid },
214
+ toFrameworkActorRef(state.currentActorRef(), meshName),
215
215
  0n
216
216
  );
217
217
  }
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  import type { Message } from '../../contracts/Common/Message';
11
11
  import type { ZLinkBackendActorRef, ZLinkBackendMeshNode } from '../backend/contracts';
12
+ import { ZLinkConfigurationException } from '../configuration';
12
13
  import { isStructuralGuardEnabled } from '../execution/state-lane';
13
14
  import {
14
15
  ZLinkFrameworkInternalErrorKind,
@@ -172,7 +173,9 @@ export class ZLinkActorRuntimeState {
172
173
  private spotIdValue: RoutingId | undefined;
173
174
  private spotGenerationValue: bigint | undefined;
174
175
  private spotMembershipEpochValue = 0n;
176
+ // Preserve the exact incarnation until teardown finalization, even after native removal.
175
177
  private nativeActorRefValue: ZLinkBackendActorRef | undefined;
178
+ private nativeActorDestroyed = false;
176
179
  private entryNodeRidValue: RoutingId | undefined;
177
180
  boundSessionValue: ZLinkRemoteBoundSessionTarget | undefined;
178
181
  private remoteActorPacketTargetValue: ZLinkRemoteActorPacketTarget | undefined;
@@ -228,6 +231,15 @@ export class ZLinkActorRuntimeState {
228
231
  return this.nativeActorRefValue;
229
232
  }
230
233
 
234
+ currentActorRef(): ZLinkBackendActorRef {
235
+ if (this.nativeActorRefValue === undefined) {
236
+ throw new ZLinkConfigurationException(
237
+ `Actor '${this.actorId}' lifecycle identity is not initialized.`
238
+ );
239
+ }
240
+ return this.nativeActorRefValue;
241
+ }
242
+
231
243
  get boundSessionBindingGeneration(): bigint {
232
244
  return this.boundSessionValue?.bindingGeneration ?? 0n;
233
245
  }
@@ -355,24 +367,26 @@ export class ZLinkActorRuntimeState {
355
367
  );
356
368
  }
357
369
 
358
- const task = Promise.resolve().then(() => destroy(actorRef));
370
+ const task = Promise.resolve()
371
+ .then(() => destroy(this.nativeActorDestroyed ? undefined : actorRef))
372
+ .catch((error: unknown) => {
373
+ // A caller may start another attempt only after this attempt has failed.
374
+ if (this.destroyTask === task) {
375
+ this.destroyTask = undefined;
376
+ this.movingValue = false;
377
+ }
378
+ throw error;
379
+ });
359
380
  this.destroyTask = task;
360
381
  return task;
361
382
  }
362
383
 
363
384
  markNativeActorDestroyed(actorRef: ZLinkBackendActorRef): void {
364
385
  if (this.nativeActorRefValue === actorRef) {
365
- this.nativeActorRefValue = undefined;
386
+ this.nativeActorDestroyed = true;
366
387
  }
367
388
  }
368
389
 
369
- clearFailedDestroy(task: Promise<void>): void {
370
- if (this.destroyTask === task) {
371
- this.destroyTask = undefined;
372
- }
373
- this.movingValue = false;
374
- }
375
-
376
390
  ensureContext(createContext: () => ZLinkActorContext): ZLinkActorContext {
377
391
  this.context ??= createContext();
378
392
  return this.context;
@@ -425,6 +439,7 @@ export class ZLinkActorRuntimeState {
425
439
  this.createRequestPayloadValue = undefined;
426
440
  this.configured = false;
427
441
  this.nativeActorRefValue = undefined;
442
+ this.nativeActorDestroyed = false;
428
443
  this.entryNodeRidValue = undefined;
429
444
  return true;
430
445
  }
@@ -474,6 +489,7 @@ export class ZLinkActorRuntimeState {
474
489
 
475
490
  setNativeActorRef(actorRef: ZLinkBackendActorRef): void {
476
491
  this.nativeActorRefValue = actorRef;
492
+ this.nativeActorDestroyed = false;
477
493
  this.entryNodeRidValue ??= toFrameworkRoutingId(actorRef.nodeRid);
478
494
  }
479
495
 
@@ -556,6 +572,7 @@ export class ZLinkActorRuntimeState {
556
572
  this.spotGenerationValue = undefined;
557
573
  this.spotMembershipEpochValue = 0n;
558
574
  this.nativeActorRefValue = undefined;
575
+ this.nativeActorDestroyed = false;
559
576
  this.entryNodeRidValue = undefined;
560
577
  this.boundSessionValue = undefined;
561
578
  this.remoteActorPacketTargetValue = undefined;
@@ -578,6 +595,7 @@ export class ZLinkActorRuntimeState {
578
595
  this.spotValue = undefined;
579
596
  this.spotIdValue = undefined;
580
597
  this.nativeActorRefValue = undefined;
598
+ this.nativeActorDestroyed = false;
581
599
  // The arriving relocation installs the binding it carries.
582
600
  this.boundSessionValue = undefined;
583
601
  this.remoteActorPacketTargetValue = undefined;
@@ -40,7 +40,6 @@ import {
40
40
  } from './actor-runtime-state';
41
41
  import { ZLinkTransferredActorRollbackCoordinator } from './transferred-actor-rollback';
42
42
  const ACTOR_CREATION_RETRY_INTERVAL_MS = 5;
43
- const LOCAL_DIAGNOSTIC_NODE_RID = 'local';
44
43
 
45
44
  export { DefaultZLinkActorClient, type ZLinkActorClientOptions } from './actor-client';
46
45
  export { DefaultZLinkActorContext, ZLINK_ACTOR_JOIN_ENTRY_SPOT_RUNTIME } from './actor-context';
@@ -167,7 +166,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
167
166
  throwIfAborted(signal);
168
167
  if (this.relocationStaged.has(actorId)) return undefined;
169
168
  const state = this.states.get(actorId);
170
- if (state?.actor !== undefined) {
169
+ if (state?.actor !== undefined && state.nativeActorRef !== undefined) {
171
170
  return this.actorRefForState(state);
172
171
  }
173
172
  return await this.options.actorRefResolver?.resolveActorRef(actorId, signal);
@@ -182,7 +181,12 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
182
181
  async findSpot(actorId: string, signal?: AbortSignal): Promise<SpotRef | undefined> {
183
182
  throwIfAborted(signal);
184
183
  const state = this.states.get(actorId);
185
- if (state?.actor === undefined || state.spotId === undefined) return undefined;
184
+ if (
185
+ state?.actor === undefined ||
186
+ state.spotId === undefined ||
187
+ state.nativeActorRef === undefined
188
+ )
189
+ return undefined;
186
190
  const spotId = state.spotId;
187
191
  const actorRef = this.actorRefForState(state);
188
192
  return {
@@ -249,12 +253,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
249
253
  await this.finalizeDestroyedActor(actor.actorId, state);
250
254
  })
251
255
  );
252
- try {
253
- await this.awaitActorDestroy(state, destroyTask);
254
- } catch (error) {
255
- state.clearFailedDestroy(destroyTask);
256
- throw error;
257
- }
256
+ await this.awaitActorDestroy(state, destroyTask);
258
257
  return true;
259
258
  }
260
259
 
@@ -362,9 +361,10 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
362
361
  if (result.status === 'failed') {
363
362
  throw actorApplicationFailure(actorId, result.error);
364
363
  }
364
+ const actorRef = result.actorRef ?? this.actorRefForState(this.states.get(actorId)!);
365
365
  return result.reply === undefined
366
- ? { status: result.status, actor: result.actorRef }
367
- : { status: result.status, actor: result.actorRef, reply: result.reply };
366
+ ? { status: result.status, actor: actorRef }
367
+ : { status: result.status, actor: actorRef, reply: result.reply };
368
368
  }
369
369
 
370
370
  private ensureActorTypeBelongsToMesh(meshName: string, actorType: string): void {
@@ -638,7 +638,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
638
638
  actorType: string,
639
639
  createRequest?: unknown,
640
640
  signal?: AbortSignal
641
- ): Promise<ZLinkActorLocalCreateResult> {
641
+ ): Promise<ZLinkActorLocalCreateResult<ActorRef | undefined>> {
642
642
  this.rememberActorMeshFromType(actorId, actorType);
643
643
  return await this.createOrGet(actorId, actorType, false, createRequest, signal);
644
644
  }
@@ -651,7 +651,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
651
651
  nativeActorRef?: ZLinkBackendActorRef
652
652
  ): Promise<ZLinkActorLocalCreateResult> {
653
653
  this.rememberActorMeshFromType(actorId, actorType);
654
- return await this.createOrGet(
654
+ const result = await this.createOrGet(
655
655
  actorId,
656
656
  actorType,
657
657
  false,
@@ -661,6 +661,11 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
661
661
  DEFAULT_REQUEST_TIMEOUT_MS,
662
662
  nativeActorRef
663
663
  );
664
+ if (result.status === 'failed' || result.status === 'rejected') return result;
665
+ return {
666
+ ...result,
667
+ actorRef: this.actorRefForState(this.states.get(actorId)!)
668
+ };
664
669
  }
665
670
 
666
671
  adoptCreatedAuthority(
@@ -712,7 +717,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
712
717
  const destroyTask = state.getOrStartDestroy(entryNodeRid, (actorRef) =>
713
718
  this.terminateActor(state, async () => {
714
719
  const destroyedActorRef =
715
- actorRef === undefined ? undefined : toFrameworkActorRef(actorRef, state.meshName ?? '');
720
+ state.nativeActorRef === undefined ? undefined : this.actorRefForState(state);
716
721
  if (actorRef !== undefined) {
717
722
  await node.destroyActor(actorRef, 0, destroySignal);
718
723
  state.markNativeActorDestroyed(actorRef);
@@ -728,12 +733,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
728
733
  })
729
734
  );
730
735
 
731
- try {
732
- await this.awaitActorDestroy(state, destroyTask);
733
- } catch (error) {
734
- state.clearFailedDestroy(destroyTask);
735
- throw error;
736
- }
736
+ await this.awaitActorDestroy(state, destroyTask);
737
737
  }
738
738
 
739
739
  private async createOrGet(
@@ -745,7 +745,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
745
745
  claimLocation = true,
746
746
  timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
747
747
  nativeActorRef?: ZLinkBackendActorRef
748
- ): Promise<ZLinkActorLocalCreateResult> {
748
+ ): Promise<ZLinkActorLocalCreateResult<ActorRef | undefined>> {
749
749
  const deadline = performance.now() + timeoutMs;
750
750
  const requestedMeshName = this.actorMeshNames.get(actorId);
751
751
  for (;;) {
@@ -801,7 +801,8 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
801
801
  try {
802
802
  const result = await operation.task;
803
803
  if (result.status === 'created') {
804
- const actorRef = this.actorRefForState(state);
804
+ const actorRef =
805
+ state.nativeActorRef === undefined ? undefined : this.actorRefForState(state);
805
806
  return {
806
807
  status: operation.created ? 'created' : 'existing',
807
808
  actor: result.actor,
@@ -886,16 +887,7 @@ export class DefaultZLinkActorManager implements ZLinkActorManager {
886
887
  }
887
888
 
888
889
  private actorRefForState(state: ZLinkActorRuntimeState): ActorRef {
889
- const nativeActorRef = state.nativeActorRef;
890
- if (nativeActorRef !== undefined) {
891
- return toFrameworkActorRef(nativeActorRef, state.meshName ?? '');
892
- }
893
- return {
894
- actorId: state.actorId,
895
- objectGeneration: 1n,
896
- meshName: state.meshName ?? '',
897
- nodeRid: this.options.actorCreatedNodeRidProvider?.() ?? LOCAL_DIAGNOSTIC_NODE_RID
898
- };
890
+ return toFrameworkActorRef(state.currentActorRef(), state.meshName ?? '');
899
891
  }
900
892
  }
901
893
 
@@ -994,11 +986,11 @@ class ZLinkActorGetOrCreateCallRuntime
994
986
  extends ZLinkActorCreateCallRuntime
995
987
  implements ZLinkActorGetOrCreateCall {}
996
988
 
997
- export type ZLinkActorLocalCreateResult =
989
+ export type ZLinkActorLocalCreateResult<TActorRef extends ActorRef | undefined = ActorRef> =
998
990
  | {
999
991
  readonly status: 'existing' | 'created';
1000
992
  readonly actor: ZLinkActor;
1001
- readonly actorRef: ActorRef;
993
+ readonly actorRef: TActorRef;
1002
994
  readonly reply?: unknown;
1003
995
  }
1004
996
  | {
@@ -1010,7 +1002,10 @@ export type ZLinkActorLocalCreateResult =
1010
1002
  readonly error: unknown;
1011
1003
  };
1012
1004
 
1013
- function requireCreatedActor(result: ZLinkActorLocalCreateResult, actorId: string): ZLinkActor {
1005
+ function requireCreatedActor(
1006
+ result: ZLinkActorLocalCreateResult<ActorRef | undefined>,
1007
+ actorId: string
1008
+ ): ZLinkActor {
1014
1009
  if (result.status === 'existing' || result.status === 'created') return result.actor;
1015
1010
  if (result.status === 'failed') {
1016
1011
  throw actorApplicationFailure(actorId, result.error);
@@ -1,15 +1,12 @@
1
- import type { ApplicationJobOrigin } from '../application-jobs/contracts';
2
- import type { ZLinkActorRequestTerminal } from '../spots/spot-actor-packet-dispatch';
3
- import type { ZLinkMessageFollowOrigin } from '../foundation/service-runtime-contracts';
4
1
  import type { ActorRef, ZLinkActor } from '../../contracts';
5
- import type { Message } from '../../contracts/Common/Message';
6
2
  import type { ZLinkBackendActorRef, ZLinkBackendSpotNode } from '../backend';
7
- import type { DefaultZLinkActorManager, ZLinkRemoteBoundSessionTarget } from '../actors';
3
+ import type { DefaultZLinkActorManager } from '../actors';
8
4
  import { ZLinkPostCommitActorBinder } from '../actors/post-commit-actor-binder';
9
5
  import type { DefaultZLinkSpotManager, ZLinkSpotNodeRuntimeManager } from '../spots';
10
6
  import type { ZLinkEntryActorRuntime } from '../spots/spot-runtime-ports';
11
7
  import type { ZLinkStreamActorLifecyclePort } from '../streams/stream-binding-runtime-ports';
12
8
  import type { ZLinkBoundSessionRelay } from './bound-session-relay';
9
+ import type { ZLinkActorIngressMembership } from '../spots/spot-actor-ingress-admission';
13
10
 
14
11
  export interface ZLinkEntryActorRuntimeOptions {
15
12
  readonly actorManager: () => DefaultZLinkActorManager | undefined;
@@ -87,35 +84,8 @@ export class ZLinkEntryActorRuntimeService implements ZLinkEntryActorRuntime {
87
84
  await this.requireActorManager().destroyActor(node, entryNodeRid, actor, signal);
88
85
  }
89
86
 
90
- routePacket(
91
- actorId: string,
92
- parts: readonly Message[],
93
- returnResponse?: boolean,
94
- remoteBoundSessionTarget?: ZLinkRemoteBoundSessionTarget,
95
- fallbackActorRef?: ActorRef,
96
- origin: ApplicationJobOrigin = 'remote',
97
- requestTerminal?: ZLinkActorRequestTerminal,
98
- messageFollowOrigin?: ZLinkMessageFollowOrigin
99
- ): { readonly handled: boolean; readonly response?: unknown } {
100
- const spotManager = this.options.spotManager();
101
- const spotId = this.options.actorManager()?.getState(actorId)?.spotId;
102
- if (spotId === undefined || spotManager === undefined) {
103
- return { handled: false };
104
- }
105
- return {
106
- handled: true,
107
- response: spotManager.dispatchRoutedActorPacket(
108
- origin,
109
- spotId,
110
- actorId,
111
- parts,
112
- returnResponse,
113
- remoteBoundSessionTarget,
114
- fallbackActorRef,
115
- requestTerminal,
116
- messageFollowOrigin
117
- )
118
- };
87
+ get actorIngressMembership(): ZLinkActorIngressMembership | undefined {
88
+ return this.options.spotManager()?.actorIngressMembership;
119
89
  }
120
90
 
121
91
  private requireActorManager(): DefaultZLinkActorManager {
@@ -315,7 +315,8 @@ export class ZLinkSpotRuntimeOptionsFactory {
315
315
  state?.meshName === undefined ? undefined : this.meshNodeRoutingId(state.meshName);
316
316
  if (
317
317
  state?.actor === undefined ||
318
- state.remoteActorPacketTarget !== undefined ||
318
+ (state.remoteActorPacketTarget !== undefined &&
319
+ !routingIdsEqual(state.remoteActorPacketTarget.targetNodeRid, localNodeRid)) ||
319
320
  actorRef === undefined ||
320
321
  localNodeRid === undefined ||
321
322
  !routingIdsEqual(actorRef.nodeRid, localNodeRid)
@@ -426,30 +426,30 @@ export class ZLinkActorLocationClaims {
426
426
  const current = await store.readAuthority(key);
427
427
  if (!matchesActorAuthority(current, actorRef, ownerToken)) return;
428
428
 
429
- const result = await store.compareExchangeAuthority(key, current.storeVersion, {
429
+ let result = await store.compareExchangeAuthority(key, current.storeVersion, {
430
430
  kind: 'delete'
431
431
  });
432
- if (
433
- result.kind === 'deleted' ||
434
- (result.kind === 'conflict' && result.current.kind === 'missing')
435
- ) {
436
- return;
437
- }
438
432
  if (
439
433
  result.kind === 'conflict' &&
440
434
  result.current.kind === 'snapshot' &&
441
435
  matchesActorAuthority(result.current, actorRef, ownerToken)
442
436
  ) {
443
- const retry = await store.compareExchangeAuthority(key, result.current.storeVersion, {
437
+ result = await store.compareExchangeAuthority(key, result.current.storeVersion, {
444
438
  kind: 'delete'
445
439
  });
446
- if (
447
- retry.kind === 'deleted' ||
448
- (retry.kind === 'conflict' && retry.current.kind === 'missing')
449
- ) {
450
- return;
451
- }
452
440
  }
441
+ if (
442
+ result.kind === 'deleted' ||
443
+ (result.kind === 'conflict' && result.current.kind === 'missing')
444
+ ) {
445
+ return;
446
+ }
447
+ throw new ZLinkFrameworkException(
448
+ result.kind === 'conflict'
449
+ ? classifyMembershipAuthorityFailure(result.current, actorRef)
450
+ : ZLinkFrameworkErrorKind.InternalFailure,
451
+ `Actor '${actorId}' authority release was rejected with status ${result.kind}.`
452
+ );
453
453
  }
454
454
  }
455
455
 
@@ -4,6 +4,11 @@ import { encodeActorReply } from '../actors/actor-reply';
4
4
  import { boundSessionErrorPayload } from '../streams/bound-session-response-target';
5
5
  import { RELOCATED_REQUEST_OWNED_BY_REQUESTER } from '../actors/actor-handoff';
6
6
  import { routingIdsEqual } from '../routing-id';
7
+ import type { ZLinkActorPacketDelivery } from './spot-actor-packet-dispatch';
8
+ import type {
9
+ ZLinkActorDispatchOwner,
10
+ ZLinkActorIngressMembership
11
+ } from './spot-actor-ingress-admission';
7
12
  import type { ApplicationJobOrigin } from '../application-jobs/contracts';
8
13
  import { dispatchReasonFromError } from '../diagnostics/dispatch-error-details';
9
14
  import type {
@@ -231,10 +236,7 @@ export interface ZLinkSpotManagerOptions {
231
236
  meshName: string,
232
237
  spotType: Type<ZLinkSpot>
233
238
  ) => import('../../contracts').ZLinkSpotRelocationCoordinationMode;
234
- readonly actorDispatchOwnerResolver?: (actorId: string) => {
235
- readonly actorRef?: ActorRef;
236
- readonly spotId?: RoutingId;
237
- };
239
+ readonly actorDispatchOwnerResolver?: (actorId: string) => ZLinkActorDispatchOwner;
238
240
  readonly channelClient?: ZLinkChannelClient;
239
241
  readonly fanoutClient?: ZLinkFanoutClient;
240
242
  readonly spotPublisherClient?: ZLinkSpotPublisherClient;
@@ -337,6 +339,7 @@ interface ZLinkTargetSpotCloseOperation {
337
339
  }
338
340
 
339
341
  export class DefaultZLinkSpotManager {
342
+ readonly actorIngressMembership: ZLinkActorIngressMembership;
340
343
  [inspectRuntime]() {
341
344
  return Object.freeze({
342
345
  activeActivationCount: this.activations.activeActivations().length,
@@ -414,6 +417,10 @@ export class DefaultZLinkSpotManager {
414
417
  spotRouteResolver: options.spotRouteResolver,
415
418
  actorTransferRuntime: options.actorTransferRuntime
416
419
  });
420
+ this.actorIngressMembership = {
421
+ resolveOwner: options.actorDispatchOwnerResolver,
422
+ dispatchToOwner: (spotId, delivery) => this.dispatchActorPacketToOwner(spotId, delivery)
423
+ };
417
424
  this.activationLifecycle = new ZLinkSpotActivationLifecycle({
418
425
  timerClock,
419
426
  spotTimerHandlers: options.spotTimerHandlers,
@@ -444,6 +451,7 @@ export class DefaultZLinkSpotManager {
444
451
  createTopicMessage: options.createTopicMessage,
445
452
  nativeSpotNodeProvider: options.nativeSpotNodeProvider,
446
453
  actorResolver: options.actorResolver,
454
+ actorIngressMembership: this.actorIngressMembership,
447
455
  detachedTaskRunner: options.detachedTaskRunner,
448
456
  actorTransferRuntime: options.actorTransferRuntime,
449
457
  boundSessionRuntime: options.boundSessionRuntime,
@@ -1628,6 +1636,28 @@ export class DefaultZLinkSpotManager {
1628
1636
  );
1629
1637
  }
1630
1638
 
1639
+ private dispatchActorPacketToOwner(
1640
+ spotId: RoutingId | undefined,
1641
+ delivery: ZLinkActorPacketDelivery
1642
+ ): Promise<unknown> {
1643
+ const args = [
1644
+ delivery.actorId,
1645
+ delivery.parts,
1646
+ delivery.returnResponse,
1647
+ delivery.remoteBoundSessionTarget,
1648
+ delivery.fallbackActorRef,
1649
+ delivery.requestTerminal,
1650
+ delivery.messageFollowOrigin
1651
+ ] as const;
1652
+ if (spotId !== undefined) {
1653
+ return this.dispatchRoutedActorPacket(delivery.origin, spotId, ...args);
1654
+ }
1655
+ if (this.options.dispatchEntryActorPacket === undefined) {
1656
+ throw new ZLinkConfigurationException('Actor ingress requires an Entry FIFO port.');
1657
+ }
1658
+ return this.options.dispatchEntryActorPacket(delivery.origin, ...args);
1659
+ }
1660
+
1631
1661
  /** @internal Replays a record already owned by the Actor handoff queue. */
1632
1662
  dispatchRoutedActorPacketDirect(
1633
1663
  origin: ApplicationJobOrigin,
@@ -82,6 +82,7 @@ import type {
82
82
  } from './spot-actor-join-dispatch';
83
83
  import { ZLinkSpotActorAdmissionCoordinator } from './spot-actor-admission-coordinator';
84
84
  import type { ZLinkRouteToActorJoinPrewarm } from './spot-actor-packet-dispatch';
85
+ import type { ZLinkActorIngressMembership } from './spot-actor-ingress-admission';
85
86
  import { ZLinkSpotActivation } from './spot-activation-state';
86
87
  import type { ZLinkSpotLocationClaim } from './spot-location-claim';
87
88
  import type {
@@ -132,6 +133,7 @@ export interface ZLinkSpotActivationLifecycleOptions {
132
133
  readonly createTopicMessage?: () => ZLinkBackendTopicMessage;
133
134
  readonly nativeSpotNodeProvider?: (meshName: string) => ZLinkBackendSpotNode | undefined;
134
135
  readonly actorResolver?: (actorId: string) => ZLinkActor | undefined;
136
+ readonly actorIngressMembership?: ZLinkActorIngressMembership;
135
137
  readonly actorTransferRuntime?: ZLinkSpotActorTransferRuntime;
136
138
  readonly boundSessionRuntime?: ZLinkSpotBoundSessionRuntime;
137
139
  readonly actorHandoffRuntime?: ZLinkSpotActorHandoffRuntime;
@@ -25,6 +25,7 @@ type AdmissionOptions = Pick<
25
25
  | 'actorHandoffRuntime'
26
26
  | 'admission'
27
27
  | 'actorResolver'
28
+ | 'actorIngressMembership'
28
29
  | 'actorTransferRuntime'
29
30
  | 'boundSessionRuntime'
30
31
  | 'detachedTaskRunner'
@@ -225,7 +226,8 @@ export class ZLinkSpotActorAdmissionCoordinator {
225
226
  this.options.actorResolver === undefined
226
227
  ? activation.resolveJoinedActor(targetActorId)
227
228
  : this.options.actorResolver(targetActorId),
228
- hasDepartedActor: (targetActorId) => activation.hasDepartedActor(targetActorId),
229
+ membershipSpotId: activation.spotId,
230
+ membership: this.options.actorIngressMembership,
229
231
  onDisconnectActor: (actor) =>
230
232
  activation.serial.execute(() => activation.spot.onDisconnectActor?.(actor)),
231
233
  actorResponseSender: this.options.boundSessionRuntime?.sendActorResponse.bind(
@@ -1,5 +1,6 @@
1
1
  import { actorMessageFollowContext } from '../actors/actor-message-follow-context';
2
- import type { ActorRef, ZLinkActor } from '../../contracts';
2
+ import type { ActorRef, RoutingId, ZLinkActor } from '../../contracts';
3
+ import { routingIdsEqual } from '../routing-id';
3
4
  import type { Message } from '../../contracts/Common/Message';
4
5
  import type {
5
6
  ZLinkActorPacketDelivery,
@@ -30,9 +31,18 @@ export type ZLinkRouteToActorJoinPrewarm = (
30
31
  }
31
32
  ) => 'parked' | 'not-found';
32
33
 
33
- export type ZLinkActorIngressRoute = (
34
- delivery: ZLinkActorPacketDelivery
35
- ) => { readonly handled: boolean; readonly response?: unknown } | undefined;
34
+ export interface ZLinkActorDispatchOwner {
35
+ readonly actorRef?: ActorRef;
36
+ readonly spotId?: RoutingId;
37
+ }
38
+
39
+ export interface ZLinkActorIngressMembership {
40
+ readonly resolveOwner?: (actorId: string) => ZLinkActorDispatchOwner;
41
+ readonly dispatchToOwner?: (
42
+ spotId: RoutingId | undefined,
43
+ delivery: ZLinkActorPacketDelivery
44
+ ) => Promise<unknown>;
45
+ }
36
46
 
37
47
  export type ZLinkActorIngressAdmission =
38
48
  | { readonly kind: 'local'; readonly actor: ZLinkActor }
@@ -45,9 +55,9 @@ export function resolveActorIngress(
45
55
  request: boolean,
46
56
  options: {
47
57
  readonly handoffRuntime?: ZLinkSpotActorHandoffRuntime;
48
- readonly routeBeforeLocal?: ZLinkActorIngressRoute;
58
+ readonly spotId?: RoutingId;
59
+ readonly membership?: ZLinkActorIngressMembership;
49
60
  readonly resolveActor: (actorId: string) => ZLinkActor | undefined;
50
- readonly hasDepartedActor?: (actorId: string) => boolean;
51
61
  readonly routeToActorJoinPrewarm?: ZLinkRouteToActorJoinPrewarm;
52
62
  }
53
63
  ): ZLinkActorIngressAdmission {
@@ -62,12 +72,20 @@ export function resolveActorIngress(
62
72
  delivery.messageFollowOrigin
63
73
  );
64
74
  if (handoff !== undefined) return { kind: 'follow', terminal: handoff };
65
- const route = options.routeBeforeLocal?.(delivery);
66
- if (route?.handled === true) return { kind: 'follow', terminal: Promise.resolve(route.response) };
75
+ const membership = options.membership;
76
+ const owner = membership?.resolveOwner?.(delivery.actorId);
77
+ if (owner?.actorRef !== undefined && !routingIdsEqual(owner.spotId, options.spotId)) {
78
+ if (membership?.dispatchToOwner === undefined) {
79
+ throw new Error('Actor ingress requires the current membership dispatch port.');
80
+ }
81
+ // Transfer the accepted delivery before releasing the source FIFO claim.
82
+ // Its terminal is awaited outside that claim by dispatchActorPacketInMailbox.
83
+ return { kind: 'follow', terminal: membership.dispatchToOwner(owner.spotId, delivery) };
84
+ }
67
85
  const actor =
68
- options.hasDepartedActor?.(delivery.actorId) === true
69
- ? undefined
70
- : options.resolveActor(delivery.actorId);
86
+ membership?.resolveOwner === undefined || owner?.actorRef !== undefined
87
+ ? options.resolveActor(delivery.actorId)
88
+ : undefined;
71
89
  if (actor !== undefined) return { kind: 'local', actor };
72
90
  if (
73
91
  options.routeToActorJoinPrewarm !== undefined &&
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resolveActorIngress,
3
3
  type ZLinkRouteToActorJoinPrewarm,
4
- type ZLinkActorIngressRoute
4
+ type ZLinkActorIngressMembership
5
5
  } from './spot-actor-ingress-admission';
6
6
  import type { ZLinkSpotActorHandoffRuntime } from './spot-runtime-ports';
7
7
  import { DefaultZLinkActorContext } from '../actors/actor-context';
@@ -97,8 +97,8 @@ interface ZLinkSpotActorPacketDispatchOptions {
97
97
  readonly spotId: () => string;
98
98
  readonly registry: ZLinkSpotActorHandlerRegistryRuntime;
99
99
  readonly resolveActor: (actorId: string) => ZLinkActor | undefined;
100
- readonly hasDepartedActor?: (actorId: string) => boolean;
101
- readonly routeBeforeLocal?: ZLinkActorIngressRoute;
100
+ readonly membershipSpotId?: RoutingId;
101
+ readonly membership?: ZLinkActorIngressMembership;
102
102
  readonly onDisconnectActor: (actor: ZLinkActor) => Promise<void>;
103
103
  readonly actorResponseSender?: (
104
104
  actor: ZLinkActor,
@@ -226,9 +226,9 @@ export class ZLinkSpotActorPacketDispatch {
226
226
  messageKind === ZLinkDispatchMessageKind.ActorRequest,
227
227
  {
228
228
  handoffRuntime,
229
- routeBeforeLocal: this.options.routeBeforeLocal,
229
+ spotId: this.options.membershipSpotId,
230
+ membership: this.options.membership,
230
231
  resolveActor: this.options.resolveActor,
231
- hasDepartedActor: this.options.hasDepartedActor,
232
232
  routeToActorJoinPrewarm: this.options.routeToActorJoinPrewarm
233
233
  }
234
234
  );
@@ -196,17 +196,7 @@ export class ZLinkEntrySpotActivation {
196
196
  spotId: () => String(this.options.nativeSpot.routingId),
197
197
  registry: this.actorHandlers,
198
198
  resolveActor: (targetActorId) => this.options.entryActorRuntime?.resolveActor(targetActorId),
199
- routeBeforeLocal: (delivery) =>
200
- this.options.entryActorRuntime?.routePacket(
201
- delivery.actorId,
202
- delivery.parts,
203
- delivery.returnResponse,
204
- delivery.remoteBoundSessionTarget,
205
- delivery.fallbackActorRef,
206
- delivery.origin,
207
- delivery.requestTerminal,
208
- delivery.messageFollowOrigin
209
- ),
199
+ membership: this.options.entryActorRuntime?.actorIngressMembership,
210
200
  onDisconnectActor: (actor) => this.notifyDisconnectActor(actor),
211
201
  actorResponseSender: this.options.boundSessionRuntime?.sendActorResponse.bind(
212
202
  this.options.boundSessionRuntime