@zlink-systems/framework 0.14.0 → 0.16.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts/Configuration/Builders.d.ts +2 -0
- package/dist/contracts/Configuration/EndpointNotation.d.ts +3 -3
- package/dist/contracts/Configuration/EndpointNotation.js +24 -7
- package/dist/contracts/Configuration/FanoutTopic.d.ts +2 -0
- package/dist/contracts/Configuration/FanoutTopic.js +14 -0
- package/dist/contracts/Configuration/RegistrationBuilders.js +13 -0
- package/dist/contracts/Configuration/RegistrationTypes.d.ts +2 -0
- package/dist/contracts/Configuration/RegistrationValidators.js +19 -14
- package/dist/runtime/backend/contracts/index.d.ts +1 -0
- package/dist/runtime/backend/node/node-backend-adapter-support.js +9 -1
- package/dist/runtime/backend/node/node-socket-backend-adapter.js +6 -0
- package/dist/runtime/channels/channel-clients.js +2 -0
- package/dist/runtime/channels/channel-socket-registry.js +16 -2
- package/dist/runtime/channels/channel-transports.d.ts +2 -0
- package/dist/runtime/channels/channel-transports.js +23 -13
- package/dist/runtime/channels/fanout-service-wire.d.ts +2 -2
- package/dist/runtime/channels/fanout-service-wire.js +5 -10
- package/dist/runtime/locations/in-memory-authority-store.js +9 -1
- package/dist/runtime/locations/location-store-repository.js +61 -3
- package/dist/runtime/streams/index.d.ts +1 -1
- package/dist/runtime/streams/index.js +7 -0
- package/package.json +3 -3
- package/src/contracts/Configuration/Builders.ts +2 -0
- package/src/contracts/Configuration/EndpointNotation.ts +23 -6
- package/src/contracts/Configuration/FanoutTopic.ts +12 -0
- package/src/contracts/Configuration/RegistrationBuilders.ts +17 -0
- package/src/contracts/Configuration/RegistrationTypes.ts +2 -0
- package/src/contracts/Configuration/RegistrationValidators.ts +19 -17
- package/src/runtime/backend/contracts/index.ts +1 -0
- package/src/runtime/backend/node/node-backend-adapter-support.ts +13 -2
- package/src/runtime/backend/node/node-socket-backend-adapter.ts +7 -0
- package/src/runtime/channels/channel-clients.ts +2 -0
- package/src/runtime/channels/channel-socket-registry.ts +22 -2
- package/src/runtime/channels/channel-transports.ts +11 -2
- package/src/runtime/channels/fanout-service-wire.ts +5 -8
- package/src/runtime/locations/in-memory-authority-store.ts +9 -1
- package/src/runtime/locations/location-store-repository.ts +73 -3
- package/src/runtime/streams/index.ts +18 -1
|
@@ -184,7 +184,7 @@ export function parseEndpointHostPort(
|
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
const ADVERTISE_SOURCE_PATTERN = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/(
|
|
187
|
+
const ADVERTISE_SOURCE_PATTERN = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/(\[[^\]]+\]|[^:]+):(\d+)$/;
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* Builds a normalized `scheme://advertiseHost:port` endpoint from a bound
|
|
@@ -198,16 +198,29 @@ const ADVERTISE_SOURCE_PATTERN = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/(?:\[[^\]]+\]|[
|
|
|
198
198
|
* endpoint, or (when `restrictToScheme` is given) when its scheme doesn't
|
|
199
199
|
* match after lowercasing -- callers translate that into their own
|
|
200
200
|
* capability-specific `ZLinkConfigurationException` message. Returns
|
|
201
|
-
* `boundEndpoint` normalized
|
|
202
|
-
*
|
|
203
|
-
*
|
|
201
|
+
* `boundEndpoint` normalized when `advertiseHost` is undefined, except that
|
|
202
|
+
* wildcard TCP hosts use the same-family loopback address. This keeps the
|
|
203
|
+
* bind-only default connectable without advertising a wildcard address.
|
|
204
204
|
*/
|
|
205
205
|
export function buildAdvertisedEndpoint(
|
|
206
206
|
boundEndpoint: string,
|
|
207
207
|
advertiseHost: string | undefined,
|
|
208
208
|
restrictToScheme?: string
|
|
209
209
|
): string | undefined {
|
|
210
|
-
if (advertiseHost === undefined)
|
|
210
|
+
if (advertiseHost === undefined) {
|
|
211
|
+
const normalized = normalizeEndpoint(boundEndpoint);
|
|
212
|
+
const match = ADVERTISE_SOURCE_PATTERN.exec(normalized);
|
|
213
|
+
if (match === null) return normalized;
|
|
214
|
+
const boundHost = unbracketHost(match[2]);
|
|
215
|
+
const defaultHost = boundHost === '0.0.0.0'
|
|
216
|
+
? '127.0.0.1'
|
|
217
|
+
: boundHost === '::'
|
|
218
|
+
? '::1'
|
|
219
|
+
: undefined;
|
|
220
|
+
if (defaultHost === undefined) return normalized;
|
|
221
|
+
const host = defaultHost.includes(':') ? `[${defaultHost}]` : defaultHost;
|
|
222
|
+
return normalizeEndpoint(`${match[1].toLowerCase()}://${host}:${match[3]}`);
|
|
223
|
+
}
|
|
211
224
|
const match = ADVERTISE_SOURCE_PATTERN.exec(boundEndpoint);
|
|
212
225
|
if (match === null) return undefined;
|
|
213
226
|
const scheme = match[1].toLowerCase();
|
|
@@ -215,7 +228,11 @@ export function buildAdvertisedEndpoint(
|
|
|
215
228
|
const host = advertiseHost.includes(':') && !advertiseHost.startsWith('[')
|
|
216
229
|
? `[${advertiseHost}]`
|
|
217
230
|
: advertiseHost;
|
|
218
|
-
return normalizeEndpoint(`${scheme}://${host}:${match[
|
|
231
|
+
return normalizeEndpoint(`${scheme}://${host}:${match[3]}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function unbracketHost(host: string): string {
|
|
235
|
+
return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
function normalizeSuffix(suffix: string): string {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ZLinkConfigurationException } from './ConfigurationException';
|
|
2
|
+
|
|
3
|
+
export const FANOUT_LIVENESS_TOPIC = '\x01ZLF1';
|
|
4
|
+
|
|
5
|
+
export function requirePublicFanoutTopic(topic: string): void {
|
|
6
|
+
if (typeof topic !== 'string') {
|
|
7
|
+
throw new ZLinkConfigurationException('Fanout topic must be a string.');
|
|
8
|
+
}
|
|
9
|
+
if (topic.startsWith(FANOUT_LIVENESS_TOPIC)) {
|
|
10
|
+
throw new ZLinkConfigurationException('Fanout topic is reserved for framework liveness.');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
}
|
|
@@ -1120,12 +1120,21 @@ function mapMeshSubmissionError(error: unknown, operation: string): Error {
|
|
|
1120
1120
|
);
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
|
-
function meshRequestFailure(meshName: string, result: number, nativeErrno: number): ZLinkFrameworkException {
|
|
1123
|
+
export function meshRequestFailure(meshName: string, result: number, nativeErrno: number): ZLinkFrameworkException {
|
|
1124
1124
|
const canonical = isCanonicalWireReplyTerminal(result, nativeErrno);
|
|
1125
1125
|
const wireKind = canonical
|
|
1126
1126
|
? internalFrameworkErrorKindFromWireReply(result, nativeErrno)
|
|
1127
1127
|
: undefined;
|
|
1128
|
-
|
|
1128
|
+
// NotFound without an errno is the selection itself coming back empty: the
|
|
1129
|
+
// send path and its connection are there, and applying eligibility and drain
|
|
1130
|
+
// left no member to pick. The spec names that Unavailable, not NotFound, and
|
|
1131
|
+
// not the protocol error a non-canonical pair would otherwise produce
|
|
1132
|
+
// (06-framework-api "no eligible select-one member"). A named target that
|
|
1133
|
+
// does not exist arrives with an errno and still ends as NotFound.
|
|
1134
|
+
const noEligibleMember = result === RequestResult.NotFound && nativeErrno === 0;
|
|
1135
|
+
const kind: ZLinkFrameworkInternalErrorKindType = noEligibleMember
|
|
1136
|
+
? ZLinkFrameworkInternalErrorKind.RouteNotConnected
|
|
1137
|
+
: !canonical
|
|
1129
1138
|
? ZLinkFrameworkInternalErrorKind.RequestProtocolError
|
|
1130
1139
|
: result === RequestResult.NotFound
|
|
1131
1140
|
? ZLinkFrameworkInternalErrorKind.RequestTargetNotFound
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Message } from '../../contracts/Common/Message';
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
|
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,
|