@zlink-systems/framework 0.22.0 → 0.24.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 (104) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/contracts/Dispatch/ZLinkDispatchOptions.d.ts +1 -0
  3. package/dist/contracts/Streams/IZLinkSession.d.ts +2 -1
  4. package/dist/runtime/actors/actor-context.js +4 -4
  5. package/dist/runtime/actors/actor-join-deferred-scope.d.ts +0 -1
  6. package/dist/runtime/actors/actor-join-deferred-scope.js +0 -18
  7. package/dist/runtime/actors/deferred-join-accepted-journal.js +2 -6
  8. package/dist/runtime/backend/contracts/index.d.ts +2 -0
  9. package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +4 -0
  10. package/dist/runtime/backend/node/node-raw-mesh-backend.js +24 -8
  11. package/dist/runtime/channels/channel-dispatch-pipeline.js +2 -2
  12. package/dist/runtime/channels/channel-transports.js +3 -9
  13. package/dist/runtime/channels/dispatch-error-reporter.d.ts +1 -0
  14. package/dist/runtime/channels/dispatch-error-reporter.js +6 -38
  15. package/dist/runtime/diagnostics/dispatch-error-details.d.ts +6 -0
  16. package/dist/runtime/diagnostics/dispatch-error-details.js +55 -0
  17. package/dist/runtime/diagnostics/flow-context.d.ts +2 -1
  18. package/dist/runtime/diagnostics/flow-context.js +2 -2
  19. package/dist/runtime/diagnostics/message-flow.js +11 -3
  20. package/dist/runtime/foundation/actor-join-recovery-codec.js +0 -4
  21. package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +4 -2
  22. package/dist/runtime/foundation/raw-service-mesh-runtime.js +34 -9
  23. package/dist/runtime/foundation/service-session-binding-ingress-port.d.ts +1 -0
  24. package/dist/runtime/foundation/service-stateful-runtime.d.ts +5 -0
  25. package/dist/runtime/foundation/service-stateful-runtime.js +52 -2
  26. package/dist/runtime/foundation/service-topology-registry.d.ts +1 -0
  27. package/dist/runtime/foundation/service-topology-registry.js +12 -4
  28. package/dist/runtime/host/index.js +7 -3
  29. package/dist/runtime/locations/in-memory-location-store.js +5 -0
  30. package/dist/runtime/locations/location-store-repository.js +3 -1
  31. package/dist/runtime/protocol/service_wire_pilot_codec.generated.js +20 -18
  32. package/dist/runtime/spots/index.js +9 -3
  33. package/dist/runtime/spots/spot-actor-packet-dispatch.d.ts +2 -1
  34. package/dist/runtime/spots/spot-actor-packet-dispatch.js +6 -1
  35. package/dist/runtime/spots/spot-actor-packet-drain.js +2 -0
  36. package/dist/runtime/spots/spot-node-runtime-manager.js +5 -1
  37. package/dist/runtime/spots/spot-route-packet-dispatch.js +1 -1
  38. package/dist/runtime/spots/spot-routed-bound-session-dispatch.js +1 -1
  39. package/dist/runtime/spots/spot-subscription-dispatch.js +1 -1
  40. package/dist/runtime/spots/spot-timer.js +1 -1
  41. package/dist/runtime/streams/actor-session-binding-registry.d.ts +21 -8
  42. package/dist/runtime/streams/actor-session-binding-registry.js +71 -42
  43. package/dist/runtime/streams/actor-session-binding-runtime-owner.d.ts +1 -0
  44. package/dist/runtime/streams/bound-actor-relay-sender.js +2 -1
  45. package/dist/runtime/streams/bound-session-response-target.js +8 -1
  46. package/dist/runtime/streams/bound-session-service.d.ts +1 -0
  47. package/dist/runtime/streams/bound-session-service.js +6 -3
  48. package/dist/runtime/streams/index.js +8 -1
  49. package/dist/runtime/streams/managed-stream.d.ts +3 -0
  50. package/dist/runtime/streams/managed-stream.js +20 -1
  51. package/dist/runtime/streams/protocol.d.ts +5 -1
  52. package/dist/runtime/streams/protocol.js +41 -4
  53. package/dist/runtime/streams/session-actor-coordinator.js +3 -4
  54. package/dist/runtime/streams/session-context.d.ts +9 -2
  55. package/dist/runtime/streams/session-context.js +20 -4
  56. package/dist/runtime/streams/session-local-actors.d.ts +3 -1
  57. package/dist/runtime/streams/session-local-actors.js +17 -2
  58. package/dist/runtime/streams/stream-frame-factory.d.ts +1 -1
  59. package/dist/runtime/streams/stream-frame-factory.js +3 -2
  60. package/dist/runtime/streams/stream-session-runtime.js +42 -12
  61. package/package.json +3 -3
  62. package/src/contracts/Dispatch/ZLinkDispatchOptions.ts +1 -0
  63. package/src/contracts/Streams/IZLinkSession.ts +2 -1
  64. package/src/runtime/actors/actor-context.ts +4 -4
  65. package/src/runtime/actors/actor-join-deferred-scope.ts +0 -32
  66. package/src/runtime/actors/deferred-join-accepted-journal.ts +2 -6
  67. package/src/runtime/backend/contracts/index.ts +5 -0
  68. package/src/runtime/backend/node/node-raw-mesh-backend.ts +43 -11
  69. package/src/runtime/channels/channel-dispatch-pipeline.ts +4 -2
  70. package/src/runtime/channels/channel-transports.ts +3 -9
  71. package/src/runtime/channels/dispatch-error-reporter.ts +7 -45
  72. package/src/runtime/diagnostics/dispatch-error-details.ts +62 -0
  73. package/src/runtime/diagnostics/flow-context.ts +6 -4
  74. package/src/runtime/diagnostics/message-flow.ts +12 -3
  75. package/src/runtime/foundation/actor-join-recovery-codec.ts +0 -4
  76. package/src/runtime/foundation/raw-service-mesh-runtime.ts +54 -13
  77. package/src/runtime/foundation/service-session-binding-ingress-port.ts +1 -0
  78. package/src/runtime/foundation/service-stateful-runtime.ts +71 -5
  79. package/src/runtime/foundation/service-topology-registry.ts +18 -5
  80. package/src/runtime/host/index.ts +11 -4
  81. package/src/runtime/locations/in-memory-location-store.ts +7 -0
  82. package/src/runtime/locations/location-store-repository.ts +4 -1
  83. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +18 -17
  84. package/src/runtime/spots/index.ts +22 -2
  85. package/src/runtime/spots/spot-actor-packet-dispatch.ts +20 -3
  86. package/src/runtime/spots/spot-actor-packet-drain.ts +2 -0
  87. package/src/runtime/spots/spot-node-runtime-manager.ts +5 -1
  88. package/src/runtime/spots/spot-route-packet-dispatch.ts +2 -1
  89. package/src/runtime/spots/spot-routed-bound-session-dispatch.ts +2 -1
  90. package/src/runtime/spots/spot-subscription-dispatch.ts +2 -1
  91. package/src/runtime/spots/spot-timer.ts +6 -1
  92. package/src/runtime/streams/actor-session-binding-registry.ts +150 -63
  93. package/src/runtime/streams/actor-session-binding-runtime-owner.ts +1 -0
  94. package/src/runtime/streams/bound-actor-relay-sender.ts +10 -2
  95. package/src/runtime/streams/bound-session-response-target.ts +10 -1
  96. package/src/runtime/streams/bound-session-service.ts +13 -3
  97. package/src/runtime/streams/index.ts +8 -1
  98. package/src/runtime/streams/managed-stream.ts +34 -12
  99. package/src/runtime/streams/protocol.ts +44 -5
  100. package/src/runtime/streams/session-actor-coordinator.ts +12 -4
  101. package/src/runtime/streams/session-context.ts +31 -4
  102. package/src/runtime/streams/session-local-actors.ts +19 -3
  103. package/src/runtime/streams/stream-frame-factory.ts +4 -2
  104. package/src/runtime/streams/stream-session-runtime.ts +58 -13
@@ -18,7 +18,11 @@ export interface ZLinkActorSessionBindingActor {
18
18
 
19
19
  export interface ZLinkActorSessionBindingContext<TActor extends ZLinkActorSessionBindingActor> {
20
20
  readonly routingId?: unknown;
21
- bindLocal(actor: TActor, bindingToken: string): void;
21
+ readonly actorSlotControls?: {
22
+ enqueueBound(actorSlot: number, actorId: string): Promise<void>;
23
+ enqueueUnbound(actorSlot: number): Promise<void>;
24
+ };
25
+ bindLocal(actor: TActor, bindingToken: string, actorSlot?: number): void;
22
26
  unbindLocal(actorId: string, bindingToken: string): void;
23
27
  }
24
28
 
@@ -29,6 +33,7 @@ export interface ZLinkActorSessionRoute<
29
33
  readonly context: TContext;
30
34
  readonly actor: TActor;
31
35
  readonly bindingToken: string;
36
+ readonly actorSlot?: number;
32
37
  readonly sessionIdentity?: string;
33
38
  readonly activeFrames: ZLinkActorSessionActiveFrames;
34
39
  sealId?: string;
@@ -137,12 +142,18 @@ export interface ZLinkActorSessionAuthorityFence {
137
142
  readonly actorType?: string;
138
143
  }
139
144
 
145
+ export enum ZLinkActorSessionBindingTermination {
146
+ BindingEnd,
147
+ PhysicalDisconnect
148
+ }
149
+
140
150
  export class ZLinkActorSessionBindingRegistry<
141
151
  TContext extends ZLinkActorSessionBindingContext<TActor>,
142
152
  TActor extends ZLinkActorSessionBindingActor
143
153
  > {
144
154
  private readonly lane = new ZLinkStateLane();
145
155
  private readonly routes = new Map<string, ZLinkActorSessionRoute<TContext, TActor>>();
156
+ private readonly nextActorSlots = new WeakMap<TContext, number>();
146
157
  private readonly sealWaiters = new Map<
147
158
  string,
148
159
  Set<{
@@ -164,7 +175,9 @@ export class ZLinkActorSessionBindingRegistry<
164
175
  // (the sealing side) must observe that bound; an unbounded wait here
165
176
  // turns a lost control or a lifecycle interlock into a silent stall
166
177
  // (spec 48:205 — transport/deadline limits apply during relocation).
167
- private readonly sealWaitTimeoutMs = 3_000
178
+ private readonly sealWaitTimeoutMs = 3_000,
179
+ private readonly errorSink?: () =>
180
+ { reportRuntimeTaskException(taskName: string, error: unknown): void } | undefined
168
181
  ) {
169
182
  if (!Number.isSafeInteger(terminalRelocationCapacity) || terminalRelocationCapacity <= 0) {
170
183
  throw new RangeError('Session relocation terminal capacity must be a positive safe integer.');
@@ -182,83 +195,120 @@ export class ZLinkActorSessionBindingRegistry<
182
195
  actor: TActor,
183
196
  bindingToken: string,
184
197
  authorityFence?: ZLinkActorSessionAuthorityFence,
185
- sessionIdentity?: string
198
+ sessionIdentity?: string,
199
+ commitActor?: () => void
186
200
  ): Promise<void> {
187
201
  await this.lane.run(() =>
188
- this.bindCore(context, actor, bindingToken, authorityFence, sessionIdentity)
202
+ this.commitCore(
203
+ undefined,
204
+ context,
205
+ actor,
206
+ bindingToken,
207
+ authorityFence,
208
+ sessionIdentity,
209
+ commitActor
210
+ )
189
211
  );
190
212
  }
191
213
 
192
- private bindCore(
193
- context: TContext,
194
- actor: TActor,
195
- bindingToken: string,
196
- authorityFence?: ZLinkActorSessionAuthorityFence,
197
- sessionIdentity?: string
198
- ): void {
199
- this.routes.set(actor.actorId, {
200
- context,
201
- actor,
202
- bindingToken,
203
- sessionIdentity: sessionIdentity ?? sessionIdentityFromContext(context),
204
- activeFrames: { count: 0, requests: new Set() },
205
- authorityFence
206
- });
207
- context.bindLocal(actor, bindingToken);
208
- }
209
-
210
- async replace(
211
- previous: ZLinkActorSessionRoute<TContext, TActor>,
214
+ private async commitCore(
215
+ previous: ZLinkActorSessionRoute<TContext, TActor> | undefined,
212
216
  context: TContext,
213
217
  actor: TActor,
214
218
  bindingToken: string,
215
219
  authorityFence?: ZLinkActorSessionAuthorityFence,
216
- sessionIdentity?: string
220
+ sessionIdentity?: string,
221
+ commitActor?: () => void
217
222
  ): Promise<void> {
218
- await this.lane.run(() =>
219
- this.replaceCore(previous, context, actor, bindingToken, authorityFence, sessionIdentity)
220
- );
221
- }
222
-
223
- private replaceCore(
224
- previous: ZLinkActorSessionRoute<TContext, TActor>,
225
- context: TContext,
226
- actor: TActor,
227
- bindingToken: string,
228
- authorityFence?: ZLinkActorSessionAuthorityFence,
229
- sessionIdentity?: string
230
- ): void {
231
223
  const current = this.routes.get(actor.actorId);
232
224
  if (current !== previous) {
233
225
  throw createInternalFrameworkException(
234
226
  ZLinkFrameworkInternalErrorKind.ActorSessionNotBound,
235
- `Actor '${actor.actorId}' session binding changed before route replacement.`,
227
+ `Actor '${actor.actorId}' session binding changed before route commit.`,
236
228
  true
237
229
  );
238
230
  }
239
231
 
240
- context.bindLocal(actor, bindingToken);
241
- const sameLocalBinding = previous.context === context && previous.bindingToken === bindingToken;
242
- if (!sameLocalBinding) {
243
- try {
232
+ const sameBinding =
233
+ previous !== undefined &&
234
+ previous.context === context &&
235
+ previous.bindingToken === bindingToken;
236
+ const actorSlot = sameBinding ? previous.actorSlot : this.issueActorSlot(context);
237
+ commitActor?.();
238
+ context.bindLocal(actor, bindingToken, actorSlot);
239
+
240
+ try {
241
+ if (previous !== undefined && !sameBinding) {
242
+ this.routes.delete(actor.actorId);
244
243
  previous.context.unbindLocal(actor.actorId, previous.bindingToken);
245
- } catch (error) {
246
- context.unbindLocal(actor.actorId, bindingToken);
247
- previous.context.bindLocal(previous.actor, previous.bindingToken);
248
- throw error;
244
+ if (previous.actorSlot !== undefined) {
245
+ let unbound: Promise<void> | undefined;
246
+ try {
247
+ unbound = previous.context.actorSlotControls?.enqueueUnbound(previous.actorSlot);
248
+ } catch (error) {
249
+ unbound = Promise.reject(error);
250
+ }
251
+ void unbound?.catch((error) =>
252
+ this.errorSink?.()?.reportRuntimeTaskException('retired session actor unbound', error)
253
+ );
254
+ }
249
255
  }
256
+
257
+ if (!sameBinding && actorSlot !== undefined) {
258
+ await context.actorSlotControls!.enqueueBound(actorSlot, actor.actorId);
259
+ }
260
+ } catch (error) {
261
+ context.unbindLocal(actor.actorId, bindingToken);
262
+ throw error;
250
263
  }
264
+
251
265
  this.routes.set(actor.actorId, {
252
266
  context,
253
267
  actor,
254
268
  bindingToken,
269
+ actorSlot,
255
270
  sessionIdentity: sessionIdentity ?? sessionIdentityFromContext(context),
256
- activeFrames: previous.activeFrames,
257
- sealId: previous.sealId,
258
- authorityFence: authorityFence ?? previous.authorityFence
271
+ activeFrames: previous?.activeFrames ?? { count: 0, requests: new Set() },
272
+ sealId: previous?.sealId,
273
+ authorityFence: authorityFence ?? previous?.authorityFence
259
274
  });
260
275
  }
261
276
 
277
+ private issueActorSlot(context: TContext): number | undefined {
278
+ if (context.actorSlotControls === undefined) return undefined;
279
+ const actorSlot = this.nextActorSlots.get(context) ?? 1;
280
+ if (actorSlot > 0xffff) {
281
+ throw createInternalFrameworkException(
282
+ ZLinkFrameworkInternalErrorKind.InvalidOperation,
283
+ `Stream session '${String(context.routingId)}' exhausted its Actor slots.`
284
+ );
285
+ }
286
+ this.nextActorSlots.set(context, actorSlot + 1);
287
+ return actorSlot;
288
+ }
289
+
290
+ async replace(
291
+ previous: ZLinkActorSessionRoute<TContext, TActor>,
292
+ context: TContext,
293
+ actor: TActor,
294
+ bindingToken: string,
295
+ authorityFence?: ZLinkActorSessionAuthorityFence,
296
+ sessionIdentity?: string,
297
+ commitActor?: () => void
298
+ ): Promise<void> {
299
+ await this.lane.run(() =>
300
+ this.commitCore(
301
+ previous,
302
+ context,
303
+ actor,
304
+ bindingToken,
305
+ authorityFence,
306
+ sessionIdentity,
307
+ commitActor
308
+ )
309
+ );
310
+ }
311
+
262
312
  async replaceAndReleaseSeal(
263
313
  previous: ZLinkActorSessionRoute<TContext, TActor>,
264
314
  context: TContext,
@@ -266,7 +316,8 @@ export class ZLinkActorSessionBindingRegistry<
266
316
  bindingToken: string,
267
317
  sealId: string,
268
318
  authorityFence?: ZLinkActorSessionAuthorityFence,
269
- sessionIdentity?: string
319
+ sessionIdentity?: string,
320
+ commitActor?: () => void
270
321
  ): Promise<void> {
271
322
  await this.lane.run(() =>
272
323
  this.replaceAndReleaseSealCore(
@@ -276,20 +327,22 @@ export class ZLinkActorSessionBindingRegistry<
276
327
  bindingToken,
277
328
  sealId,
278
329
  authorityFence,
279
- sessionIdentity
330
+ sessionIdentity,
331
+ commitActor
280
332
  )
281
333
  );
282
334
  }
283
335
 
284
- private replaceAndReleaseSealCore(
336
+ private async replaceAndReleaseSealCore(
285
337
  previous: ZLinkActorSessionRoute<TContext, TActor>,
286
338
  context: TContext,
287
339
  actor: TActor,
288
340
  bindingToken: string,
289
341
  sealId: string,
290
342
  authorityFence?: ZLinkActorSessionAuthorityFence,
291
- sessionIdentity?: string
292
- ): void {
343
+ sessionIdentity?: string,
344
+ commitActor?: () => void
345
+ ): Promise<void> {
293
346
  if (previous.sealId !== sealId) {
294
347
  throw createInternalFrameworkException(
295
348
  ZLinkFrameworkInternalErrorKind.ActorLocationStale,
@@ -300,7 +353,15 @@ export class ZLinkActorSessionBindingRegistry<
300
353
  // JavaScript cannot interleave another ingress turn between these two
301
354
  // synchronous mutations. The replacement preserves the seal, then the
302
355
  // exact release publishes the route to held ingress as one owner turn.
303
- this.replaceCore(previous, context, actor, bindingToken, authorityFence, sessionIdentity);
356
+ await this.commitCore(
357
+ previous,
358
+ context,
359
+ actor,
360
+ bindingToken,
361
+ authorityFence,
362
+ sessionIdentity,
363
+ commitActor
364
+ );
304
365
  if (!this.abortSealCore(actor.actorId, sealId)) {
305
366
  throw createInternalFrameworkException(
306
367
  ZLinkFrameworkInternalErrorKind.ActorLocationStale,
@@ -348,11 +409,21 @@ export class ZLinkActorSessionBindingRegistry<
348
409
  return Object.freeze({ context: route.context });
349
410
  }
350
411
 
351
- async unbind(actorId: string, context: TContext, bindingToken: string): Promise<void> {
352
- await this.lane.run(() => this.unbindCore(actorId, context, bindingToken));
412
+ async unbind(
413
+ actorId: string,
414
+ context: TContext,
415
+ bindingToken: string,
416
+ termination = ZLinkActorSessionBindingTermination.BindingEnd
417
+ ): Promise<void> {
418
+ await this.lane.run(() => this.unbindCore(actorId, context, bindingToken, termination));
353
419
  }
354
420
 
355
- private unbindCore(actorId: string, context: TContext, bindingToken: string): void {
421
+ private async unbindCore(
422
+ actorId: string,
423
+ context: TContext,
424
+ bindingToken: string,
425
+ termination: ZLinkActorSessionBindingTermination
426
+ ): Promise<void> {
356
427
  const route = this.routes.get(actorId);
357
428
  if (route === undefined || route.context !== context || route.bindingToken !== bindingToken) {
358
429
  return;
@@ -372,28 +443,44 @@ export class ZLinkActorSessionBindingRegistry<
372
443
  route,
373
444
  new Error(`Actor '${actorId}' session binding was removed.`)
374
445
  );
446
+ if (
447
+ termination === ZLinkActorSessionBindingTermination.BindingEnd &&
448
+ route.actorSlot !== undefined
449
+ ) {
450
+ await context.actorSlotControls?.enqueueUnbound(route.actorSlot);
451
+ }
375
452
  }
376
453
 
377
454
  async unbindActor(actorId: string): Promise<void> {
378
455
  await this.lane.run(() => this.unbindActorCore(actorId));
379
456
  }
380
457
 
381
- private unbindActorCore(actorId: string): void {
458
+ private async unbindActorCore(actorId: string): Promise<void> {
382
459
  const route = this.routes.get(actorId);
383
460
  if (route === undefined) {
384
461
  return;
385
462
  }
386
- this.unbindCore(actorId, route.context, route.bindingToken);
463
+ await this.unbindCore(
464
+ actorId,
465
+ route.context,
466
+ route.bindingToken,
467
+ ZLinkActorSessionBindingTermination.BindingEnd
468
+ );
387
469
  }
388
470
 
389
471
  async cleanup(context: TContext): Promise<void> {
390
472
  await this.lane.run(() => this.cleanupCore(context));
391
473
  }
392
474
 
393
- private cleanupCore(context: TContext): void {
475
+ private async cleanupCore(context: TContext): Promise<void> {
394
476
  for (const route of [...this.routes.values()]) {
395
477
  if (route.context === context) {
396
- this.unbindCore(route.actor.actorId, context, route.bindingToken);
478
+ await this.unbindCore(
479
+ route.actor.actorId,
480
+ context,
481
+ route.bindingToken,
482
+ ZLinkActorSessionBindingTermination.PhysicalDisconnect
483
+ );
397
484
  }
398
485
  }
399
486
  }
@@ -13,6 +13,7 @@ import type {
13
13
  } from './actor-session-binding-registry';
14
14
 
15
15
  export interface ZLinkActorSessionBindingRuntimeOwner {
16
+ actorSlot(actorId: string, sessionRid: string): Promise<number | undefined>;
16
17
  sealRelocation(
17
18
  claim: ZLinkActorSessionRelocationClaim,
18
19
  expected: ZLinkActorSessionRouteFence,
@@ -10,7 +10,10 @@ import {
10
10
  } from './protocol';
11
11
  import { throwIfAborted } from '../abort';
12
12
  import { captureZLinkExecutionTurn } from '../execution';
13
- import { ZLinkActorSessionBindingRegistry } from './actor-session-binding-registry';
13
+ import {
14
+ ZLinkActorSessionBindingRegistry,
15
+ ZLinkActorSessionBindingTermination
16
+ } from './actor-session-binding-registry';
14
17
  import { ZLinkActorSessionLifecycleCoordinator } from './actor-session-lifecycle-coordinator';
15
18
  import { ZLinkManagedStream } from './managed-stream';
16
19
  import { DefaultZLinkSessionActor, DefaultZLinkSessionContext } from './session-context';
@@ -246,7 +249,12 @@ export class ZLinkBoundActorRelaySender {
246
249
  // The transport is already closed, so remove its exact route before
247
250
  // invoking application lifecycle code. A reconnect can then install
248
251
  // a successor while this best-effort notification is in flight.
249
- await this.routes.unbind(actor.actorId, context, actor.bindingToken);
252
+ await this.routes.unbind(
253
+ actor.actorId,
254
+ context,
255
+ actor.bindingToken,
256
+ ZLinkActorSessionBindingTermination.PhysicalDisconnect
257
+ );
250
258
  return true;
251
259
  });
252
260
  if (!detached) return;
@@ -1,4 +1,7 @@
1
1
  import type { Message } from '../../contracts/Common/Message';
2
+ import { ZLinkFrameworkErrorKind, ZLinkFrameworkException } from '../../contracts';
3
+ import { channelErrorCodeName } from '../channels/channel-envelope';
4
+ import { ZLinkRouteDisconnectedError } from '../channels/route-disconnected-error';
2
5
  import { ZLinkSubmitStatus, type ZLinkSubmitResult } from '../messaging/submission-result';
3
6
  import { ZLinkStreamMessageKind } from './protocol';
4
7
  import type { ZLinkStreamFrameMessageFactory } from './stream-frame-factory';
@@ -96,7 +99,13 @@ export function boundSessionErrorPayload(error: unknown): {
96
99
  readonly message: string;
97
100
  } {
98
101
  return {
99
- code: error instanceof Error ? error.constructor.name : 'RemoteError',
102
+ code: channelErrorCodeName(
103
+ error instanceof ZLinkFrameworkException
104
+ ? error.kind
105
+ : error instanceof ZLinkRouteDisconnectedError
106
+ ? ZLinkFrameworkErrorKind.Unavailable
107
+ : ZLinkFrameworkErrorKind.InternalFailure
108
+ ),
100
109
  message: errorMessage(error)
101
110
  };
102
111
  }
@@ -87,7 +87,9 @@ export class ZLinkBoundSessionService {
87
87
  metadata,
88
88
  false,
89
89
  undefined,
90
- message
90
+ message,
91
+ undefined,
92
+ this.actorSlot(route)
91
93
  );
92
94
  try {
93
95
  const result = await this.requireTransport().send(actorId, frame, {
@@ -120,7 +122,9 @@ export class ZLinkBoundSessionService {
120
122
  metadata,
121
123
  false,
122
124
  undefined,
123
- message
125
+ message,
126
+ undefined,
127
+ this.actorSlot(route)
124
128
  );
125
129
  try {
126
130
  throwIfAborted(signal);
@@ -348,7 +352,9 @@ export class ZLinkBoundSessionService {
348
352
  metadata,
349
353
  compressPayload,
350
354
  requestSeq,
351
- payload
355
+ payload,
356
+ undefined,
357
+ this.actorSlot(route)
352
358
  );
353
359
  try {
354
360
  if ((await this.routes.route(actorId))?.bindingToken !== route.bindingToken) {
@@ -448,6 +454,10 @@ export class ZLinkBoundSessionService {
448
454
  }
449
455
  return this.options.transport;
450
456
  }
457
+
458
+ private actorSlot(route: ZLinkStreamActorSessionRoute): number | undefined {
459
+ return route.actorSlot;
460
+ }
451
461
  }
452
462
 
453
463
  function requireBoundSessionGeneration(actorRef: ActorRef): bigint {
@@ -264,6 +264,7 @@ export class ZLinkStreamRuntimeManager {
264
264
  const service = createService.call(meshNode, socket.nativeInstance as never);
265
265
  const bindingOwner = actorSessionBindingRuntimeOwner(this.options.bindingRuntime);
266
266
  registerServiceSessionBindingIngressPort(service, {
267
+ actorSlot: (actorId, sessionRid) => bindingOwner.actorSlot(actorId, sessionRid),
267
268
  retainOutbound: (claim, delivery) =>
268
269
  bindingOwner.admitRelocationOutbound(claim, delivery),
269
270
  clearOutbound: (actorId, error) => bindingOwner.clearRelocation(actorId, error)
@@ -419,7 +420,7 @@ export class ZLinkStreamBindingRuntime {
419
420
  this.routes = new ZLinkActorSessionBindingRegistry<
420
421
  DefaultZLinkSessionContext,
421
422
  DefaultZLinkSessionActor
422
- >(4096, 4096, runtimeOptions.sessionRelocationSealTimeoutMs ?? 3_000);
423
+ >(4096, 4096, runtimeOptions.sessionRelocationSealTimeoutMs ?? 3_000, runtimeOptions.errorSink);
423
424
  this.compressionCodec = resolveStreamCompressionCodec(runtimeOptions.streamCompression);
424
425
  this.frameMessages = new ZLinkStreamFrameMessageFactory(runtimeOptions);
425
426
  this.boundSessions = new ZLinkBoundSessionService(
@@ -441,6 +442,12 @@ export class ZLinkStreamBindingRuntime {
441
442
  actorSessionLifecycle
442
443
  );
443
444
  registerActorSessionBindingRuntimeOwner(this, {
445
+ actorSlot: async (actorId, sessionRid) => {
446
+ const route = await this.routes.route(actorId);
447
+ return route !== undefined && route.sessionIdentity === sessionRid
448
+ ? route.actorSlot
449
+ : undefined;
450
+ },
444
451
  sealRelocation: (claim, expected, signal) =>
445
452
  this.routes.sealRelocation(claim, expected, signal),
446
453
  relocationSnapshot: (actorId, sealId) => this.routes.relocationSnapshot(actorId, sealId),
@@ -33,6 +33,8 @@ import {
33
33
  type ZLinkMeshCompletionTable
34
34
  } from '../backend/mesh-completion-table';
35
35
  import {
36
+ encodeActorBoundFrame,
37
+ encodeActorUnboundFrame,
36
38
  encodeSessionClosingFrame,
37
39
  encodeStreamControlFrame,
38
40
  ZLinkStreamCloseReasonCode
@@ -51,18 +53,17 @@ export interface ZLinkNativeSessionRoute {
51
53
  readonly completions: ZLinkMeshCompletionTable;
52
54
  }
53
55
 
56
+ interface ZLinkManagedActorBinding {
57
+ readonly actor: ZLinkBackendActorRef;
58
+ readonly bindingGeneration?: bigint;
59
+ readonly route?: ZLinkNativeSessionRoute;
60
+ }
61
+
54
62
  export class ZLinkManagedStream implements ZLinkStream {
55
63
  private currentLocalAddr: string | undefined;
56
64
  private currentRemoteAddr: string | undefined;
57
65
  private transportClosed = false;
58
- private readonly nativeActorBindings = new Map<
59
- string,
60
- {
61
- readonly actor: ZLinkBackendActorRef;
62
- readonly bindingGeneration: bigint;
63
- readonly route?: ZLinkNativeSessionRoute;
64
- }
65
- >();
66
+ private readonly nativeActorBindings = new Map<string, ZLinkManagedActorBinding>();
66
67
 
67
68
  constructor(
68
69
  private readonly socket: ZLinkBackendStreamSocket,
@@ -195,12 +196,12 @@ export class ZLinkManagedStream implements ZLinkStream {
195
196
  `Stream session '${this.sessionId}' is disconnected.`
196
197
  );
197
198
  }
199
+ const nativeActor = toNativeActorRef(actor);
198
200
  const route = this.nativeRoute(actor.meshName);
199
201
  if (route !== undefined) {
200
202
  if (route.service.status().state === 1) {
201
203
  route.service.start();
202
204
  }
203
- const nativeActor = toNativeActorRef(actor);
204
205
  await this.ensureNativeActorRoute(route, actor, timeoutMs, signal);
205
206
  if (this.isTransportClosed()) {
206
207
  throw createInternalFrameworkException(
@@ -278,6 +279,7 @@ export class ZLinkManagedStream implements ZLinkStream {
278
279
  timeoutMs,
279
280
  signal
280
281
  );
282
+ this.nativeActorBindings.set(actor.actorId, { actor: nativeActor });
281
283
  }
282
284
 
283
285
  private async ensureNativeActorRoute(
@@ -327,7 +329,7 @@ export class ZLinkManagedStream implements ZLinkStream {
327
329
  const binding = this.nativeActorBindings.get(actorId);
328
330
  if (binding?.route !== undefined) {
329
331
  const route = binding.route;
330
- if (!this.hasNativeBinding(route, actorId, binding.bindingGeneration)) {
332
+ if (!this.hasNativeBinding(route, actorId, binding.bindingGeneration!)) {
331
333
  this.nativeActorBindings.delete(actorId);
332
334
  return;
333
335
  }
@@ -338,7 +340,7 @@ export class ZLinkManagedStream implements ZLinkStream {
338
340
  route.service.unbindActor(
339
341
  this.backendRoutingId(),
340
342
  binding.actor as never,
341
- binding.bindingGeneration,
343
+ binding.bindingGeneration!,
342
344
  timeoutMs
343
345
  ),
344
346
  signal
@@ -354,7 +356,7 @@ export class ZLinkManagedStream implements ZLinkStream {
354
356
  // tombstone. A transport teardown can therefore report an internal
355
357
  // completion after the exact binding is already gone. Treat only that
356
358
  // exact missing binding as stale cleanup; preserve other failures.
357
- if (this.hasNativeBinding(route, actorId, binding.bindingGeneration)) {
359
+ if (this.hasNativeBinding(route, actorId, binding.bindingGeneration!)) {
358
360
  throw error;
359
361
  }
360
362
  }
@@ -364,6 +366,26 @@ export class ZLinkManagedStream implements ZLinkStream {
364
366
  return;
365
367
  }
366
368
  await this.socket.unbindActor(this.backendRoutingId(), actorId, timeoutMs, signal);
369
+ if (binding !== undefined && this.nativeActorBindings.get(actorId) === binding) {
370
+ this.nativeActorBindings.delete(actorId);
371
+ }
372
+ }
373
+
374
+ async enqueueActorBound(actorSlot: number, actorId: string): Promise<void> {
375
+ await this.submitActorBindingControl(encodeActorBoundFrame(actorSlot, actorId));
376
+ }
377
+
378
+ async enqueueActorUnbound(actorSlot: number): Promise<void> {
379
+ await this.submitActorBindingControl(encodeActorUnboundFrame(actorSlot));
380
+ }
381
+
382
+ private async submitActorBindingControl(frame: Uint8Array): Promise<void> {
383
+ const message = NativeMessage.from(frame);
384
+ try {
385
+ await this.socket.submit(this.backendRoutingId(), message);
386
+ } finally {
387
+ message.close();
388
+ }
367
389
  }
368
390
 
369
391
  async sendBoundActor(