@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.
- 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/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/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
|
@@ -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
|
}
|
|
@@ -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,
|