@zlink-systems/framework 0.30.2 → 0.30.3

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.
@@ -192,6 +192,11 @@ function isCurrentZLinkSpotSerialTurn(executor) {
192
192
  current.executor === executor &&
193
193
  executor.isActiveTurn(current.turn, current.turnId));
194
194
  }
195
+ function createExecutionBarrierAdmissionException(seal, message) {
196
+ return (0, framework_errors_internal_1.createInternalFrameworkException)(seal.kind === 'close'
197
+ ? framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestTargetNotFound
198
+ : framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.SpotMoving, message);
199
+ }
195
200
  /**
196
201
  * Seals application-lane admission and waits for every active Promise turn to
197
202
  * finish. A seal generation prevents a stale abort from reopening a newer
@@ -215,14 +220,9 @@ class ZLinkExecutionBarrier {
215
220
  }
216
221
  enter() {
217
222
  if (this.committedSeal !== undefined) {
218
- throw (0, framework_errors_internal_1.createInternalFrameworkException)(this.committedSeal.kind === 'close'
219
- ? framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected
220
- : framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.SpotMoving, 'ZLink execution barrier is committed.');
223
+ throw createExecutionBarrierAdmissionException(this.committedSeal, 'ZLink execution barrier is committed.');
221
224
  }
222
225
  if (this.currentSeal !== undefined) {
223
- if (this.currentSeal.kind === 'close') {
224
- throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected, 'ZLink execution barrier is sealed for Close.');
225
- }
226
226
  return new Promise((resolve, reject) => {
227
227
  this.admissionWaiters.push({ resolve, reject });
228
228
  });
@@ -302,9 +302,7 @@ class ZLinkExecutionBarrier {
302
302
  return false;
303
303
  this.currentSeal = undefined;
304
304
  this.committedSeal = seal;
305
- const error = (0, framework_errors_internal_1.createInternalFrameworkException)(seal.kind === 'close'
306
- ? framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected
307
- : framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.SpotMoving, 'ZLink execution barrier is committed.');
305
+ const error = createExecutionBarrierAdmissionException(seal, 'ZLink execution barrier is committed.');
308
306
  for (const waiter of this.admissionWaiters)
309
307
  waiter.reject(error);
310
308
  this.admissionWaiters.length = 0;
@@ -18,6 +18,10 @@ function startOutsideStateLane(work) {
18
18
  }
19
19
  const currentLaneStorage = new node_async_hooks_1.AsyncLocalStorage();
20
20
  const diagnosticPending = isStructuralGuardEnabled() ? new Map() : undefined;
21
+ const diagnosticLaneIds = isStructuralGuardEnabled()
22
+ ? new WeakMap()
23
+ : undefined;
24
+ let nextDiagnosticLane = 0;
21
25
  let nextDiagnosticWork = 0;
22
26
  const diagnosticTurnStorage = diagnosticPending === undefined ? undefined : new node_async_hooks_1.AsyncLocalStorage();
23
27
  /**
@@ -132,15 +136,24 @@ if (isStructuralGuardEnabled()) {
132
136
  });
133
137
  const run = ZLinkStateLane.prototype.run;
134
138
  ZLinkStateLane.prototype.run = function (work) {
135
- const completion = run.call(this, () => runDiagnosticTurn(this, work));
136
- return guardStateLaneCompletion(trackDiagnosticCompletion(completion, `state lane ${diagnosticWorkName(work)}`), 'another state lane completion', this);
139
+ const release = registerDiagnosticWork(`${diagnosticLaneName(this)} ${diagnosticWorkName(work)}`);
140
+ let completion;
141
+ try {
142
+ completion = run.call(this, () => runDiagnosticTurn(this, work, release.description));
143
+ }
144
+ catch (error) {
145
+ release();
146
+ throw error;
147
+ }
148
+ void completion.then(release, release);
149
+ return guardStateLaneCompletion(completion, `another state lane completion [${release.description}]`, this);
137
150
  };
138
151
  const tryPost = ZLinkStateLane.prototype.tryPost;
139
152
  ZLinkStateLane.prototype.tryPost = function (work) {
140
- const release = registerDiagnosticWork(`state lane post ${diagnosticWorkName(work)}`);
153
+ const release = registerDiagnosticWork(`${diagnosticLaneName(this)} post ${diagnosticWorkName(work)}`);
141
154
  const accepted = tryPost.call(this, () => {
142
155
  try {
143
- const result = runDiagnosticTurn(this, work);
156
+ const result = runDiagnosticTurn(this, work, release.description);
144
157
  if (result instanceof Promise)
145
158
  return result.finally(release);
146
159
  release();
@@ -156,8 +169,8 @@ if (isStructuralGuardEnabled()) {
156
169
  return accepted;
157
170
  };
158
171
  }
159
- function runDiagnosticTurn(lane, work) {
160
- const turn = { lane, active: true };
172
+ function runDiagnosticTurn(lane, work, description) {
173
+ const turn = { lane, description, active: true };
161
174
  return diagnosticTurnStorage.run(turn, () => {
162
175
  try {
163
176
  const result = work();
@@ -183,7 +196,8 @@ function guardStateLaneCompletion(completion, label, owner) {
183
196
  return (onFulfilled, onRejected) => {
184
197
  const current = ZLinkStateLane.current;
185
198
  if (current !== undefined && current !== owner) {
186
- throw new Error(`A state lane turn cannot wait for ${label}.`);
199
+ throw new Error(`A state lane turn cannot wait for ${label}. ` +
200
+ `current turn: ${diagnosticTurnStorage?.getStore()?.description ?? UNKNOWN_DIAGNOSTIC_CALLER}`);
187
201
  }
188
202
  return target.then(onFulfilled, onRejected);
189
203
  };
@@ -210,16 +224,25 @@ function debugPendingWorkNames() {
210
224
  function diagnosticWorkName(work) {
211
225
  return work.name.length > 0 ? work.name : '<anonymous>';
212
226
  }
227
+ function diagnosticLaneName(lane) {
228
+ let id = diagnosticLaneIds.get(lane);
229
+ if (id === undefined) {
230
+ id = ++nextDiagnosticLane;
231
+ diagnosticLaneIds.set(lane, id);
232
+ }
233
+ return `state lane #${id}`;
234
+ }
213
235
  function registerDiagnosticWork(name) {
214
236
  if (diagnosticPending === undefined)
215
- return () => undefined;
237
+ return Object.assign(() => undefined, { description: name });
216
238
  const id = ++nextDiagnosticWork;
217
239
  const frames = new Error().stack?.split('\n').slice(2) ?? [];
218
240
  const caller = frames
219
241
  .find((frame) => !frame.includes('state-lane.') && !frame.includes('serial-execution-queue.'))
220
242
  ?.trim() ?? UNKNOWN_DIAGNOSTIC_CALLER;
221
- diagnosticPending.set(id, `${name} ${caller}`);
222
- return () => {
243
+ const description = `${name} ${caller}`;
244
+ diagnosticPending.set(id, description);
245
+ return Object.assign(() => {
223
246
  diagnosticPending.delete(id);
224
- };
247
+ }, { description });
225
248
  }
@@ -26,6 +26,7 @@ export declare class ZLinkAutoConnectLoop {
26
26
  private lastStamp?;
27
27
  private lastLiveOwnerSetVersion?;
28
28
  private lastTickFailed;
29
+ private inFlightTick;
29
30
  constructor(options: ZLinkAutoConnectLoopOptions);
30
31
  start(signal?: AbortSignal): Promise<void>;
31
32
  requestPeerReconcile(meshName: string, _peerRid: string): Promise<void>;
@@ -33,6 +34,7 @@ export declare class ZLinkAutoConnectLoop {
33
34
  prepareTransportShutdown(): Promise<void>;
34
35
  finishTransportShutdown(signal?: AbortSignal): Promise<void>;
35
36
  tick(signal?: AbortSignal): Promise<void>;
37
+ private enqueueTick;
36
38
  private tickCore;
37
39
  private runReconcile;
38
40
  private scheduleNext;
@@ -17,6 +17,8 @@ class ZLinkAutoConnectLoop {
17
17
  lastStamp;
18
18
  lastLiveOwnerSetVersion;
19
19
  lastTickFailed = false;
20
+ // This settled tail orders whole operations. The tick fields below are touched only inside it.
21
+ inFlightTick = Promise.resolve();
20
22
  constructor(options) {
21
23
  this.reconciler = options.reconciler;
22
24
  this.options = { ...Options_1.zlinkRuntimeDefaultLocationOptions, ...options.options };
@@ -45,7 +47,7 @@ class ZLinkAutoConnectLoop {
45
47
  if (meshName !== this.meshName || controller === undefined)
46
48
  return;
47
49
  // A fresh descriptor scan uses the same RID ordering and connection owner as a periodic tick.
48
- await this.lane.run(async () => {
50
+ await this.enqueueTick(async () => {
49
51
  if (!controller.signal.aborted)
50
52
  await this.runReconcile(controller.signal);
51
53
  });
@@ -64,13 +66,24 @@ class ZLinkAutoConnectLoop {
64
66
  this.clearTimer(this.timer);
65
67
  this.timer = undefined;
66
68
  }
69
+ await this.inFlightTick;
67
70
  this.reconciler.disconnectPeers();
68
71
  }
69
72
  async finishTransportShutdown(signal) {
70
73
  await this.reconciler.unpublishLocal(signal);
71
74
  }
72
75
  async tick(signal) {
73
- await this.lane.run(() => this.tickCore(signal));
76
+ const controller = this.controller;
77
+ await this.enqueueTick(async () => {
78
+ if (controller?.signal.aborted !== true)
79
+ await this.tickCore(signal ?? controller?.signal);
80
+ });
81
+ }
82
+ enqueueTick(operation) {
83
+ const completion = this.inFlightTick.then(operation);
84
+ // The caller receives the failure; the successor still owns its place in the order.
85
+ this.inFlightTick = completion.then(() => undefined, () => undefined);
86
+ return completion;
74
87
  }
75
88
  async tickCore(signal) {
76
89
  const lastTickFailed = this.lastTickFailed;
@@ -874,9 +874,6 @@ class DefaultZLinkSpotManager {
874
874
  async admitActorJoin(spotId, actor, request, commit, signal, leaveSource, contentType = 'application/json') {
875
875
  const meshName = this.activations.resolveUnique(spotId)?.meshName;
876
876
  this.options.admission?.requireRequest('Actor join admission', meshName);
877
- if (meshName !== undefined && this.isSpotClosing(meshName, spotId)) {
878
- throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected, `Spot '${String(spotId)}' is closing.`);
879
- }
880
877
  return await this.actorMembership.admitActorJoin(spotId, actor, request, commit, signal, leaveSource, contentType);
881
878
  }
882
879
  async leaveActor(spotId, actor, signal) {
@@ -146,11 +146,14 @@ class ZLinkSpotSerialTurnExecutor {
146
146
  }
147
147
  /** Admits membership work and holds the lifecycle FIFO until its result settles. */
148
148
  executeLifecycleOperation(operation) {
149
- let entry;
150
- try {
149
+ const requireAdmission = () => {
151
150
  if (this.lifecycleAdmissionClosed) {
152
- throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected, 'Spot lifecycle admission is closed by a committed Close.');
151
+ throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestTargetNotFound, 'Spot incarnation is closing (committed Close).');
153
152
  }
153
+ };
154
+ let entry;
155
+ try {
156
+ requireAdmission();
154
157
  entry = this.executionBarrier?.enter();
155
158
  }
156
159
  catch (error) {
@@ -158,7 +161,10 @@ class ZLinkSpotSerialTurnExecutor {
158
161
  }
159
162
  const submit = (claim) => {
160
163
  try {
161
- return this.scheduler.submitLifecycleOperation(operation);
164
+ return this.scheduler.submitLifecycleOperation(() => {
165
+ requireAdmission();
166
+ return operation();
167
+ });
162
168
  }
163
169
  catch (error) {
164
170
  return Promise.reject(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zlink-systems/framework",
3
- "version": "0.30.2",
3
+ "version": "0.30.3",
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.30.2",
22
+ "@zlink-systems/stream-wire": "0.30.3",
23
23
  "@zlink-systems/zlink": "1.20.0"
24
24
  }
25
25
  }
@@ -270,6 +270,18 @@ export interface ZLinkExecutionBarrierSeal {
270
270
  readonly kind: 'close' | 'relocation' | 'activation';
271
271
  }
272
272
 
273
+ function createExecutionBarrierAdmissionException(
274
+ seal: ZLinkExecutionBarrierSeal,
275
+ message: string
276
+ ) {
277
+ return createInternalFrameworkException(
278
+ seal.kind === 'close'
279
+ ? ZLinkFrameworkInternalErrorKind.RequestTargetNotFound
280
+ : ZLinkFrameworkInternalErrorKind.SpotMoving,
281
+ message
282
+ );
283
+ }
284
+
273
285
  export interface ZLinkExecutionBarrierClaim {
274
286
  release(): void;
275
287
  /** The turn yielded; it no longer runs until resume() admits it again. */
@@ -308,20 +320,12 @@ export class ZLinkExecutionBarrier {
308
320
 
309
321
  enter(): ZLinkExecutionBarrierClaim | Promise<ZLinkExecutionBarrierClaim> {
310
322
  if (this.committedSeal !== undefined) {
311
- throw createInternalFrameworkException(
312
- this.committedSeal.kind === 'close'
313
- ? ZLinkFrameworkInternalErrorKind.RequestRejected
314
- : ZLinkFrameworkInternalErrorKind.SpotMoving,
323
+ throw createExecutionBarrierAdmissionException(
324
+ this.committedSeal,
315
325
  'ZLink execution barrier is committed.'
316
326
  );
317
327
  }
318
328
  if (this.currentSeal !== undefined) {
319
- if (this.currentSeal.kind === 'close') {
320
- throw createInternalFrameworkException(
321
- ZLinkFrameworkInternalErrorKind.RequestRejected,
322
- 'ZLink execution barrier is sealed for Close.'
323
- );
324
- }
325
329
  return new Promise<ZLinkExecutionBarrierClaim>((resolve, reject) => {
326
330
  this.admissionWaiters.push({ resolve, reject });
327
331
  });
@@ -400,10 +404,8 @@ export class ZLinkExecutionBarrier {
400
404
  if (!this.isCurrent(seal)) return false;
401
405
  this.currentSeal = undefined;
402
406
  this.committedSeal = seal;
403
- const error = createInternalFrameworkException(
404
- seal.kind === 'close'
405
- ? ZLinkFrameworkInternalErrorKind.RequestRejected
406
- : ZLinkFrameworkInternalErrorKind.SpotMoving,
407
+ const error = createExecutionBarrierAdmissionException(
408
+ seal,
407
409
  'ZLink execution barrier is committed.'
408
410
  );
409
411
  for (const waiter of this.admissionWaiters) waiter.reject(error);
@@ -5,6 +5,7 @@ type ZLinkStateLaneWork<T> = () => Promise<T> | T;
5
5
 
6
6
  interface ZLinkStateLaneTurn {
7
7
  readonly lane: ZLinkStateLane;
8
+ readonly description: string;
8
9
  active: boolean;
9
10
  }
10
11
 
@@ -21,6 +22,10 @@ export function startOutsideStateLane<T>(work: () => T): T {
21
22
 
22
23
  const currentLaneStorage = new AsyncLocalStorage<ZLinkStateLane>();
23
24
  const diagnosticPending = isStructuralGuardEnabled() ? new Map<number, string>() : undefined;
25
+ const diagnosticLaneIds = isStructuralGuardEnabled()
26
+ ? new WeakMap<ZLinkStateLane, number>()
27
+ : undefined;
28
+ let nextDiagnosticLane = 0;
24
29
  let nextDiagnosticWork = 0;
25
30
  const diagnosticTurnStorage =
26
31
  diagnosticPending === undefined ? undefined : new AsyncLocalStorage<ZLinkStateLaneTurn>();
@@ -152,10 +157,22 @@ if (isStructuralGuardEnabled()) {
152
157
  this: ZLinkStateLane,
153
158
  work: ZLinkStateLaneWork<T>
154
159
  ): Promise<T> {
155
- const completion = run.call(this, () => runDiagnosticTurn(this, work)) as Promise<T>;
160
+ const release = registerDiagnosticWork(
161
+ `${diagnosticLaneName(this)} ${diagnosticWorkName(work)}`
162
+ );
163
+ let completion: Promise<T>;
164
+ try {
165
+ completion = run.call(this, () =>
166
+ runDiagnosticTurn(this, work, release.description)
167
+ ) as Promise<T>;
168
+ } catch (error) {
169
+ release();
170
+ throw error;
171
+ }
172
+ void completion.then(release, release);
156
173
  return guardStateLaneCompletion(
157
- trackDiagnosticCompletion(completion, `state lane ${diagnosticWorkName(work)}`),
158
- 'another state lane completion',
174
+ completion,
175
+ `another state lane completion [${release.description}]`,
159
176
  this
160
177
  );
161
178
  };
@@ -165,10 +182,12 @@ if (isStructuralGuardEnabled()) {
165
182
  this: ZLinkStateLane,
166
183
  work: ZLinkStateLaneWork<void>
167
184
  ): boolean {
168
- const release = registerDiagnosticWork(`state lane post ${diagnosticWorkName(work)}`);
185
+ const release = registerDiagnosticWork(
186
+ `${diagnosticLaneName(this)} post ${diagnosticWorkName(work)}`
187
+ );
169
188
  const accepted = tryPost.call(this, () => {
170
189
  try {
171
- const result = runDiagnosticTurn(this, work);
190
+ const result = runDiagnosticTurn(this, work, release.description);
172
191
  if (result instanceof Promise) return result.finally(release);
173
192
  release();
174
193
  return result;
@@ -182,8 +201,12 @@ if (isStructuralGuardEnabled()) {
182
201
  };
183
202
  }
184
203
 
185
- function runDiagnosticTurn<T>(lane: ZLinkStateLane, work: ZLinkStateLaneWork<T>): Promise<T> | T {
186
- const turn: ZLinkStateLaneTurn = { lane, active: true };
204
+ function runDiagnosticTurn<T>(
205
+ lane: ZLinkStateLane,
206
+ work: ZLinkStateLaneWork<T>,
207
+ description: string
208
+ ): Promise<T> | T {
209
+ const turn: ZLinkStateLaneTurn = { lane, description, active: true };
187
210
  return diagnosticTurnStorage!.run(turn, () => {
188
211
  try {
189
212
  const result = work();
@@ -213,7 +236,10 @@ export function guardStateLaneCompletion<T>(
213
236
  return (onFulfilled: (value: T) => unknown, onRejected: (error: unknown) => unknown) => {
214
237
  const current = ZLinkStateLane.current;
215
238
  if (current !== undefined && current !== owner) {
216
- throw new Error(`A state lane turn cannot wait for ${label}.`);
239
+ throw new Error(
240
+ `A state lane turn cannot wait for ${label}. ` +
241
+ `current turn: ${diagnosticTurnStorage?.getStore()?.description ?? UNKNOWN_DIAGNOSTIC_CALLER}`
242
+ );
217
243
  }
218
244
  return target.then(onFulfilled, onRejected);
219
245
  };
@@ -246,16 +272,29 @@ export function diagnosticWorkName(work: { readonly name: string }): string {
246
272
  return work.name.length > 0 ? work.name : '<anonymous>';
247
273
  }
248
274
 
249
- function registerDiagnosticWork(name: string): () => void {
250
- if (diagnosticPending === undefined) return () => undefined;
275
+ function diagnosticLaneName(lane: ZLinkStateLane): string {
276
+ let id = diagnosticLaneIds!.get(lane);
277
+ if (id === undefined) {
278
+ id = ++nextDiagnosticLane;
279
+ diagnosticLaneIds!.set(lane, id);
280
+ }
281
+ return `state lane #${id}`;
282
+ }
283
+
284
+ function registerDiagnosticWork(name: string): (() => void) & { readonly description: string } {
285
+ if (diagnosticPending === undefined) return Object.assign(() => undefined, { description: name });
251
286
  const id = ++nextDiagnosticWork;
252
287
  const frames = new Error().stack?.split('\n').slice(2) ?? [];
253
288
  const caller =
254
289
  frames
255
290
  .find((frame) => !frame.includes('state-lane.') && !frame.includes('serial-execution-queue.'))
256
291
  ?.trim() ?? UNKNOWN_DIAGNOSTIC_CALLER;
257
- diagnosticPending.set(id, `${name} ${caller}`);
258
- return () => {
259
- diagnosticPending.delete(id);
260
- };
292
+ const description = `${name} ${caller}`;
293
+ diagnosticPending.set(id, description);
294
+ return Object.assign(
295
+ () => {
296
+ diagnosticPending.delete(id);
297
+ },
298
+ { description }
299
+ );
261
300
  }
@@ -32,6 +32,8 @@ export class ZLinkAutoConnectLoop {
32
32
  private lastStamp?: bigint;
33
33
  private lastLiveOwnerSetVersion?: number;
34
34
  private lastTickFailed = false;
35
+ // This settled tail orders whole operations. The tick fields below are touched only inside it.
36
+ private inFlightTick: Promise<void> = Promise.resolve();
35
37
 
36
38
  constructor(options: ZLinkAutoConnectLoopOptions) {
37
39
  this.reconciler = options.reconciler;
@@ -59,7 +61,7 @@ export class ZLinkAutoConnectLoop {
59
61
  const controller = this.controller;
60
62
  if (meshName !== this.meshName || controller === undefined) return;
61
63
  // A fresh descriptor scan uses the same RID ordering and connection owner as a periodic tick.
62
- await this.lane.run(async () => {
64
+ await this.enqueueTick(async () => {
63
65
  if (!controller.signal.aborted) await this.runReconcile(controller.signal);
64
66
  });
65
67
  }
@@ -79,6 +81,7 @@ export class ZLinkAutoConnectLoop {
79
81
  this.clearTimer(this.timer);
80
82
  this.timer = undefined;
81
83
  }
84
+ await this.inFlightTick;
82
85
  this.reconciler.disconnectPeers();
83
86
  }
84
87
 
@@ -87,7 +90,20 @@ export class ZLinkAutoConnectLoop {
87
90
  }
88
91
 
89
92
  async tick(signal?: AbortSignal): Promise<void> {
90
- await this.lane.run(() => this.tickCore(signal));
93
+ const controller = this.controller;
94
+ await this.enqueueTick(async () => {
95
+ if (controller?.signal.aborted !== true) await this.tickCore(signal ?? controller?.signal);
96
+ });
97
+ }
98
+
99
+ private enqueueTick(operation: () => Promise<void>): Promise<void> {
100
+ const completion = this.inFlightTick.then(operation);
101
+ // The caller receives the failure; the successor still owns its place in the order.
102
+ this.inFlightTick = completion.then(
103
+ () => undefined,
104
+ () => undefined
105
+ );
106
+ return completion;
91
107
  }
92
108
 
93
109
  private async tickCore(signal?: AbortSignal): Promise<void> {
@@ -1521,12 +1521,6 @@ export class DefaultZLinkSpotManager {
1521
1521
  ): Promise<ZLinkSpotActorJoinResult> {
1522
1522
  const meshName = this.activations.resolveUnique(spotId)?.meshName;
1523
1523
  this.options.admission?.requireRequest('Actor join admission', meshName);
1524
- if (meshName !== undefined && this.isSpotClosing(meshName, spotId)) {
1525
- throw createInternalFrameworkException(
1526
- ZLinkFrameworkInternalErrorKind.RequestRejected,
1527
- `Spot '${String(spotId)}' is closing.`
1528
- );
1529
- }
1530
1524
  return await this.actorMembership.admitActorJoin(
1531
1525
  spotId,
1532
1526
  actor,
@@ -217,21 +217,27 @@ export class ZLinkSpotSerialTurnExecutor {
217
217
 
218
218
  /** Admits membership work and holds the lifecycle FIFO until its result settles. */
219
219
  executeLifecycleOperation<T>(operation: () => Promise<T> | T): Promise<T> {
220
- let entry: ZLinkExecutionBarrierClaim | Promise<ZLinkExecutionBarrierClaim> | undefined;
221
- try {
220
+ const requireAdmission = (): void => {
222
221
  if (this.lifecycleAdmissionClosed) {
223
222
  throw createInternalFrameworkException(
224
- ZLinkFrameworkInternalErrorKind.RequestRejected,
225
- 'Spot lifecycle admission is closed by a committed Close.'
223
+ ZLinkFrameworkInternalErrorKind.RequestTargetNotFound,
224
+ 'Spot incarnation is closing (committed Close).'
226
225
  );
227
226
  }
227
+ };
228
+ let entry: ZLinkExecutionBarrierClaim | Promise<ZLinkExecutionBarrierClaim> | undefined;
229
+ try {
230
+ requireAdmission();
228
231
  entry = this.executionBarrier?.enter();
229
232
  } catch (error) {
230
233
  return Promise.reject(error);
231
234
  }
232
235
  const submit = (claim?: ZLinkExecutionBarrierClaim): Promise<T> => {
233
236
  try {
234
- return this.scheduler.submitLifecycleOperation(operation);
237
+ return this.scheduler.submitLifecycleOperation(() => {
238
+ requireAdmission();
239
+ return operation();
240
+ });
235
241
  } catch (error) {
236
242
  return Promise.reject(error);
237
243
  } finally {