@zlink-systems/stream-wire 0.10.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/LICENSE +105 -0
- package/dist/esm/index.d.ts +40 -0
- package/dist/esm/index.mjs +475 -0
- package/dist/esm/lz4-pickle.d.ts +3 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +377 -0
- package/dist/lz4-pickle.d.ts +3 -0
- package/dist/lz4-pickle.js +115 -0
- package/package.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 ZLink Systems
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ZLinkStreamWireFrame {
|
|
2
|
+
readonly header: Uint8Array;
|
|
3
|
+
readonly payload: Uint8Array;
|
|
4
|
+
}
|
|
5
|
+
/** Numeric payload codec values shared by browser and server wire paths. */
|
|
6
|
+
export declare enum ZlinkStreamCodec {
|
|
7
|
+
Raw = 0,
|
|
8
|
+
Json = 1,
|
|
9
|
+
MessagePack = 2,
|
|
10
|
+
Protobuf = 3
|
|
11
|
+
}
|
|
12
|
+
export interface ZLinkStreamWireHeader {
|
|
13
|
+
readonly kind: number;
|
|
14
|
+
readonly codec: number;
|
|
15
|
+
readonly flags: number;
|
|
16
|
+
readonly requestSeq?: bigint;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly metadata: ReadonlyMap<string, string>;
|
|
19
|
+
readonly correlationId?: string;
|
|
20
|
+
readonly flowId?: string;
|
|
21
|
+
readonly flowOrigin?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ZLinkStreamWireHeaderFlags {
|
|
24
|
+
readonly hasRequestSeq: number;
|
|
25
|
+
readonly hasMetadata: number;
|
|
26
|
+
readonly hasCorrelationId: number;
|
|
27
|
+
readonly hasFlowId: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const ZLINK_STREAM_FORMAT_MARKER = 242;
|
|
30
|
+
export declare function encodeStreamWireFrame(header: Uint8Array, payload: Uint8Array): Uint8Array;
|
|
31
|
+
export declare function decodeStreamWireFrame(frame: Uint8Array): ZLinkStreamWireFrame;
|
|
32
|
+
export declare function tryDecodeStreamWireFrame(frame: Uint8Array): ZLinkStreamWireFrame | undefined;
|
|
33
|
+
export declare function encodeStreamWireHeader(header: ZLinkStreamWireHeader, flags?: ZLinkStreamWireHeaderFlags): Uint8Array;
|
|
34
|
+
export declare function decodeStreamWireHeader(header: Uint8Array, flags?: ZLinkStreamWireHeaderFlags, includeFlow?: boolean): ZLinkStreamWireHeader;
|
|
35
|
+
export declare function encodeStreamWireMetadata(metadata: ReadonlyMap<string, string>): Uint8Array;
|
|
36
|
+
export declare function decodeStreamWireMetadata(metadata: Uint8Array): Map<string, string>;
|
|
37
|
+
export declare function lz4PickleUncompressed(payload: Uint8Array): Uint8Array;
|
|
38
|
+
export declare function lz4UnpicklePayload(payload: Uint8Array, maxDecompressedSize?: number): Uint8Array;
|
|
39
|
+
export declare function utf8Encode(value: string): Uint8Array;
|
|
40
|
+
export declare function utf8Decode(value: Uint8Array): string;
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// packages/stream-wire/src/lz4-pickle.ts
|
|
2
|
+
var defaultMaxDecompressedPayloadSize = 64 * 1024;
|
|
3
|
+
function lz4PickleUncompressed(payload) {
|
|
4
|
+
if (payload.length === 0) {
|
|
5
|
+
return new Uint8Array();
|
|
6
|
+
}
|
|
7
|
+
const pickled = new Uint8Array(payload.length + 1);
|
|
8
|
+
pickled[0] = 0;
|
|
9
|
+
pickled.set(payload, 1);
|
|
10
|
+
return pickled;
|
|
11
|
+
}
|
|
12
|
+
function lz4UnpicklePayload(payload, maxDecompressedSize = defaultMaxDecompressedPayloadSize) {
|
|
13
|
+
if (payload.length === 0) {
|
|
14
|
+
return new Uint8Array();
|
|
15
|
+
}
|
|
16
|
+
const header = payload[0];
|
|
17
|
+
if ((header & 7) !== 0) {
|
|
18
|
+
throw new Error("Unexpected LZ4 pickle version.");
|
|
19
|
+
}
|
|
20
|
+
const sizeOfDiff = decodeDiffSize(header >>> 6 & 3);
|
|
21
|
+
const dataOffset = 1 + sizeOfDiff;
|
|
22
|
+
if (payload.length < dataOffset) {
|
|
23
|
+
throw new Error("LZ4 pickle header is incomplete.");
|
|
24
|
+
}
|
|
25
|
+
const data = payload.subarray(dataOffset);
|
|
26
|
+
const resultDiff = sizeOfDiff === 0 ? 0 : readLittleEndian(payload, 1, sizeOfDiff);
|
|
27
|
+
const resultLength = data.length + resultDiff;
|
|
28
|
+
if (resultLength > maxDecompressedSize) {
|
|
29
|
+
throw new Error("LZ4 decoded payload exceeds maximum stream payload size.");
|
|
30
|
+
}
|
|
31
|
+
if (resultDiff === 0) {
|
|
32
|
+
return data.slice();
|
|
33
|
+
}
|
|
34
|
+
return decodeLz4Block(data, resultLength);
|
|
35
|
+
}
|
|
36
|
+
function decodeDiffSize(encoded) {
|
|
37
|
+
return encoded === 3 ? 4 : encoded;
|
|
38
|
+
}
|
|
39
|
+
function readLittleEndian(source, offset, size) {
|
|
40
|
+
if (size === 1) {
|
|
41
|
+
return source[offset];
|
|
42
|
+
}
|
|
43
|
+
if (size === 2) {
|
|
44
|
+
return source[offset] | source[offset + 1] << 8;
|
|
45
|
+
}
|
|
46
|
+
if (size === 4) {
|
|
47
|
+
return source[offset] | source[offset + 1] << 8 | source[offset + 2] << 16 | source[offset + 3] * 16777216;
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Unexpected LZ4 pickle field size: ${size}`);
|
|
50
|
+
}
|
|
51
|
+
function decodeLz4Block(source, resultLength) {
|
|
52
|
+
const target = new Uint8Array(resultLength);
|
|
53
|
+
let sourceOffset = 0;
|
|
54
|
+
let targetOffset = 0;
|
|
55
|
+
while (sourceOffset < source.length) {
|
|
56
|
+
const token = source[sourceOffset++];
|
|
57
|
+
const literalLength = readLz4Length(source, token >>> 4, () => sourceOffset++);
|
|
58
|
+
if (source.length - sourceOffset < literalLength) {
|
|
59
|
+
throw new Error("LZ4 literal run is incomplete.");
|
|
60
|
+
}
|
|
61
|
+
if (target.length - targetOffset < literalLength) {
|
|
62
|
+
throw new Error("LZ4 literal run exceeds output size.");
|
|
63
|
+
}
|
|
64
|
+
target.set(source.subarray(sourceOffset, sourceOffset + literalLength), targetOffset);
|
|
65
|
+
sourceOffset += literalLength;
|
|
66
|
+
targetOffset += literalLength;
|
|
67
|
+
if (sourceOffset >= source.length) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (source.length - sourceOffset < 2) {
|
|
71
|
+
throw new Error("LZ4 match offset is incomplete.");
|
|
72
|
+
}
|
|
73
|
+
const matchOffset = source[sourceOffset] | source[sourceOffset + 1] << 8;
|
|
74
|
+
sourceOffset += 2;
|
|
75
|
+
if (matchOffset === 0 || matchOffset > targetOffset) {
|
|
76
|
+
throw new Error("LZ4 match offset is invalid.");
|
|
77
|
+
}
|
|
78
|
+
const matchLength = readLz4Length(source, token & 15, () => sourceOffset++) + 4;
|
|
79
|
+
if (target.length - targetOffset < matchLength) {
|
|
80
|
+
throw new Error("LZ4 match run exceeds output size.");
|
|
81
|
+
}
|
|
82
|
+
for (let index = 0; index < matchLength; index += 1) {
|
|
83
|
+
target[targetOffset + index] = target[targetOffset - matchOffset + index];
|
|
84
|
+
}
|
|
85
|
+
targetOffset += matchLength;
|
|
86
|
+
}
|
|
87
|
+
if (targetOffset !== resultLength) {
|
|
88
|
+
throw new Error("LZ4 decoded length does not match pickle header.");
|
|
89
|
+
}
|
|
90
|
+
return target;
|
|
91
|
+
}
|
|
92
|
+
function readLz4Length(source, nibble, nextOffset) {
|
|
93
|
+
let length = nibble;
|
|
94
|
+
if (length !== 15) {
|
|
95
|
+
return length;
|
|
96
|
+
}
|
|
97
|
+
for (; ; ) {
|
|
98
|
+
const offset = nextOffset();
|
|
99
|
+
if (offset >= source.length) {
|
|
100
|
+
throw new Error("LZ4 extended length is incomplete.");
|
|
101
|
+
}
|
|
102
|
+
const value = source[offset];
|
|
103
|
+
length += value;
|
|
104
|
+
if (value !== 255) {
|
|
105
|
+
return length;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// packages/stream-wire/src/index.ts
|
|
111
|
+
var ZlinkStreamCodec = /* @__PURE__ */ ((ZlinkStreamCodec2) => {
|
|
112
|
+
ZlinkStreamCodec2[ZlinkStreamCodec2["Raw"] = 0] = "Raw";
|
|
113
|
+
ZlinkStreamCodec2[ZlinkStreamCodec2["Json"] = 1] = "Json";
|
|
114
|
+
ZlinkStreamCodec2[ZlinkStreamCodec2["MessagePack"] = 2] = "MessagePack";
|
|
115
|
+
ZlinkStreamCodec2[ZlinkStreamCodec2["Protobuf"] = 3] = "Protobuf";
|
|
116
|
+
return ZlinkStreamCodec2;
|
|
117
|
+
})(ZlinkStreamCodec || {});
|
|
118
|
+
var defaultHeaderFlags = {
|
|
119
|
+
hasRequestSeq: 1,
|
|
120
|
+
hasMetadata: 2,
|
|
121
|
+
hasCorrelationId: 8,
|
|
122
|
+
hasFlowId: 16
|
|
123
|
+
};
|
|
124
|
+
var ZLINK_STREAM_FORMAT_MARKER = 242;
|
|
125
|
+
var ZLINK_STREAM_RESPONSE_KIND = 3;
|
|
126
|
+
var ZLINK_STREAM_ERROR_KIND = 4;
|
|
127
|
+
function encodeStreamWireFrame(header, payload) {
|
|
128
|
+
if (header.length > 65535) {
|
|
129
|
+
throw new Error("Stream header is too large.");
|
|
130
|
+
}
|
|
131
|
+
if (payload.length > 4294967295) {
|
|
132
|
+
throw new Error("Stream payload is too large.");
|
|
133
|
+
}
|
|
134
|
+
const frame = new Uint8Array(6 + header.length + payload.length);
|
|
135
|
+
writeUInt16BE(frame, 0, header.length);
|
|
136
|
+
writeUInt32BE(frame, 2, payload.length);
|
|
137
|
+
frame.set(header, 6);
|
|
138
|
+
frame.set(payload, 6 + header.length);
|
|
139
|
+
return frame;
|
|
140
|
+
}
|
|
141
|
+
function decodeStreamWireFrame(frame) {
|
|
142
|
+
if (frame.length < 6) {
|
|
143
|
+
throw new Error("Stream frame prefix is incomplete.");
|
|
144
|
+
}
|
|
145
|
+
const headerLength = readUInt16BE(frame, 0);
|
|
146
|
+
const payloadLength = readUInt32BE(frame, 2);
|
|
147
|
+
if (frame.length !== 6 + headerLength + payloadLength) {
|
|
148
|
+
throw new Error("Stream frame length does not match prefix.");
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
header: frame.slice(6, 6 + headerLength),
|
|
152
|
+
payload: frame.slice(6 + headerLength)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function tryDecodeStreamWireFrame(frame) {
|
|
156
|
+
if (frame.length < 6) {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
const headerLength = readUInt16BE(frame, 0);
|
|
160
|
+
const payloadLength = readUInt32BE(frame, 2);
|
|
161
|
+
if (frame.length !== 6 + headerLength + payloadLength) {
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
header: frame.slice(6, 6 + headerLength),
|
|
166
|
+
payload: frame.slice(6 + headerLength)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function encodeStreamWireHeader(header, flags = defaultHeaderFlags) {
|
|
170
|
+
const reply = isReplyKind(header.kind);
|
|
171
|
+
const packetName = reply ? "" : header.name;
|
|
172
|
+
if (!reply) validateStreamWirePacketName(packetName);
|
|
173
|
+
const nameBytes = utf8Encode(packetName);
|
|
174
|
+
const hasRequestSeq = header.requestSeq !== void 0;
|
|
175
|
+
const hasMetadata = header.metadata.size > 0;
|
|
176
|
+
const correlationBytes = header.correlationId !== void 0 && header.correlationId.length > 0 ? utf8Encode(header.correlationId) : void 0;
|
|
177
|
+
if (correlationBytes !== void 0 && correlationBytes.length > 255) {
|
|
178
|
+
throw new Error("Stream correlation id is too large.");
|
|
179
|
+
}
|
|
180
|
+
const hasCorrelation = correlationBytes !== void 0;
|
|
181
|
+
const hasFlow = header.flowId !== void 0 || header.flowOrigin !== void 0;
|
|
182
|
+
if (hasFlow && (header.flowId === void 0 || header.flowOrigin === void 0)) {
|
|
183
|
+
throw new Error("Stream flow id and origin must be provided together.");
|
|
184
|
+
}
|
|
185
|
+
if (header.flowId !== void 0) validateFlowId(header.flowId);
|
|
186
|
+
if (header.flowOrigin !== void 0 && ![1, 2, 3, 4].includes(header.flowOrigin)) {
|
|
187
|
+
throw new Error("Stream flow origin is invalid.");
|
|
188
|
+
}
|
|
189
|
+
let headerFlags = header.flags;
|
|
190
|
+
headerFlags = hasRequestSeq ? headerFlags | flags.hasRequestSeq : headerFlags & ~flags.hasRequestSeq;
|
|
191
|
+
headerFlags = hasMetadata ? headerFlags | flags.hasMetadata : headerFlags & ~flags.hasMetadata;
|
|
192
|
+
headerFlags = hasCorrelation ? headerFlags | flags.hasCorrelationId : headerFlags & ~flags.hasCorrelationId;
|
|
193
|
+
headerFlags = hasFlow ? headerFlags | flags.hasFlowId : headerFlags & ~flags.hasFlowId;
|
|
194
|
+
const metadataBytes = hasMetadata ? encodeStreamWireMetadata(header.metadata) : new Uint8Array();
|
|
195
|
+
const size = 4 + (hasRequestSeq ? 8 : 0) + 1 + nameBytes.length + (hasMetadata ? 2 + metadataBytes.length : 0) + (hasCorrelation ? 1 + correlationBytes.length : 0) + (hasFlow ? 37 : 0);
|
|
196
|
+
const buffer = new Uint8Array(size);
|
|
197
|
+
let offset = 0;
|
|
198
|
+
buffer[offset++] = ZLINK_STREAM_FORMAT_MARKER;
|
|
199
|
+
buffer[offset++] = header.kind;
|
|
200
|
+
buffer[offset++] = header.codec;
|
|
201
|
+
buffer[offset++] = headerFlags;
|
|
202
|
+
if (hasRequestSeq) {
|
|
203
|
+
if (header.requestSeq === 0n) {
|
|
204
|
+
throw new Error("Request sequence must not be zero.");
|
|
205
|
+
}
|
|
206
|
+
writeBigUInt64BE(buffer, offset, header.requestSeq);
|
|
207
|
+
offset += 8;
|
|
208
|
+
}
|
|
209
|
+
buffer[offset++] = nameBytes.length;
|
|
210
|
+
buffer.set(nameBytes, offset);
|
|
211
|
+
offset += nameBytes.length;
|
|
212
|
+
if (hasMetadata) {
|
|
213
|
+
writeUInt16BE(buffer, offset, metadataBytes.length);
|
|
214
|
+
offset += 2;
|
|
215
|
+
buffer.set(metadataBytes, offset);
|
|
216
|
+
offset += metadataBytes.length;
|
|
217
|
+
}
|
|
218
|
+
if (hasCorrelation) {
|
|
219
|
+
buffer[offset++] = correlationBytes.length;
|
|
220
|
+
buffer.set(correlationBytes, offset);
|
|
221
|
+
offset += correlationBytes.length;
|
|
222
|
+
}
|
|
223
|
+
if (hasFlow) {
|
|
224
|
+
buffer.set(asciiEncode(header.flowId), offset);
|
|
225
|
+
offset += 36;
|
|
226
|
+
buffer[offset++] = header.flowOrigin;
|
|
227
|
+
}
|
|
228
|
+
return buffer;
|
|
229
|
+
}
|
|
230
|
+
function decodeStreamWireHeader(header, flags = defaultHeaderFlags, includeFlow = true) {
|
|
231
|
+
let offset = 0;
|
|
232
|
+
if (header.length < 5) {
|
|
233
|
+
throw new Error("Stream header is incomplete.");
|
|
234
|
+
}
|
|
235
|
+
if (header[offset++] !== ZLINK_STREAM_FORMAT_MARKER) {
|
|
236
|
+
throw new Error("Stream header format marker is invalid.");
|
|
237
|
+
}
|
|
238
|
+
const kind = header[offset++];
|
|
239
|
+
const codec = header[offset++];
|
|
240
|
+
const headerFlags = header[offset++];
|
|
241
|
+
const hasRequestSeq = (headerFlags & flags.hasRequestSeq) !== 0;
|
|
242
|
+
const hasMetadata = (headerFlags & flags.hasMetadata) !== 0;
|
|
243
|
+
const hasCorrelation = (headerFlags & flags.hasCorrelationId) !== 0;
|
|
244
|
+
const hasFlow = (headerFlags & flags.hasFlowId) !== 0;
|
|
245
|
+
if ((headerFlags & ~31) !== 0) {
|
|
246
|
+
throw new Error("Unknown mandatory stream header flag.");
|
|
247
|
+
}
|
|
248
|
+
let requestSeq;
|
|
249
|
+
if (hasRequestSeq) {
|
|
250
|
+
if (header.length - offset < 8) {
|
|
251
|
+
throw new Error("Stream request sequence is incomplete.");
|
|
252
|
+
}
|
|
253
|
+
requestSeq = readBigUInt64BE(header, offset);
|
|
254
|
+
if (requestSeq === 0n) {
|
|
255
|
+
throw new Error("Request sequence must not be zero.");
|
|
256
|
+
}
|
|
257
|
+
offset += 8;
|
|
258
|
+
}
|
|
259
|
+
if (header.length - offset < 1) {
|
|
260
|
+
throw new Error("Stream packet name length is missing.");
|
|
261
|
+
}
|
|
262
|
+
const nameLength = header[offset++];
|
|
263
|
+
if (!isReplyKind(kind) && nameLength === 0 || header.length - offset < nameLength) {
|
|
264
|
+
throw new Error("Stream packet name is invalid.");
|
|
265
|
+
}
|
|
266
|
+
const decodedName = utf8Decode(header.subarray(offset, offset + nameLength));
|
|
267
|
+
const name = isReplyKind(kind) ? "" : decodedName;
|
|
268
|
+
offset += nameLength;
|
|
269
|
+
const decodedMetadata = hasMetadata ? decodeStreamWireHeaderMetadata(header, offset) : { metadata: /* @__PURE__ */ new Map(), offset };
|
|
270
|
+
offset = decodedMetadata.offset;
|
|
271
|
+
let correlationId;
|
|
272
|
+
if (hasCorrelation) {
|
|
273
|
+
if (header.length - offset < 1) {
|
|
274
|
+
throw new Error("Stream correlation id is incomplete.");
|
|
275
|
+
}
|
|
276
|
+
const correlationLength = header[offset++];
|
|
277
|
+
if (header.length - offset < correlationLength) {
|
|
278
|
+
throw new Error("Stream correlation id is incomplete.");
|
|
279
|
+
}
|
|
280
|
+
correlationId = utf8Decode(header.subarray(offset, offset + correlationLength));
|
|
281
|
+
offset += correlationLength;
|
|
282
|
+
}
|
|
283
|
+
let flowId;
|
|
284
|
+
let flowOrigin;
|
|
285
|
+
if (hasFlow) {
|
|
286
|
+
if (header.length - offset < 37) {
|
|
287
|
+
throw new Error("Stream flow fields are incomplete.");
|
|
288
|
+
}
|
|
289
|
+
if (includeFlow) {
|
|
290
|
+
flowId = asciiDecode(header.subarray(offset, offset + 36));
|
|
291
|
+
validateFlowId(flowId);
|
|
292
|
+
}
|
|
293
|
+
offset += 36;
|
|
294
|
+
const decodedFlowOrigin = header[offset++];
|
|
295
|
+
if (includeFlow && ![1, 2, 3, 4].includes(decodedFlowOrigin)) {
|
|
296
|
+
throw new Error("Stream flow origin is invalid.");
|
|
297
|
+
}
|
|
298
|
+
flowOrigin = includeFlow ? decodedFlowOrigin : void 0;
|
|
299
|
+
}
|
|
300
|
+
if (offset !== header.length) {
|
|
301
|
+
throw new Error("Stream header has trailing bytes.");
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
kind,
|
|
305
|
+
codec,
|
|
306
|
+
flags: headerFlags,
|
|
307
|
+
requestSeq,
|
|
308
|
+
name,
|
|
309
|
+
metadata: decodedMetadata.metadata,
|
|
310
|
+
correlationId,
|
|
311
|
+
flowId,
|
|
312
|
+
flowOrigin
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function validateFlowId(flowId) {
|
|
316
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(flowId)) {
|
|
317
|
+
throw new Error("Stream flow id must be a lowercase UUIDv7.");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function asciiEncode(value) {
|
|
321
|
+
return Uint8Array.from(value, (character) => character.charCodeAt(0));
|
|
322
|
+
}
|
|
323
|
+
function asciiDecode(value) {
|
|
324
|
+
if (value.some((byte) => byte > 127)) throw new Error("Stream flow id must be ASCII.");
|
|
325
|
+
return String.fromCharCode(...value);
|
|
326
|
+
}
|
|
327
|
+
function encodeStreamWireMetadata(metadata) {
|
|
328
|
+
if (metadata.size > 255) {
|
|
329
|
+
throw new Error("Metadata entry count must not exceed 255.");
|
|
330
|
+
}
|
|
331
|
+
let size = 1;
|
|
332
|
+
const encoded = [...metadata].map(([key, value]) => {
|
|
333
|
+
const keyBytes = utf8Encode(key);
|
|
334
|
+
const valueBytes = utf8Encode(value);
|
|
335
|
+
if (keyBytes.length === 0 || keyBytes.length > 255) {
|
|
336
|
+
throw new Error("Metadata key length is invalid.");
|
|
337
|
+
}
|
|
338
|
+
if (valueBytes.length > 65535) {
|
|
339
|
+
throw new Error("Metadata value is too large.");
|
|
340
|
+
}
|
|
341
|
+
size += 1 + keyBytes.length + 2 + valueBytes.length;
|
|
342
|
+
return { keyBytes, valueBytes };
|
|
343
|
+
});
|
|
344
|
+
const buffer = new Uint8Array(size);
|
|
345
|
+
let offset = 0;
|
|
346
|
+
buffer[offset++] = metadata.size;
|
|
347
|
+
for (const { keyBytes, valueBytes } of encoded) {
|
|
348
|
+
buffer[offset++] = keyBytes.length;
|
|
349
|
+
buffer.set(keyBytes, offset);
|
|
350
|
+
offset += keyBytes.length;
|
|
351
|
+
writeUInt16BE(buffer, offset, valueBytes.length);
|
|
352
|
+
offset += 2;
|
|
353
|
+
buffer.set(valueBytes, offset);
|
|
354
|
+
offset += valueBytes.length;
|
|
355
|
+
}
|
|
356
|
+
return buffer;
|
|
357
|
+
}
|
|
358
|
+
function decodeStreamWireMetadata(metadata) {
|
|
359
|
+
const decoded = decodeStreamWireMetadataAt(metadata, 0, metadata.length);
|
|
360
|
+
if (decoded.offset !== metadata.length) {
|
|
361
|
+
throw new Error("Stream metadata payload has trailing bytes.");
|
|
362
|
+
}
|
|
363
|
+
return decoded.metadata;
|
|
364
|
+
}
|
|
365
|
+
function lz4PickleUncompressed2(payload) {
|
|
366
|
+
return lz4PickleUncompressed(payload);
|
|
367
|
+
}
|
|
368
|
+
function lz4UnpicklePayload2(payload, maxDecompressedSize = defaultMaxDecompressedPayloadSize) {
|
|
369
|
+
return lz4UnpicklePayload(payload, maxDecompressedSize);
|
|
370
|
+
}
|
|
371
|
+
function utf8Encode(value) {
|
|
372
|
+
return new TextEncoder().encode(value);
|
|
373
|
+
}
|
|
374
|
+
function utf8Decode(value) {
|
|
375
|
+
return new TextDecoder().decode(value);
|
|
376
|
+
}
|
|
377
|
+
function decodeStreamWireHeaderMetadata(header, offset) {
|
|
378
|
+
if (header.length - offset < 2) {
|
|
379
|
+
throw new Error("Stream metadata section is incomplete.");
|
|
380
|
+
}
|
|
381
|
+
const metadataLength = readUInt16BE(header, offset);
|
|
382
|
+
offset += 2;
|
|
383
|
+
if (header.length - offset < metadataLength) {
|
|
384
|
+
throw new Error("Stream metadata payload is incomplete.");
|
|
385
|
+
}
|
|
386
|
+
return decodeStreamWireMetadataAt(header, offset, offset + metadataLength);
|
|
387
|
+
}
|
|
388
|
+
function decodeStreamWireMetadataAt(source, offset, end) {
|
|
389
|
+
if (offset >= end) {
|
|
390
|
+
throw new Error("Stream metadata entry count is missing.");
|
|
391
|
+
}
|
|
392
|
+
const count = source[offset++];
|
|
393
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
394
|
+
for (let index = 0; index < count; index += 1) {
|
|
395
|
+
if (offset >= end) {
|
|
396
|
+
throw new Error("Stream metadata key length is missing.");
|
|
397
|
+
}
|
|
398
|
+
const keyLength = source[offset++];
|
|
399
|
+
if (keyLength === 0 || end - offset < keyLength) {
|
|
400
|
+
throw new Error("Stream metadata key is invalid.");
|
|
401
|
+
}
|
|
402
|
+
const key = utf8Decode(source.subarray(offset, offset + keyLength));
|
|
403
|
+
offset += keyLength;
|
|
404
|
+
if (end - offset < 2) {
|
|
405
|
+
throw new Error("Stream metadata value length is missing.");
|
|
406
|
+
}
|
|
407
|
+
const valueLength = readUInt16BE(source, offset);
|
|
408
|
+
offset += 2;
|
|
409
|
+
if (end - offset < valueLength) {
|
|
410
|
+
throw new Error("Stream metadata value is incomplete.");
|
|
411
|
+
}
|
|
412
|
+
if (metadata.has(key)) {
|
|
413
|
+
throw new Error("Duplicate metadata key is duplicated.");
|
|
414
|
+
}
|
|
415
|
+
metadata.set(key, utf8Decode(source.subarray(offset, offset + valueLength)));
|
|
416
|
+
offset += valueLength;
|
|
417
|
+
}
|
|
418
|
+
if (offset !== end) {
|
|
419
|
+
throw new Error("Stream metadata payload has trailing bytes.");
|
|
420
|
+
}
|
|
421
|
+
return { metadata, offset };
|
|
422
|
+
}
|
|
423
|
+
function validateStreamWirePacketName(name) {
|
|
424
|
+
const nameBytes = utf8Encode(name);
|
|
425
|
+
if (name.trim().length === 0 || nameBytes.length > 255) {
|
|
426
|
+
throw new Error("Stream packet name is invalid.");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function isReplyKind(kind) {
|
|
430
|
+
return kind === ZLINK_STREAM_RESPONSE_KIND || kind === ZLINK_STREAM_ERROR_KIND;
|
|
431
|
+
}
|
|
432
|
+
function writeUInt16BE(buffer, offset, value) {
|
|
433
|
+
buffer[offset] = value >>> 8 & 255;
|
|
434
|
+
buffer[offset + 1] = value & 255;
|
|
435
|
+
}
|
|
436
|
+
function readUInt16BE(buffer, offset) {
|
|
437
|
+
return buffer[offset] << 8 | buffer[offset + 1];
|
|
438
|
+
}
|
|
439
|
+
function readUInt32BE(buffer, offset) {
|
|
440
|
+
return buffer[offset] * 16777216 + (buffer[offset + 1] << 16 | buffer[offset + 2] << 8 | buffer[offset + 3]);
|
|
441
|
+
}
|
|
442
|
+
function writeUInt32BE(buffer, offset, value) {
|
|
443
|
+
buffer[offset] = value >>> 24 & 255;
|
|
444
|
+
buffer[offset + 1] = value >>> 16 & 255;
|
|
445
|
+
buffer[offset + 2] = value >>> 8 & 255;
|
|
446
|
+
buffer[offset + 3] = value & 255;
|
|
447
|
+
}
|
|
448
|
+
function writeBigUInt64BE(buffer, offset, value) {
|
|
449
|
+
for (let index = 7; index >= 0; index -= 1) {
|
|
450
|
+
buffer[offset + index] = Number(value & 0xffn);
|
|
451
|
+
value >>= 8n;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function readBigUInt64BE(buffer, offset) {
|
|
455
|
+
let value = 0n;
|
|
456
|
+
for (let index = 0; index < 8; index += 1) {
|
|
457
|
+
value = value << 8n | BigInt(buffer[offset + index]);
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
export {
|
|
462
|
+
ZLINK_STREAM_FORMAT_MARKER,
|
|
463
|
+
ZlinkStreamCodec,
|
|
464
|
+
decodeStreamWireFrame,
|
|
465
|
+
decodeStreamWireHeader,
|
|
466
|
+
decodeStreamWireMetadata,
|
|
467
|
+
encodeStreamWireFrame,
|
|
468
|
+
encodeStreamWireHeader,
|
|
469
|
+
encodeStreamWireMetadata,
|
|
470
|
+
lz4PickleUncompressed2 as lz4PickleUncompressed,
|
|
471
|
+
lz4UnpicklePayload2 as lz4UnpicklePayload,
|
|
472
|
+
tryDecodeStreamWireFrame,
|
|
473
|
+
utf8Decode,
|
|
474
|
+
utf8Encode
|
|
475
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ZLinkStreamWireFrame {
|
|
2
|
+
readonly header: Uint8Array;
|
|
3
|
+
readonly payload: Uint8Array;
|
|
4
|
+
}
|
|
5
|
+
/** Numeric payload codec values shared by browser and server wire paths. */
|
|
6
|
+
export declare enum ZlinkStreamCodec {
|
|
7
|
+
Raw = 0,
|
|
8
|
+
Json = 1,
|
|
9
|
+
MessagePack = 2,
|
|
10
|
+
Protobuf = 3
|
|
11
|
+
}
|
|
12
|
+
export interface ZLinkStreamWireHeader {
|
|
13
|
+
readonly kind: number;
|
|
14
|
+
readonly codec: number;
|
|
15
|
+
readonly flags: number;
|
|
16
|
+
readonly requestSeq?: bigint;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly metadata: ReadonlyMap<string, string>;
|
|
19
|
+
readonly correlationId?: string;
|
|
20
|
+
readonly flowId?: string;
|
|
21
|
+
readonly flowOrigin?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ZLinkStreamWireHeaderFlags {
|
|
24
|
+
readonly hasRequestSeq: number;
|
|
25
|
+
readonly hasMetadata: number;
|
|
26
|
+
readonly hasCorrelationId: number;
|
|
27
|
+
readonly hasFlowId: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const ZLINK_STREAM_FORMAT_MARKER = 242;
|
|
30
|
+
export declare function encodeStreamWireFrame(header: Uint8Array, payload: Uint8Array): Uint8Array;
|
|
31
|
+
export declare function decodeStreamWireFrame(frame: Uint8Array): ZLinkStreamWireFrame;
|
|
32
|
+
export declare function tryDecodeStreamWireFrame(frame: Uint8Array): ZLinkStreamWireFrame | undefined;
|
|
33
|
+
export declare function encodeStreamWireHeader(header: ZLinkStreamWireHeader, flags?: ZLinkStreamWireHeaderFlags): Uint8Array;
|
|
34
|
+
export declare function decodeStreamWireHeader(header: Uint8Array, flags?: ZLinkStreamWireHeaderFlags, includeFlow?: boolean): ZLinkStreamWireHeader;
|
|
35
|
+
export declare function encodeStreamWireMetadata(metadata: ReadonlyMap<string, string>): Uint8Array;
|
|
36
|
+
export declare function decodeStreamWireMetadata(metadata: Uint8Array): Map<string, string>;
|
|
37
|
+
export declare function lz4PickleUncompressed(payload: Uint8Array): Uint8Array;
|
|
38
|
+
export declare function lz4UnpicklePayload(payload: Uint8Array, maxDecompressedSize?: number): Uint8Array;
|
|
39
|
+
export declare function utf8Encode(value: string): Uint8Array;
|
|
40
|
+
export declare function utf8Decode(value: Uint8Array): string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZLINK_STREAM_FORMAT_MARKER = exports.ZlinkStreamCodec = void 0;
|
|
4
|
+
exports.encodeStreamWireFrame = encodeStreamWireFrame;
|
|
5
|
+
exports.decodeStreamWireFrame = decodeStreamWireFrame;
|
|
6
|
+
exports.tryDecodeStreamWireFrame = tryDecodeStreamWireFrame;
|
|
7
|
+
exports.encodeStreamWireHeader = encodeStreamWireHeader;
|
|
8
|
+
exports.decodeStreamWireHeader = decodeStreamWireHeader;
|
|
9
|
+
exports.encodeStreamWireMetadata = encodeStreamWireMetadata;
|
|
10
|
+
exports.decodeStreamWireMetadata = decodeStreamWireMetadata;
|
|
11
|
+
exports.lz4PickleUncompressed = lz4PickleUncompressed;
|
|
12
|
+
exports.lz4UnpicklePayload = lz4UnpicklePayload;
|
|
13
|
+
exports.utf8Encode = utf8Encode;
|
|
14
|
+
exports.utf8Decode = utf8Decode;
|
|
15
|
+
const lz4_pickle_1 = require("./lz4-pickle");
|
|
16
|
+
/** Numeric payload codec values shared by browser and server wire paths. */
|
|
17
|
+
var ZlinkStreamCodec;
|
|
18
|
+
(function (ZlinkStreamCodec) {
|
|
19
|
+
ZlinkStreamCodec[ZlinkStreamCodec["Raw"] = 0] = "Raw";
|
|
20
|
+
ZlinkStreamCodec[ZlinkStreamCodec["Json"] = 1] = "Json";
|
|
21
|
+
ZlinkStreamCodec[ZlinkStreamCodec["MessagePack"] = 2] = "MessagePack";
|
|
22
|
+
ZlinkStreamCodec[ZlinkStreamCodec["Protobuf"] = 3] = "Protobuf";
|
|
23
|
+
})(ZlinkStreamCodec || (exports.ZlinkStreamCodec = ZlinkStreamCodec = {}));
|
|
24
|
+
const defaultHeaderFlags = {
|
|
25
|
+
hasRequestSeq: 0x01,
|
|
26
|
+
hasMetadata: 0x02,
|
|
27
|
+
hasCorrelationId: 0x08,
|
|
28
|
+
hasFlowId: 0x10
|
|
29
|
+
};
|
|
30
|
+
exports.ZLINK_STREAM_FORMAT_MARKER = 0xf2;
|
|
31
|
+
const ZLINK_STREAM_RESPONSE_KIND = 3;
|
|
32
|
+
const ZLINK_STREAM_ERROR_KIND = 4;
|
|
33
|
+
function encodeStreamWireFrame(header, payload) {
|
|
34
|
+
if (header.length > 0xffff) {
|
|
35
|
+
throw new Error('Stream header is too large.');
|
|
36
|
+
}
|
|
37
|
+
if (payload.length > 0xffffffff) {
|
|
38
|
+
throw new Error('Stream payload is too large.');
|
|
39
|
+
}
|
|
40
|
+
const frame = new Uint8Array(6 + header.length + payload.length);
|
|
41
|
+
writeUInt16BE(frame, 0, header.length);
|
|
42
|
+
writeUInt32BE(frame, 2, payload.length);
|
|
43
|
+
frame.set(header, 6);
|
|
44
|
+
frame.set(payload, 6 + header.length);
|
|
45
|
+
return frame;
|
|
46
|
+
}
|
|
47
|
+
function decodeStreamWireFrame(frame) {
|
|
48
|
+
if (frame.length < 6) {
|
|
49
|
+
throw new Error('Stream frame prefix is incomplete.');
|
|
50
|
+
}
|
|
51
|
+
const headerLength = readUInt16BE(frame, 0);
|
|
52
|
+
const payloadLength = readUInt32BE(frame, 2);
|
|
53
|
+
if (frame.length !== 6 + headerLength + payloadLength) {
|
|
54
|
+
throw new Error('Stream frame length does not match prefix.');
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
header: frame.slice(6, 6 + headerLength),
|
|
58
|
+
payload: frame.slice(6 + headerLength)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function tryDecodeStreamWireFrame(frame) {
|
|
62
|
+
if (frame.length < 6) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const headerLength = readUInt16BE(frame, 0);
|
|
66
|
+
const payloadLength = readUInt32BE(frame, 2);
|
|
67
|
+
if (frame.length !== 6 + headerLength + payloadLength) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
header: frame.slice(6, 6 + headerLength),
|
|
72
|
+
payload: frame.slice(6 + headerLength)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function encodeStreamWireHeader(header, flags = defaultHeaderFlags) {
|
|
76
|
+
const reply = isReplyKind(header.kind);
|
|
77
|
+
const packetName = reply ? '' : header.name;
|
|
78
|
+
if (!reply)
|
|
79
|
+
validateStreamWirePacketName(packetName);
|
|
80
|
+
const nameBytes = utf8Encode(packetName);
|
|
81
|
+
const hasRequestSeq = header.requestSeq !== undefined;
|
|
82
|
+
const hasMetadata = header.metadata.size > 0;
|
|
83
|
+
const correlationBytes = header.correlationId !== undefined && header.correlationId.length > 0
|
|
84
|
+
? utf8Encode(header.correlationId)
|
|
85
|
+
: undefined;
|
|
86
|
+
if (correlationBytes !== undefined && correlationBytes.length > 0xff) {
|
|
87
|
+
throw new Error('Stream correlation id is too large.');
|
|
88
|
+
}
|
|
89
|
+
const hasCorrelation = correlationBytes !== undefined;
|
|
90
|
+
const hasFlow = header.flowId !== undefined || header.flowOrigin !== undefined;
|
|
91
|
+
if (hasFlow && (header.flowId === undefined || header.flowOrigin === undefined)) {
|
|
92
|
+
throw new Error('Stream flow id and origin must be provided together.');
|
|
93
|
+
}
|
|
94
|
+
if (header.flowId !== undefined)
|
|
95
|
+
validateFlowId(header.flowId);
|
|
96
|
+
if (header.flowOrigin !== undefined && ![1, 2, 3, 4].includes(header.flowOrigin)) {
|
|
97
|
+
throw new Error('Stream flow origin is invalid.');
|
|
98
|
+
}
|
|
99
|
+
let headerFlags = header.flags;
|
|
100
|
+
headerFlags = hasRequestSeq ? headerFlags | flags.hasRequestSeq : headerFlags & ~flags.hasRequestSeq;
|
|
101
|
+
headerFlags = hasMetadata ? headerFlags | flags.hasMetadata : headerFlags & ~flags.hasMetadata;
|
|
102
|
+
headerFlags = hasCorrelation ? headerFlags | flags.hasCorrelationId : headerFlags & ~flags.hasCorrelationId;
|
|
103
|
+
headerFlags = hasFlow ? headerFlags | flags.hasFlowId : headerFlags & ~flags.hasFlowId;
|
|
104
|
+
const metadataBytes = hasMetadata ? encodeStreamWireMetadata(header.metadata) : new Uint8Array();
|
|
105
|
+
const size = 4 + (hasRequestSeq ? 8 : 0) + 1 + nameBytes.length
|
|
106
|
+
+ (hasMetadata ? 2 + metadataBytes.length : 0)
|
|
107
|
+
+ (hasCorrelation ? 1 + correlationBytes.length : 0)
|
|
108
|
+
+ (hasFlow ? 37 : 0);
|
|
109
|
+
const buffer = new Uint8Array(size);
|
|
110
|
+
let offset = 0;
|
|
111
|
+
buffer[offset++] = exports.ZLINK_STREAM_FORMAT_MARKER;
|
|
112
|
+
buffer[offset++] = header.kind;
|
|
113
|
+
buffer[offset++] = header.codec;
|
|
114
|
+
buffer[offset++] = headerFlags;
|
|
115
|
+
if (hasRequestSeq) {
|
|
116
|
+
if (header.requestSeq === 0n) {
|
|
117
|
+
throw new Error('Request sequence must not be zero.');
|
|
118
|
+
}
|
|
119
|
+
writeBigUInt64BE(buffer, offset, header.requestSeq);
|
|
120
|
+
offset += 8;
|
|
121
|
+
}
|
|
122
|
+
buffer[offset++] = nameBytes.length;
|
|
123
|
+
buffer.set(nameBytes, offset);
|
|
124
|
+
offset += nameBytes.length;
|
|
125
|
+
if (hasMetadata) {
|
|
126
|
+
writeUInt16BE(buffer, offset, metadataBytes.length);
|
|
127
|
+
offset += 2;
|
|
128
|
+
buffer.set(metadataBytes, offset);
|
|
129
|
+
offset += metadataBytes.length;
|
|
130
|
+
}
|
|
131
|
+
if (hasCorrelation) {
|
|
132
|
+
buffer[offset++] = correlationBytes.length;
|
|
133
|
+
buffer.set(correlationBytes, offset);
|
|
134
|
+
offset += correlationBytes.length;
|
|
135
|
+
}
|
|
136
|
+
if (hasFlow) {
|
|
137
|
+
buffer.set(asciiEncode(header.flowId), offset);
|
|
138
|
+
offset += 36;
|
|
139
|
+
buffer[offset++] = header.flowOrigin;
|
|
140
|
+
}
|
|
141
|
+
return buffer;
|
|
142
|
+
}
|
|
143
|
+
function decodeStreamWireHeader(header, flags = defaultHeaderFlags, includeFlow = true) {
|
|
144
|
+
let offset = 0;
|
|
145
|
+
if (header.length < 5) {
|
|
146
|
+
throw new Error('Stream header is incomplete.');
|
|
147
|
+
}
|
|
148
|
+
if (header[offset++] !== exports.ZLINK_STREAM_FORMAT_MARKER) {
|
|
149
|
+
throw new Error('Stream header format marker is invalid.');
|
|
150
|
+
}
|
|
151
|
+
const kind = header[offset++];
|
|
152
|
+
const codec = header[offset++];
|
|
153
|
+
const headerFlags = header[offset++];
|
|
154
|
+
const hasRequestSeq = (headerFlags & flags.hasRequestSeq) !== 0;
|
|
155
|
+
const hasMetadata = (headerFlags & flags.hasMetadata) !== 0;
|
|
156
|
+
const hasCorrelation = (headerFlags & flags.hasCorrelationId) !== 0;
|
|
157
|
+
const hasFlow = (headerFlags & flags.hasFlowId) !== 0;
|
|
158
|
+
if ((headerFlags & ~0x1f) !== 0) {
|
|
159
|
+
throw new Error('Unknown mandatory stream header flag.');
|
|
160
|
+
}
|
|
161
|
+
let requestSeq;
|
|
162
|
+
if (hasRequestSeq) {
|
|
163
|
+
if (header.length - offset < 8) {
|
|
164
|
+
throw new Error('Stream request sequence is incomplete.');
|
|
165
|
+
}
|
|
166
|
+
requestSeq = readBigUInt64BE(header, offset);
|
|
167
|
+
if (requestSeq === 0n) {
|
|
168
|
+
throw new Error('Request sequence must not be zero.');
|
|
169
|
+
}
|
|
170
|
+
offset += 8;
|
|
171
|
+
}
|
|
172
|
+
if (header.length - offset < 1) {
|
|
173
|
+
throw new Error('Stream packet name length is missing.');
|
|
174
|
+
}
|
|
175
|
+
const nameLength = header[offset++];
|
|
176
|
+
if ((!isReplyKind(kind) && nameLength === 0) || header.length - offset < nameLength) {
|
|
177
|
+
throw new Error('Stream packet name is invalid.');
|
|
178
|
+
}
|
|
179
|
+
const decodedName = utf8Decode(header.subarray(offset, offset + nameLength));
|
|
180
|
+
const name = isReplyKind(kind) ? '' : decodedName;
|
|
181
|
+
offset += nameLength;
|
|
182
|
+
const decodedMetadata = hasMetadata
|
|
183
|
+
? decodeStreamWireHeaderMetadata(header, offset)
|
|
184
|
+
: { metadata: new Map(), offset };
|
|
185
|
+
offset = decodedMetadata.offset;
|
|
186
|
+
let correlationId;
|
|
187
|
+
if (hasCorrelation) {
|
|
188
|
+
if (header.length - offset < 1) {
|
|
189
|
+
throw new Error('Stream correlation id is incomplete.');
|
|
190
|
+
}
|
|
191
|
+
const correlationLength = header[offset++];
|
|
192
|
+
if (header.length - offset < correlationLength) {
|
|
193
|
+
throw new Error('Stream correlation id is incomplete.');
|
|
194
|
+
}
|
|
195
|
+
correlationId = utf8Decode(header.subarray(offset, offset + correlationLength));
|
|
196
|
+
offset += correlationLength;
|
|
197
|
+
}
|
|
198
|
+
let flowId;
|
|
199
|
+
let flowOrigin;
|
|
200
|
+
if (hasFlow) {
|
|
201
|
+
if (header.length - offset < 37) {
|
|
202
|
+
throw new Error('Stream flow fields are incomplete.');
|
|
203
|
+
}
|
|
204
|
+
if (includeFlow) {
|
|
205
|
+
flowId = asciiDecode(header.subarray(offset, offset + 36));
|
|
206
|
+
validateFlowId(flowId);
|
|
207
|
+
}
|
|
208
|
+
offset += 36;
|
|
209
|
+
const decodedFlowOrigin = header[offset++];
|
|
210
|
+
if (includeFlow && ![1, 2, 3, 4].includes(decodedFlowOrigin)) {
|
|
211
|
+
throw new Error('Stream flow origin is invalid.');
|
|
212
|
+
}
|
|
213
|
+
flowOrigin = includeFlow ? decodedFlowOrigin : undefined;
|
|
214
|
+
}
|
|
215
|
+
if (offset !== header.length) {
|
|
216
|
+
throw new Error('Stream header has trailing bytes.');
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
kind,
|
|
220
|
+
codec,
|
|
221
|
+
flags: headerFlags,
|
|
222
|
+
requestSeq,
|
|
223
|
+
name,
|
|
224
|
+
metadata: decodedMetadata.metadata,
|
|
225
|
+
correlationId,
|
|
226
|
+
flowId,
|
|
227
|
+
flowOrigin
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function validateFlowId(flowId) {
|
|
231
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(flowId)) {
|
|
232
|
+
throw new Error('Stream flow id must be a lowercase UUIDv7.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function asciiEncode(value) {
|
|
236
|
+
return Uint8Array.from(value, (character) => character.charCodeAt(0));
|
|
237
|
+
}
|
|
238
|
+
function asciiDecode(value) {
|
|
239
|
+
if (value.some((byte) => byte > 0x7f))
|
|
240
|
+
throw new Error('Stream flow id must be ASCII.');
|
|
241
|
+
return String.fromCharCode(...value);
|
|
242
|
+
}
|
|
243
|
+
function encodeStreamWireMetadata(metadata) {
|
|
244
|
+
if (metadata.size > 255) {
|
|
245
|
+
throw new Error('Metadata entry count must not exceed 255.');
|
|
246
|
+
}
|
|
247
|
+
let size = 1;
|
|
248
|
+
const encoded = [...metadata].map(([key, value]) => {
|
|
249
|
+
const keyBytes = utf8Encode(key);
|
|
250
|
+
const valueBytes = utf8Encode(value);
|
|
251
|
+
if (keyBytes.length === 0 || keyBytes.length > 255) {
|
|
252
|
+
throw new Error('Metadata key length is invalid.');
|
|
253
|
+
}
|
|
254
|
+
if (valueBytes.length > 0xffff) {
|
|
255
|
+
throw new Error('Metadata value is too large.');
|
|
256
|
+
}
|
|
257
|
+
size += 1 + keyBytes.length + 2 + valueBytes.length;
|
|
258
|
+
return { keyBytes, valueBytes };
|
|
259
|
+
});
|
|
260
|
+
const buffer = new Uint8Array(size);
|
|
261
|
+
let offset = 0;
|
|
262
|
+
buffer[offset++] = metadata.size;
|
|
263
|
+
for (const { keyBytes, valueBytes } of encoded) {
|
|
264
|
+
buffer[offset++] = keyBytes.length;
|
|
265
|
+
buffer.set(keyBytes, offset);
|
|
266
|
+
offset += keyBytes.length;
|
|
267
|
+
writeUInt16BE(buffer, offset, valueBytes.length);
|
|
268
|
+
offset += 2;
|
|
269
|
+
buffer.set(valueBytes, offset);
|
|
270
|
+
offset += valueBytes.length;
|
|
271
|
+
}
|
|
272
|
+
return buffer;
|
|
273
|
+
}
|
|
274
|
+
function decodeStreamWireMetadata(metadata) {
|
|
275
|
+
const decoded = decodeStreamWireMetadataAt(metadata, 0, metadata.length);
|
|
276
|
+
if (decoded.offset !== metadata.length) {
|
|
277
|
+
throw new Error('Stream metadata payload has trailing bytes.');
|
|
278
|
+
}
|
|
279
|
+
return decoded.metadata;
|
|
280
|
+
}
|
|
281
|
+
function lz4PickleUncompressed(payload) {
|
|
282
|
+
return (0, lz4_pickle_1.lz4PickleUncompressed)(payload);
|
|
283
|
+
}
|
|
284
|
+
function lz4UnpicklePayload(payload, maxDecompressedSize = lz4_pickle_1.defaultMaxDecompressedPayloadSize) {
|
|
285
|
+
return (0, lz4_pickle_1.lz4UnpicklePayload)(payload, maxDecompressedSize);
|
|
286
|
+
}
|
|
287
|
+
function utf8Encode(value) {
|
|
288
|
+
return new TextEncoder().encode(value);
|
|
289
|
+
}
|
|
290
|
+
function utf8Decode(value) {
|
|
291
|
+
return new TextDecoder().decode(value);
|
|
292
|
+
}
|
|
293
|
+
function decodeStreamWireHeaderMetadata(header, offset) {
|
|
294
|
+
if (header.length - offset < 2) {
|
|
295
|
+
throw new Error('Stream metadata section is incomplete.');
|
|
296
|
+
}
|
|
297
|
+
const metadataLength = readUInt16BE(header, offset);
|
|
298
|
+
offset += 2;
|
|
299
|
+
if (header.length - offset < metadataLength) {
|
|
300
|
+
throw new Error('Stream metadata payload is incomplete.');
|
|
301
|
+
}
|
|
302
|
+
return decodeStreamWireMetadataAt(header, offset, offset + metadataLength);
|
|
303
|
+
}
|
|
304
|
+
function decodeStreamWireMetadataAt(source, offset, end) {
|
|
305
|
+
if (offset >= end) {
|
|
306
|
+
throw new Error('Stream metadata entry count is missing.');
|
|
307
|
+
}
|
|
308
|
+
const count = source[offset++];
|
|
309
|
+
const metadata = new Map();
|
|
310
|
+
for (let index = 0; index < count; index += 1) {
|
|
311
|
+
if (offset >= end) {
|
|
312
|
+
throw new Error('Stream metadata key length is missing.');
|
|
313
|
+
}
|
|
314
|
+
const keyLength = source[offset++];
|
|
315
|
+
if (keyLength === 0 || end - offset < keyLength) {
|
|
316
|
+
throw new Error('Stream metadata key is invalid.');
|
|
317
|
+
}
|
|
318
|
+
const key = utf8Decode(source.subarray(offset, offset + keyLength));
|
|
319
|
+
offset += keyLength;
|
|
320
|
+
if (end - offset < 2) {
|
|
321
|
+
throw new Error('Stream metadata value length is missing.');
|
|
322
|
+
}
|
|
323
|
+
const valueLength = readUInt16BE(source, offset);
|
|
324
|
+
offset += 2;
|
|
325
|
+
if (end - offset < valueLength) {
|
|
326
|
+
throw new Error('Stream metadata value is incomplete.');
|
|
327
|
+
}
|
|
328
|
+
if (metadata.has(key)) {
|
|
329
|
+
throw new Error('Duplicate metadata key is duplicated.');
|
|
330
|
+
}
|
|
331
|
+
metadata.set(key, utf8Decode(source.subarray(offset, offset + valueLength)));
|
|
332
|
+
offset += valueLength;
|
|
333
|
+
}
|
|
334
|
+
if (offset !== end) {
|
|
335
|
+
throw new Error('Stream metadata payload has trailing bytes.');
|
|
336
|
+
}
|
|
337
|
+
return { metadata, offset };
|
|
338
|
+
}
|
|
339
|
+
function validateStreamWirePacketName(name) {
|
|
340
|
+
const nameBytes = utf8Encode(name);
|
|
341
|
+
if (name.trim().length === 0 || nameBytes.length > 255) {
|
|
342
|
+
throw new Error('Stream packet name is invalid.');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function isReplyKind(kind) {
|
|
346
|
+
return kind === ZLINK_STREAM_RESPONSE_KIND || kind === ZLINK_STREAM_ERROR_KIND;
|
|
347
|
+
}
|
|
348
|
+
function writeUInt16BE(buffer, offset, value) {
|
|
349
|
+
buffer[offset] = (value >>> 8) & 0xff;
|
|
350
|
+
buffer[offset + 1] = value & 0xff;
|
|
351
|
+
}
|
|
352
|
+
function readUInt16BE(buffer, offset) {
|
|
353
|
+
return (buffer[offset] << 8) | buffer[offset + 1];
|
|
354
|
+
}
|
|
355
|
+
function readUInt32BE(buffer, offset) {
|
|
356
|
+
return (buffer[offset] * 0x1000000
|
|
357
|
+
+ ((buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | buffer[offset + 3]));
|
|
358
|
+
}
|
|
359
|
+
function writeUInt32BE(buffer, offset, value) {
|
|
360
|
+
buffer[offset] = (value >>> 24) & 0xff;
|
|
361
|
+
buffer[offset + 1] = (value >>> 16) & 0xff;
|
|
362
|
+
buffer[offset + 2] = (value >>> 8) & 0xff;
|
|
363
|
+
buffer[offset + 3] = value & 0xff;
|
|
364
|
+
}
|
|
365
|
+
function writeBigUInt64BE(buffer, offset, value) {
|
|
366
|
+
for (let index = 7; index >= 0; index -= 1) {
|
|
367
|
+
buffer[offset + index] = Number(value & 0xffn);
|
|
368
|
+
value >>= 8n;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function readBigUInt64BE(buffer, offset) {
|
|
372
|
+
let value = 0n;
|
|
373
|
+
for (let index = 0; index < 8; index += 1) {
|
|
374
|
+
value = (value << 8n) | BigInt(buffer[offset + index]);
|
|
375
|
+
}
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultMaxDecompressedPayloadSize = void 0;
|
|
4
|
+
exports.lz4PickleUncompressed = lz4PickleUncompressed;
|
|
5
|
+
exports.lz4UnpicklePayload = lz4UnpicklePayload;
|
|
6
|
+
exports.defaultMaxDecompressedPayloadSize = 64 * 1024;
|
|
7
|
+
function lz4PickleUncompressed(payload) {
|
|
8
|
+
if (payload.length === 0) {
|
|
9
|
+
return new Uint8Array();
|
|
10
|
+
}
|
|
11
|
+
const pickled = new Uint8Array(payload.length + 1);
|
|
12
|
+
pickled[0] = 0;
|
|
13
|
+
pickled.set(payload, 1);
|
|
14
|
+
return pickled;
|
|
15
|
+
}
|
|
16
|
+
function lz4UnpicklePayload(payload, maxDecompressedSize = exports.defaultMaxDecompressedPayloadSize) {
|
|
17
|
+
if (payload.length === 0) {
|
|
18
|
+
return new Uint8Array();
|
|
19
|
+
}
|
|
20
|
+
const header = payload[0];
|
|
21
|
+
if ((header & 0x07) !== 0) {
|
|
22
|
+
throw new Error('Unexpected LZ4 pickle version.');
|
|
23
|
+
}
|
|
24
|
+
const sizeOfDiff = decodeDiffSize((header >>> 6) & 0x03);
|
|
25
|
+
const dataOffset = 1 + sizeOfDiff;
|
|
26
|
+
if (payload.length < dataOffset) {
|
|
27
|
+
throw new Error('LZ4 pickle header is incomplete.');
|
|
28
|
+
}
|
|
29
|
+
const data = payload.subarray(dataOffset);
|
|
30
|
+
const resultDiff = sizeOfDiff === 0 ? 0 : readLittleEndian(payload, 1, sizeOfDiff);
|
|
31
|
+
const resultLength = data.length + resultDiff;
|
|
32
|
+
if (resultLength > maxDecompressedSize) {
|
|
33
|
+
throw new Error('LZ4 decoded payload exceeds maximum stream payload size.');
|
|
34
|
+
}
|
|
35
|
+
if (resultDiff === 0) {
|
|
36
|
+
return data.slice();
|
|
37
|
+
}
|
|
38
|
+
return decodeLz4Block(data, resultLength);
|
|
39
|
+
}
|
|
40
|
+
function decodeDiffSize(encoded) {
|
|
41
|
+
return encoded === 3 ? 4 : encoded;
|
|
42
|
+
}
|
|
43
|
+
function readLittleEndian(source, offset, size) {
|
|
44
|
+
if (size === 1) {
|
|
45
|
+
return source[offset];
|
|
46
|
+
}
|
|
47
|
+
if (size === 2) {
|
|
48
|
+
return source[offset] | (source[offset + 1] << 8);
|
|
49
|
+
}
|
|
50
|
+
if (size === 4) {
|
|
51
|
+
return (source[offset]
|
|
52
|
+
| (source[offset + 1] << 8)
|
|
53
|
+
| (source[offset + 2] << 16)
|
|
54
|
+
| (source[offset + 3] * 0x1000000));
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Unexpected LZ4 pickle field size: ${size}`);
|
|
57
|
+
}
|
|
58
|
+
function decodeLz4Block(source, resultLength) {
|
|
59
|
+
const target = new Uint8Array(resultLength);
|
|
60
|
+
let sourceOffset = 0;
|
|
61
|
+
let targetOffset = 0;
|
|
62
|
+
while (sourceOffset < source.length) {
|
|
63
|
+
const token = source[sourceOffset++];
|
|
64
|
+
const literalLength = readLz4Length(source, token >>> 4, () => sourceOffset++);
|
|
65
|
+
if (source.length - sourceOffset < literalLength) {
|
|
66
|
+
throw new Error('LZ4 literal run is incomplete.');
|
|
67
|
+
}
|
|
68
|
+
if (target.length - targetOffset < literalLength) {
|
|
69
|
+
throw new Error('LZ4 literal run exceeds output size.');
|
|
70
|
+
}
|
|
71
|
+
target.set(source.subarray(sourceOffset, sourceOffset + literalLength), targetOffset);
|
|
72
|
+
sourceOffset += literalLength;
|
|
73
|
+
targetOffset += literalLength;
|
|
74
|
+
if (sourceOffset >= source.length) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (source.length - sourceOffset < 2) {
|
|
78
|
+
throw new Error('LZ4 match offset is incomplete.');
|
|
79
|
+
}
|
|
80
|
+
const matchOffset = source[sourceOffset] | (source[sourceOffset + 1] << 8);
|
|
81
|
+
sourceOffset += 2;
|
|
82
|
+
if (matchOffset === 0 || matchOffset > targetOffset) {
|
|
83
|
+
throw new Error('LZ4 match offset is invalid.');
|
|
84
|
+
}
|
|
85
|
+
const matchLength = readLz4Length(source, token & 0x0f, () => sourceOffset++) + 4;
|
|
86
|
+
if (target.length - targetOffset < matchLength) {
|
|
87
|
+
throw new Error('LZ4 match run exceeds output size.');
|
|
88
|
+
}
|
|
89
|
+
for (let index = 0; index < matchLength; index += 1) {
|
|
90
|
+
target[targetOffset + index] = target[targetOffset - matchOffset + index];
|
|
91
|
+
}
|
|
92
|
+
targetOffset += matchLength;
|
|
93
|
+
}
|
|
94
|
+
if (targetOffset !== resultLength) {
|
|
95
|
+
throw new Error('LZ4 decoded length does not match pickle header.');
|
|
96
|
+
}
|
|
97
|
+
return target;
|
|
98
|
+
}
|
|
99
|
+
function readLz4Length(source, nibble, nextOffset) {
|
|
100
|
+
let length = nibble;
|
|
101
|
+
if (length !== 15) {
|
|
102
|
+
return length;
|
|
103
|
+
}
|
|
104
|
+
for (;;) {
|
|
105
|
+
const offset = nextOffset();
|
|
106
|
+
if (offset >= source.length) {
|
|
107
|
+
throw new Error('LZ4 extended length is incomplete.');
|
|
108
|
+
}
|
|
109
|
+
const value = source[offset];
|
|
110
|
+
length += value;
|
|
111
|
+
if (value !== 255) {
|
|
112
|
+
return length;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zlink-systems/stream-wire",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/esm",
|
|
9
|
+
"dist/index.js",
|
|
10
|
+
"dist/index.d.ts",
|
|
11
|
+
"dist/lz4-pickle.js",
|
|
12
|
+
"dist/lz4-pickle.d.ts",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
"import": "./dist/esm/index.mjs",
|
|
19
|
+
"require": "./dist/index.js",
|
|
20
|
+
"default": "./dist/esm/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|