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.
- package/.gitignore +15 -0
- package/LICENSE +201 -0
- package/PROTOCOL.md +245 -0
- package/README.md +103 -0
- package/build/Frame.d.ts +118 -0
- package/build/Frame.js +241 -0
- package/build/Frame.js.map +1 -0
- package/build/Isomorphic.d.ts +7 -0
- package/build/Isomorphic.js +2 -0
- package/build/Isomorphic.js.map +1 -0
- package/build/RequestManager.d.ts +74 -0
- package/build/RequestManager.js +61 -0
- package/build/RequestManager.js.map +1 -0
- package/build/VIO.d.ts +82 -0
- package/build/VIO.js +219 -0
- package/build/VIO.js.map +1 -0
- package/build/VIOError.d.ts +17 -0
- package/build/VIOError.js +25 -0
- package/build/VIOError.js.map +1 -0
- package/build/VIOStrict.d.ts +62 -0
- package/build/VIOStrict.js +103 -0
- package/build/VIOStrict.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/support/LineParser.d.ts +25 -0
- package/build/support/LineParser.js +50 -0
- package/build/support/LineParser.js.map +1 -0
- package/build/support/UTF8Codec.d.ts +34 -0
- package/build/support/UTF8Codec.js +155 -0
- package/build/support/UTF8Codec.js.map +1 -0
- package/build/support/UUID.d.ts +62 -0
- package/build/support/UUID.js +109 -0
- package/build/support/UUID.js.map +1 -0
- package/build/support/Varint.d.ts +26 -0
- package/build/support/Varint.js +61 -0
- package/build/support/Varint.js.map +1 -0
- package/docs/PROTOCOL-PROPOSE.md +252 -0
- package/package.json +35 -0
- package/scripts/build.sh +14 -0
- package/src/Frame.ts +312 -0
- package/src/Isomorphic.ts +7 -0
- package/src/RequestManager.ts +114 -0
- package/src/VIO.ts +194 -0
- package/src/VIOError.ts +22 -0
- package/src/VIOStrict.ts +123 -0
- package/src/index.ts +4 -0
- package/src/support/LineParser.ts +55 -0
- package/src/support/UTF8Codec.ts +166 -0
- package/src/support/UUID.ts +125 -0
- package/src/support/Varint.ts +62 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,114 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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;
|