@zlink-systems/framework 0.13.0 → 0.15.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 (36) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/contracts/Configuration/Builders.d.ts +2 -0
  3. package/dist/contracts/Configuration/EndpointNotation.d.ts +3 -3
  4. package/dist/contracts/Configuration/EndpointNotation.js +24 -7
  5. package/dist/contracts/Configuration/FanoutTopic.d.ts +2 -0
  6. package/dist/contracts/Configuration/FanoutTopic.js +14 -0
  7. package/dist/contracts/Configuration/RegistrationBuilders.js +13 -0
  8. package/dist/contracts/Configuration/RegistrationTypes.d.ts +2 -0
  9. package/dist/contracts/Configuration/RegistrationValidators.js +19 -14
  10. package/dist/runtime/backend/contracts/index.d.ts +1 -0
  11. package/dist/runtime/backend/node/node-backend-adapter-support.js +9 -1
  12. package/dist/runtime/backend/node/node-socket-backend-adapter.js +6 -0
  13. package/dist/runtime/channels/channel-clients.js +2 -0
  14. package/dist/runtime/channels/channel-socket-registry.js +16 -2
  15. package/dist/runtime/channels/fanout-service-wire.d.ts +2 -2
  16. package/dist/runtime/channels/fanout-service-wire.js +5 -10
  17. package/dist/runtime/locations/in-memory-authority-store.js +9 -1
  18. package/dist/runtime/locations/location-store-repository.js +61 -3
  19. package/dist/runtime/streams/index.d.ts +1 -1
  20. package/dist/runtime/streams/index.js +7 -0
  21. package/package.json +3 -3
  22. package/src/contracts/Configuration/Builders.ts +2 -0
  23. package/src/contracts/Configuration/EndpointNotation.ts +23 -6
  24. package/src/contracts/Configuration/FanoutTopic.ts +12 -0
  25. package/src/contracts/Configuration/RegistrationBuilders.ts +17 -0
  26. package/src/contracts/Configuration/RegistrationTypes.ts +2 -0
  27. package/src/contracts/Configuration/RegistrationValidators.ts +19 -17
  28. package/src/runtime/backend/contracts/index.ts +1 -0
  29. package/src/runtime/backend/node/node-backend-adapter-support.ts +13 -2
  30. package/src/runtime/backend/node/node-socket-backend-adapter.ts +7 -0
  31. package/src/runtime/channels/channel-clients.ts +2 -0
  32. package/src/runtime/channels/channel-socket-registry.ts +22 -2
  33. package/src/runtime/channels/fanout-service-wire.ts +5 -8
  34. package/src/runtime/locations/in-memory-authority-store.ts +9 -1
  35. package/src/runtime/locations/location-store-repository.ts +73 -3
  36. package/src/runtime/streams/index.ts +18 -1
@@ -100,6 +100,7 @@ import {
100
100
  validateSessionReplacementCallbackTimeout
101
101
  } from './RegistrationBuilderPolicy';
102
102
  import { requireMessageFlowLogMode, requireTraceSampleRate } from './DiagnosticsValidation';
103
+ import { requirePublicFanoutTopic } from './FanoutTopic';
103
104
 
104
105
  export function createFrameworkOptions(
105
106
  configure: (options: ZLinkFrameworkOptions) => void
@@ -507,6 +508,11 @@ class DefaultFanoutChannelBuilder implements ZLinkFanoutChannelBuilder {
507
508
  return this;
508
509
  }
509
510
 
511
+ setNoDrop(noDrop = true): this {
512
+ this.channel.noDrop = noDrop;
513
+ return this;
514
+ }
515
+
510
516
  enableSubscriber(endpoint?: string): this {
511
517
  this.selectSubscriberMode(endpoint === undefined ? 'automatic' : 'manual');
512
518
  this.channel.subscriber ??= { manualConnections: [] };
@@ -519,6 +525,15 @@ class DefaultFanoutChannelBuilder implements ZLinkFanoutChannelBuilder {
519
525
  return this;
520
526
  }
521
527
 
528
+ subscribe(topic: string): this {
529
+ requirePublicFanoutTopic(topic);
530
+ this.channel.subscriptions ??= [];
531
+ if (!this.channel.subscriptions.includes(topic)) {
532
+ this.channel.subscriptions.push(topic);
533
+ }
534
+ return this;
535
+ }
536
+
522
537
  connect(endpoint: string): this {
523
538
  requireRegistrationName(endpoint, `Fanout channel '${this.name}' subscriber endpoint`);
524
539
  this.selectSubscriberMode('manual');
@@ -1528,8 +1543,10 @@ type MutableLocationOptionValues = {
1528
1543
  interface MutableChannelOptions {
1529
1544
  routingId?: string;
1530
1545
  routingIdPrefix?: string;
1546
+ noDrop?: boolean;
1531
1547
  publisher?: MutablePublisherCapabilityOptions;
1532
1548
  publishHandlers?: ZLinkChannelPublishHandlerRegistration[];
1549
+ subscriptions?: string[];
1533
1550
  subscriber?: MutableClientCapabilityOptions;
1534
1551
  client?: MutableClientCapabilityOptions;
1535
1552
  server?: MutableServerCapabilityOptions;
@@ -170,11 +170,13 @@ export interface ZLinkFrameworkRegistrationOptions {
170
170
  export interface ZLinkChannelOptions {
171
171
  readonly routingId?: string;
172
172
  readonly routingIdPrefix?: string;
173
+ readonly noDrop?: boolean;
173
174
  readonly requestTimeoutMs?: number;
174
175
  readonly client?: ZLinkClientCapabilityOptions;
175
176
  readonly publisher?: ZLinkPublisherCapabilityOptions;
176
177
  readonly routeMesh?: ZLinkRouteMeshChannelOptions;
177
178
  readonly publishHandlers?: readonly ZLinkChannelPublishHandlerRegistration[];
179
+ readonly subscriptions?: readonly string[];
178
180
  readonly requestHandlers?: readonly ZLinkChannelRequestHandlerRegistration[];
179
181
  readonly sendHandlers?: readonly ZLinkChannelSendHandlerRegistration[];
180
182
  readonly server?: {
@@ -20,6 +20,7 @@ import {
20
20
  import { validateTimerRegistration } from './TimerRegistrationValidator';
21
21
  import { zlinkDefaultLocationOptions } from '../Locations';
22
22
  import { requireValidSendTimeoutMs } from './SendTimeoutValidation';
23
+ import { requirePublicFanoutTopic } from './FanoutTopic';
23
24
 
24
25
  export function validateFrameworkRegistration(
25
26
  registration: ZLinkFrameworkRegistration,
@@ -27,7 +28,6 @@ export function validateFrameworkRegistration(
27
28
  ): void {
28
29
  validateListenerNetworkIdentity(
29
30
  'process network',
30
- undefined,
31
31
  registration.network.bindHost,
32
32
  registration.network.advertiseHost
33
33
  );
@@ -203,11 +203,28 @@ function validateChannelCapabilities(
203
203
  peerLocationConfigured: boolean
204
204
  ): void {
205
205
  for (const [channelName, channel] of Object.entries(channels ?? {})) {
206
+ for (const topic of channel.subscriptions ?? []) {
207
+ requirePublicFanoutTopic(topic);
208
+ }
209
+ if ((channel.subscriptions ?? []).length > 0 && channel.subscriber === undefined) {
210
+ throw new ZLinkConfigurationException(
211
+ `Channel '${channelName}' fanout subscriptions require a subscriber capability.`
212
+ );
213
+ }
214
+ if (channel.noDrop !== undefined && typeof channel.noDrop !== 'boolean') {
215
+ throw new ZLinkConfigurationException(
216
+ `Channel '${channelName}' NoDrop must be a boolean.`
217
+ );
218
+ }
219
+ if (channel.noDrop !== undefined && channel.publisher === undefined) {
220
+ throw new ZLinkConfigurationException(
221
+ `Channel '${channelName}' NoDrop requires a publisher role.`
222
+ );
223
+ }
206
224
  if (channel.server !== undefined) {
207
225
  requireEndpoint(`channel '${channelName}' server`, channel.server.bind);
208
226
  validateListenerNetworkIdentity(
209
227
  `channel '${channelName}' server`,
210
- channel.server.bind,
211
228
  channel.server.bindHost,
212
229
  channel.server.advertiseHost
213
230
  );
@@ -221,7 +238,6 @@ function validateChannelCapabilities(
221
238
  requireEndpoint(`channel '${channelName}' publisher`, channel.publisher.bind);
222
239
  validateListenerNetworkIdentity(
223
240
  `channel '${channelName}' publisher`,
224
- channel.publisher.bind,
225
241
  channel.publisher.bindHost,
226
242
  channel.publisher.advertiseHost
227
243
  );
@@ -307,7 +323,6 @@ function validateSpotNodes(registration: ZLinkFrameworkRegistration): void {
307
323
  if (spotNode.router !== undefined) {
308
324
  validateListenerNetworkIdentity(
309
325
  `SpotNode '${spotNodeName}' router`,
310
- spotNode.router.bind,
311
326
  spotNode.router.bindHost,
312
327
  spotNode.router.advertiseHost
313
328
  );
@@ -418,11 +433,9 @@ function validateSpotNodeCapability(
418
433
 
419
434
  function validateListenerNetworkIdentity(
420
435
  capabilityName: string,
421
- bindEndpoint: string | undefined,
422
436
  configuredBindHost: string | undefined,
423
437
  advertiseHost: string | undefined
424
438
  ): void {
425
- const bindHost = configuredBindHost ?? tcpEndpointHost(bindEndpoint);
426
439
  if (configuredBindHost !== undefined) {
427
440
  requireName(`${capabilityName} bind host`, configuredBindHost);
428
441
  }
@@ -434,16 +447,6 @@ function validateListenerNetworkIdentity(
434
447
  );
435
448
  }
436
449
  }
437
- if (bindHost !== undefined && isWildcardHost(bindHost) && advertiseHost === undefined) {
438
- throw new ZLinkConfigurationException(
439
- `${capabilityName} must define an advertise host when its bind host is a wildcard address.`
440
- );
441
- }
442
- }
443
-
444
- function tcpEndpointHost(endpoint: string | undefined): string | undefined {
445
- const match = /^tcp:\/\/(\[[^\]]+\]|[^:]+):\d+$/.exec(endpoint ?? '');
446
- return match?.[1];
447
450
  }
448
451
 
449
452
  function isWildcardHost(host: string): boolean {
@@ -550,7 +553,6 @@ function validateStreamNodes(registration: ZLinkFrameworkRegistration): void {
550
553
  requireEndpoint(`STREAM node '${streamNodeName}'`, streamNode.bind);
551
554
  validateListenerNetworkIdentity(
552
555
  `STREAM node '${streamNodeName}'`,
553
- streamNode.bind,
554
556
  streamNode.bindHost,
555
557
  streamNode.advertiseHost
556
558
  );
@@ -568,6 +568,7 @@ export interface ZLinkBackendRouterSocket extends ZLinkBackendConnectableSocket
568
568
 
569
569
  export interface ZLinkBackendPublisherSocket extends ZLinkBackendSocket {
570
570
  sendHighWaterMark: number;
571
+ noDrop: boolean;
571
572
  publish(topic: string, message: Message | readonly Message[]): void;
572
573
  }
573
574
 
@@ -1,5 +1,6 @@
1
1
  import { loadBinding } from '../node-backend-adapter';
2
- import { ZLinkBackendResultError } from '../runtime-values';
2
+ import { SubmitResult, ZLinkBackendResultError } from '../runtime-values';
3
+ import { requireOneWayCompletion, ZLinkSubmitStatus } from '../../messaging/submission-result';
3
4
 
4
5
  export type ZLinkBindingModule = typeof import('@zlink-systems/zlink');
5
6
  export const zlink = loadBinding() as ZLinkBindingModule;
@@ -80,7 +81,17 @@ export function submitBindingPublish(
80
81
  }
81
82
  current!.submit();
82
83
  } catch (error) {
83
- throw translateBindingResultError(error);
84
+ const translated = translateBindingResultError(error);
85
+ if (translated instanceof ZLinkBackendResultError
86
+ && translated.operation === 'submit'
87
+ && (translated.result === SubmitResult.Backpressured
88
+ || translated.result === SubmitResult.NotAdmitted)) {
89
+ requireOneWayCompletion(
90
+ { status: ZLinkSubmitStatus.Backpressured },
91
+ 'Classic fanout publish'
92
+ );
93
+ }
94
+ throw translated;
84
95
  }
85
96
  }
86
97
 
@@ -33,6 +33,7 @@ export function wrapSocket<T extends { close(): void }>(
33
33
  sendHwm?: number;
34
34
  recvHwm?: number;
35
35
  sendTimeout?: number;
36
+ noDrop?: boolean;
36
37
  maxMsgSize?: bigint;
37
38
  lastEndpoint?: string;
38
39
  };
@@ -116,6 +117,12 @@ export function wrapSocket<T extends { close(): void }>(
116
117
  set sendHighWaterMark(value: number) {
117
118
  requireSocketOptions(socket).sendHwm = value;
118
119
  },
120
+ get noDrop(): boolean {
121
+ return socket.options?.noDrop ?? false;
122
+ },
123
+ set noDrop(value: boolean) {
124
+ requireSocketOptions(socket).noDrop = value;
125
+ },
119
126
  get receiveHighWaterMark(): number {
120
127
  return Number(socket.options?.recvHwm ?? 0n);
121
128
  },
@@ -38,6 +38,7 @@ import {
38
38
  requireZLinkYieldTurn,
39
39
  type ZLinkSpotSerialTurn
40
40
  } from '../execution';
41
+ import { requirePublicFanoutTopic } from './fanout-service-wire';
41
42
  import {
42
43
  requestToSpotHandle,
43
44
  sendToSpotHandle,
@@ -128,6 +129,7 @@ export class DefaultZLinkFanoutClient implements ZLinkFanoutClient {
128
129
  const topic = hasExplicitTopic
129
130
  ? topicOrEvent as string
130
131
  : resolveFrameworkPacketName(event, undefined, 'Fanout');
132
+ requirePublicFanoutTopic(topic);
131
133
  const packetName = resolveFrameworkPacketName(event, undefined, 'Fanout');
132
134
  return new DefaultZLinkFanoutPublishCall(
133
135
  () => this.requirePublisherChannel(channelName),
@@ -4,6 +4,7 @@ import {
4
4
  type ZLinkClientServerServerDescriptor,
5
5
  type ZLinkFanoutPublisherDescriptor
6
6
  } from '../../contracts';
7
+ import type { ZLinkChannelOptions } from '../../contracts/Configuration/RegistrationTypes';
7
8
  import { ZLinkSocketNativeEventType } from '../diagnostics/internal-event-contracts';
8
9
  import {
9
10
  buildAdvertisedEndpoint,
@@ -942,8 +943,7 @@ export class ZLinkChannelSocketRegistry {
942
943
  }
943
944
  const subscriber = this.adapter.createSubscriberSocket(this.context);
944
945
  subscriber.setChannelName(channelName);
945
- subscriber.setSubscription('');
946
- subscriber.setSubscription(FANOUT_LIVENESS_TOPIC);
946
+ setFanoutSubscriptions(subscriber, channel.subscriptions);
947
947
  const monitor = this.monitoringAdapter.openSocketMonitor(subscriber);
948
948
  const connection: FanoutPublisherConnection = {
949
949
  channelName,
@@ -1166,6 +1166,8 @@ export class ZLinkChannelSocketRegistry {
1166
1166
  const payload = RuntimeMessage.from(FANOUT_LIVENESS_PAYLOAD);
1167
1167
  try {
1168
1168
  publisher.publish(FANOUT_LIVENESS_TOPIC, payload);
1169
+ } catch (error) {
1170
+ this.oneWayFailureSink?.(error);
1169
1171
  } finally {
1170
1172
  payload.close();
1171
1173
  }
@@ -1566,6 +1568,7 @@ export class ZLinkChannelSocketRegistry {
1566
1568
 
1567
1569
  const publisher = this.adapter.createPublisherSocket(this.context);
1568
1570
  publisher.setChannelName(channelName);
1571
+ applyFanoutPublisherSocketOptions(publisher, channel);
1569
1572
  publisher.bind(channel.publisher.bind);
1570
1573
  this.publishers.set(channelName, publisher);
1571
1574
  this.fanoutPublisherNextBeacon.set(
@@ -1780,6 +1783,16 @@ function closeMessages(parts: readonly Message[]): void {
1780
1783
  for (const part of parts) part.close();
1781
1784
  }
1782
1785
 
1786
+ function setFanoutSubscriptions(
1787
+ subscriber: ZLinkBackendSubscriberSocket,
1788
+ applicationTopics: readonly string[] | undefined
1789
+ ): void {
1790
+ const topics = new Set(applicationTopics);
1791
+ if (topics.size === 0) topics.add('');
1792
+ topics.add(FANOUT_LIVENESS_TOPIC);
1793
+ for (const topic of topics) subscriber.setSubscription(topic);
1794
+ }
1795
+
1783
1796
  function deriveRoutingId(baseRoutingId: string, suffix: string): string {
1784
1797
  const derived = `${baseRoutingId}\0${suffix}`;
1785
1798
  if (Buffer.byteLength(derived, 'utf8') > 255) {
@@ -1790,6 +1803,13 @@ function deriveRoutingId(baseRoutingId: string, suffix: string): string {
1790
1803
  return derived;
1791
1804
  }
1792
1805
 
1806
+ function applyFanoutPublisherSocketOptions(
1807
+ publisher: ZLinkBackendPublisherSocket,
1808
+ channel: ZLinkChannelOptions
1809
+ ): void {
1810
+ publisher.noDrop = channel.noDrop ?? false;
1811
+ }
1812
+
1793
1813
  function fanoutDiscoveryConnectionId(connectionId: string): string {
1794
1814
  return `fanout:${connectionId.replaceAll('\0', '/')}`;
1795
1815
  }
@@ -1,9 +1,12 @@
1
1
  import type { Message } from '../../contracts/Common/Message';
2
- import { ZLinkConfigurationException } from '../configuration';
2
+ import {
3
+ FANOUT_LIVENESS_TOPIC,
4
+ requirePublicFanoutTopic
5
+ } from '../../contracts/Configuration/FanoutTopic';
3
6
  import { tryDecodeChannelHeader } from './channel-envelope-inspection';
4
7
  import type { ZLinkChannelEnvelopeHeader } from './channel-envelope';
5
8
 
6
- export const FANOUT_LIVENESS_TOPIC = '\x01ZLF1';
9
+ export { FANOUT_LIVENESS_TOPIC, requirePublicFanoutTopic };
7
10
  export const FANOUT_LIVENESS_PAYLOAD =
8
11
  Uint8Array.from([0x5a, 0x46, 0x01, 0x01]);
9
12
 
@@ -35,9 +38,3 @@ export function inspectFanoutInbound(
35
38
  ? { kind: 'beacon' }
36
39
  : { kind: 'protocolError' };
37
40
  }
38
-
39
- export function requirePublicFanoutTopic(topic: string): void {
40
- if (topic === FANOUT_LIVENESS_TOPIC) {
41
- throw new ZLinkConfigurationException('Fanout topic is reserved for framework liveness.');
42
- }
43
- }
@@ -273,7 +273,15 @@ export class ZLinkInMemoryAuthorityStore {
273
273
  if (current.snapshot.allocation.state === 'active') {
274
274
  return { kind: 'alreadyExists', current: this.snapshot(current.snapshot) };
275
275
  }
276
- return { kind: 'conflict', current: this.read(key) };
276
+ // An unfinished creation whose owner lease ended is cancellable.
277
+ // Reclaim it so the key does not stay blocked forever.
278
+ if (this.isOwnerLive(current.snapshot) || current.creation === undefined) {
279
+ return { kind: 'conflict', current: this.read(key) };
280
+ }
281
+ this.adjustCapacity(this.pendingCapacity, current.snapshot.allocation, -1);
282
+ this.rows.delete(key);
283
+ this.creationTerminals.set(current.creation.reservationId, 'aborted');
284
+ this.scanRevision++;
277
285
  }
278
286
  const target = creationTarget(request);
279
287
  if (!this.validation.isTargetLive(target.descriptor, target.lifecycleGeneration, target.owner)) {
@@ -1082,9 +1082,62 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1082
1082
  ) {
1083
1083
  return { kind: 'typeMismatch', current: snapshot };
1084
1084
  }
1085
- return snapshot.allocation.state === 'active'
1086
- ? { kind: 'alreadyExists', current: snapshot }
1087
- : { kind: 'conflict', current: snapshot };
1085
+ if (snapshot.allocation.state === 'active') {
1086
+ return { kind: 'alreadyExists', current: snapshot };
1087
+ }
1088
+ if (record.aggregate !== undefined
1089
+ || isCanonicalAuthorityPayload(record.snapshot.payload)) {
1090
+ // A relocation record carries its own recovery protocol. Reserve
1091
+ // may reclaim only an unfinished plain creation.
1092
+ return { kind: 'conflict', current: snapshot };
1093
+ }
1094
+ // The row is Reserved by another owner. An unfinished creation
1095
+ // whose owner lease ended is cancellable, so reclaim it and retry
1096
+ // instead of blocking the key forever.
1097
+ const staleOwnerKey = ownerKey(snapshot.ownerId);
1098
+ const staleCapacityKey = capacityKey(
1099
+ snapshot.allocation.descriptor.meshName,
1100
+ String(snapshot.allocation.descriptor.rid)
1101
+ );
1102
+ const [staleOwnerRead, staleCapacityRead] = await Promise.all([
1103
+ this.provider.read(staleOwnerKey, signal),
1104
+ this.provider.read(staleCapacityKey, signal)
1105
+ ]);
1106
+ if (staleOwnerRead.kind === 'found'
1107
+ && staleOwnerRead.value.expiresAt === undefined) {
1108
+ // A lease is always written with a positive TTL, so a missing
1109
+ // expiry is a corrupt record. Reclaiming deletes authority state,
1110
+ // so refuse rather than read the absence as expiry.
1111
+ throw new Error('Location Store owner lease record is invalid.');
1112
+ }
1113
+ if (sameLiveOwner(staleOwnerRead, record.snapshot)) {
1114
+ return { kind: 'conflict', current: snapshot };
1115
+ }
1116
+ const staleCapacity = staleCapacityRead.kind === 'missing'
1117
+ ? emptyCapacityRecord()
1118
+ : decodeJson<CapacityRecord>(staleCapacityRead.value.bytes);
1119
+ await this.provider.write({
1120
+ conditions: [
1121
+ { kind: 'version', key: rowKey, expected: current.value.version },
1122
+ versionCondition(staleOwnerKey, staleOwnerRead),
1123
+ conditionFor(staleCapacityKey, staleCapacityRead)
1124
+ ],
1125
+ mutations: [
1126
+ { kind: 'delete', key: rowKey },
1127
+ {
1128
+ kind: 'put',
1129
+ key: staleCapacityKey,
1130
+ bytes: encodeJson({
1131
+ active: staleCapacity.active,
1132
+ pending: subtractCapacity(
1133
+ staleCapacity.pending,
1134
+ record.snapshot.allocation.capacity
1135
+ )
1136
+ } satisfies CapacityRecord)
1137
+ }
1138
+ ]
1139
+ }, signal);
1140
+ continue;
1088
1141
  }
1089
1142
  const descriptor = liveTargetDescriptor(
1090
1143
  descriptorRead,
@@ -1291,6 +1344,12 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1291
1344
  this.provider.read(leaseKey, signal),
1292
1345
  this.provider.read(capacityRowKey, signal)
1293
1346
  ]);
1347
+ if (leaseRead.kind === 'found' && leaseRead.value.expiresAt === undefined) {
1348
+ // A lease is always written with a positive TTL, so a missing expiry
1349
+ // is a corrupt record. Abort deletes authority state, so refuse
1350
+ // rather than read the absence as expiry.
1351
+ throw new Error('Location Store owner lease record is invalid.');
1352
+ }
1294
1353
  if (!sameLiveOwner(leaseRead, record.snapshot)) return { kind: 'stale' };
1295
1354
  const capacity = capacityRead.kind === 'missing'
1296
1355
  ? emptyCapacityRecord()
@@ -3879,6 +3938,17 @@ function sameCreationTarget(
3879
3938
  && snapshot.ownerLeaseGeneration === target.owner.leaseGeneration;
3880
3939
  }
3881
3940
 
3941
+ const CANONICAL_AUTHORITY_MAGIC = Buffer.from('ZLAU');
3942
+
3943
+ function isCanonicalAuthorityPayload(payload: Uint8Array): boolean {
3944
+ return payload.byteLength >= CANONICAL_AUTHORITY_MAGIC.byteLength
3945
+ && Buffer.from(
3946
+ payload.buffer,
3947
+ payload.byteOffset,
3948
+ CANONICAL_AUTHORITY_MAGIC.byteLength
3949
+ ).equals(CANONICAL_AUTHORITY_MAGIC);
3950
+ }
3951
+
3882
3952
  function sameLiveOwner(
3883
3953
  lease: ZLinkStoreReadResult,
3884
3954
  snapshot: StoredAuthoritySnapshot
@@ -16,7 +16,11 @@ import {
16
16
  ZLinkMessage
17
17
  } from '../../contracts';
18
18
  import type { Message } from '../../contracts/Common/Message';
19
- import type { ZLinkFrameworkRegistration } from '../configuration';
19
+ import {
20
+ buildAdvertisedEndpoint,
21
+ ZLinkConfigurationException,
22
+ type ZLinkFrameworkRegistration
23
+ } from '../configuration';
20
24
  import { ZLinkDispatchErrorReporter } from '../channels';
21
25
  import type {
22
26
  ZLinkBackendAdapterFactory,
@@ -178,6 +182,7 @@ export interface ZLinkStreamRuntimeManagerOptions {
178
182
 
179
183
  interface ZLinkStartedStreamNode {
180
184
  readonly meshName?: string;
185
+ readonly advertisedEndpoint: string;
181
186
  readonly runtime: ZLinkStreamSessionNodeRuntimeCore;
182
187
  readonly socket: ZLinkBackendStreamSocket;
183
188
  readonly monitor: ZLinkBackendSocketMonitor;
@@ -220,6 +225,17 @@ export class ZLinkStreamRuntimeManager {
220
225
  );
221
226
  }
222
227
  socket.bind(streamNode.bind!);
228
+ const boundEndpoint = socket.lastEndpoint ?? streamNode.bind!;
229
+ const advertisedEndpoint = buildAdvertisedEndpoint(
230
+ boundEndpoint,
231
+ streamNode.advertiseHost,
232
+ 'tcp'
233
+ );
234
+ if (advertisedEndpoint === undefined) {
235
+ throw new ZLinkConfigurationException(
236
+ `STREAM node '${nodeName}' advertised host requires a TCP endpoint, received '${boundEndpoint}'.`
237
+ );
238
+ }
223
239
  const readablePoller = streamAdapter.createReadablePoller(socket);
224
240
  const nativeSessionRoutes = new Map<string, {
225
241
  readonly service: StreamSessionService;
@@ -288,6 +304,7 @@ export class ZLinkStreamRuntimeManager {
288
304
  runtime.start();
289
305
  this.nodes.set(nodeName, {
290
306
  meshName: applicationMeshName,
307
+ advertisedEndpoint,
291
308
  runtime,
292
309
  socket,
293
310
  monitor,