@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
@@ -13,6 +13,7 @@ import {
13
13
  type ZLinkDiagnosticsContext
14
14
  } from '../diagnostics';
15
15
  import type { ZLinkDispatchErrorSink } from '../diagnostics/dispatch-error-port';
16
+ import { dispatchErrorDetails } from '../diagnostics/dispatch-error-details';
16
17
 
17
18
  export type { ZLinkDispatchErrorSink } from '../diagnostics/dispatch-error-port';
18
19
 
@@ -46,7 +47,6 @@ export class ZLinkDispatchErrorReporter {
46
47
 
47
48
  report(event: ZLinkRuntimeDispatchFailure): void {
48
49
  const normalized = normalizeDispatchFailure(event);
49
- const tracePoint = this.flow.begin(ZLinkMessageFlowOutcome.Error);
50
50
  const dropReason = channelDropReason(normalized);
51
51
  if (dropReason !== undefined) {
52
52
  this.metrics?.count('zlink.mesh_node.messages.dropped', 1, {
@@ -55,8 +55,9 @@ export class ZLinkDispatchErrorReporter {
55
55
  reason: dropReason
56
56
  });
57
57
  }
58
+ const tracePoint = this.flow.begin(ZLinkMessageFlowOutcome.Error);
58
59
  if (tracePoint === undefined) return;
59
- const errorInfo = dispatchErrorInfo(normalized);
60
+ const errorInfo = dispatchErrorDetails(normalized.error);
60
61
  this.reportedEvents += 1;
61
62
  tracePoint.trace({
62
63
  outcome: ZLinkMessageFlowOutcome.Error,
@@ -87,6 +88,10 @@ export class ZLinkDispatchErrorReporter {
87
88
  });
88
89
  }
89
90
 
91
+ captureEnabled(): boolean {
92
+ return this.flow.enabled(ZLinkMessageFlowOutcome.Error);
93
+ }
94
+
90
95
  get reportedCount(): number {
91
96
  return this.reportedEvents;
92
97
  }
@@ -123,46 +128,3 @@ function channelDropReason(event: ZLinkRuntimeDispatchFailure): string | undefin
123
128
  return undefined;
124
129
  }
125
130
  }
126
-
127
- function dispatchErrorInfo(event: ZLinkRuntimeDispatchFailure): {
128
- readonly errorType?: string;
129
- readonly errorMessage?: string;
130
- readonly errorCauseType?: string;
131
- readonly errorCauseMessage?: string;
132
- } {
133
- if (event.errorType !== undefined || event.errorMessage !== undefined) {
134
- return {
135
- errorType: event.errorType,
136
- errorMessage: event.errorMessage,
137
- errorCauseType: event.errorCauseType,
138
- errorCauseMessage: event.errorCauseMessage
139
- };
140
- }
141
- if (event.error === undefined) {
142
- return {};
143
- }
144
- if (event.error instanceof Error) {
145
- const cause = deepestErrorCause(event.error);
146
- return {
147
- errorType: event.error.name,
148
- errorMessage: event.error.message,
149
- ...(cause === event.error
150
- ? {}
151
- : {
152
- errorCauseType: cause instanceof Error ? cause.name : typeof cause,
153
- errorCauseMessage: cause instanceof Error ? cause.message : String(cause)
154
- })
155
- };
156
- }
157
- return { errorType: typeof event.error, errorMessage: String(event.error) };
158
- }
159
-
160
- function deepestErrorCause(error: Error): unknown {
161
- let current: unknown = error;
162
- const seen = new Set<unknown>();
163
- while (current instanceof Error && current.cause !== undefined && !seen.has(current)) {
164
- seen.add(current);
165
- current = current.cause;
166
- }
167
- return current;
168
- }
@@ -0,0 +1,62 @@
1
+ const ERROR_MESSAGE_MAX_LENGTH = 512;
2
+
3
+ const CREDENTIAL_PATTERNS: ReadonlyArray<readonly [RegExp, string]> = [
4
+ [/Authorization\s*:\s*(?:(?:Bearer|Basic)\s+)?[^\s,;]+/gi, 'Authorization: <redacted>'],
5
+ [/Bearer\s+[^\s,;]+/gi, 'Bearer <redacted>'],
6
+ [/password\s*=\s*[^\s,;]+/gi, 'password=<redacted>'],
7
+ [/token\s*=\s*[^\s,;]+/gi, 'token=<redacted>']
8
+ ];
9
+
10
+ export function dispatchErrorDetails(error: unknown): {
11
+ readonly errorType?: string;
12
+ readonly errorMessage?: string;
13
+ readonly errorCauseType?: string;
14
+ readonly errorCauseMessage?: string;
15
+ } {
16
+ if (error === undefined || error === null) return {};
17
+ if (error instanceof Error) {
18
+ const cause = deepestErrorCause(error);
19
+ return {
20
+ errorType: error.name,
21
+ errorMessage: sanitizeErrorMessage(error.message),
22
+ ...(cause === error
23
+ ? {}
24
+ : {
25
+ errorCauseType: cause instanceof Error ? cause.name : typeof cause,
26
+ errorCauseMessage: sanitizeErrorMessage(
27
+ cause instanceof Error ? cause.message : String(cause)
28
+ )
29
+ })
30
+ };
31
+ }
32
+ const errorType = typeof error === 'object' ? error.constructor.name : typeof error;
33
+ const message =
34
+ typeof error === 'string' ||
35
+ typeof error === 'number' ||
36
+ typeof error === 'boolean' ||
37
+ typeof error === 'bigint'
38
+ ? `${error}`
39
+ : Object.prototype.toString.call(error);
40
+ return { errorType, errorMessage: sanitizeErrorMessage(message) };
41
+ }
42
+
43
+ function deepestErrorCause(error: Error): unknown {
44
+ let current: unknown = error;
45
+ const seen = new Set<unknown>();
46
+ while (current instanceof Error && current.cause !== undefined && !seen.has(current)) {
47
+ seen.add(current);
48
+ current = current.cause;
49
+ }
50
+ return current;
51
+ }
52
+
53
+ function sanitizeErrorMessage(message: string): string {
54
+ if (message.length === 0) return '';
55
+ const lineEnd = message.search(/[\r\n]/u);
56
+ let sanitized = lineEnd < 0 ? message : message.slice(0, lineEnd);
57
+ for (const [pattern, replacement] of CREDENTIAL_PATTERNS) {
58
+ pattern.lastIndex = 0;
59
+ sanitized = sanitized.replace(pattern, replacement);
60
+ }
61
+ return sanitized.slice(0, ERROR_MESSAGE_MAX_LENGTH);
62
+ }
@@ -5,6 +5,7 @@ import type { ZLinkFlowOrigin } from '../../contracts';
5
5
  export interface ZLinkFlowContextValue {
6
6
  readonly flowId: string;
7
7
  readonly flowOrigin: ZLinkFlowOrigin;
8
+ readonly streamSessionId?: string;
8
9
  }
9
10
 
10
11
  const flowStorage = new AsyncLocalStorage<ZLinkFlowContextValue | undefined>();
@@ -71,14 +72,15 @@ export function runWithOutboundFlow<T>(enabled: boolean, callback: () => T): T {
71
72
  }
72
73
 
73
74
  export function createInboundFlow(
74
- flowId?: string,
75
- flowOrigin?: ZLinkFlowOrigin,
76
- createIfAbsent = true
75
+ flowId: string | undefined,
76
+ flowOrigin: ZLinkFlowOrigin | undefined,
77
+ createIfAbsent: boolean,
78
+ streamSessionId: string | undefined
77
79
  ): ZLinkFlowContextValue | undefined {
78
80
  if (!createIfAbsent) {
79
81
  return undefined;
80
82
  }
81
- return { flowId: flowId ?? createFlowId(), flowOrigin: flowOrigin ?? 'Inbound' };
83
+ return { flowId: flowId ?? createFlowId(), flowOrigin: flowOrigin ?? 'Inbound', streamSessionId };
82
84
  }
83
85
 
84
86
  function createFlowId(): string {
@@ -34,6 +34,7 @@ export const DEFAULT_ZLINK_DIAGNOSTICS: ZLinkDiagnosticsOptions = {
34
34
 
35
35
  const telemetryLogger = openTelemetryLogs.getLogger('@zlink-systems/framework');
36
36
  const telemetryTracer = openTelemetryTrace.getTracer('@zlink-systems/framework');
37
+ const ERROR_MESSAGE_MAX_LENGTH = 512;
37
38
  let diagnosticsGeneration = 0n;
38
39
 
39
40
  interface ZLinkTelemetryRecord {
@@ -68,6 +69,7 @@ interface ZLinkTelemetryRecord {
68
69
  readonly instanceSpotType?: string;
69
70
  readonly activationState?: string;
70
71
  readonly actorId?: string;
72
+ readonly streamSessionId?: string;
71
73
  readonly commandId?: number;
72
74
  readonly messageSizeBytes?: number;
73
75
  readonly durationSeconds?: number;
@@ -226,12 +228,13 @@ export class ZLinkMessageFlowTracer {
226
228
  acceptedFlowId?: string,
227
229
  acceptedFlowOrigin?: ZLinkFlowOrigin
228
230
  ): void {
229
- const ambient = acceptedFlowId === undefined ? currentFlowContext() : undefined;
231
+ const ambient = currentFlowContext();
230
232
  const flowId = flowInput.flowId ?? acceptedFlowId ?? ambient?.flowId;
231
233
  const flowOrigin = flowInput.flowOrigin ?? acceptedFlowOrigin ?? ambient?.flowOrigin;
232
234
  const flow: ZLinkRuntimeMessageFlowEvent = {
233
235
  ...flowInput,
234
236
  ...(flowId === undefined ? {} : { flowId, flowOrigin }),
237
+ streamSessionId: flowInput.streamSessionId ?? ambient?.streamSessionId,
235
238
  effectiveMode
236
239
  };
237
240
  this.tracedEvents += 1;
@@ -380,11 +383,12 @@ function toTelemetryRecord(
380
383
  instanceSpotType: flow.instanceSpotType,
381
384
  activationState: flow.activationState,
382
385
  actorId: flow.actorId,
386
+ streamSessionId: flow.streamSessionId,
383
387
  commandId: flow.commandId,
384
388
  messageSizeBytes,
385
389
  durationSeconds,
386
390
  errorType: flow.errorType,
387
- errorMessage: boundedText(flow.errorMessage),
391
+ errorMessage: flow.errorMessage,
388
392
  errorCauseType: flow.errorCauseType,
389
393
  errorCauseMessage: boundedText(flow.errorCauseMessage)
390
394
  };
@@ -421,6 +425,7 @@ function structuredLogAttributes(record: ZLinkTelemetryRecord): Attributes {
421
425
  instance_spot_type: record.instanceSpotType,
422
426
  activation_state: record.activationState,
423
427
  actor_id: record.actorId,
428
+ stream_session_id: record.streamSessionId,
424
429
  correlation_id: record.correlationId,
425
430
  flow_id: record.flowId,
426
431
  flow_origin: record.flowOrigin,
@@ -456,11 +461,15 @@ function structuredLogBodyAttributes(record: ZLinkTelemetryRecord): Attributes {
456
461
  instance_type: record.instanceSpotType,
457
462
  activation_state: record.activationState,
458
463
  actor: record.actorId,
464
+ session: record.streamSessionId,
459
465
  corr: record.correlationId,
460
466
  flow: record.flowId,
461
467
  origin: record.flowOrigin,
462
468
  outcome: record.outcome,
463
469
  reason: record.reason,
470
+ action: record.action,
471
+ error_type: record.errorType,
472
+ error_message: record.errorMessage,
464
473
  size: record.messageSizeBytes
465
474
  });
466
475
  }
@@ -571,7 +580,7 @@ function hashFlowId(flowId: string): number {
571
580
  }
572
581
 
573
582
  function boundedText(value: string | undefined): string | undefined {
574
- return value === undefined ? undefined : value.slice(0, 512);
583
+ return value === undefined ? undefined : value.slice(0, ERROR_MESSAGE_MAX_LENGTH);
575
584
  }
576
585
 
577
586
  function toSnakeCase(value: string): string {
@@ -10,7 +10,6 @@ import { decodeServiceWireFrozenRecord } from './service-stateful-wire-codec';
10
10
  const RECOVERY_PACKET_NAME = '__zlink.actor.routed_join.recovery';
11
11
  const RECOVERY_CONTENT_TYPE = 'application/x-zlink-actor-routed-join-recovery-v1';
12
12
  const MAXIMUM_METADATA_BYTES = 256 * 1024;
13
- const MAXIMUM_MESSAGE_BYTES = 1024 * 1024;
14
13
  const FRAMEWORK_METADATA_UPPER_BOUND_BYTES = 64 * 1024;
15
14
  const ACCEPTED_JOURNAL_UPPER_BOUND_BYTES = 16 * 1024 * 1024;
16
15
  const SNAPSHOT_APPLICATION_STATE_RESERVATION_BYTES = 64 * 1024 * 1024;
@@ -124,9 +123,6 @@ export function encodeCanonicalActorJoinRecoverySavedWork(
124
123
  const operationId = input.operationId;
125
124
  const reply = operationId === undefined ? Buffer.alloc(0) : Buffer.from(input.reply ?? []);
126
125
  const request = Buffer.from(input.request);
127
- if (request.byteLength > MAXIMUM_MESSAGE_BYTES || reply.byteLength > MAXIMUM_MESSAGE_BYTES) {
128
- throw new RangeError('Actor Join recovery message exceeds 1 MiB.');
129
- }
130
126
  const replyContentType =
131
127
  operationId === undefined
132
128
  ? null
@@ -7,7 +7,7 @@ import type {
7
7
  ZLinkRawReceivedRecord,
8
8
  ZLinkRawRouterPort
9
9
  } from '../backend/raw-binding-port';
10
- import { RequestResult } from '../backend/runtime-values';
10
+ import { RequestResult, SubmitResult } from '../backend/runtime-values';
11
11
  import type {
12
12
  ApplicationJobPermitPort,
13
13
  ApplicationJobQueuePort
@@ -63,6 +63,19 @@ export interface RawServiceRequestResult {
63
63
  readonly payload?: ServiceApplicationPayload;
64
64
  }
65
65
 
66
+ type RawServiceChannelTargetSelection =
67
+ | { readonly kind: 'selected'; readonly peer: AdmittedServicePeer }
68
+ | {
69
+ readonly kind: 'noMember';
70
+ readonly submitResult: typeof SubmitResult.NotFound;
71
+ readonly requestResult: RawServiceRequestResult;
72
+ }
73
+ | {
74
+ readonly kind: 'knownButNotReady';
75
+ readonly submitResult: typeof SubmitResult.NotConnected;
76
+ readonly requestResult: RawServiceRequestResult;
77
+ };
78
+
66
79
  /** An M6A application frame already owned by the Framework runtime. */
67
80
  type ServiceApplicationPayloadInput = ServiceApplicationPayload | Buffer;
68
81
 
@@ -463,11 +476,10 @@ export class RawServiceMeshRuntime {
463
476
  async sendToChannel(
464
477
  channelName: string,
465
478
  payload: ServiceApplicationPayloadInput
466
- ): Promise<boolean> {
467
- const selected = this.topology.selectChannel(channelName, (peer) =>
468
- this.isLocalOrReadyPeer(peer.descriptor.nodeRoutingId)
469
- );
470
- if (selected === undefined) return false;
479
+ ): Promise<SubmitResult> {
480
+ const selection = this.selectChannelTarget(channelName);
481
+ if (selection.kind !== 'selected') return selection.submitResult;
482
+ const selected = selection.peer;
471
483
  const applicationFrame = this.applicationFrame(payload);
472
484
  if (selected.descriptor.nodeRoutingId === this.descriptor.nodeRoutingId) {
473
485
  const applicationJobOwner = await this.reserveLocalIngress();
@@ -481,15 +493,17 @@ export class RawServiceMeshRuntime {
481
493
  applicationJob
482
494
  });
483
495
  if (!accepted) applicationJob.close();
484
- return accepted;
496
+ return accepted ? SubmitResult.Ok : SubmitResult.NotAdmitted;
485
497
  } finally {
486
498
  applicationJobOwner.close();
487
499
  }
488
500
  }
489
- return this.send(selected.descriptor.nodeRoutingId, [
501
+ return (await this.send(selected.descriptor.nodeRoutingId, [
490
502
  encodeChannelSendHeader(channelName),
491
503
  applicationFrame
492
- ]);
504
+ ]))
505
+ ? SubmitResult.Ok
506
+ : SubmitResult.NotConnected;
493
507
  }
494
508
 
495
509
  requestToNode(
@@ -504,13 +518,40 @@ export class RawServiceMeshRuntime {
504
518
  channelName: string,
505
519
  payload: ServiceApplicationPayloadInput,
506
520
  timeoutMs: number
507
- ): PendingOperation<RawServiceRequestResult> | undefined {
521
+ ): PendingOperation<RawServiceRequestResult> {
522
+ const selection = this.selectChannelTarget(channelName);
523
+ if (selection.kind !== 'selected') {
524
+ const pending = this.operations.register(timeoutMs);
525
+ this.operations.complete(pending.id, selection.requestResult);
526
+ return pending;
527
+ }
528
+ return this.requestToTarget(
529
+ selection.peer.descriptor.nodeRoutingId,
530
+ payload,
531
+ timeoutMs,
532
+ channelName
533
+ );
534
+ }
535
+
536
+ private selectChannelTarget(channelName: string): RawServiceChannelTargetSelection {
508
537
  const selected = this.topology.selectChannel(channelName, (peer) =>
509
538
  this.isLocalOrReadyPeer(peer.descriptor.nodeRoutingId)
510
539
  );
511
- return selected === undefined
512
- ? undefined
513
- : this.requestToTarget(selected.descriptor.nodeRoutingId, payload, timeoutMs, channelName);
540
+ if (selected !== undefined) return { kind: 'selected', peer: selected };
541
+ return this.topology.hasKnownChannelTarget(channelName)
542
+ ? {
543
+ kind: 'knownButNotReady',
544
+ submitResult: SubmitResult.NotConnected,
545
+ requestResult: { terminalResult: RequestResult.NotConnected, failureCode: 0 }
546
+ }
547
+ : {
548
+ kind: 'noMember',
549
+ submitResult: SubmitResult.NotFound,
550
+ requestResult: {
551
+ terminalResult: RequestResult.NotFound,
552
+ failureCode: REQUEST_TARGET_NOT_FOUND_FAILURE_CODE
553
+ }
554
+ };
514
555
  }
515
556
 
516
557
  private isLocalOrReadyPeer(nodeRoutingId: string): boolean {
@@ -19,6 +19,7 @@ export interface ServiceSessionBindingAdmissionClaim {
19
19
  export type ServiceSessionBindingAdmissionResult = 'passThrough' | 'retained' | 'rejected';
20
20
 
21
21
  export interface ServiceSessionBindingIngressPort {
22
+ actorSlot(actorId: string, sessionRid: string): Promise<number | undefined>;
22
23
  retainOutbound(
23
24
  claim: ServiceSessionBindingAdmissionClaim,
24
25
  delivery: ServiceSessionBindingRetainedDelivery
@@ -9,6 +9,12 @@ import {
9
9
  translateWireReplyDecodeError
10
10
  } from '../framework-errors-internal';
11
11
  import { RequestResult, SubmitResult, isZLinkBackendResultError } from '../backend/runtime-values';
12
+ import type { ZLinkDispatchErrorReporter } from '../channels/dispatch-error-reporter';
13
+ import {
14
+ ZLinkRuntimeDispatchErrorAction as ZLinkDispatchErrorAction,
15
+ ZLinkDispatchErrorSurface,
16
+ ZLinkDispatchMessageKind
17
+ } from '../../contracts/Dispatch/ZLinkDispatchOptions';
12
18
  import type {
13
19
  RawServiceIngressRecord,
14
20
  RawServiceMeshRuntime,
@@ -401,6 +407,8 @@ export class ServiceStatefulRuntime {
401
407
  readonly kind: 'spot_multicast' | 'actor_control' | 'actor_binding';
402
408
  readonly owner: string;
403
409
  }) => void;
410
+ private dispatchErrors?: ZLinkDispatchErrorReporter;
411
+ private dispatchErrorMeshName?: string;
404
412
  private readonly admittedUserSpotOperations = new Map<
405
413
  string,
406
414
  {
@@ -438,6 +446,11 @@ export class ServiceStatefulRuntime {
438
446
  this.mailboxDropHandler = handler;
439
447
  }
440
448
 
449
+ setDispatchErrorReporter(reporter: ZLinkDispatchErrorReporter, meshName: string): void {
450
+ this.dispatchErrors = reporter;
451
+ this.dispatchErrorMeshName = meshName;
452
+ }
453
+
441
454
  createSpot(
442
455
  routingId?: string,
443
456
  kind: 'user' | 'instance' = 'user',
@@ -673,6 +686,13 @@ export class ServiceStatefulRuntime {
673
686
  }
674
687
  }
675
688
 
689
+ completeClosedInstance(spotId: string, objectGeneration: bigint): void {
690
+ const current = this.instanceIntents.get(spotId);
691
+ if (current !== undefined && current.route.objectGeneration === objectGeneration) {
692
+ this.forgetClosedInstanceRoute(current.route);
693
+ }
694
+ }
695
+
676
696
  registerInstanceActivationAuthority(authority: ServiceInstanceActivationAuthority): void {
677
697
  this.requireOpen();
678
698
  if (
@@ -1229,17 +1249,41 @@ export class ServiceStatefulRuntime {
1229
1249
  await this.enqueueLogicalMulticast(channelName, topic, sourceSpotId, payload);
1230
1250
  const targets = this.raw.topology
1231
1251
  .peers()
1232
- .filter((peer) =>
1233
- peer.descriptor.channels.some(
1234
- (channel) => channel.name === channelName && channel.weight > 0
1235
- )
1252
+ .filter(
1253
+ (peer) =>
1254
+ peer.descriptor.channels.some(
1255
+ (channel) => channel.name === channelName && channel.weight > 0
1256
+ ) && this.raw.isPeerRouteReady(peer.descriptor.nodeRoutingId)
1236
1257
  );
1237
1258
  const header = encodeLogicalMulticastHeader(channelName, topic, sourceSpotId);
1238
1259
  const payloadFrame = encodeApplicationPayload(payload);
1260
+ const reporter = this.dispatchErrors;
1261
+ const captureFailures = reporter?.captureEnabled() === true;
1239
1262
  for (const target of targets) {
1240
1263
  // Remote admission ends at the source outbound transport queue. The
1241
1264
  // receiver's Spot queue and handler completion are not publish results.
1242
- await this.raw.sendService(target.descriptor.nodeRoutingId, [header, payloadFrame]);
1265
+ const accepted = await this.raw.sendService(target.descriptor.nodeRoutingId, [
1266
+ header,
1267
+ payloadFrame
1268
+ ]);
1269
+ if (!accepted) {
1270
+ if (captureFailures) {
1271
+ reporter.report({
1272
+ surface: ZLinkDispatchErrorSurface.SpotRoute,
1273
+ messageKind: ZLinkDispatchMessageKind.Send,
1274
+ reason: serviceSpotPublishFailureReason(
1275
+ undefined,
1276
+ this.raw.isPeerRouteReady(target.descriptor.nodeRoutingId),
1277
+ this.closed
1278
+ ),
1279
+ action: ZLinkDispatchErrorAction.Drop,
1280
+ meshName: this.dispatchErrorMeshName,
1281
+ channelName,
1282
+ topic,
1283
+ targetRid: target.descriptor.nodeRoutingId
1284
+ });
1285
+ }
1286
+ }
1243
1287
  }
1244
1288
  }
1245
1289
 
@@ -5251,6 +5295,28 @@ export class ServiceStatefulRuntime {
5251
5295
  }
5252
5296
  }
5253
5297
 
5298
+ function serviceSpotPublishFailureReason(
5299
+ result: number | undefined,
5300
+ routeReady: boolean,
5301
+ closed: boolean
5302
+ ): 'stale_target' | 'backpressure' | 'shutdown' {
5303
+ if (closed || result === SubmitResult.Terminated) return 'shutdown';
5304
+ switch (result) {
5305
+ case SubmitResult.Backpressured:
5306
+ return 'backpressure';
5307
+ case SubmitResult.NotAdmitted:
5308
+ case SubmitResult.NotConnected:
5309
+ case SubmitResult.NotFound:
5310
+ case SubmitResult.InvalidHandle:
5311
+ return 'stale_target';
5312
+ default:
5313
+ // RawServiceMeshRuntime exposes failed Core submissions as false. A
5314
+ // ready route therefore identifies queue admission failure; a route
5315
+ // that became unready after selection is a stale target.
5316
+ return routeReady ? 'backpressure' : 'stale_target';
5317
+ }
5318
+ }
5319
+
5254
5320
  function isEntrySpotFence(fence: ServiceDirectSpotRouteFence): boolean {
5255
5321
  return (
5256
5322
  fence.spot.spotId === fence.targetNodeRid &&
@@ -257,6 +257,13 @@ export class ServiceTopologyRegistry {
257
257
  return descriptor === undefined ? undefined : cloneDescriptor(descriptor);
258
258
  }
259
259
 
260
+ hasKnownChannelTarget(channelName: string): boolean {
261
+ requireText(channelName, 'channelName');
262
+ return [this.local, ...this.knownByRid.values()].some(
263
+ (descriptor) => selectableChannel(descriptor, channelName) !== undefined
264
+ );
265
+ }
266
+
260
267
  private remember(descriptor: ServiceNodeDescriptor): void {
261
268
  const current = this.knownByRid.get(descriptor.nodeRoutingId);
262
269
  if (
@@ -285,7 +292,7 @@ export class ServiceTopologyRegistry {
285
292
  return [local, ...this.peersByRid.values()]
286
293
  .map((peer) => ({
287
294
  peer: clonePeer(peer),
288
- channel: findChannel(peer.descriptor, channelName)
295
+ channel: selectableChannel(peer.descriptor, channelName)
289
296
  }))
290
297
  .filter(
291
298
  (
@@ -293,10 +300,7 @@ export class ServiceTopologyRegistry {
293
300
  ): value is {
294
301
  peer: AdmittedServicePeer;
295
302
  channel: ServiceChannelDescriptor;
296
- } =>
297
- value.channel !== undefined &&
298
- value.peer.descriptor.state === 'serving' &&
299
- value.channel.weight > 0
303
+ } => value.channel !== undefined
300
304
  );
301
305
  },
302
306
  (value) => value.channel.weight,
@@ -519,6 +523,15 @@ function findChannel(
519
523
  return descriptor.channels.find((channel) => channel.name === channelName);
520
524
  }
521
525
 
526
+ function selectableChannel(
527
+ descriptor: ServiceNodeDescriptor,
528
+ channelName: string
529
+ ): ServiceChannelDescriptor | undefined {
530
+ if (descriptor.state !== 'serving') return undefined;
531
+ const channel = findChannel(descriptor, channelName);
532
+ return channel !== undefined && channel.weight > 0 ? channel : undefined;
533
+ }
534
+
522
535
  function requireText(value: string, field: string): void {
523
536
  if (value.length === 0 || value.includes('\0')) {
524
537
  throw new TypeError(`${field} must be non-empty text without NUL.`);
@@ -1982,9 +1982,16 @@ export class ZLinkFrameworkRuntimeHost
1982
1982
  // coordinator after application initialization. The legacy location
1983
1983
  // claim would expose a Creating object before that Ready barrier.
1984
1984
  locationLifecycle: () => undefined,
1985
- releaseInstanceAuthority: (meshName, spotId, objectGeneration) =>
1986
- this.locationOwner.currentLifecycle?.releaseSpot(meshName, spotId, objectGeneration) ??
1987
- Promise.resolve(),
1985
+ releaseInstanceAuthority: async (meshName, spotId, objectGeneration) => {
1986
+ await this.locationOwner.currentLifecycle?.releaseSpot(
1987
+ meshName,
1988
+ spotId,
1989
+ objectGeneration
1990
+ );
1991
+ this.spotNodeRuntime
1992
+ ?.meshNode(meshName)
1993
+ ?.completeClosedInstance?.(spotId, objectGeneration);
1994
+ },
1988
1995
  beginInstanceIdleClosingAuthority: (meshName, spotId) =>
1989
1996
  this.locationOwner.currentLifecycle?.beginInstanceSpotClosing(meshName, spotId) ??
1990
1997
  Promise.resolve(undefined),
@@ -3047,7 +3054,7 @@ export class ZLinkFrameworkRuntimeHost
3047
3054
  const flow =
3048
3055
  current?.flowOrigin === 'Lifecycle'
3049
3056
  ? current
3050
- : createInboundFlow(undefined, 'Lifecycle', this.flowCreationEnabled());
3057
+ : createInboundFlow(undefined, 'Lifecycle', this.flowCreationEnabled(), undefined);
3051
3058
  return runWithFlow(flow, operation);
3052
3059
  }
3053
3060
 
@@ -278,6 +278,13 @@ export class ZLinkInMemoryLocationStore
278
278
  ) {
279
279
  return stored(this.meshNodes.generations.get(key) ?? 1n, current.updatedAt);
280
280
  }
281
+ if (
282
+ intent === ZLinkLocationWriteIntent.NewClaim &&
283
+ currentLease !== undefined &&
284
+ currentLease.leaseExpiresAt.getTime() > updatedAt.getTime()
285
+ ) {
286
+ return rejectedConflict();
287
+ }
281
288
  if (
282
289
  current.ownerId !== descriptor.ownerId ||
283
290
  current.leaseGeneration !== descriptor.leaseGeneration ||
@@ -2329,7 +2329,10 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
2329
2329
  const renew = intent === 2 && canRenew(stored, descriptor);
2330
2330
  if (!takeover && !renew) {
2331
2331
  return {
2332
- status: WriteStatus.IgnoredStale,
2332
+ status:
2333
+ intent === ZLinkLocationWriteIntent.NewClaim && currentLeaseGeneration !== undefined
2334
+ ? WriteStatus.RejectedConflict
2335
+ : WriteStatus.IgnoredStale,
2333
2336
  generation,
2334
2337
  updatedAt: current.value.storeNow
2335
2338
  };