vortez-io 1.0.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.
Files changed (52) hide show
  1. package/.gitignore +15 -0
  2. package/LICENSE +201 -0
  3. package/PROTOCOL.md +245 -0
  4. package/README.md +103 -0
  5. package/build/Frame.d.ts +118 -0
  6. package/build/Frame.js +241 -0
  7. package/build/Frame.js.map +1 -0
  8. package/build/Isomorphic.d.ts +7 -0
  9. package/build/Isomorphic.js +2 -0
  10. package/build/Isomorphic.js.map +1 -0
  11. package/build/RequestManager.d.ts +74 -0
  12. package/build/RequestManager.js +61 -0
  13. package/build/RequestManager.js.map +1 -0
  14. package/build/VIO.d.ts +82 -0
  15. package/build/VIO.js +219 -0
  16. package/build/VIO.js.map +1 -0
  17. package/build/VIOError.d.ts +17 -0
  18. package/build/VIOError.js +25 -0
  19. package/build/VIOError.js.map +1 -0
  20. package/build/VIOStrict.d.ts +62 -0
  21. package/build/VIOStrict.js +103 -0
  22. package/build/VIOStrict.js.map +1 -0
  23. package/build/index.d.ts +4 -0
  24. package/build/index.js +5 -0
  25. package/build/index.js.map +1 -0
  26. package/build/support/LineParser.d.ts +25 -0
  27. package/build/support/LineParser.js +50 -0
  28. package/build/support/LineParser.js.map +1 -0
  29. package/build/support/UTF8Codec.d.ts +34 -0
  30. package/build/support/UTF8Codec.js +155 -0
  31. package/build/support/UTF8Codec.js.map +1 -0
  32. package/build/support/UUID.d.ts +62 -0
  33. package/build/support/UUID.js +109 -0
  34. package/build/support/UUID.js.map +1 -0
  35. package/build/support/Varint.d.ts +26 -0
  36. package/build/support/Varint.js +61 -0
  37. package/build/support/Varint.js.map +1 -0
  38. package/docs/PROTOCOL-PROPOSE.md +252 -0
  39. package/package.json +35 -0
  40. package/scripts/build.sh +14 -0
  41. package/src/Frame.ts +312 -0
  42. package/src/Isomorphic.ts +7 -0
  43. package/src/RequestManager.ts +114 -0
  44. package/src/VIO.ts +194 -0
  45. package/src/VIOError.ts +22 -0
  46. package/src/VIOStrict.ts +123 -0
  47. package/src/index.ts +4 -0
  48. package/src/support/LineParser.ts +55 -0
  49. package/src/support/UTF8Codec.ts +166 -0
  50. package/src/support/UUID.ts +125 -0
  51. package/src/support/Varint.ts +62 -0
  52. package/tsconfig.json +17 -0
@@ -0,0 +1,252 @@
1
+ > [!IMPORTANT]
2
+ > THIS IS THE FIRST REVISION OF THE PROTOCOL, THIS IS NOT APPLICABLE
3
+ > THIS DOCUMENT IS ONLY FOR HISTORIC DOCUMENTATION
4
+ > SEE THE [REAL SPECIFICATION HERE](https://github.com/NetFeez/vortez-io-protocol/blob/main/PROTOCOL.md)
5
+
6
+ # Vortez IO Protocol
7
+
8
+ Vortez IO Protocol works over WebSocket protocol and defines a binary frame structure for multiplexed messaging (events, requests, responses and errors).
9
+
10
+ Each WebSocket frame **MUST** contain exactly **one Vortez frame**.
11
+
12
+ ---
13
+
14
+ ## Protocol Frame Description
15
+
16
+ ### Header description map
17
+
18
+ | byte | bit | Function | description |
19
+ | ---- | --- | -------- | --------------------------- |
20
+ | 0 | 0-3 | TYPE | [Reference for type](#type) |
21
+ | 0 | 4-6 | MODE | [Reference for mode](#mode) |
22
+ | 0 | 7 | HUID | [Reference for huid](#huid) |
23
+
24
+ > [!IMPORTANT]
25
+ >
26
+ > 🔒 **Invariant:** Header byte (byte 0) is always present.
27
+ >
28
+ > 🔒 **Invariant:** Header byte (byte 1) is always present and reserved.
29
+
30
+ ---
31
+
32
+ ## Frame Layout
33
+
34
+ ### If `HUID = 0b1`
35
+
36
+ | byte | Function | description |
37
+ | ---- | -------- | --------------------------- |
38
+ | 2-17 | UUID | 16-byte identifier (binary) |
39
+ | 18.. | DATA | Payload |
40
+
41
+ ### If `HUID = 0b0`
42
+
43
+ | byte | Function | description |
44
+ | ---- | -------- | ----------- |
45
+ | 2.. | DATA | Payload |
46
+
47
+ ---
48
+
49
+ ## UUID Specification
50
+
51
+ * UUID **MUST** be exactly **16 bytes**
52
+ * UUID **MUST** be treated as opaque binary (no string encoding)
53
+ * Endianness is **network order (big-endian)**
54
+ * UUID is used for **request-response correlation**
55
+
56
+ ---
57
+
58
+ ## TYPE
59
+
60
+ Defines the semantic meaning of the frame.
61
+
62
+ | Value | Name | Description |
63
+ | ----- | -------- | -------------------------- |
64
+ | `0x0` | RAW | Unstructured binary data |
65
+ | `0x1` | EVENT | Fire-and-forget message |
66
+ | `0x2` | REQUEST | Request expecting response |
67
+ | `0x3` | RESPONSE | Response to a request |
68
+ | `0x4` | ERROR | Error frame |
69
+
70
+ ---
71
+
72
+ ### TYPE Rules
73
+
74
+ #### `0x0` RAW
75
+
76
+ * **MAY** include any MODE
77
+ * **MUST NOT** rely on protocol-level semantics
78
+ * **MUST NOT** require UUID
79
+ * Receiver **MUST NOT** assume structure
80
+
81
+ ---
82
+
83
+ #### `0x1` EVENT
84
+
85
+ * **MUST NOT** include UUID (`HUID = 0`)
86
+ * **MUST** be treated as one-way message
87
+ * Receiver **MUST NOT** send RESPONSE automatically
88
+
89
+ ---
90
+
91
+ #### `0x2` REQUEST
92
+
93
+ * **MUST** include UUID (`HUID = 1`)
94
+ * UUID **MUST** be unique per connection (until resolved)
95
+ * Receiver **MUST** respond with either:
96
+
97
+ * `RESPONSE (0x3)`
98
+ * or `ERROR (0x4)`
99
+
100
+ ---
101
+
102
+ #### `0x3` RESPONSE
103
+
104
+ * **MUST** include UUID (`HUID = 1`)
105
+ * UUID **MUST** match an existing REQUEST
106
+ * Receiver **MUST** resolve the associated request
107
+
108
+ ---
109
+
110
+ #### `0x4` ERROR
111
+
112
+ Error frames represent protocol or application failures.
113
+
114
+ * **MAY** include UUID
115
+ * If UUID is present:
116
+
117
+ * it **MUST** reference the related REQUEST
118
+ * If UUID is absent:
119
+
120
+ * it represents a **connection-level or protocol-level error**
121
+
122
+ ---
123
+
124
+ ## ERROR Semantics
125
+
126
+ We define two clear categories:
127
+
128
+ ### 1. Error Response (Request-bound)
129
+
130
+ Used as a response to a request.
131
+
132
+ * TYPE: `0x4`
133
+ * HUID: `1`
134
+ * MUST include UUID
135
+ * MUST correspond to a REQUEST
136
+
137
+ Example causes:
138
+
139
+ * invalid payload
140
+ * handler failure
141
+ * validation error
142
+
143
+ ---
144
+
145
+ ### 2. Error Feedback (Unbound / Protocol-level)
146
+
147
+ Used when no request context exists.
148
+
149
+ * TYPE: `0x4`
150
+ * HUID: `0`
151
+ * MUST NOT include UUID
152
+
153
+ Example causes:
154
+
155
+ * malformed frame
156
+ * invalid header bits
157
+ * unsupported TYPE or MODE
158
+ * protocol violation
159
+
160
+ ---
161
+
162
+ ### Error Behavior Rules
163
+
164
+ If a frame violates protocol:
165
+
166
+ * Receiver **MUST** send `ERROR (0x4)`
167
+ * If UUID is available → **MUST include it**
168
+ * Otherwise → **MUST send without UUID**
169
+
170
+ ---
171
+
172
+ ## MODE
173
+
174
+ Defines how DATA should be interpreted.
175
+
176
+ | Value | Name | Description |
177
+ | ----- | ------ | ------------------------------------ |
178
+ | `0x0` | BINARY | Binary |
179
+ | `0x1` | JSON | UTF-8 encoded JSON |
180
+ | `0x2` | LINE | UTF-8 text terminated by LF (`0x0A`) |
181
+ | `0x3` | CUSTOM | Application-defined |
182
+
183
+ ---
184
+
185
+ ### MODE Rules
186
+
187
+ #### JSON (`0x1`)
188
+
189
+ * DATA **MUST** be valid UTF-8 JSON
190
+ * Receiver **MUST** parse as JSON
191
+
192
+ ---
193
+
194
+ #### LINE (`0x2`)
195
+
196
+ * DATA **MUST** be UTF-8 text
197
+ * Messages **SHOULD** end with `\n` (`0x0A`)
198
+ * Receiver **MAY** trim trailing newline
199
+
200
+ ---
201
+
202
+ #### CUSTOM (`0x3`)
203
+
204
+ * Interpretation is application-defined
205
+ * Protocol **MUST NOT** enforce structure
206
+
207
+ ---
208
+
209
+ ## HUID
210
+
211
+ Defines presence of UUID.
212
+
213
+ | Value | Meaning |
214
+ | ----- | ------------ |
215
+ | `0b0` | No UUID |
216
+ | `0b1` | UUID present |
217
+
218
+ ---
219
+
220
+ ## HUID Rules
221
+
222
+ | TYPE | HUID | Rule |
223
+ | ---------------- | ---- | --------------------- |
224
+ | RAW (`0x0`) | ANY | MAY include UUID |
225
+ | EVENT (`0x1`) | 0 | MUST NOT include UUID |
226
+ | REQUEST (`0x2`) | 1 | MUST include UUID |
227
+ | RESPONSE (`0x3`) | 1 | MUST include UUID |
228
+ | ERROR (`0x4`) | ANY | Context-dependent |
229
+
230
+ ---
231
+
232
+ ## Reserved Space (Future Use)
233
+
234
+ * Byte `1` is **implicitly reserved**
235
+ * Future versions **MAY** introduce:
236
+
237
+ * length prefix
238
+ * flags
239
+ * versioning
240
+
241
+ Current implementations:
242
+
243
+ * **MUST ignore** any future extension unless negotiated
244
+
245
+ ---
246
+
247
+ ## Protocol Guarantees
248
+
249
+ * One WebSocket frame = one Vortez frame
250
+ * No internal framing is used
251
+ * Ordering is delegated to WebSocket
252
+ * Reliability is delegated to transport
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "vortez-io",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Binary WebSocket protocol for multiplexed events, requests, responses, and errors in Vortez or other applications.",
6
+ "main": "build/index.js",
7
+ "scripts": {
8
+ "build": "bash scripts/build.sh"
9
+ },
10
+ "keywords": [
11
+ "vortez",
12
+ "vortez-io",
13
+ "websocket",
14
+ "protocol",
15
+ "binary protocol",
16
+ "request response",
17
+ "event-driven",
18
+ "messaging",
19
+ "typescript",
20
+ "realtime"
21
+ ],
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
+ "license": "Apache-2.0",
32
+ "dependencies": {
33
+ "@netfeez/common": "^3.1.1"
34
+ }
35
+ }
@@ -0,0 +1,14 @@
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 ADDED
@@ -0,0 +1,312 @@
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;
@@ -0,0 +1,7 @@
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;