@zlink-systems/framework 0.17.0 → 0.18.1
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/runtime/host/user-spot-creation-coordinator.d.ts +2 -2
- package/dist/runtime/host/user-spot-creation-coordinator.js +18 -31
- package/dist/runtime/streams/stream-session-runtime.d.ts +3 -1
- package/dist/runtime/streams/stream-session-runtime.js +58 -5
- package/package.json +3 -3
- package/src/runtime/host/user-spot-creation-coordinator.ts +22 -49
- package/src/runtime/streams/stream-session-runtime.ts +63 -6
|
@@ -65,5 +65,5 @@ export declare class ZLinkUserSpotCreationCoordinator {
|
|
|
65
65
|
private awaitReady;
|
|
66
66
|
private remoteCreate;
|
|
67
67
|
}
|
|
68
|
-
export declare function encodeLocationCreationContent(payload: Uint8Array
|
|
69
|
-
export declare function decodeLocationCreationContent(reference: string, expectedSha256
|
|
68
|
+
export declare function encodeLocationCreationContent(payload: Uint8Array): string;
|
|
69
|
+
export declare function decodeLocationCreationContent(reference: string, expectedSha256: Uint8Array, expectedEncodedSize: bigint): Buffer;
|
|
@@ -25,9 +25,8 @@ class ZLinkUserSpotCreationCoordinator {
|
|
|
25
25
|
const deadlineUnixMs = Date.now() + request.timeoutMs;
|
|
26
26
|
const signal = deadline.signal;
|
|
27
27
|
signal.throwIfAborted();
|
|
28
|
-
const checksum = crc32c(request.requestPayload);
|
|
29
28
|
const sha256 = (0, node_crypto_1.createHash)('sha256').update(request.requestPayload).digest();
|
|
30
|
-
const contentReference = encodeLocationCreationContent(request.requestPayload
|
|
29
|
+
const contentReference = encodeLocationCreationContent(request.requestPayload);
|
|
31
30
|
const excludedNodeRids = new Set();
|
|
32
31
|
let target;
|
|
33
32
|
let authorityTarget;
|
|
@@ -334,12 +333,7 @@ class ZLinkUserSpotCreationCoordinator {
|
|
|
334
333
|
|| pending.reservationId !== record.reservation.reservationId) {
|
|
335
334
|
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestFailed, 'Remote User Spot Pending creation projection is missing.');
|
|
336
335
|
}
|
|
337
|
-
const requestPayload = decodeLocationCreationContent(pending.requestContentReference);
|
|
338
|
-
if (BigInt(requestPayload.byteLength) !== pending.requestEncodedSize
|
|
339
|
-
|| !(0, node_crypto_1.createHash)('sha256').update(requestPayload).digest()
|
|
340
|
-
.equals(Buffer.from(pending.requestSha256))) {
|
|
341
|
-
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestFailed, 'Remote User Spot Pending creation content failed integrity validation.');
|
|
342
|
-
}
|
|
336
|
+
const requestPayload = decodeLocationCreationContent(pending.requestContentReference, pending.requestSha256, pending.requestEncodedSize);
|
|
343
337
|
let local;
|
|
344
338
|
try {
|
|
345
339
|
local = await materialize(requestPayload, current, signal ?? new AbortController().signal);
|
|
@@ -647,24 +641,27 @@ function localCreationRecord(request, snapshot, targetNodeRid, targetNodeGenerat
|
|
|
647
641
|
deadlineUnixMs: BigInt(deadlineUnixMs)
|
|
648
642
|
};
|
|
649
643
|
}
|
|
650
|
-
|
|
651
|
-
|
|
644
|
+
// 21-location-runtime.md#2.4: requestContentReference is `inline-v1:{base64url}`
|
|
645
|
+
// over `A-Z a-z 0-9 - _` with no `=` padding, and no other form is recognized.
|
|
646
|
+
// The same atomic record's requestEncodedSize and requestSha256 decide the
|
|
647
|
+
// content's integrity, so the reference carries no checksum segment of its own.
|
|
648
|
+
function encodeLocationCreationContent(payload) {
|
|
649
|
+
return `inline-v1:${Buffer.from(payload).toString('base64url')}`;
|
|
652
650
|
}
|
|
651
|
+
// Decodes the reference and verifies it against the same record's
|
|
652
|
+
// requestEncodedSize and requestSha256. A reference outside the specified form,
|
|
653
|
+
// a length mismatch or a digest mismatch all throw, and the caller must record
|
|
654
|
+
// the creation as failed without running the factory.
|
|
653
655
|
function decodeLocationCreationContent(reference, expectedSha256, expectedEncodedSize) {
|
|
654
|
-
const match = /^inline-v1:([
|
|
655
|
-
if (match === null) {
|
|
656
|
-
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestFailed, '
|
|
657
|
-
}
|
|
658
|
-
const payload = Buffer.from(match[2], 'base64url');
|
|
659
|
-
if (crc32c(payload) !== Number.parseInt(match[1], 16)) {
|
|
660
|
-
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestFailed, 'User Spot creation content checksum does not match.');
|
|
656
|
+
const match = /^inline-v1:([A-Za-z0-9_-]*)$/.exec(reference);
|
|
657
|
+
if (match === null || match[1].length % 4 === 1) {
|
|
658
|
+
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestFailed, 'Creation content reference is not in the inline-v1 form.');
|
|
661
659
|
}
|
|
662
|
-
|
|
663
|
-
|
|
660
|
+
const payload = Buffer.from(match[1], 'base64url');
|
|
661
|
+
if (BigInt(payload.byteLength) !== expectedEncodedSize) {
|
|
664
662
|
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestFailed, 'Creation content encoded size does not match its Pending reservation.');
|
|
665
663
|
}
|
|
666
|
-
if (expectedSha256
|
|
667
|
-
&& !(0, node_crypto_1.createHash)('sha256').update(payload).digest().equals(Buffer.from(expectedSha256))) {
|
|
664
|
+
if (!(0, node_crypto_1.createHash)('sha256').update(payload).digest().equals(Buffer.from(expectedSha256))) {
|
|
668
665
|
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestFailed, 'Creation content SHA-256 does not match its Pending reservation.');
|
|
669
666
|
}
|
|
670
667
|
return payload;
|
|
@@ -695,16 +692,6 @@ function userSpotAuthorityPayload(snapshot, spotId, state) {
|
|
|
695
692
|
ownerNodeGeneration: snapshot.allocation.descriptorLifecycleGeneration
|
|
696
693
|
});
|
|
697
694
|
}
|
|
698
|
-
function crc32c(payload) {
|
|
699
|
-
let crc = 0xffff_ffff;
|
|
700
|
-
for (const value of payload) {
|
|
701
|
-
crc ^= value;
|
|
702
|
-
for (let bit = 0; bit < 8; bit++) {
|
|
703
|
-
crc = (crc >>> 1) ^ (0x82f6_3b78 & -(crc & 1));
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return (~crc) >>> 0;
|
|
707
|
-
}
|
|
708
695
|
function remoteUserSpotFailure(terminalResult, failureCode, message) {
|
|
709
696
|
// Route every User Spot create terminal through the shared ownership-aware
|
|
710
697
|
// translator (spec 32-framework-error-model:81-118, 99-108) rather than an
|
|
@@ -131,6 +131,7 @@ export declare class ZLinkStreamSessionNodeRuntime {
|
|
|
131
131
|
private stopped;
|
|
132
132
|
private readonly receiveAbortController;
|
|
133
133
|
private receiveLoop;
|
|
134
|
+
private monitorLoop;
|
|
134
135
|
private readonly receiveWake;
|
|
135
136
|
private receiveWorkSinceYield;
|
|
136
137
|
constructor(options: ZLinkStreamSessionNodeRuntimeOptions & ZLinkStreamLivenessOptions);
|
|
@@ -140,11 +141,12 @@ export declare class ZLinkStreamSessionNodeRuntime {
|
|
|
140
141
|
findSession(routingId: unknown): ZLinkStreamSessionRuntime | undefined;
|
|
141
142
|
dispose(): Promise<void>;
|
|
142
143
|
drainCloseSessions(): Promise<void>;
|
|
144
|
+
private runMonitorLoop;
|
|
145
|
+
private monitorIdleDelay;
|
|
143
146
|
private runReceiveLoop;
|
|
144
147
|
private acceptPacket;
|
|
145
148
|
private takePacket;
|
|
146
149
|
private recyclePacket;
|
|
147
|
-
private drainMonitorEvents;
|
|
148
150
|
private handleMalformedPacket;
|
|
149
151
|
private isReceiveStopped;
|
|
150
152
|
private onMonitorEvent;
|
|
@@ -25,6 +25,9 @@ const ZLINK_STREAM_APPLICATION_IDLE_TIMEOUT_MS = 30_000;
|
|
|
25
25
|
const ZLINK_STREAM_RECEIVE_FRAME_BATCH_LIMIT = 64;
|
|
26
26
|
const ZLINK_STREAM_ACTOR_BINDING_REPLACEMENT_CALLBACK_TIMEOUT_MS = 30_000;
|
|
27
27
|
const ZLINK_STREAM_ACTOR_BINDING_REPLACEMENT_CLOSE_DELAY_MS = 100;
|
|
28
|
+
const ZLINK_STREAM_MONITOR_IDLE_MIN_DELAY_MS = 1;
|
|
29
|
+
const ZLINK_STREAM_MONITOR_IDLE_MAX_DELAY_MS = 20;
|
|
30
|
+
const ZLINK_STREAM_MONITOR_IDLE_MAX_STEPS = 7;
|
|
28
31
|
const systemLivenessClock = {
|
|
29
32
|
now: () => performance.now(),
|
|
30
33
|
setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
@@ -654,6 +657,7 @@ class ZLinkStreamSessionNodeRuntime {
|
|
|
654
657
|
stopped = false;
|
|
655
658
|
receiveAbortController = new AbortController();
|
|
656
659
|
receiveLoop;
|
|
660
|
+
monitorLoop;
|
|
657
661
|
receiveWake = new ZLinkStreamReceiveWake();
|
|
658
662
|
receiveWorkSinceYield = 0;
|
|
659
663
|
constructor(options) {
|
|
@@ -663,7 +667,8 @@ class ZLinkStreamSessionNodeRuntime {
|
|
|
663
667
|
if (this.stopped || this.receiveLoop !== undefined) {
|
|
664
668
|
return;
|
|
665
669
|
}
|
|
666
|
-
this.options.monitor
|
|
670
|
+
const monitor = this.options.monitor;
|
|
671
|
+
monitor?.onEvent(event => this.onMonitorEvent(event));
|
|
667
672
|
const running = this.runReceiveLoop(this.receiveAbortController.signal)
|
|
668
673
|
.catch((error) => {
|
|
669
674
|
if (!this.stopped) {
|
|
@@ -671,6 +676,14 @@ class ZLinkStreamSessionNodeRuntime {
|
|
|
671
676
|
}
|
|
672
677
|
});
|
|
673
678
|
this.receiveLoop = running;
|
|
679
|
+
if (monitor !== undefined) {
|
|
680
|
+
this.monitorLoop = this.runMonitorLoop(monitor, this.receiveAbortController.signal)
|
|
681
|
+
.catch((error) => {
|
|
682
|
+
if (!this.stopped) {
|
|
683
|
+
this.options.onError?.(error);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
674
687
|
}
|
|
675
688
|
markConnected(routingId, localAddr, remoteAddr) {
|
|
676
689
|
const session = this.getOrCreateSession(routingId);
|
|
@@ -689,6 +702,7 @@ class ZLinkStreamSessionNodeRuntime {
|
|
|
689
702
|
this.wakeReceiveLoop();
|
|
690
703
|
try {
|
|
691
704
|
await this.receiveLoop;
|
|
705
|
+
await this.monitorLoop;
|
|
692
706
|
const sessions = [...this.sessions.values()];
|
|
693
707
|
this.sessions.clear();
|
|
694
708
|
for (const packet of this.availablePackets.splice(0))
|
|
@@ -710,9 +724,47 @@ class ZLinkStreamSessionNodeRuntime {
|
|
|
710
724
|
async drainCloseSessions() {
|
|
711
725
|
await Promise.allSettled([...this.sessions.values()].map((session) => session.drainClose()));
|
|
712
726
|
}
|
|
727
|
+
// Spec server 04-session §2/§7: a session exists once the handshake succeeds,
|
|
728
|
+
// and connect/disconnect callbacks are its base surface. A stream node learns
|
|
729
|
+
// of both from the socket monitor, which is a source of its own with no
|
|
730
|
+
// event-loop readiness callback. Draining it from the receive loop would tie
|
|
731
|
+
// a connection event to inbound traffic, so a session that never sends - a
|
|
732
|
+
// push-only console - would stay unknown to the node until it happened to
|
|
733
|
+
// write something. The .NET runtime already drains the monitor in its own
|
|
734
|
+
// loop (ZLinkStreamNodeRuntime.RunMonitorLoopAsync); this is the same rule.
|
|
735
|
+
async runMonitorLoop(monitor, signal) {
|
|
736
|
+
let misses = 0;
|
|
737
|
+
while (!this.isReceiveStopped(signal)) {
|
|
738
|
+
if (monitor.drain() > 0) {
|
|
739
|
+
misses = 0;
|
|
740
|
+
// Yield so a burst of monitor events cannot starve the event loop.
|
|
741
|
+
await this.monitorIdleDelay(0, signal);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
misses = Math.min(misses + 1, ZLINK_STREAM_MONITOR_IDLE_MAX_STEPS);
|
|
745
|
+
await this.monitorIdleDelay(monitorIdleDelayMs(misses), signal);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
monitorIdleDelay(delayMs, signal) {
|
|
749
|
+
if (signal.aborted)
|
|
750
|
+
return Promise.resolve();
|
|
751
|
+
return new Promise((resolve) => {
|
|
752
|
+
const onAbort = () => {
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
resolve();
|
|
755
|
+
};
|
|
756
|
+
const timer = setTimeout(() => {
|
|
757
|
+
signal.removeEventListener('abort', onAbort);
|
|
758
|
+
resolve();
|
|
759
|
+
}, delayMs);
|
|
760
|
+
// The bound socket already holds the event loop open for this node. The
|
|
761
|
+
// idle poll must not become a second reason for a process to stay alive.
|
|
762
|
+
timer.unref();
|
|
763
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
764
|
+
});
|
|
765
|
+
}
|
|
713
766
|
async runReceiveLoop(signal) {
|
|
714
767
|
while (!this.isReceiveStopped(signal)) {
|
|
715
|
-
this.drainMonitorEvents();
|
|
716
768
|
let permit;
|
|
717
769
|
let packet;
|
|
718
770
|
let packetTransferred = false;
|
|
@@ -839,9 +891,6 @@ class ZLinkStreamSessionNodeRuntime {
|
|
|
839
891
|
if (!this.stopped)
|
|
840
892
|
this.availablePackets.push(packet);
|
|
841
893
|
}
|
|
842
|
-
drainMonitorEvents() {
|
|
843
|
-
this.options.monitor?.drain();
|
|
844
|
-
}
|
|
845
894
|
handleMalformedPacket(routingId, session, error) {
|
|
846
895
|
this.options.onError?.(error);
|
|
847
896
|
try {
|
|
@@ -1112,6 +1161,10 @@ class ZLinkStreamSessionNodeRuntime {
|
|
|
1112
1161
|
}
|
|
1113
1162
|
}
|
|
1114
1163
|
exports.ZLinkStreamSessionNodeRuntime = ZLinkStreamSessionNodeRuntime;
|
|
1164
|
+
function monitorIdleDelayMs(misses) {
|
|
1165
|
+
const scaled = ZLINK_STREAM_MONITOR_IDLE_MIN_DELAY_MS * 2 ** (misses - 1);
|
|
1166
|
+
return Math.min(scaled, ZLINK_STREAM_MONITOR_IDLE_MAX_DELAY_MS);
|
|
1167
|
+
}
|
|
1115
1168
|
function streamMonitorEndpointKey(localAddr, remoteAddr) {
|
|
1116
1169
|
return `${localAddr ?? ''}\n${remoteAddr ?? ''}`;
|
|
1117
1170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zlink-systems/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@opentelemetry/api": "^1.9.1",
|
|
21
21
|
"@opentelemetry/api-logs": "^0.221.0",
|
|
22
|
-
"@zlink-systems/stream-wire": "0.
|
|
23
|
-
"@zlink-systems/zlink": "1.2.
|
|
22
|
+
"@zlink-systems/stream-wire": "0.18.1",
|
|
23
|
+
"@zlink-systems/zlink": "1.2.1"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -104,9 +104,8 @@ export class ZLinkUserSpotCreationCoordinator {
|
|
|
104
104
|
const deadlineUnixMs = Date.now() + request.timeoutMs;
|
|
105
105
|
const signal = deadline.signal;
|
|
106
106
|
signal.throwIfAborted();
|
|
107
|
-
const checksum = crc32c(request.requestPayload);
|
|
108
107
|
const sha256 = createHash('sha256').update(request.requestPayload).digest();
|
|
109
|
-
const contentReference = encodeLocationCreationContent(request.requestPayload
|
|
108
|
+
const contentReference = encodeLocationCreationContent(request.requestPayload);
|
|
110
109
|
const excludedNodeRids = new Set<string>();
|
|
111
110
|
let target: Awaited<ReturnType<ZLinkUserSpotCreationCoordinatorOptions['target']>>;
|
|
112
111
|
let authorityTarget;
|
|
@@ -542,18 +541,10 @@ export class ZLinkUserSpotCreationCoordinator {
|
|
|
542
541
|
);
|
|
543
542
|
}
|
|
544
543
|
const requestPayload = decodeLocationCreationContent(
|
|
545
|
-
pending.requestContentReference
|
|
544
|
+
pending.requestContentReference,
|
|
545
|
+
pending.requestSha256,
|
|
546
|
+
pending.requestEncodedSize
|
|
546
547
|
);
|
|
547
|
-
if (
|
|
548
|
-
BigInt(requestPayload.byteLength) !== pending.requestEncodedSize
|
|
549
|
-
|| !createHash('sha256').update(requestPayload).digest()
|
|
550
|
-
.equals(Buffer.from(pending.requestSha256))
|
|
551
|
-
) {
|
|
552
|
-
throw createInternalFrameworkException(
|
|
553
|
-
ZLinkFrameworkInternalErrorKind.RequestFailed,
|
|
554
|
-
'Remote User Spot Pending creation content failed integrity validation.'
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
548
|
let local: ZLinkLocalSpotCreateResult | undefined;
|
|
558
549
|
try {
|
|
559
550
|
local = await materialize(
|
|
@@ -1002,45 +993,38 @@ function localCreationRecord(
|
|
|
1002
993
|
};
|
|
1003
994
|
}
|
|
1004
995
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
996
|
+
// 21-location-runtime.md#2.4: requestContentReference is `inline-v1:{base64url}`
|
|
997
|
+
// over `A-Z a-z 0-9 - _` with no `=` padding, and no other form is recognized.
|
|
998
|
+
// The same atomic record's requestEncodedSize and requestSha256 decide the
|
|
999
|
+
// content's integrity, so the reference carries no checksum segment of its own.
|
|
1000
|
+
export function encodeLocationCreationContent(payload: Uint8Array): string {
|
|
1001
|
+
return `inline-v1:${Buffer.from(payload).toString('base64url')}`;
|
|
1010
1002
|
}
|
|
1011
1003
|
|
|
1004
|
+
// Decodes the reference and verifies it against the same record's
|
|
1005
|
+
// requestEncodedSize and requestSha256. A reference outside the specified form,
|
|
1006
|
+
// a length mismatch or a digest mismatch all throw, and the caller must record
|
|
1007
|
+
// the creation as failed without running the factory.
|
|
1012
1008
|
export function decodeLocationCreationContent(
|
|
1013
1009
|
reference: string,
|
|
1014
|
-
expectedSha256
|
|
1015
|
-
expectedEncodedSize
|
|
1010
|
+
expectedSha256: Uint8Array,
|
|
1011
|
+
expectedEncodedSize: bigint
|
|
1016
1012
|
): Buffer {
|
|
1017
|
-
const match = /^inline-v1:([
|
|
1018
|
-
if (match === null) {
|
|
1019
|
-
throw createInternalFrameworkException(
|
|
1020
|
-
ZLinkFrameworkInternalErrorKind.RequestFailed,
|
|
1021
|
-
'User Spot creation content reference is invalid.'
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1024
|
-
const payload = Buffer.from(match[2]!, 'base64url');
|
|
1025
|
-
if (crc32c(payload) !== Number.parseInt(match[1]!, 16)) {
|
|
1013
|
+
const match = /^inline-v1:([A-Za-z0-9_-]*)$/.exec(reference);
|
|
1014
|
+
if (match === null || match[1]!.length % 4 === 1) {
|
|
1026
1015
|
throw createInternalFrameworkException(
|
|
1027
1016
|
ZLinkFrameworkInternalErrorKind.RequestFailed,
|
|
1028
|
-
'
|
|
1017
|
+
'Creation content reference is not in the inline-v1 form.'
|
|
1029
1018
|
);
|
|
1030
1019
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
&& BigInt(payload.byteLength) !== expectedEncodedSize
|
|
1034
|
-
) {
|
|
1020
|
+
const payload = Buffer.from(match[1]!, 'base64url');
|
|
1021
|
+
if (BigInt(payload.byteLength) !== expectedEncodedSize) {
|
|
1035
1022
|
throw createInternalFrameworkException(
|
|
1036
1023
|
ZLinkFrameworkInternalErrorKind.RequestFailed,
|
|
1037
1024
|
'Creation content encoded size does not match its Pending reservation.'
|
|
1038
1025
|
);
|
|
1039
1026
|
}
|
|
1040
|
-
if (
|
|
1041
|
-
expectedSha256 !== undefined
|
|
1042
|
-
&& !createHash('sha256').update(payload).digest().equals(Buffer.from(expectedSha256))
|
|
1043
|
-
) {
|
|
1027
|
+
if (!createHash('sha256').update(payload).digest().equals(Buffer.from(expectedSha256))) {
|
|
1044
1028
|
throw createInternalFrameworkException(
|
|
1045
1029
|
ZLinkFrameworkInternalErrorKind.RequestFailed,
|
|
1046
1030
|
'Creation content SHA-256 does not match its Pending reservation.'
|
|
@@ -1088,17 +1072,6 @@ function userSpotAuthorityPayload(
|
|
|
1088
1072
|
});
|
|
1089
1073
|
}
|
|
1090
1074
|
|
|
1091
|
-
function crc32c(payload: Uint8Array): number {
|
|
1092
|
-
let crc = 0xffff_ffff;
|
|
1093
|
-
for (const value of payload) {
|
|
1094
|
-
crc ^= value;
|
|
1095
|
-
for (let bit = 0; bit < 8; bit++) {
|
|
1096
|
-
crc = (crc >>> 1) ^ (0x82f6_3b78 & -(crc & 1));
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
return (~crc) >>> 0;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
1075
|
function remoteUserSpotFailure(
|
|
1103
1076
|
terminalResult: number,
|
|
1104
1077
|
failureCode: number,
|
|
@@ -69,6 +69,9 @@ const ZLINK_STREAM_APPLICATION_IDLE_TIMEOUT_MS = 30_000;
|
|
|
69
69
|
const ZLINK_STREAM_RECEIVE_FRAME_BATCH_LIMIT = 64;
|
|
70
70
|
const ZLINK_STREAM_ACTOR_BINDING_REPLACEMENT_CALLBACK_TIMEOUT_MS = 30_000;
|
|
71
71
|
const ZLINK_STREAM_ACTOR_BINDING_REPLACEMENT_CLOSE_DELAY_MS = 100;
|
|
72
|
+
const ZLINK_STREAM_MONITOR_IDLE_MIN_DELAY_MS = 1;
|
|
73
|
+
const ZLINK_STREAM_MONITOR_IDLE_MAX_DELAY_MS = 20;
|
|
74
|
+
const ZLINK_STREAM_MONITOR_IDLE_MAX_STEPS = 7;
|
|
72
75
|
|
|
73
76
|
interface ZLinkStreamLivenessClock {
|
|
74
77
|
now(): number;
|
|
@@ -884,6 +887,7 @@ export class ZLinkStreamSessionNodeRuntime {
|
|
|
884
887
|
private stopped = false;
|
|
885
888
|
private readonly receiveAbortController = new AbortController();
|
|
886
889
|
private receiveLoop: Promise<void> | undefined;
|
|
890
|
+
private monitorLoop: Promise<void> | undefined;
|
|
887
891
|
private readonly receiveWake = new ZLinkStreamReceiveWake();
|
|
888
892
|
private receiveWorkSinceYield = 0;
|
|
889
893
|
constructor(
|
|
@@ -894,7 +898,8 @@ export class ZLinkStreamSessionNodeRuntime {
|
|
|
894
898
|
if (this.stopped || this.receiveLoop !== undefined) {
|
|
895
899
|
return;
|
|
896
900
|
}
|
|
897
|
-
this.options.monitor
|
|
901
|
+
const monitor = this.options.monitor;
|
|
902
|
+
monitor?.onEvent(event => this.onMonitorEvent(event));
|
|
898
903
|
const running = this.runReceiveLoop(this.receiveAbortController.signal)
|
|
899
904
|
.catch((error) => {
|
|
900
905
|
if (!this.stopped) {
|
|
@@ -902,6 +907,14 @@ export class ZLinkStreamSessionNodeRuntime {
|
|
|
902
907
|
}
|
|
903
908
|
});
|
|
904
909
|
this.receiveLoop = running;
|
|
910
|
+
if (monitor !== undefined) {
|
|
911
|
+
this.monitorLoop = this.runMonitorLoop(monitor, this.receiveAbortController.signal)
|
|
912
|
+
.catch((error) => {
|
|
913
|
+
if (!this.stopped) {
|
|
914
|
+
this.options.onError?.(error);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
905
918
|
}
|
|
906
919
|
|
|
907
920
|
markConnected(routingId: unknown, localAddr?: string, remoteAddr?: string): void {
|
|
@@ -924,6 +937,7 @@ export class ZLinkStreamSessionNodeRuntime {
|
|
|
924
937
|
this.wakeReceiveLoop();
|
|
925
938
|
try {
|
|
926
939
|
await this.receiveLoop;
|
|
940
|
+
await this.monitorLoop;
|
|
927
941
|
const sessions = [...this.sessions.values()];
|
|
928
942
|
this.sessions.clear();
|
|
929
943
|
for (const packet of this.availablePackets.splice(0)) packet.close();
|
|
@@ -945,9 +959,51 @@ export class ZLinkStreamSessionNodeRuntime {
|
|
|
945
959
|
await Promise.allSettled([...this.sessions.values()].map((session) => session.drainClose()));
|
|
946
960
|
}
|
|
947
961
|
|
|
962
|
+
// Spec server 04-session §2/§7: a session exists once the handshake succeeds,
|
|
963
|
+
// and connect/disconnect callbacks are its base surface. A stream node learns
|
|
964
|
+
// of both from the socket monitor, which is a source of its own with no
|
|
965
|
+
// event-loop readiness callback. Draining it from the receive loop would tie
|
|
966
|
+
// a connection event to inbound traffic, so a session that never sends - a
|
|
967
|
+
// push-only console - would stay unknown to the node until it happened to
|
|
968
|
+
// write something. The .NET runtime already drains the monitor in its own
|
|
969
|
+
// loop (ZLinkStreamNodeRuntime.RunMonitorLoopAsync); this is the same rule.
|
|
970
|
+
private async runMonitorLoop(
|
|
971
|
+
monitor: ZLinkBackendSocketMonitor,
|
|
972
|
+
signal: AbortSignal
|
|
973
|
+
): Promise<void> {
|
|
974
|
+
let misses = 0;
|
|
975
|
+
while (!this.isReceiveStopped(signal)) {
|
|
976
|
+
if (monitor.drain() > 0) {
|
|
977
|
+
misses = 0;
|
|
978
|
+
// Yield so a burst of monitor events cannot starve the event loop.
|
|
979
|
+
await this.monitorIdleDelay(0, signal);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
misses = Math.min(misses + 1, ZLINK_STREAM_MONITOR_IDLE_MAX_STEPS);
|
|
983
|
+
await this.monitorIdleDelay(monitorIdleDelayMs(misses), signal);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private monitorIdleDelay(delayMs: number, signal: AbortSignal): Promise<void> {
|
|
988
|
+
if (signal.aborted) return Promise.resolve();
|
|
989
|
+
return new Promise((resolve) => {
|
|
990
|
+
const onAbort = (): void => {
|
|
991
|
+
clearTimeout(timer);
|
|
992
|
+
resolve();
|
|
993
|
+
};
|
|
994
|
+
const timer = setTimeout(() => {
|
|
995
|
+
signal.removeEventListener('abort', onAbort);
|
|
996
|
+
resolve();
|
|
997
|
+
}, delayMs);
|
|
998
|
+
// The bound socket already holds the event loop open for this node. The
|
|
999
|
+
// idle poll must not become a second reason for a process to stay alive.
|
|
1000
|
+
timer.unref();
|
|
1001
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
948
1005
|
private async runReceiveLoop(signal: AbortSignal): Promise<void> {
|
|
949
1006
|
while (!this.isReceiveStopped(signal)) {
|
|
950
|
-
this.drainMonitorEvents();
|
|
951
1007
|
let permit: ApplicationJobPermitPort | undefined;
|
|
952
1008
|
let packet: ZLinkBackendStreamPacket | undefined;
|
|
953
1009
|
let packetTransferred = false;
|
|
@@ -1078,10 +1134,6 @@ export class ZLinkStreamSessionNodeRuntime {
|
|
|
1078
1134
|
if (!this.stopped) this.availablePackets.push(packet);
|
|
1079
1135
|
}
|
|
1080
1136
|
|
|
1081
|
-
private drainMonitorEvents(): void {
|
|
1082
|
-
this.options.monitor?.drain();
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
1137
|
private handleMalformedPacket(
|
|
1086
1138
|
routingId: unknown,
|
|
1087
1139
|
session: ZLinkStreamSessionRuntime,
|
|
@@ -1387,6 +1439,11 @@ export class ZLinkStreamSessionNodeRuntime {
|
|
|
1387
1439
|
}
|
|
1388
1440
|
}
|
|
1389
1441
|
|
|
1442
|
+
function monitorIdleDelayMs(misses: number): number {
|
|
1443
|
+
const scaled = ZLINK_STREAM_MONITOR_IDLE_MIN_DELAY_MS * 2 ** (misses - 1);
|
|
1444
|
+
return Math.min(scaled, ZLINK_STREAM_MONITOR_IDLE_MAX_DELAY_MS);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1390
1447
|
function streamMonitorEndpointKey(localAddr: string | undefined, remoteAddr: string | undefined): string {
|
|
1391
1448
|
return `${localAddr ?? ''}\n${remoteAddr ?? ''}`;
|
|
1392
1449
|
}
|