vortez-io 1.0.0 → 1.0.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/package.json +17 -10
- package/.gitignore +0 -15
- package/scripts/build.sh +0 -14
- package/src/Frame.ts +0 -312
- package/src/Isomorphic.ts +0 -7
- package/src/RequestManager.ts +0 -114
- package/src/VIO.ts +0 -194
- package/src/VIOError.ts +0 -22
- package/src/VIOStrict.ts +0 -123
- package/src/index.ts +0 -4
- package/src/support/LineParser.ts +0 -55
- package/src/support/UTF8Codec.ts +0 -166
- package/src/support/UUID.ts +0 -125
- package/src/support/Varint.ts +0 -62
- package/tsconfig.json +0 -17
package/package.json
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vortez-io",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Binary WebSocket protocol for multiplexed events, requests, responses, and errors in Vortez or other applications.",
|
|
6
6
|
"main": "build/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "bash scripts/build.sh"
|
|
9
9
|
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/NetFeez/vortez-io.git"
|
|
13
|
+
},
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "NetFeez",
|
|
16
|
+
"email": "netfeez.dev@gmail.com",
|
|
17
|
+
"url": "https://netfeez.github.io"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"build",
|
|
21
|
+
"docs",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"PROTOCOL.md"
|
|
25
|
+
],
|
|
10
26
|
"keywords": [
|
|
11
27
|
"vortez",
|
|
12
28
|
"vortez-io",
|
|
@@ -19,15 +35,6 @@
|
|
|
19
35
|
"typescript",
|
|
20
36
|
"realtime"
|
|
21
37
|
],
|
|
22
|
-
"repository": {
|
|
23
|
-
"type": "git",
|
|
24
|
-
"url": "https://github.com/NetFeez/vortez-io.git"
|
|
25
|
-
},
|
|
26
|
-
"author": {
|
|
27
|
-
"name": "NetFeez",
|
|
28
|
-
"email": "netfeez.dev@gmail.com",
|
|
29
|
-
"url": "https://netfeez.github.io"
|
|
30
|
-
},
|
|
31
38
|
"license": "Apache-2.0",
|
|
32
39
|
"dependencies": {
|
|
33
40
|
"@netfeez/common": "^3.1.1"
|
package/.gitignore
DELETED
package/scripts/build.sh
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
set -eo pipefail
|
|
4
|
-
output_file='build.log'
|
|
5
|
-
|
|
6
|
-
if (npx tsc 2>&1 | tee "$output_file"); then
|
|
7
|
-
echo -e "\x1b[32mBuild succeeded.\x1b[0m"
|
|
8
|
-
rm -f "$output_file"
|
|
9
|
-
else
|
|
10
|
-
echo -e "\x1b[31mBuild failed. See \x1b[36m$output_file\x1b[0m for details.\x1b[0m"
|
|
11
|
-
tail -n 20 "$output_file"
|
|
12
|
-
echo -e "use 'cat $output_file' to see the full log."
|
|
13
|
-
exit 1
|
|
14
|
-
fi
|
package/src/Frame.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module vortez-io
|
|
3
|
-
* @author NetFeez <netfeez.dev@gemail.com>
|
|
4
|
-
* @license Apache-2.0
|
|
5
|
-
* @description Implements the core Frame structure of the Vortez IO protocol, including encoding and decoding logic for binary frames.
|
|
6
|
-
* The Frame class provides static methods to encode and decode frames according to the protocol specification, handling various frame types, modes, and identifier formats.
|
|
7
|
-
* It also includes validation to ensure that frames adhere to the protocol rules regarding type and identifier combinations.
|
|
8
|
-
* This module is essential for constructing and interpreting the binary messages exchanged in the Vortez IO protocol.
|
|
9
|
-
* The Frame structure consists of a header byte that encodes the frame type and mode, followed by an optional identifier (which can be a UUID or a string), and the payload data.
|
|
10
|
-
* The encoding and decoding processes involve careful parsing of the header and identifier fields, as well as proper handling of the payload data to ensure compliance with the protocol.
|
|
11
|
-
* The Frame class also defines the various types, modes, and identifier formats supported by the protocol, along with utility methods for converting between different representations (e.g., UUID to binary).
|
|
12
|
-
* The implementation emphasizes robustness and error handling, throwing VIOError exceptions when encountering invalid input or protocol violations during encoding and decoding operations.
|
|
13
|
-
* This module is a critical component of the Vortez IO protocol implementation, enabling the construction and interpretation of the binary frames that form the basis of communication within the protocol.
|
|
14
|
-
* The Frame class is designed to be flexible and extensible, allowing for future additions of frame types, modes, and identifier formats as the protocol evolves.
|
|
15
|
-
* The encoding and decoding logic is optimized for performance while maintaining strict adherence to the protocol specification, ensuring efficient processing of frames in real-world applications.
|
|
16
|
-
* Overall, this module serves as the foundation for handling the binary communication aspects of the Vortez IO protocol, providing a clear and robust implementation of the Frame structure and its associated encoding and decoding mechanisms.
|
|
17
|
-
*
|
|
18
|
-
* ⚠️ IMPORTANT: This module assumes that the input data for decoding is well-formed according to the protocol specification.
|
|
19
|
-
* It performs basic validations but does not attempt to recover from malformed frames.
|
|
20
|
-
* Users of this module should ensure that they are providing valid input to avoid unexpected errors during decoding.
|
|
21
|
-
*
|
|
22
|
-
* @see https://github.com/NetFeez/Vortez-IO-Protocol/blob/main/PROTOCOL.md for the full protocol specification and documentation.
|
|
23
|
-
*/
|
|
24
|
-
import UTF8Codec from "./support/UTF8Codec.js";
|
|
25
|
-
import UUID from "./support/UUID.js";
|
|
26
|
-
import Varint from "./support/Varint.js";
|
|
27
|
-
import VIOError from "./VIOError.js";
|
|
28
|
-
|
|
29
|
-
const MAX_ID_LENGTH = 1024;
|
|
30
|
-
|
|
31
|
-
export class Frame<T extends Frame.Type = Frame.Type> implements Frame.Frame<T> {
|
|
32
|
-
protected static readonly TYPE_TO_BITS: Record<Frame.Type, number> = {
|
|
33
|
-
RAW: 0x0, EVENT: 0x1, REQUEST: 0x2, RESPONSE: 0x3, ERROR: 0x4
|
|
34
|
-
};
|
|
35
|
-
protected static readonly TYPE_MAP: Record<number, Frame.Type> = {
|
|
36
|
-
0x0: 'RAW', 0x1: 'EVENT', 0x2: 'REQUEST', 0x3: 'RESPONSE', 0x4: 'ERROR'
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
protected static readonly MODE_TO_BITS: Record<Frame.Mode, number> = {
|
|
40
|
-
BINARY: 0x0, JSON: 0x1, LINE: 0x2, CUSTOM: 0x3
|
|
41
|
-
};
|
|
42
|
-
protected static readonly MODE_MAP: Record<number, Frame.Mode> = {
|
|
43
|
-
0x0: 'BINARY', 0x1: 'JSON', 0x2: 'LINE', 0x3: 'CUSTOM'
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
protected static readonly ID_TYPE_TO_BITS: Record<Frame.IDType, number> = {
|
|
47
|
-
NONE: 0x0, UUID: 0x1, STRING: 0x2
|
|
48
|
-
};
|
|
49
|
-
protected static readonly ID_TYPE_MAP: Record<number, Frame.IDType> = {
|
|
50
|
-
0x0: 'NONE', 0x1: 'UUID', 0x2: 'STRING',
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
public constructor(
|
|
54
|
-
public readonly type: T,
|
|
55
|
-
public readonly mode: Frame.Mode,
|
|
56
|
-
public readonly identifier: Frame.IdentifierMap[T],
|
|
57
|
-
public readonly data: ArrayBuffer,
|
|
58
|
-
public readonly reservedRaw: number
|
|
59
|
-
) {}
|
|
60
|
-
/**
|
|
61
|
-
* Decodes a binary frame from the given ArrayBuffer or Uint8Array according to the protocol specification.
|
|
62
|
-
* The method parses the frame header to determine the type, mode, and identifier, and then extracts the payload data.
|
|
63
|
-
* It performs various validations to ensure the frame adheres to the protocol rules, such as checking reserved bits and validating identifier types based on frame type.
|
|
64
|
-
* @param buffer The input buffer containing the binary frame data to decode.
|
|
65
|
-
* @returns A Frame object representing the decoded frame with its components (type, mode, identifier, data).
|
|
66
|
-
* @throws VIOError if the buffer is too short, contains invalid header values, or violates protocol rules regarding type and identifier combinations.
|
|
67
|
-
*/
|
|
68
|
-
public static decode(buffer: ArrayBuffer | Uint8Array): Frame.FrameUnion {
|
|
69
|
-
const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
70
|
-
|
|
71
|
-
if (view.length < 3) throw new VIOError(VIOError.Code.BUFFER_TOO_SHORT, 'Frame too short');
|
|
72
|
-
|
|
73
|
-
const head = view[0];
|
|
74
|
-
|
|
75
|
-
const typeBits = head & 0x0F;
|
|
76
|
-
const modeBits = (head >> 4) & 0x0F;
|
|
77
|
-
|
|
78
|
-
const type = this.assertDefined(this.TYPE_MAP[typeBits], VIOError.Code.INVALID_HEADER, `Invalid type: ${typeBits}`);
|
|
79
|
-
const mode = this.assertDefined(this.MODE_MAP[modeBits], VIOError.Code.INVALID_HEADER, `Invalid mode: ${modeBits}`);
|
|
80
|
-
|
|
81
|
-
const reserved = view[1];
|
|
82
|
-
|
|
83
|
-
if (reserved !== 0) throw new VIOError(VIOError.Code.INVALID_HEADER, `Reserved byte must be 0 (got ${reserved})`);
|
|
84
|
-
|
|
85
|
-
const idMeta = view[2];
|
|
86
|
-
const idTypeBits = idMeta & 0x03;
|
|
87
|
-
const idReservedBits = idMeta >> 2;
|
|
88
|
-
|
|
89
|
-
if (idReservedBits !== 0) throw new VIOError(VIOError.Code.INVALID_HEADER, 'ID_META reserved bits must be 0');
|
|
90
|
-
|
|
91
|
-
const idType = this.assertDefined(this.ID_TYPE_MAP[idTypeBits], VIOError.Code.INVALID_HEADER, `Invalid ID_TYPE: ${idTypeBits}`);
|
|
92
|
-
|
|
93
|
-
let identifier: Frame.Identifier;
|
|
94
|
-
let offset = 3;
|
|
95
|
-
switch (idType) {
|
|
96
|
-
case 'NONE':
|
|
97
|
-
identifier = { type: 'NONE' };
|
|
98
|
-
break;
|
|
99
|
-
case 'UUID':
|
|
100
|
-
if (view.length < offset + 16) throw new VIOError(VIOError.Code.BUFFER_TOO_SHORT, 'UUID missing');
|
|
101
|
-
|
|
102
|
-
const raw = view.slice(offset, offset + 16);
|
|
103
|
-
identifier = {
|
|
104
|
-
type: 'UUID',
|
|
105
|
-
id: UUID.binToUuid(raw),
|
|
106
|
-
idRaw: raw
|
|
107
|
-
}
|
|
108
|
-
offset += 16;
|
|
109
|
-
break;
|
|
110
|
-
case 'STRING': {
|
|
111
|
-
const { value: len, offset: next } = Varint.readVarint(view, offset);
|
|
112
|
-
if (len < 0 || len > MAX_ID_LENGTH) throw new VIOError(VIOError.Code.INVALID_HEADER, `Invalid String ID length: ${len}`);
|
|
113
|
-
offset = next;
|
|
114
|
-
|
|
115
|
-
if (view.length < offset + len) throw new VIOError(VIOError.Code.BUFFER_TOO_SHORT, 'String ID truncated');
|
|
116
|
-
|
|
117
|
-
const raw = view.slice(offset, offset + len);
|
|
118
|
-
identifier = {
|
|
119
|
-
type: 'STRING',
|
|
120
|
-
id: UTF8Codec.decode(raw),
|
|
121
|
-
idRaw: raw
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
offset += len;
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const data = this.toArrayBuffer(view.slice(offset));
|
|
129
|
-
return this.assemble({ type, mode, identifier, data, reservedRaw: reserved });
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Assembles a Frame object from the given components, performing additional validation to ensure that the combination of type and identifier adheres to protocol rules.
|
|
133
|
-
* This method is called after decoding the raw components of a frame to construct the final Frame object with proper typing and validation.
|
|
134
|
-
* It checks that REQUEST and RESPONSE frames use UUID identifiers, EVENT frames use STRING identifiers, and ERROR frames can use either NONE or UUID identifiers.
|
|
135
|
-
* If the components are valid, it returns a new Frame object; otherwise, it throws a VIOError indicating the specific protocol violation.
|
|
136
|
-
* @param parts An object containing the decoded components of the frame (type, mode, identifier, data, reservedRaw) to be assembled into a Frame object.
|
|
137
|
-
* @returns A Frame object representing the assembled frame with its components.
|
|
138
|
-
* @throws VIOError if the combination of type and identifier is invalid according to protocol rules.
|
|
139
|
-
*/
|
|
140
|
-
protected static assemble(parts: Frame.Frame): Frame.FrameUnion {
|
|
141
|
-
const { type, mode, identifier, data, reservedRaw } = parts;
|
|
142
|
-
let frame: Frame.FrameUnion;
|
|
143
|
-
switch (type) {
|
|
144
|
-
case 'REQUEST':
|
|
145
|
-
case 'RESPONSE':
|
|
146
|
-
if (identifier.type !== 'UUID') throw new VIOError(VIOError.Code.INVALID_HEADER, `${type} MUST use UUID`);
|
|
147
|
-
frame = new Frame(type, mode, identifier, data, reservedRaw);
|
|
148
|
-
break;
|
|
149
|
-
case 'EVENT':
|
|
150
|
-
if (identifier.type !== 'STRING') throw new VIOError(VIOError.Code.INVALID_HEADER, `EVENT MUST use STRING`);
|
|
151
|
-
frame = new Frame(type, mode, identifier, data, reservedRaw);
|
|
152
|
-
break;
|
|
153
|
-
case 'ERROR':
|
|
154
|
-
if (identifier.type !== 'NONE' && identifier.type !== 'UUID') throw new VIOError(VIOError.Code.INVALID_HEADER, `ERROR MUST use NONE or UUID`);
|
|
155
|
-
frame = new Frame(type, mode, identifier, data, reservedRaw);
|
|
156
|
-
break;
|
|
157
|
-
case 'RAW':
|
|
158
|
-
frame = new Frame(type, mode, identifier, data, reservedRaw);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
return frame;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Encodes the given frame components into a binary format according to the protocol specification.
|
|
165
|
-
* The resulting ArrayBuffer will contain the frame header followed by the identifier (if applicable) and the payload data.
|
|
166
|
-
* @param type The type of the frame (e.g., 'REQUEST', 'RESPONSE', etc.).
|
|
167
|
-
* @param mode The mode of the frame (e.g., 'BINARY', 'JSON', etc.).
|
|
168
|
-
* @param data The payload data to include in the frame, as an ArrayBuffer or Uint8Array.
|
|
169
|
-
* @param identifier An optional identifier for the frame, which can be of type NONE, UUID, or STRING. Defaults to NONE if not provided.
|
|
170
|
-
* @returns An ArrayBuffer containing the encoded frame ready for transmission.
|
|
171
|
-
* @throws VIOError if the provided type, mode, or identifier combination is invalid according to the protocol rules.
|
|
172
|
-
*/
|
|
173
|
-
public static encode(type: 'RAW', mode: Frame.Mode, data: ArrayBuffer | Uint8Array, identifier?: Frame.IdentifierMap['RAW']): ArrayBuffer;
|
|
174
|
-
public static encode(type: 'EVENT', mode: Frame.Mode, data: ArrayBuffer | Uint8Array, identifier: Frame.IdentifierMap['EVENT']): ArrayBuffer;
|
|
175
|
-
public static encode(type: 'REQUEST', mode: Frame.Mode, data: ArrayBuffer | Uint8Array, identifier: Frame.IdentifierMap['REQUEST']): ArrayBuffer;
|
|
176
|
-
public static encode(type: 'RESPONSE', mode: Frame.Mode, data: ArrayBuffer | Uint8Array, identifier: Frame.IdentifierMap['RESPONSE']): ArrayBuffer;
|
|
177
|
-
public static encode(type: 'ERROR', mode: Frame.Mode, data: ArrayBuffer | Uint8Array, identifier?: Frame.IdentifierMap['ERROR']): ArrayBuffer;
|
|
178
|
-
public static encode(type: Frame.Type, mode: Frame.Mode, data: ArrayBuffer | Uint8Array, identifier: Frame.Identifier = { type: 'NONE' }): ArrayBuffer {
|
|
179
|
-
const typeBits = this.assertDefined(this.TYPE_TO_BITS[type], VIOError.Code.UNKNOWN_TYPE, `Unknown type: ${type}`);
|
|
180
|
-
|
|
181
|
-
const modeBits = this.assertDefined(this.MODE_TO_BITS[mode], VIOError.Code.UNKNOWN_MODE, `Unknown mode: ${mode}`);
|
|
182
|
-
|
|
183
|
-
const idTypeBits = this.assertDefined(this.ID_TYPE_TO_BITS[identifier.type], VIOError.Code.INVALID_HEADER, `Unknown ID_TYPE: ${identifier.type}`);
|
|
184
|
-
|
|
185
|
-
this.validate(type, identifier.type);
|
|
186
|
-
|
|
187
|
-
let idBytes: Uint8Array = new Uint8Array(0);
|
|
188
|
-
|
|
189
|
-
if (identifier.type === 'UUID') {
|
|
190
|
-
if (!UUID.isValid(identifier.id)) throw new VIOError(VIOError.Code.INVALID_HEADER, `Invalid UUID: ${identifier.id}`);
|
|
191
|
-
idBytes = UUID.uuidToBin(identifier.id);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
else if (identifier.type === 'STRING') {
|
|
195
|
-
const encoded = UTF8Codec.encode(identifier.id);
|
|
196
|
-
const varint = Varint.writeVarint(encoded.length);
|
|
197
|
-
|
|
198
|
-
if (encoded.length > MAX_ID_LENGTH) throw new VIOError(VIOError.Code.INVALID_HEADER, `String ID too long (max ${MAX_ID_LENGTH} bytes)`);
|
|
199
|
-
|
|
200
|
-
idBytes = new Uint8Array(varint.length + encoded.length);
|
|
201
|
-
idBytes.set(varint, 0);
|
|
202
|
-
idBytes.set(encoded, varint.length);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const payload = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
206
|
-
|
|
207
|
-
const totalLength = 3 + idBytes.length + payload.length;
|
|
208
|
-
const buffer = new Uint8Array(totalLength);
|
|
209
|
-
|
|
210
|
-
buffer[0] = (modeBits << 4) | typeBits;
|
|
211
|
-
buffer[1] = 0;
|
|
212
|
-
buffer[2] = idTypeBits & 0x03;
|
|
213
|
-
|
|
214
|
-
let offset = 3;
|
|
215
|
-
|
|
216
|
-
buffer.set(idBytes, offset);
|
|
217
|
-
offset += idBytes.length;
|
|
218
|
-
|
|
219
|
-
buffer.set(payload, offset);
|
|
220
|
-
|
|
221
|
-
return this.toArrayBuffer(buffer);
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Validates the combination of frame type and identifier type according to protocol rules:
|
|
225
|
-
* - REQUEST and RESPONSE frames MUST use UUID identifiers.
|
|
226
|
-
* - EVENT frames MUST NOT use UUID identifiers (they can use STRING or NONE).
|
|
227
|
-
* - ERROR and RAW frames have no restrictions on identifier type.
|
|
228
|
-
* @param type The frame type to validate.
|
|
229
|
-
* @param idType The identifier type to validate against the frame type.
|
|
230
|
-
* @throws VIOError if the combination of type and idType is invalid according to the protocol rules.
|
|
231
|
-
*/
|
|
232
|
-
protected static validate(type: Frame.Type, idType: Frame.IDType) {
|
|
233
|
-
switch (type) {
|
|
234
|
-
case 'REQUEST':
|
|
235
|
-
case 'RESPONSE':
|
|
236
|
-
if (idType !== 'UUID') throw new VIOError(VIOError.Code.INVALID_HEADER, `${type} MUST use UUID`);
|
|
237
|
-
break;
|
|
238
|
-
case 'EVENT':
|
|
239
|
-
if (idType !== 'STRING') throw new VIOError(VIOError.Code.INVALID_HEADER, `EVENT MUST use STRING`);
|
|
240
|
-
break;
|
|
241
|
-
case 'ERROR': break;
|
|
242
|
-
case 'RAW': break;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Creates a copy of the ArrayBuffer from a Uint8Array view, ensuring it is not a shared buffer.
|
|
247
|
-
* This is necessary because the input Uint8Array may be a view into a larger buffer, and we want to ensure that the Frame's data is isolated and immutable.
|
|
248
|
-
* @param view The Uint8Array view to convert into an ArrayBuffer.
|
|
249
|
-
* @returns A new ArrayBuffer containing the data from the view.
|
|
250
|
-
*/
|
|
251
|
-
protected static toArrayBuffer(view: Uint8Array): ArrayBuffer {
|
|
252
|
-
const out = new Uint8Array(view.byteLength);
|
|
253
|
-
out.set(view);
|
|
254
|
-
return out.buffer;
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Utility method to assert that a value is defined (not undefined) and throw a VIOError with the provided message if it is not.
|
|
258
|
-
* This is used to ensure that required values are present during decoding and validation processes.
|
|
259
|
-
* @param value The value to check for being defined.
|
|
260
|
-
* @param code The VIOError code to use if the value is undefined.
|
|
261
|
-
* @param msg The error message to include in the VIOError if the value is undefined.
|
|
262
|
-
* @returns The input value if it is defined, otherwise throws a VIOError.
|
|
263
|
-
* @throws VIOError if the value is undefined, with the provided message.
|
|
264
|
-
*/
|
|
265
|
-
protected static assertDefined<T>(value: T | undefined, code: VIOError.Code, msg: string): T {
|
|
266
|
-
if (value === undefined ) throw new VIOError(code, msg);
|
|
267
|
-
return value;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
export namespace Frame {
|
|
271
|
-
export type Type = 'RAW' | 'EVENT' | 'REQUEST' | 'RESPONSE' | 'ERROR';
|
|
272
|
-
export type Mode = 'BINARY' | 'JSON' | 'LINE' | 'CUSTOM';
|
|
273
|
-
export type IDType = 'NONE' | 'UUID' | 'STRING';
|
|
274
|
-
|
|
275
|
-
export namespace Identifier {
|
|
276
|
-
export interface Base {
|
|
277
|
-
type: IDType;
|
|
278
|
-
}
|
|
279
|
-
export interface None extends Base {
|
|
280
|
-
type: 'NONE';
|
|
281
|
-
}
|
|
282
|
-
export interface UUID extends Base {
|
|
283
|
-
type: 'UUID';
|
|
284
|
-
id: string;
|
|
285
|
-
idRaw?: Uint8Array;
|
|
286
|
-
}
|
|
287
|
-
export interface String extends Base {
|
|
288
|
-
type: 'STRING';
|
|
289
|
-
id: string;
|
|
290
|
-
idRaw?: Uint8Array;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
export type Identifier = Identifier.None | Identifier.UUID | Identifier.String;
|
|
294
|
-
export interface IdentifierMap {
|
|
295
|
-
RAW: Identifier.None | Identifier.UUID | Identifier.String;
|
|
296
|
-
EVENT: Identifier.String;
|
|
297
|
-
REQUEST: Identifier.UUID;
|
|
298
|
-
RESPONSE: Identifier.UUID;
|
|
299
|
-
ERROR: Identifier.None | Identifier.UUID;
|
|
300
|
-
}
|
|
301
|
-
export interface Frame<T extends Type = Type> {
|
|
302
|
-
type: T;
|
|
303
|
-
mode: Mode;
|
|
304
|
-
identifier: IdentifierMap[T];
|
|
305
|
-
data: ArrayBuffer;
|
|
306
|
-
reservedRaw: number;
|
|
307
|
-
}
|
|
308
|
-
export type FrameUnion = {
|
|
309
|
-
[K in Type]: Frame<K>
|
|
310
|
-
}[Type];
|
|
311
|
-
}
|
|
312
|
-
export default Frame;
|
package/src/Isomorphic.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export namespace Isomorphic {
|
|
2
|
-
export type TimeoutHandler = () => void | Promise<void>;
|
|
3
|
-
export type setTimeout<T> = (handler: TimeoutHandler, timeout: number) => T;
|
|
4
|
-
export type clearTimeout<T> = (id: T) => void;
|
|
5
|
-
export type createUUID = () => string;
|
|
6
|
-
}
|
|
7
|
-
export default Isomorphic;
|
package/src/RequestManager.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { Events } from "@netfeez/common";
|
|
2
|
-
|
|
3
|
-
import UUID from "./support/UUID.js";
|
|
4
|
-
import VIOError from "./VIOError.js";
|
|
5
|
-
|
|
6
|
-
import type Frame from "./Frame.js";
|
|
7
|
-
import type Isomorphic from "./Isomorphic.js";
|
|
8
|
-
|
|
9
|
-
export class RequestManager<T extends any> extends Events<RequestManager.EventMap> {
|
|
10
|
-
protected pending: Map<string, RequestManager.PendingEntry<T>> = new Map();
|
|
11
|
-
|
|
12
|
-
protected setTimeout: Isomorphic.setTimeout<T>;
|
|
13
|
-
protected clearTimeout: Isomorphic.clearTimeout<T>;
|
|
14
|
-
protected createUUID: Isomorphic.createUUID;
|
|
15
|
-
|
|
16
|
-
public constructor(dependencies: RequestManager.DEPENDENCIES<T>) { super();
|
|
17
|
-
this.setTimeout = dependencies.setTimeout;
|
|
18
|
-
this.clearTimeout = dependencies.clearTimeout;
|
|
19
|
-
this.createUUID = dependencies.createUUID || UUID.create;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Send a request with the given data and return a Promise that resolves with the response or rejects with an error.
|
|
23
|
-
* The method generates a unique UUID for the request, stores the resolve and reject functions in the pending map, and emits a 'send' event with the UUID and data.
|
|
24
|
-
* The handler method should be called when a response or error is received, which will look up the corresponding entry in the pending map and resolve or reject the Promise accordingly.
|
|
25
|
-
* @param data The data to be sent with the request. This can be of any type depending on the application's needs.
|
|
26
|
-
* @param options The options for the request, including a timeout duration.
|
|
27
|
-
*/
|
|
28
|
-
public send(data: any, options: RequestManager.SendOptions = {}): Promise<RequestManager.Response> {
|
|
29
|
-
const { timeoutMs = 30000 } = options;
|
|
30
|
-
const uuid = this.createUUID();
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
const timeout: T = this.setTimeout(() => {
|
|
33
|
-
this.pending.delete(uuid);
|
|
34
|
-
reject(new VIOError(VIOError.Code.INVALID_DATA, 'Request timed out'));
|
|
35
|
-
}, timeoutMs);
|
|
36
|
-
|
|
37
|
-
this.pending.set(uuid, { resolve, reject, timeout });
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
this.emit('send', uuid, data);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
this.clearTimeout(timeout);
|
|
43
|
-
this.pending.delete(uuid);
|
|
44
|
-
reject(error instanceof VIOError ? error : new VIOError(VIOError.Code.INVALID_DATA, 'Failed to queue request'));
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Handle a response or error for a request identified by the given UUID. This method should be called when a response or error is received for a previously sent request.
|
|
50
|
-
* It looks up the corresponding entry in the pending map using the UUID. If an entry is found, it resolves the Promise with the provided data if there is no error, or rejects it with the provided error if there is one. Finally, it removes the entry from the pending map to clean up resources.
|
|
51
|
-
* @param uuid The unique identifier for the request, which should match the UUID generated when the request was sent.
|
|
52
|
-
* @param data The response data to resolve the Promise with if there is no error.
|
|
53
|
-
* @param error The error to reject the Promise with if an error occurred.
|
|
54
|
-
*/
|
|
55
|
-
public handle(uuid: string, data?: RequestManager.Response, error?: VIOError): void {
|
|
56
|
-
const entry = this.pending.get(uuid);
|
|
57
|
-
if (!entry) return;
|
|
58
|
-
|
|
59
|
-
this.clearTimeout(entry.timeout);
|
|
60
|
-
|
|
61
|
-
if (error) entry.reject(error);
|
|
62
|
-
else entry.resolve(data ?? { mode: 'BINARY', data: new Uint8Array(0) });
|
|
63
|
-
|
|
64
|
-
this.pending.delete(uuid);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
export namespace RequestManager {
|
|
68
|
-
export interface DEPENDENCIES<T extends any> {
|
|
69
|
-
setTimeout: Isomorphic.setTimeout<T>;
|
|
70
|
-
clearTimeout: Isomorphic.clearTimeout<T>;
|
|
71
|
-
createUUID?: Isomorphic.createUUID;
|
|
72
|
-
}
|
|
73
|
-
export namespace Response {
|
|
74
|
-
export type Base = { mode: Frame.Mode };
|
|
75
|
-
export interface Binary extends Base {
|
|
76
|
-
mode: 'BINARY' | 'CUSTOM';
|
|
77
|
-
data: Uint8Array;
|
|
78
|
-
}
|
|
79
|
-
export interface Json extends Base {
|
|
80
|
-
mode: 'JSON';
|
|
81
|
-
data: any;
|
|
82
|
-
}
|
|
83
|
-
export interface Line extends Base {
|
|
84
|
-
mode: 'LINE';
|
|
85
|
-
data: Record<string, string>;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
export type Response = Response.Binary | Response.Json | Response.Line;
|
|
89
|
-
export type EventMap = {
|
|
90
|
-
send: [uuid: string, data?: any];
|
|
91
|
-
error: [error: VIOError];
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Represents a pending request entry in the RequestManager.
|
|
96
|
-
* Each entry contains the resolve and reject functions for the Promise associated with the request, as well as a timeout identifier if a timeout mechanism is implemented.
|
|
97
|
-
* The resolve function is called with the response data when a successful response is received, while the reject function is called with an error if an error occurs or if a timeout is reached.
|
|
98
|
-
* The timeout field can be used to store the identifier returned by a setTimeout function, allowing for the implementation of request timeouts where the Promise is rejected if a response is not received within a certain timeframe.
|
|
99
|
-
* @template T The type of the timeout identifier, which can vary depending on the environment (e.g., NodeJS.Timeout in Node.js or number in browsers).
|
|
100
|
-
*/
|
|
101
|
-
export interface PendingEntry<T> {
|
|
102
|
-
resolve: (value: Response) => void;
|
|
103
|
-
reject: (reason: VIOError) => void;
|
|
104
|
-
timeout: T;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface SendOptions {
|
|
108
|
-
timeoutMs?: number;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export type Done<T> = (data: T) => void;
|
|
112
|
-
export type Fail = (error: VIOError) => void;
|
|
113
|
-
}
|
|
114
|
-
export default RequestManager;
|
package/src/VIO.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { Events } from "@netfeez/common";
|
|
2
|
-
|
|
3
|
-
import RequestManager from "./RequestManager.js";
|
|
4
|
-
import VIOError from "./VIOError.js";
|
|
5
|
-
import Frame from "./Frame.js";
|
|
6
|
-
import UTF8Codec from "./support/UTF8Codec.js";
|
|
7
|
-
import LineCodec from "./support/LineParser.js";
|
|
8
|
-
|
|
9
|
-
export class VIO<T extends any> extends Events<VIO.EventMap> {
|
|
10
|
-
protected mRequest: RequestManager<T>;
|
|
11
|
-
public vTransport: Events.Emitter<VIO.TransportEventMap>;
|
|
12
|
-
|
|
13
|
-
public constructor(dependencies: RequestManager.DEPENDENCIES<T>) {super();
|
|
14
|
-
this.mRequest = new RequestManager(dependencies);
|
|
15
|
-
this.mRequest.on('error', (error) => this.emit('error', error));
|
|
16
|
-
this.mRequest.on('send', this.sendRequest.bind(this));
|
|
17
|
-
this.vTransport = new Events.Emitter();
|
|
18
|
-
}
|
|
19
|
-
public get transport(): Events<VIO.TransportEventMap> { return this.vTransport; }
|
|
20
|
-
public async request(data: any, options?: RequestManager.SendOptions): Promise<RequestManager.Response> {
|
|
21
|
-
return this.mRequest.send(data, options);
|
|
22
|
-
}
|
|
23
|
-
public event(name: string, data: any): void {
|
|
24
|
-
this.sendEvent(name, data);
|
|
25
|
-
}
|
|
26
|
-
public handle(data: ArrayBuffer | Uint8Array): void {
|
|
27
|
-
try {
|
|
28
|
-
const frame = Frame.decode(data);
|
|
29
|
-
switch (frame.type) {
|
|
30
|
-
case 'RAW': this.handleRaw(frame); break;
|
|
31
|
-
case 'EVENT': this.handleEvent(frame); break;
|
|
32
|
-
case 'REQUEST': this.handleRequest(frame); break;
|
|
33
|
-
case 'RESPONSE': this.handleResponse(frame); break;
|
|
34
|
-
case 'ERROR': this.handleError(frame); break;
|
|
35
|
-
default: this.sendError(new VIOError(VIOError.Code.INVALID_DATA, 'Unknown frame type')); break;
|
|
36
|
-
}
|
|
37
|
-
} catch (error) {
|
|
38
|
-
this.emit('error', error instanceof VIOError ? error : new VIOError(VIOError.Code.INVALID_DATA, 'Failed to decode frame'));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
protected async handleRaw(frame: Frame): Promise<void> {
|
|
42
|
-
this.emit('raw', frame);
|
|
43
|
-
}
|
|
44
|
-
protected async handleEvent(frame: Frame<'EVENT'>): Promise<void> {
|
|
45
|
-
try {
|
|
46
|
-
if (frame.mode == 'BINARY' || frame.mode == 'CUSTOM') {
|
|
47
|
-
const data = new Uint8Array(frame.data);
|
|
48
|
-
this.emit('event', frame.identifier.id, frame.mode, data);
|
|
49
|
-
} else if (frame.mode == 'JSON') {
|
|
50
|
-
const json = UTF8Codec.decode(new Uint8Array(frame.data));
|
|
51
|
-
const data = JSON.parse(json);
|
|
52
|
-
this.emit('event', frame.identifier.id, frame.mode, data);
|
|
53
|
-
} else if (frame.mode == 'LINE') {
|
|
54
|
-
const data = LineCodec.decode(frame.data);
|
|
55
|
-
this.emit('event', frame.identifier.id, frame.mode, data);
|
|
56
|
-
} else this.sendError(new VIOError(VIOError.Code.UNKNOWN_MODE, `Unknown mode: ${frame.mode}`));
|
|
57
|
-
} catch (error) { this.emit('error', error instanceof VIOError ? error : new VIOError(VIOError.Code.INVALID_DATA, 'Failed to process event data')); }
|
|
58
|
-
}
|
|
59
|
-
protected async handleRequest(frame: Frame<'REQUEST'>): Promise<void> {
|
|
60
|
-
if (!frame.identifier.id) return this.sendError(new VIOError(VIOError.Code.INVALID_HEADER, 'Request frame missing UUID'));
|
|
61
|
-
const uuid = frame.identifier.id;
|
|
62
|
-
try {
|
|
63
|
-
if (frame.mode == 'BINARY' || frame.mode == 'CUSTOM') {
|
|
64
|
-
const data = new Uint8Array(frame.data);
|
|
65
|
-
const done = (data: Uint8Array) => this.sendResponse(uuid, data);
|
|
66
|
-
const fail = (error: VIOError) => this.sendError(error, uuid);
|
|
67
|
-
this.emit('request', frame.mode, data, done, fail);
|
|
68
|
-
} else if (frame.mode == 'JSON') {
|
|
69
|
-
const json = UTF8Codec.decode(new Uint8Array(frame.data));
|
|
70
|
-
const data = JSON.parse(json);
|
|
71
|
-
const done = (data: any) => this.sendResponse(uuid, data);
|
|
72
|
-
const fail = (error: VIOError) => this.sendError(error, uuid);
|
|
73
|
-
this.emit('request', frame.mode, data, done, fail);
|
|
74
|
-
} else if (frame.mode == 'LINE') {
|
|
75
|
-
const data = LineCodec.decode(frame.data);
|
|
76
|
-
const done = (data: Record<string, string>) => this.sendResponse(uuid, data);
|
|
77
|
-
const fail = (error: VIOError) => this.sendError(error, uuid);
|
|
78
|
-
this.emit('request', frame.mode, data, done, fail);
|
|
79
|
-
} else this.sendError(new VIOError(VIOError.Code.UNKNOWN_MODE, `Unknown mode: ${frame.mode}`), uuid);
|
|
80
|
-
} catch (error) { this.sendError(new VIOError(VIOError.Code.INVALID_DATA, 'Failed to process request data'), uuid); }
|
|
81
|
-
}
|
|
82
|
-
protected async handleResponse(frame: Frame<'RESPONSE'>): Promise<void> {
|
|
83
|
-
if (!frame.identifier.id) return this.sendError(new VIOError(VIOError.Code.INVALID_HEADER, 'Response frame missing UUID'));
|
|
84
|
-
try {
|
|
85
|
-
if (frame.mode == 'BINARY' || frame.mode == 'CUSTOM') {
|
|
86
|
-
const data = new Uint8Array(frame.data);
|
|
87
|
-
this.mRequest.handle(frame.identifier.id, { mode: frame.mode, data });
|
|
88
|
-
} else if (frame.mode == 'JSON') {
|
|
89
|
-
const json = UTF8Codec.decode(new Uint8Array(frame.data));
|
|
90
|
-
const data = JSON.parse(json);
|
|
91
|
-
this.mRequest.handle(frame.identifier.id, { mode: 'JSON', data });
|
|
92
|
-
} else if (frame.mode == 'LINE') {
|
|
93
|
-
const data = LineCodec.decode(frame.data);
|
|
94
|
-
this.mRequest.handle(frame.identifier.id, { mode: 'LINE', data });
|
|
95
|
-
} else this.mRequest.handle(frame.identifier.id, undefined, new VIOError(VIOError.Code.UNKNOWN_MODE, `Unknown mode: ${frame.mode}`));
|
|
96
|
-
} catch (error) { this.mRequest.handle(frame.identifier.id, undefined, new VIOError(VIOError.Code.INVALID_DATA, 'Failed to process response data')); }
|
|
97
|
-
}
|
|
98
|
-
protected async handleError(frame: Frame<'ERROR'>): Promise<void> {
|
|
99
|
-
try {
|
|
100
|
-
if (frame.identifier.type === 'NONE') {
|
|
101
|
-
const error = new VIOError(VIOError.Code.INVALID_DATA, 'Unknown error');
|
|
102
|
-
return this.emit('error', error);
|
|
103
|
-
}
|
|
104
|
-
if (frame.mode == 'BINARY' || frame.mode == 'CUSTOM') {
|
|
105
|
-
const data = new Uint8Array(frame.data);
|
|
106
|
-
const error = new VIOError(VIOError.Code.INVALID_DATA, `Error response with binary data: ${data.length} bytes`);
|
|
107
|
-
this.mRequest.handle(frame.identifier.id, undefined, error);
|
|
108
|
-
} else if (frame.mode == 'JSON') {
|
|
109
|
-
const json = UTF8Codec.decode(new Uint8Array(frame.data));
|
|
110
|
-
const data = JSON.parse(json);
|
|
111
|
-
const error = new VIOError(VIOError.Code.INVALID_DATA, data.error || 'Unknown error');
|
|
112
|
-
this.mRequest.handle(frame.identifier.id, undefined, error);
|
|
113
|
-
} else if (frame.mode == 'LINE') {
|
|
114
|
-
const data = LineCodec.decode(frame.data);
|
|
115
|
-
const error = new VIOError(VIOError.Code.INVALID_DATA, `Error response with line data: ${JSON.stringify(data)}`);
|
|
116
|
-
this.mRequest.handle(frame.identifier.id, undefined, error);
|
|
117
|
-
} else {
|
|
118
|
-
const error = new VIOError(VIOError.Code.UNKNOWN_MODE, `Unknown mode: ${frame.mode}`);
|
|
119
|
-
this.mRequest.handle(frame.identifier.id, undefined, error);
|
|
120
|
-
}
|
|
121
|
-
} catch (error) {
|
|
122
|
-
const vioError = new VIOError(VIOError.Code.INVALID_DATA, 'Failed to process error data');
|
|
123
|
-
if (frame.identifier.type === 'UUID') this.mRequest.handle(frame.identifier.id, undefined, vioError);
|
|
124
|
-
this.emit('error', vioError);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
protected sendEvent(name: string, data: Uint8Array | ArrayBuffer): void;
|
|
128
|
-
protected sendEvent(name: string, data: LineCodec.ContentMap, mode: 'LINE'): void;
|
|
129
|
-
protected sendEvent(name: string, data: any): void;
|
|
130
|
-
protected sendEvent(name: string, data: any, mode?: 'LINE'): void {
|
|
131
|
-
const { content, finalMode } = this.serialize(data, mode);
|
|
132
|
-
const frame = Frame.encode('EVENT', finalMode, content, { type: 'STRING', id: name });
|
|
133
|
-
try { this.vTransport.emit('send', frame); }
|
|
134
|
-
catch (err) { this.vTransport.emit('transport:error', err instanceof Error ? err : new Error(String(err)), { stage: 'send', frameType: 'EVENT', info: { name } }); }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
protected sendRequest(uuid: string, data: Uint8Array | ArrayBuffer): void;
|
|
138
|
-
protected sendRequest(uuid: string, data: LineCodec.ContentMap, mode: 'LINE'): void;
|
|
139
|
-
protected sendRequest(uuid: string, data: any): void;
|
|
140
|
-
protected sendRequest(uuid: string, data?: any, mode?: 'LINE'): void {
|
|
141
|
-
const { content, finalMode } = this.serialize(data, mode);
|
|
142
|
-
const frame = Frame.encode('REQUEST', finalMode, content, { type: 'UUID', id: uuid });
|
|
143
|
-
try { this.vTransport.emit('send', frame); }
|
|
144
|
-
catch (err) { this.vTransport.emit('transport:error', err instanceof Error ? err : new Error(String(err)), { stage: 'send', frameType: 'REQUEST', uuid, info: {} }); }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
protected sendResponse(uuid: string, data: Uint8Array | ArrayBuffer): void;
|
|
148
|
-
protected sendResponse(uuid: string, data: LineCodec.ContentMap, mode: 'LINE'): void;
|
|
149
|
-
protected sendResponse(uuid: string, data: any): void;
|
|
150
|
-
protected sendResponse(uuid: string, data?: any, mode?: 'LINE'): void {
|
|
151
|
-
const { content, finalMode } = this.serialize(data, mode);
|
|
152
|
-
const frame = Frame.encode('RESPONSE', finalMode, content, { type: 'UUID', id: uuid });
|
|
153
|
-
try { this.vTransport.emit('send', frame); }
|
|
154
|
-
catch (err) { this.vTransport.emit('transport:error', err instanceof Error ? err : new Error(String(err)), { stage: 'send', frameType: 'RESPONSE', uuid, info: {} }); }
|
|
155
|
-
}
|
|
156
|
-
protected sendError(error: VIOError, uuid?: string): void {
|
|
157
|
-
const json = JSON.stringify({ error: error.message, code: error.code });
|
|
158
|
-
const content = UTF8Codec.encode(json);
|
|
159
|
-
const frame = Frame.encode('ERROR', 'JSON', content, uuid ? { type: 'UUID', id: uuid } : { type: 'NONE' });
|
|
160
|
-
try { this.vTransport.emit('send', frame); }
|
|
161
|
-
catch (err) { this.vTransport.emit('transport:error', err instanceof Error ? err : new Error(String(err)), { stage: 'send', frameType: 'ERROR', uuid, info: {} }); }
|
|
162
|
-
}
|
|
163
|
-
protected serialize(data: any, mode?: 'LINE'): { content: Uint8Array, finalMode: Frame.Mode } {
|
|
164
|
-
if (data instanceof Uint8Array) return { content: data, finalMode: 'BINARY' };
|
|
165
|
-
if (data instanceof ArrayBuffer) return { content: new Uint8Array(data), finalMode: 'BINARY' };
|
|
166
|
-
if (mode === 'LINE') return { content: LineCodec.encode(data), finalMode: 'LINE' };
|
|
167
|
-
const json = JSON.stringify(data ?? {});
|
|
168
|
-
return { content: UTF8Codec.encode(json), finalMode: 'JSON' };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
export namespace VIO {
|
|
172
|
-
export type TransportEventMap = {
|
|
173
|
-
send: [data: ArrayBuffer];
|
|
174
|
-
'transport:error': [err: Error, ctx?: { stage: 'send' | 'recv' | 'connect', frameType?: Frame.Type | string, uuid?: string, info?: any }];
|
|
175
|
-
}
|
|
176
|
-
type EventTuple = (
|
|
177
|
-
[name: string, type: 'BINARY' | 'CUSTOM', data: Uint8Array] |
|
|
178
|
-
[name: string, type: 'JSON', data: any] |
|
|
179
|
-
[name: string, type: 'LINE', data: Record<string, string>]
|
|
180
|
-
);
|
|
181
|
-
type requestTuple = (
|
|
182
|
-
[type: 'BINARY' | 'CUSTOM', data: Uint8Array, done: RequestManager.Done<Uint8Array>, fail: RequestManager.Fail] |
|
|
183
|
-
[type: 'JSON', data: any, done: RequestManager.Done<any>, fail: RequestManager.Fail] |
|
|
184
|
-
[type: 'LINE', data: Record<string, string>, done: RequestManager.Done<Record<string, string>>, fail: RequestManager.Fail]
|
|
185
|
-
);
|
|
186
|
-
export type EventMap = {
|
|
187
|
-
request: requestTuple;
|
|
188
|
-
event: EventTuple;
|
|
189
|
-
error: [error: VIOError];
|
|
190
|
-
raw: [data: Frame];
|
|
191
|
-
response: [uuid: string, response: RequestManager.Response];
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
export default VIO;
|
package/src/VIOError.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export class VIOError extends Error {
|
|
2
|
-
public name: string = 'VIOError';
|
|
3
|
-
public code: VIOError.Code;
|
|
4
|
-
public constructor(code: VIOError.Code, message: string) { super(message);
|
|
5
|
-
this.name = 'VIOError';
|
|
6
|
-
this.code = code;
|
|
7
|
-
}
|
|
8
|
-
public toString(): string {
|
|
9
|
-
return `${this.name} [${this.code}]: ${this.message}`;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export namespace VIOError {
|
|
13
|
-
export enum Code {
|
|
14
|
-
INVALID_HEADER = 'INVALID_HEADER',
|
|
15
|
-
INVALID_UUID = 'INVALID_UUID',
|
|
16
|
-
INVALID_DATA = 'INVALID_DATA',
|
|
17
|
-
UNKNOWN_TYPE = 'UNKNOWN_TYPE',
|
|
18
|
-
UNKNOWN_MODE = 'UNKNOWN_MODE',
|
|
19
|
-
BUFFER_TOO_SHORT = 'BUFFER_TOO_SHORT'
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export default VIOError;
|
package/src/VIOStrict.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Events, Schema } from "@netfeez/common";
|
|
2
|
-
import RequestManager from "./RequestManager.js";
|
|
3
|
-
import VIO from "./VIO.js";
|
|
4
|
-
import VIOError from "./VIOError.js";
|
|
5
|
-
import { Frame } from "./Frame.js";
|
|
6
|
-
|
|
7
|
-
export const RRSchema = Schema.fromObject({
|
|
8
|
-
name: { type: 'string', required: true },
|
|
9
|
-
content: { type: 'object', required: true }
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export class VIOStrict<
|
|
13
|
-
const T extends any,
|
|
14
|
-
const IEMap extends VIOStrict.EMap,
|
|
15
|
-
const OEMap extends VIOStrict.EMap,
|
|
16
|
-
const IRRMap extends VIOStrict.RRMap,
|
|
17
|
-
const ORRMap extends VIOStrict.RRMap
|
|
18
|
-
> {
|
|
19
|
-
protected readonly VIO: VIO<T>;
|
|
20
|
-
|
|
21
|
-
protected readonly vioEmitter: Events.Emitter<VIOStrict.VIOEventMap>;
|
|
22
|
-
protected readonly eventEmitter: Events.Emitter<VIOStrict.EventMap<IEMap>>;
|
|
23
|
-
protected readonly requestEmitter: Events.Emitter<VIOStrict.ReqEventMap<IRRMap>>;
|
|
24
|
-
|
|
25
|
-
public constructor(
|
|
26
|
-
dependencies: RequestManager.DEPENDENCIES<T>,
|
|
27
|
-
protected inEMap: IEMap,
|
|
28
|
-
protected outEMap: OEMap,
|
|
29
|
-
protected inRRMap: IRRMap,
|
|
30
|
-
protected outRRMap: ORRMap
|
|
31
|
-
) {
|
|
32
|
-
this.VIO = new VIO(dependencies);
|
|
33
|
-
this.vioEmitter = new Events.Emitter();
|
|
34
|
-
this.eventEmitter = new Events.Emitter();
|
|
35
|
-
this.requestEmitter = new Events.Emitter();
|
|
36
|
-
|
|
37
|
-
this.VIO.on('event', this.eventHandler.bind(this));
|
|
38
|
-
this.VIO.on('request', this.requestHandler.bind(this));
|
|
39
|
-
this.VIO.on('raw', (frame) => this.vioEmitter.emit('raw', frame));
|
|
40
|
-
this.VIO.on('error', (error) => this.vioEmitter.emit('error', error));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
public get trp() { return this.VIO.transport; }
|
|
44
|
-
public get vio(): Events<VIOStrict.VIOEventMap> { return this.vioEmitter; }
|
|
45
|
-
public get evt(): Events<VIOStrict.EventMap<IEMap>> { return this.eventEmitter; }
|
|
46
|
-
public get rqt(): Events<VIOStrict.ReqEventMap<IRRMap>> { return this.requestEmitter; }
|
|
47
|
-
|
|
48
|
-
public event<Name extends string & keyof OEMap>(name: Name, data: OEMap[Name]['infer']): void {
|
|
49
|
-
const schema = this.outEMap[name];
|
|
50
|
-
if (!schema) throw new VIOError(VIOError.Code.UNKNOWN_TYPE, `Unknown event type: ${String(name)}`);
|
|
51
|
-
const processedData = schema.processData(data);
|
|
52
|
-
this.VIO.event(name, processedData);
|
|
53
|
-
}
|
|
54
|
-
public async request<id extends string & keyof ORRMap>(action: id, data: ORRMap[id]['request']['inferToProcess'], options?: RequestManager.SendOptions): Promise<ORRMap[id]['response']['infer']> {
|
|
55
|
-
const { request: RQS, response: RSS } = this.outRRMap[action];
|
|
56
|
-
if (!RQS) throw new VIOError(VIOError.Code.UNKNOWN_TYPE, `Unknown request type: ${String(action)}`);
|
|
57
|
-
if (!RSS) throw new VIOError(VIOError.Code.UNKNOWN_TYPE, `Unknown response type: ${String(action)}`);
|
|
58
|
-
const readyData = RQS.processData(data);
|
|
59
|
-
const toSend = RRSchema.processData({ name: action, content: readyData });
|
|
60
|
-
const response = await this.VIO.request(toSend, options);
|
|
61
|
-
if (response.mode !== 'JSON') throw new VIOError(VIOError.Code.INVALID_DATA, `Invalid response mode: ${response.mode}`);
|
|
62
|
-
const parsed = RRSchema.processData(response.data);
|
|
63
|
-
if (parsed.name !== action) throw new VIOError(VIOError.Code.INVALID_DATA, `Response action mismatch: expected ${action}, got ${parsed.name}`);
|
|
64
|
-
return RSS.processData(parsed.content);
|
|
65
|
-
}
|
|
66
|
-
public handle(data: ArrayBuffer | Uint8Array): void {
|
|
67
|
-
this.VIO.handle(data);
|
|
68
|
-
}
|
|
69
|
-
protected eventHandler(name: string, _mode: Frame.Mode, data: any): void {
|
|
70
|
-
const schema = this.inEMap[name];
|
|
71
|
-
if (!schema) throw new VIOError(VIOError.Code.UNKNOWN_TYPE, `Unknown event type: ${String(name)}`);
|
|
72
|
-
const processedData = schema.processData(data);
|
|
73
|
-
this.eventEmitter.emit(name, processedData);
|
|
74
|
-
}
|
|
75
|
-
protected requestHandler(mode: Frame.Mode, data: any, done: (data: any) => void, fail: (error: VIOError) => void): void {
|
|
76
|
-
if (mode !== 'JSON') return fail(new VIOError(VIOError.Code.INVALID_DATA, `Invalid request mode: ${mode}`));
|
|
77
|
-
const parsed = RRSchema.processData(data);
|
|
78
|
-
const { name, content } = parsed;
|
|
79
|
-
const rrEntry = this.inRRMap[name];
|
|
80
|
-
if (!rrEntry) return fail(new VIOError(VIOError.Code.UNKNOWN_TYPE, `Unknown request type: ${String(name)}`));
|
|
81
|
-
const {request: RQS, response: RSS } = rrEntry;
|
|
82
|
-
if (!RQS) return fail(new VIOError(VIOError.Code.UNKNOWN_TYPE, `Missing request schema for type: ${String(name)}`));
|
|
83
|
-
if (!RSS) return fail(new VIOError(VIOError.Code.UNKNOWN_TYPE, `Missing response schema for type: ${String(name)}`));
|
|
84
|
-
let processedRequest: typeof RQS.infer;
|
|
85
|
-
try { processedRequest = RQS.processData(content); }
|
|
86
|
-
catch (error) { return fail(new VIOError(VIOError.Code.INVALID_DATA, `Failed to process request data for type ${String(name)}: ${(error as Error).message}`)); }
|
|
87
|
-
const respond = (responseData: typeof RSS.inferToProcess) => {
|
|
88
|
-
let readyResponse: any;
|
|
89
|
-
try { readyResponse = RSS.processData(responseData); }
|
|
90
|
-
catch (error) { return fail(new VIOError(VIOError.Code.INVALID_DATA, `Failed to process response data for type ${String(name)}: ${(error as Error).message}`)); }
|
|
91
|
-
const toSend = RRSchema.processData({ name, content: readyResponse });
|
|
92
|
-
done(toSend);
|
|
93
|
-
};
|
|
94
|
-
this.requestEmitter.emit(name, processedRequest, respond, fail);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
export namespace VIOStrict {
|
|
98
|
-
export interface EMap {
|
|
99
|
-
[name: string]: Schema<Schema.Property>;
|
|
100
|
-
}
|
|
101
|
-
export interface RREntry {
|
|
102
|
-
request: Schema<Schema.Definition.Object>;
|
|
103
|
-
response: Schema<Schema.Definition.Object>;
|
|
104
|
-
}
|
|
105
|
-
export interface RRMap {
|
|
106
|
-
[name: string]: RREntry;
|
|
107
|
-
}
|
|
108
|
-
export type VIOEventMap = {
|
|
109
|
-
error: [error: VIOError];
|
|
110
|
-
raw: [frame: Frame];
|
|
111
|
-
}
|
|
112
|
-
export type ReqEventMap<RRMap extends VIOStrict.RRMap> = {
|
|
113
|
-
[Name in string & keyof RRMap]: [
|
|
114
|
-
data: RRMap[Name]['request']['infer'],
|
|
115
|
-
respond: (data: RRMap[Name]['response']['inferToProcess']) => void,
|
|
116
|
-
reject: (error: VIOError) => void
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
export type EventMap<EMap extends VIOStrict.EMap> = {
|
|
120
|
-
[Name in string & keyof EMap]: [data: EMap[Name]['infer']];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
export default VIOStrict;
|
package/src/index.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module vortez-io
|
|
3
|
-
* @author NetFeez <netfeez.dev@gemail.com>
|
|
4
|
-
* @license Apache-2.0
|
|
5
|
-
* @description Provides encoding and decoding functionality for line-based content used in the Vortez IO protocol.
|
|
6
|
-
* The LineCodec class allows for converting between a ContentMap (a mapping of string keys to string values) and a line-based text format where each key-value pair is represented as a block of text with specific formatting rules.
|
|
7
|
-
* This is useful for encoding structured data in a human-readable format that can be easily transmitted and parsed within the Vortez IO protocol.
|
|
8
|
-
*/
|
|
9
|
-
import UTF8Codec from "./UTF8Codec.js";
|
|
10
|
-
|
|
11
|
-
export class LineCodec {
|
|
12
|
-
/**
|
|
13
|
-
* Encodes a ContentMap into a Uint8Array containing line-based content.
|
|
14
|
-
* Each key-value pair in the ContentMap is converted into a block of text where the key is on the first line, followed by the value on subsequent lines.
|
|
15
|
-
* Lines are separated by \r\n, and key-value pairs are separated by \r\n\r\n.
|
|
16
|
-
* If a value contains multiple lines, each subsequent line after the first will start with a space character to indicate that it is a continuation of the previous line.
|
|
17
|
-
* @param data A ContentMap object where each key is a string and each value is the corresponding string value to be encoded.
|
|
18
|
-
* @returns A Uint8Array containing the UTF-8 encoded line-based content representing the input ContentMap.
|
|
19
|
-
*/
|
|
20
|
-
public static encode(data: LineCodec.ContentMap): Uint8Array {
|
|
21
|
-
let content = '';
|
|
22
|
-
for (const [key, value] of Object.entries(data)) {
|
|
23
|
-
const valueProcessed = value.replace(/\r\n/g, '\r\n ');
|
|
24
|
-
content += `${key}\r\n${valueProcessed}\r\n\r\n`;
|
|
25
|
-
}
|
|
26
|
-
return UTF8Codec.encode(content);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Decodes a Uint8Array containing line-based content into a ContentMap.
|
|
30
|
-
* The input is expected to be UTF-8 encoded and formatted with lines separated by \r\n, and key-value pairs separated by \r\n\r\n.
|
|
31
|
-
* Values that span multiple lines should have subsequent lines starting with a space character.
|
|
32
|
-
* @param content A Uint8Array containing the UTF-8 encoded line-based content.
|
|
33
|
-
* @returns A ContentMap object where each key is a string and each value is the corresponding string value from the input, with multi-line values properly concatenated.
|
|
34
|
-
*/
|
|
35
|
-
public static decode(content: ArrayBuffer | Uint8Array): LineCodec.ContentMap {
|
|
36
|
-
const view = content instanceof ArrayBuffer ? new Uint8Array(content) : content;
|
|
37
|
-
const decodedContent = UTF8Codec.decode(view);
|
|
38
|
-
const lines = decodedContent.split('\r\n\r\n');
|
|
39
|
-
const result: LineCodec.ContentMap = {};
|
|
40
|
-
|
|
41
|
-
for (const line of lines) {
|
|
42
|
-
const [key, ...valueParts] = line.split('\r\n');
|
|
43
|
-
if (!key) continue;
|
|
44
|
-
const value = valueParts.join('\r\n').replace(/\r\n /g, '\r\n');
|
|
45
|
-
result[key] = value;
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export namespace LineCodec {
|
|
51
|
-
export interface ContentMap {
|
|
52
|
-
[key: string]: string;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
export default LineCodec;
|
package/src/support/UTF8Codec.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module vortez-io
|
|
3
|
-
* @author NetFeez <netfeez.dev@netfeez.dev>
|
|
4
|
-
* @license Apache-2.0
|
|
5
|
-
* @description UTF-8 encoder/decoder optimized for:
|
|
6
|
-
* - predictable memory usage (pre-allocation)
|
|
7
|
-
* - safe decoding (malformed/truncated input)
|
|
8
|
-
* - zero external dependencies
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ Fully standards-compliant behavior for replacement (U+FFFD)
|
|
11
|
-
*/
|
|
12
|
-
export class UTF8Codec {
|
|
13
|
-
private static readonly REPLACEMENT_CHAR = '\uFFFD';
|
|
14
|
-
/**
|
|
15
|
-
* Encodes a string into a UTF-8 byte array.
|
|
16
|
-
* @param text The string to encode.
|
|
17
|
-
* @returns A Uint8Array containing the UTF-8 encoded bytes of the input string.
|
|
18
|
-
*/
|
|
19
|
-
public static encode(text: string): Uint8Array {
|
|
20
|
-
const len = text.length;
|
|
21
|
-
let byteLength = 0;
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < len; i++) {
|
|
24
|
-
const code = text.charCodeAt(i);
|
|
25
|
-
|
|
26
|
-
if (code < 0x80) { byteLength += 1; }
|
|
27
|
-
else if (code < 0x800) { byteLength += 2; }
|
|
28
|
-
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
29
|
-
const next = i + 1 < len ? text.charCodeAt(i + 1) : 0;
|
|
30
|
-
|
|
31
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
32
|
-
byteLength += 4;
|
|
33
|
-
i++;
|
|
34
|
-
} else { byteLength += 3; }
|
|
35
|
-
} else { byteLength += 3; }
|
|
36
|
-
}
|
|
37
|
-
const out = new Uint8Array(byteLength);
|
|
38
|
-
let offset = 0;
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < len; i++) {
|
|
41
|
-
const code = text.charCodeAt(i);
|
|
42
|
-
if (code < 0x80) {
|
|
43
|
-
out[offset++] = code;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (code < 0x800) {
|
|
47
|
-
out[offset++] = 0xc0 | (code >> 6);
|
|
48
|
-
out[offset++] = 0x80 | (code & 0x3f);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
52
|
-
const next = i + 1 < len ? text.charCodeAt(i + 1) : 0;
|
|
53
|
-
|
|
54
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
55
|
-
const cp = 0x10000 + ((code & 0x3ff) << 10) + (next & 0x3ff);
|
|
56
|
-
|
|
57
|
-
out[offset++] = 0xf0 | (cp >> 18);
|
|
58
|
-
out[offset++] = 0x80 | ((cp >> 12) & 0x3f);
|
|
59
|
-
out[offset++] = 0x80 | ((cp >> 6) & 0x3f);
|
|
60
|
-
out[offset++] = 0x80 | (cp & 0x3f);
|
|
61
|
-
|
|
62
|
-
i++;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
offset = this.writeReplacement(out, offset);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
out[offset++] = 0xe0 | (code >> 12);
|
|
69
|
-
out[offset++] = 0x80 | ((code >> 6) & 0x3f);
|
|
70
|
-
out[offset++] = 0x80 | (code & 0x3f);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Decodes a UTF-8 byte array into a string. Malformed sequences are replaced with the Unicode replacement character (U+FFFD).
|
|
77
|
-
* @param bytes The Uint8Array containing the UTF-8 encoded bytes to decode.
|
|
78
|
-
* @returns The decoded string, with any malformed sequences replaced by U+FFFD.
|
|
79
|
-
*/
|
|
80
|
-
public static decode(bytes: Uint8Array): string {
|
|
81
|
-
let out = '';
|
|
82
|
-
let i = 0;
|
|
83
|
-
const len = bytes.length;
|
|
84
|
-
|
|
85
|
-
while (i < len) {
|
|
86
|
-
const b1 = bytes[i++];
|
|
87
|
-
if (b1 < 0x80) {
|
|
88
|
-
out += String.fromCharCode(b1);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if ((b1 & 0xe0) === 0xc0) {
|
|
92
|
-
if (i >= len) {
|
|
93
|
-
out += this.REPLACEMENT_CHAR;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const b2 = bytes[i++];
|
|
98
|
-
|
|
99
|
-
if ((b2 & 0xc0) !== 0x80) {
|
|
100
|
-
out += this.REPLACEMENT_CHAR;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const code = ((b1 & 0x1f) << 6) | (b2 & 0x3f);
|
|
105
|
-
out += String.fromCharCode(code);
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if ((b1 & 0xf0) === 0xe0) {
|
|
109
|
-
if (i + 1 >= len) {
|
|
110
|
-
out += this.REPLACEMENT_CHAR;
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const b2 = bytes[i++];
|
|
115
|
-
const b3 = bytes[i++];
|
|
116
|
-
|
|
117
|
-
if ((b2 & 0xc0) !== 0x80 || (b3 & 0xc0) !== 0x80) {
|
|
118
|
-
out += this.REPLACEMENT_CHAR;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const code = ((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
|
|
123
|
-
|
|
124
|
-
out += String.fromCharCode(code);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if ((b1 & 0xf8) === 0xf0) {
|
|
128
|
-
if (i + 2 >= len) {
|
|
129
|
-
out += this.REPLACEMENT_CHAR;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const b2 = bytes[i++];
|
|
134
|
-
const b3 = bytes[i++];
|
|
135
|
-
const b4 = bytes[i++];
|
|
136
|
-
|
|
137
|
-
if ((b2 & 0xc0) !== 0x80 || (b3 & 0xc0) !== 0x80 || (b4 & 0xc0) !== 0x80) {
|
|
138
|
-
out += this.REPLACEMENT_CHAR;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const cp = ((b1 & 0x07) << 18) | ((b2 & 0x3f) << 12) | ((b3 & 0x3f) << 6) | (b4 & 0x3f);
|
|
143
|
-
|
|
144
|
-
out += String.fromCodePoint(cp);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
out += this.REPLACEMENT_CHAR;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Writes the UTF-8 encoding of the replacement character (U+FFFD) to the output buffer at the specified offset.
|
|
154
|
-
* @param out The output buffer to write to.
|
|
155
|
-
* @param offset The offset in the buffer where the replacement character should be written.
|
|
156
|
-
* @returns The new offset after writing the replacement character.
|
|
157
|
-
*/
|
|
158
|
-
protected static writeReplacement(out: Uint8Array, offset: number): number {
|
|
159
|
-
out[offset++] = 0xef;
|
|
160
|
-
out[offset++] = 0xbf;
|
|
161
|
-
out[offset++] = 0xbd;
|
|
162
|
-
return offset;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export default UTF8Codec;
|
package/src/support/UUID.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module vortez-io
|
|
3
|
-
* @author NetFeez <netfeez.dev@gemail.com>
|
|
4
|
-
* @license Apache-2.0
|
|
5
|
-
* @description Isomorphic UUID v4 generator.
|
|
6
|
-
*
|
|
7
|
-
* ⚠️ IMPORTANT: READ THIS BEFORE USING.
|
|
8
|
-
*
|
|
9
|
-
* IS NO LONGER CRYPTOGRAPHICALLY SECURE.
|
|
10
|
-
* This module provides a fallback implementation of UUID v4 generation using a Linear Congruential Generator (LCG) algorithm
|
|
11
|
-
* The generated UUIDs are suitable for non-cryptographic use cases, such as generating unique identifiers for events or requests within the Vortez IO protocol, but should not be used in security-sensitive contexts.
|
|
12
|
-
*/
|
|
13
|
-
export class UUID {
|
|
14
|
-
private static seed = UUID.initSeed();
|
|
15
|
-
|
|
16
|
-
private static readonly HEX_TABLE = Array.from(
|
|
17
|
-
{ length: 256 },
|
|
18
|
-
(_, i) => i.toString(16).padStart(2, '0')
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Initialize the seed for the LCG fallback using a combination of current time and random value.
|
|
23
|
-
* This ensures that the generated UUIDs are not the same across different runs of the application, even in fallback mode.
|
|
24
|
-
* @returns A 32-bit unsigned integer to be used as the initial seed for the LCG.
|
|
25
|
-
*/
|
|
26
|
-
private static initSeed(): number {
|
|
27
|
-
return (Date.now() ^ Math.floor(Math.random() * 0xffffffff) ) >>> 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Generate the next 32-bit unsigned integer using a Linear Congruential Generator (LCG) algorithm.
|
|
32
|
-
* This method is used as a fallback for generating random values when crypto APIs are not available.
|
|
33
|
-
* The LCG parameters are chosen to provide a reasonable period and distribution for non-cryptographic use cases.
|
|
34
|
-
* @returns A 32-bit unsigned integer generated by the LCG algorithm.
|
|
35
|
-
*/
|
|
36
|
-
private static next32(): number {
|
|
37
|
-
UUID.seed = (1664525 * UUID.seed + 1013904223) >>> 0;
|
|
38
|
-
return UUID.seed;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Generate a UUID v4 string in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
|
|
43
|
-
* This method uses the createRaw() method to generate a 16-byte Uint8Array and then converts it to the standard UUID string format.
|
|
44
|
-
* @returns A string representation of the generated UUID.
|
|
45
|
-
*/
|
|
46
|
-
public static create(): string {
|
|
47
|
-
return this.binToUuid(this.createRaw());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Generate a 16-byte Uint8Array containing a UUID v4.
|
|
52
|
-
* This method uses the LCG fallback to generate random values when crypto APIs are not available.
|
|
53
|
-
* @returns A Uint8Array containing the 16 bytes of the UUID.
|
|
54
|
-
*/
|
|
55
|
-
public static createRaw(): Uint8Array {
|
|
56
|
-
const bytes = new Uint8Array(16);
|
|
57
|
-
|
|
58
|
-
for (let i = 0; i < 16; i += 4) {
|
|
59
|
-
const n = UUID.next32();
|
|
60
|
-
|
|
61
|
-
bytes[i] = (n >>> 24) & 0xff;
|
|
62
|
-
bytes[i + 1] = (n >>> 16) & 0xff;
|
|
63
|
-
bytes[i + 2] = (n >>> 8) & 0xff;
|
|
64
|
-
bytes[i + 3] = n & 0xff;
|
|
65
|
-
}
|
|
66
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
67
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
68
|
-
|
|
69
|
-
return bytes;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Convert a 16-byte Uint8Array to a UUID string in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
|
|
74
|
-
* @param bin A Uint8Array containing the 16 bytes of the UUID.
|
|
75
|
-
* @returns A string representation of the UUID.
|
|
76
|
-
*/
|
|
77
|
-
public static binToUuid(bin: Uint8Array): string {
|
|
78
|
-
const hex = UUID.HEX_TABLE;
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
hex[bin[0]] + hex[bin[1]] + hex[bin[2]] + hex[bin[3]] + "-" +
|
|
82
|
-
hex[bin[4]] + hex[bin[5]] + "-" +
|
|
83
|
-
hex[bin[6]] + hex[bin[7]] + "-" +
|
|
84
|
-
hex[bin[8]] + hex[bin[9]] + "-" +
|
|
85
|
-
hex[bin[10]] + hex[bin[11]] + hex[bin[12]] +
|
|
86
|
-
hex[bin[13]] + hex[bin[14]] + hex[bin[15]]
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Convert a UUID string in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx to a 16-byte Uint8Array.
|
|
91
|
-
* @param uuid A string representation of the UUID.
|
|
92
|
-
* @returns A Uint8Array containing the 16 bytes of the UUID.
|
|
93
|
-
*/
|
|
94
|
-
public static uuidToBin(uuid: string): Uint8Array {
|
|
95
|
-
const hex = UUID.HEX_TABLE;
|
|
96
|
-
const bin = new Uint8Array(16);
|
|
97
|
-
|
|
98
|
-
const parts = uuid.split('-');
|
|
99
|
-
if (parts.length !== 5) throw new Error(`Invalid UUID format: ${uuid}`);
|
|
100
|
-
|
|
101
|
-
const hexStr = parts.join('');
|
|
102
|
-
if (hexStr.length !== 32) throw new Error(`Invalid UUID format: ${uuid}`);
|
|
103
|
-
|
|
104
|
-
for (let i = 0; i < 16; i++) {
|
|
105
|
-
const byteHex = hexStr.substr(i * 2, 2);
|
|
106
|
-
const byteValue = parseInt(byteHex, 16);
|
|
107
|
-
if (isNaN(byteValue)) throw new Error(`Invalid UUID format: ${uuid}`);
|
|
108
|
-
bin[i] = byteValue;
|
|
109
|
-
}
|
|
110
|
-
return bin;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Validate whether a given string is a valid UUID v4 according to the standard format and version/type bits.
|
|
114
|
-
* This method checks that the string matches the expected pattern for UUID v4, including the correct placement of hyphens and the specific bits that indicate version 4 and the variant.
|
|
115
|
-
* @param uuid The string to validate as a UUID v4.
|
|
116
|
-
* @returns True if the input string is a valid UUID v4, false otherwise.
|
|
117
|
-
*/
|
|
118
|
-
public static isValid(uuid: string): boolean {
|
|
119
|
-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
120
|
-
return uuidRegex.test(uuid);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export namespace UUID {}
|
|
125
|
-
export default UUID;
|
package/src/support/Varint.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module vortez-io
|
|
3
|
-
* @author NetFeez <netfeez.dev@gemail.com>
|
|
4
|
-
* @license Apache-2.0
|
|
5
|
-
* @description Provides encoding and decoding functionality for variable-length integers (varints) used in the Vortez IO protocol.
|
|
6
|
-
* Varints are a compact way to encode integers, where smaller numbers take fewer bytes. Each byte uses the lower 7 bits to store data and the highest bit as a continuation flag (1 if more bytes follow, 0 if this is the last byte).
|
|
7
|
-
* This module includes methods for reading varints from a Uint8Array and writing numbers as varints into a Uint8Array, with error handling for invalid input and buffer overflows.
|
|
8
|
-
*/
|
|
9
|
-
import VIOError from "../VIOError.js";
|
|
10
|
-
|
|
11
|
-
export class Varint {
|
|
12
|
-
/**
|
|
13
|
-
* Reads a varint-encoded integer from the given Uint8Array starting at the specified offset.
|
|
14
|
-
* Varints are a compact way to encode integers, where smaller numbers take fewer bytes.
|
|
15
|
-
* Each byte uses the lower 7 bits to store data and the highest bit as a continuation flag (1 if more bytes follow, 0 if this is the last byte).
|
|
16
|
-
* @param view The Uint8Array to read from.
|
|
17
|
-
* @param offset The starting index in the array to read the varint from.
|
|
18
|
-
* @returns An object containing the decoded integer value and the new offset after reading the varint.
|
|
19
|
-
*/
|
|
20
|
-
public static readVarint(view: Uint8Array, offset: number): Varint.Result {
|
|
21
|
-
let value = 0;
|
|
22
|
-
let shift = 0;
|
|
23
|
-
let pos = offset;
|
|
24
|
-
|
|
25
|
-
while (true) {
|
|
26
|
-
if (pos >= view.length) throw new VIOError(VIOError.Code.BUFFER_TOO_SHORT, 'Varint truncated');
|
|
27
|
-
const byte = view[pos++];
|
|
28
|
-
value |= (byte & 0x7F) << shift;
|
|
29
|
-
if ((byte & 0x80) === 0) break;
|
|
30
|
-
shift += 7;
|
|
31
|
-
if (shift > 35) throw new VIOError(VIOError.Code.INVALID_DATA, 'Varint too large');
|
|
32
|
-
}
|
|
33
|
-
return { value, offset: pos };
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Encodes a number into a varint format (base-128 encoding) and returns it as a Uint8Array.
|
|
37
|
-
* Varints are a compact way to encode integers, where smaller numbers take fewer bytes.
|
|
38
|
-
* Each byte uses the lower 7 bits to store data and the highest bit as a continuation flag (1 if more bytes follow, 0 if this is the last byte).
|
|
39
|
-
* @param value The number to encode as a varint. Must be a non-negative integer.
|
|
40
|
-
* @returns A Uint8Array containing the varint-encoded bytes of the input number.
|
|
41
|
-
*/
|
|
42
|
-
public static writeVarint(value: number): Uint8Array {
|
|
43
|
-
if (value < 0) throw new VIOError(VIOError.Code.INVALID_DATA, 'Varint cannot be negative');
|
|
44
|
-
const bytes: number[] = [];
|
|
45
|
-
|
|
46
|
-
while (true) {
|
|
47
|
-
let byte = value & 0x7F;
|
|
48
|
-
value >>>= 7;
|
|
49
|
-
if (value !== 0) { byte |= 0x80; }
|
|
50
|
-
bytes.push(byte);
|
|
51
|
-
if (value === 0) break;
|
|
52
|
-
}
|
|
53
|
-
return new Uint8Array(bytes);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export namespace Varint {
|
|
57
|
-
export interface Result {
|
|
58
|
-
value: number;
|
|
59
|
-
offset: number;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export default Varint;
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"sourceMap": true,
|
|
9
|
-
"strict": true,
|
|
10
|
-
"strictNullChecks": true,
|
|
11
|
-
"rootDir": "src",
|
|
12
|
-
"outDir": "build",
|
|
13
|
-
"types": [],
|
|
14
|
-
"lib": [ "ESNext" ],
|
|
15
|
-
},
|
|
16
|
-
"include": [ "src/**/*.ts" ]
|
|
17
|
-
}
|