@nmtjs/ws-client 0.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/LICENSE.md +7 -0
- package/README.md +1 -0
- package/dist/index.js +342 -0
- package/dist/index.js.map +1 -0
- package/index.ts +446 -0
- package/package.json +26 -0
- package/tsconfig.json +6 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2024 Denis Ilchyshyn
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
## WebSockets transport for [Neemata](https://github.com/neematajs/neemata)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { ClientTransport, ClientUpStream, Subscription, createClientDownStream, onAbort, once } from '@nmtjs/client';
|
|
2
|
+
import { MessageType, MessageTypeName, TransportType, concat, decodeNumber, encodeNumber } from '@nmtjs/common';
|
|
3
|
+
export class WsClientTransport extends ClientTransport {
|
|
4
|
+
options;
|
|
5
|
+
type;
|
|
6
|
+
calls;
|
|
7
|
+
subscriptions;
|
|
8
|
+
upStreams;
|
|
9
|
+
downStreams;
|
|
10
|
+
upStreamId;
|
|
11
|
+
queryParams;
|
|
12
|
+
ws;
|
|
13
|
+
isHealthy;
|
|
14
|
+
checkHealthAttempts;
|
|
15
|
+
autoreconnect;
|
|
16
|
+
isConnected;
|
|
17
|
+
wsFactory;
|
|
18
|
+
encodeRpcContext;
|
|
19
|
+
decodeRpcContext;
|
|
20
|
+
constructor(options){
|
|
21
|
+
super();
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.type = TransportType.WS;
|
|
24
|
+
this.calls = new Map();
|
|
25
|
+
this.subscriptions = new Map();
|
|
26
|
+
this.upStreams = new Map();
|
|
27
|
+
this.downStreams = new Map();
|
|
28
|
+
this.upStreamId = 0;
|
|
29
|
+
this.queryParams = {};
|
|
30
|
+
this.isHealthy = false;
|
|
31
|
+
this.checkHealthAttempts = 0;
|
|
32
|
+
this.isConnected = false;
|
|
33
|
+
this.autoreconnect = this.options.autoreconnect ?? true;
|
|
34
|
+
if (options.wsFactory) {
|
|
35
|
+
this.wsFactory = options.wsFactory;
|
|
36
|
+
} else {
|
|
37
|
+
this.wsFactory = (url)=>new WebSocket(url.toString());
|
|
38
|
+
}
|
|
39
|
+
this.on('close', (error)=>this.clear(error));
|
|
40
|
+
this.encodeRpcContext = {
|
|
41
|
+
addStream: (blob)=>{
|
|
42
|
+
const id = ++this.upStreamId;
|
|
43
|
+
const upstream = new ClientUpStream(id, blob);
|
|
44
|
+
this.upStreams.set(id, upstream);
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
metadata: blob.metadata
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
getStream: (id)=>this.upStreams.get(id)
|
|
51
|
+
};
|
|
52
|
+
this.decodeRpcContext = {
|
|
53
|
+
addStream: (id, metadata)=>{
|
|
54
|
+
const downstream = createClientDownStream(metadata, ()=>this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32')));
|
|
55
|
+
this.downStreams.set(id, downstream);
|
|
56
|
+
return downstream.blob;
|
|
57
|
+
},
|
|
58
|
+
getStream: (id)=>this.downStreams.get(id)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async connect() {
|
|
62
|
+
this.autoreconnect = this.options.autoreconnect ?? true;
|
|
63
|
+
await this.healthCheck();
|
|
64
|
+
this.ws = this.wsFactory(this.getURL('api', 'ws', {
|
|
65
|
+
...this.queryParams,
|
|
66
|
+
accept: this.client.format.contentType,
|
|
67
|
+
'content-type': this.client.format.contentType,
|
|
68
|
+
services: this.client.services
|
|
69
|
+
}));
|
|
70
|
+
this.ws.binaryType = 'arraybuffer';
|
|
71
|
+
this.ws.onmessage = (event)=>{
|
|
72
|
+
const buffer = event.data;
|
|
73
|
+
const type = decodeNumber(buffer, 'Uint8');
|
|
74
|
+
const handler = this[type];
|
|
75
|
+
if (handler) {
|
|
76
|
+
handler.call(this, buffer.slice(Uint8Array.BYTES_PER_ELEMENT), this.ws);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
this.ws.onopen = (event)=>{
|
|
80
|
+
this.isConnected = true;
|
|
81
|
+
this.emit('open');
|
|
82
|
+
this.checkHealthAttempts = 0;
|
|
83
|
+
};
|
|
84
|
+
this.ws.onclose = (event)=>{
|
|
85
|
+
this.isConnected = false;
|
|
86
|
+
this.isHealthy = false;
|
|
87
|
+
this.emit('close', event.code === 1000 ? undefined : new Error(`Connection closed with code ${event.code}: ${event.reason}`));
|
|
88
|
+
if (this.autoreconnect) this.connect();
|
|
89
|
+
};
|
|
90
|
+
this.ws.onerror = (event)=>{
|
|
91
|
+
this.isHealthy = false;
|
|
92
|
+
};
|
|
93
|
+
await once(this, 'open');
|
|
94
|
+
this.emit('connect');
|
|
95
|
+
}
|
|
96
|
+
async disconnect() {
|
|
97
|
+
this.autoreconnect = false;
|
|
98
|
+
this.ws?.close();
|
|
99
|
+
await once(this, 'close');
|
|
100
|
+
}
|
|
101
|
+
async rpc(call) {
|
|
102
|
+
const { signal, callId, payload, procedure, service } = call;
|
|
103
|
+
const data = this.client.format.encodeRpc({
|
|
104
|
+
callId,
|
|
105
|
+
service,
|
|
106
|
+
procedure,
|
|
107
|
+
payload
|
|
108
|
+
}, this.encodeRpcContext);
|
|
109
|
+
onAbort(signal, ()=>{
|
|
110
|
+
const call = this.calls.get(callId);
|
|
111
|
+
if (call) {
|
|
112
|
+
const { reject } = call;
|
|
113
|
+
reject(new Error('Request aborted'));
|
|
114
|
+
this.calls.delete(callId);
|
|
115
|
+
this.send(MessageType.RpcAbort, encodeNumber(callId, 'Uint32'));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
if (!this.isConnected) await once(this, 'connect');
|
|
119
|
+
return new Promise((resolve, reject)=>{
|
|
120
|
+
this.calls.set(callId, {
|
|
121
|
+
resolve,
|
|
122
|
+
reject
|
|
123
|
+
});
|
|
124
|
+
this.send(MessageType.Rpc, data);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
setQueryParams(params) {
|
|
128
|
+
this.queryParams = params;
|
|
129
|
+
}
|
|
130
|
+
send(type, ...payload) {
|
|
131
|
+
this.ws?.send(concat(encodeNumber(type, 'Uint8'), ...payload));
|
|
132
|
+
if (this.options.debug) {
|
|
133
|
+
console.groupCollapsed(`[WS] Sent ${MessageTypeName[type]}`);
|
|
134
|
+
console.trace();
|
|
135
|
+
console.groupEnd();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async clear(error = new Error('Connection closed')) {
|
|
139
|
+
for (const call of this.calls.values()){
|
|
140
|
+
const { reject } = call;
|
|
141
|
+
reject(error);
|
|
142
|
+
}
|
|
143
|
+
for (const stream of this.upStreams.values()){
|
|
144
|
+
stream.reader.cancel(error);
|
|
145
|
+
}
|
|
146
|
+
for (const stream of this.downStreams.values()){
|
|
147
|
+
stream.writer.abort(error);
|
|
148
|
+
}
|
|
149
|
+
for (const subscription of this.subscriptions.values()){
|
|
150
|
+
subscription.unsubscribe();
|
|
151
|
+
}
|
|
152
|
+
this.calls.clear();
|
|
153
|
+
this.upStreams.clear();
|
|
154
|
+
this.downStreams.clear();
|
|
155
|
+
this.subscriptions.clear();
|
|
156
|
+
}
|
|
157
|
+
async healthCheck() {
|
|
158
|
+
while(!this.isHealthy){
|
|
159
|
+
try {
|
|
160
|
+
const signal = AbortSignal.timeout(10000);
|
|
161
|
+
const url = this.getURL('healthy', 'http', {
|
|
162
|
+
auth: this.client.auth
|
|
163
|
+
});
|
|
164
|
+
const { ok } = await fetch(url, {
|
|
165
|
+
signal
|
|
166
|
+
});
|
|
167
|
+
this.isHealthy = ok;
|
|
168
|
+
} catch (e) {}
|
|
169
|
+
if (!this.isHealthy) {
|
|
170
|
+
this.checkHealthAttempts++;
|
|
171
|
+
const seconds = Math.min(this.checkHealthAttempts, 15);
|
|
172
|
+
await new Promise((r)=>setTimeout(r, seconds * 1000));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.emit('healthy');
|
|
176
|
+
}
|
|
177
|
+
getURL(path = '', protocol, params = {}) {
|
|
178
|
+
const base = new URL(this.options.origin);
|
|
179
|
+
const secure = base.protocol === 'https:';
|
|
180
|
+
const url = new URL(`${secure ? protocol + 's' : protocol}://${base.host}/${path}`);
|
|
181
|
+
for (let [key, values] of Object.entries(params)){
|
|
182
|
+
if (!Array.isArray(values)) values = [
|
|
183
|
+
values
|
|
184
|
+
];
|
|
185
|
+
for (const value of values){
|
|
186
|
+
url.searchParams.append(key, value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return url;
|
|
190
|
+
}
|
|
191
|
+
resolveRpc(callId, value) {
|
|
192
|
+
const call = this.calls.get(callId);
|
|
193
|
+
if (call) {
|
|
194
|
+
const { resolve } = call;
|
|
195
|
+
this.calls.delete(callId);
|
|
196
|
+
resolve(value);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async [MessageType.Event](buffer) {
|
|
200
|
+
const [service, event, payload] = this.client.format.decode(buffer);
|
|
201
|
+
if (this.options.debug) {
|
|
202
|
+
console.groupCollapsed(`[WS] Received "Event" ${service}/${event}`);
|
|
203
|
+
console.log(payload);
|
|
204
|
+
console.groupEnd();
|
|
205
|
+
}
|
|
206
|
+
this.emit('event', service, event, payload);
|
|
207
|
+
}
|
|
208
|
+
async [MessageType.Rpc](buffer) {
|
|
209
|
+
const { callId, error, payload } = this.client.format.decodeRpc(buffer, this.decodeRpcContext);
|
|
210
|
+
if (this.calls.has(callId)) {
|
|
211
|
+
this.resolveRpc(callId, error ? {
|
|
212
|
+
success: false,
|
|
213
|
+
error
|
|
214
|
+
} : {
|
|
215
|
+
success: true,
|
|
216
|
+
value: payload
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async [MessageType.UpStreamPull](buffer) {
|
|
221
|
+
const id = decodeNumber(buffer, 'Uint32');
|
|
222
|
+
const size = decodeNumber(buffer, 'Uint32', Uint32Array.BYTES_PER_ELEMENT);
|
|
223
|
+
if (this.options.debug) {
|
|
224
|
+
console.log(`[WS] Received "UpStreamPull" ${id}`);
|
|
225
|
+
}
|
|
226
|
+
const stream = this.upStreams.get(id);
|
|
227
|
+
if (stream) {
|
|
228
|
+
const buf = new Uint8Array(new ArrayBuffer(size));
|
|
229
|
+
const { done, value } = await stream.reader.read(buf);
|
|
230
|
+
if (done) {
|
|
231
|
+
this.send(MessageType.UpStreamEnd, encodeNumber(id, 'Uint32'));
|
|
232
|
+
} else {
|
|
233
|
+
this.send(MessageType.UpStreamPush, concat(encodeNumber(id, 'Uint32'), value.buffer.slice(0, value.byteLength)));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async [MessageType.UpStreamEnd](buffer) {
|
|
238
|
+
const id = decodeNumber(buffer, 'Uint32');
|
|
239
|
+
if (this.options.debug) {
|
|
240
|
+
console.log(`[WS] Received "UpStreamEnd" ${id}`);
|
|
241
|
+
}
|
|
242
|
+
const stream = this.upStreams.get(id);
|
|
243
|
+
if (stream) {
|
|
244
|
+
stream.reader.cancel();
|
|
245
|
+
this.upStreams.delete(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
[MessageType.UpStreamAbort](buffer) {
|
|
249
|
+
const id = decodeNumber(buffer, 'Uint32');
|
|
250
|
+
if (this.options.debug) {
|
|
251
|
+
console.log(`[WS] Received "UpStreamAbort" ${id}`);
|
|
252
|
+
}
|
|
253
|
+
const stream = this.upStreams.get(id);
|
|
254
|
+
if (stream) {
|
|
255
|
+
try {
|
|
256
|
+
stream.reader.cancel(new Error('Aborted by server'));
|
|
257
|
+
} finally{
|
|
258
|
+
this.upStreams.delete(id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async [MessageType.DownStreamPush](buffer) {
|
|
263
|
+
const id = decodeNumber(buffer, 'Uint32');
|
|
264
|
+
if (this.options.debug) {
|
|
265
|
+
console.log(`[WS] Received "DownStreamPush" ${id}`);
|
|
266
|
+
}
|
|
267
|
+
const stream = this.downStreams.get(id);
|
|
268
|
+
if (stream) {
|
|
269
|
+
try {
|
|
270
|
+
await stream.writer.ready;
|
|
271
|
+
const chunk = buffer.slice(Uint32Array.BYTES_PER_ELEMENT);
|
|
272
|
+
await stream.writer.write(new Uint8Array(chunk));
|
|
273
|
+
await stream.writer.ready;
|
|
274
|
+
this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32'));
|
|
275
|
+
} catch (e) {
|
|
276
|
+
this.send(MessageType.DownStreamAbort, encodeNumber(id, 'Uint32'));
|
|
277
|
+
this.downStreams.delete(id);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async [MessageType.DownStreamEnd](buffer) {
|
|
282
|
+
const id = decodeNumber(buffer, 'Uint32');
|
|
283
|
+
if (this.options.debug) {
|
|
284
|
+
console.log(`[WS] Received "DownStreamEnd" ${id}`);
|
|
285
|
+
}
|
|
286
|
+
const stream = this.downStreams.get(id);
|
|
287
|
+
if (stream) {
|
|
288
|
+
this.downStreams.delete(id);
|
|
289
|
+
stream.writer.close().catch(()=>{});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async [MessageType.DownStreamAbort](buffer) {
|
|
293
|
+
const id = decodeNumber(buffer, 'Uint32');
|
|
294
|
+
if (this.options.debug) {
|
|
295
|
+
console.log(`[WS] Received "DownStreamAbort" ${id}`);
|
|
296
|
+
}
|
|
297
|
+
const stream = this.downStreams.get(id);
|
|
298
|
+
if (stream) {
|
|
299
|
+
this.downStreams.delete(id);
|
|
300
|
+
stream.writer.abort(new Error('Aborted by server')).catch(()=>{});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
[MessageType.RpcSubscription](buffer) {
|
|
304
|
+
const { callId, payload: [key, payload] } = this.client.format.decodeRpc(buffer, this.decodeRpcContext);
|
|
305
|
+
if (this.calls.has(callId)) {
|
|
306
|
+
const subscription = new Subscription(key, ()=>{
|
|
307
|
+
subscription.emit('end');
|
|
308
|
+
this.subscriptions.delete(key);
|
|
309
|
+
this.send(MessageType.ClientUnsubscribe, this.client.format.encode([
|
|
310
|
+
key
|
|
311
|
+
]));
|
|
312
|
+
});
|
|
313
|
+
this.subscriptions.set(key, subscription);
|
|
314
|
+
this.resolveRpc(callId, {
|
|
315
|
+
success: true,
|
|
316
|
+
value: {
|
|
317
|
+
payload,
|
|
318
|
+
subscription
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
[MessageType.ServerSubscriptionEvent](buffer) {
|
|
324
|
+
const [key, event, payload] = this.client.format.decode(buffer);
|
|
325
|
+
if (this.options.debug) {
|
|
326
|
+
console.groupCollapsed(`[WS] Received "ServerSubscriptionEvent" ${key}/${event}`);
|
|
327
|
+
console.log(payload);
|
|
328
|
+
console.groupEnd();
|
|
329
|
+
}
|
|
330
|
+
const subscription = this.subscriptions.get(key);
|
|
331
|
+
if (subscription) subscription.emit(event, payload);
|
|
332
|
+
}
|
|
333
|
+
[MessageType.ServerUnsubscribe](buffer) {
|
|
334
|
+
const [key] = this.client.format.decode(buffer);
|
|
335
|
+
if (this.options.debug) {
|
|
336
|
+
console.log(`[WS] Received "ServerUnsubscribe" ${key}`);
|
|
337
|
+
}
|
|
338
|
+
const subscription = this.subscriptions.get(key);
|
|
339
|
+
subscription?.emit('end');
|
|
340
|
+
this.subscriptions.delete(key);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../index.ts"],"sourcesContent":["import {\n type ClientDownStreamWrapper,\n ClientTransport,\n type ClientTransportRpcCall,\n type ClientTransportRpcResult,\n ClientUpStream,\n Subscription,\n createClientDownStream,\n onAbort,\n once,\n} from '@nmtjs/client'\nimport {\n type DecodeRpcContext,\n type EncodeRpcContext,\n MessageType,\n MessageTypeName,\n TransportType,\n concat,\n decodeNumber,\n encodeNumber,\n} from '@nmtjs/common'\n\nexport type WsClientTransportOptions = {\n /**\n * The origin of the server\n * @example 'http://localhost:3000'\n */\n origin: string\n /**\n * Whether to autoreconnect on close\n * @default true\n */\n autoreconnect?: boolean\n /**\n * Custom WebSocket class\n * @default globalThis.WebSocket\n */\n wsFactory?: (url: URL) => WebSocket\n\n debug?: boolean\n}\n\ntype WsCall = {\n resolve: (value: ClientTransportRpcResult) => void\n reject: (error: Error) => void\n}\n\nexport class WsClientTransport extends ClientTransport<{\n open: []\n close: [Error?]\n connect: []\n healthy: []\n}> {\n readonly type = TransportType.WS\n\n protected readonly calls = new Map<number, WsCall>()\n protected readonly subscriptions = new Map<number, Subscription>()\n protected readonly upStreams = new Map<number, ClientUpStream>()\n protected readonly downStreams = new Map<number, ClientDownStreamWrapper>()\n protected upStreamId = 0\n protected queryParams = {}\n protected ws?: WebSocket\n protected isHealthy = false\n protected checkHealthAttempts = 0\n protected autoreconnect: boolean\n protected isConnected = false\n\n private wsFactory!: (url: URL) => WebSocket\n private encodeRpcContext: EncodeRpcContext\n private decodeRpcContext: DecodeRpcContext\n\n constructor(private readonly options: WsClientTransportOptions) {\n super()\n\n this.autoreconnect = this.options.autoreconnect ?? true\n\n if (options.wsFactory) {\n this.wsFactory = options.wsFactory\n } else {\n this.wsFactory = (url: URL) => new WebSocket(url.toString())\n }\n\n // TODO: wtf is this for?\n this.on('close', (error) => this.clear(error))\n\n this.encodeRpcContext = {\n addStream: (blob) => {\n const id = ++this.upStreamId\n const upstream = new ClientUpStream(id, blob)\n this.upStreams.set(id, upstream)\n return { id, metadata: blob.metadata }\n },\n getStream: (id) => this.upStreams.get(id),\n }\n\n this.decodeRpcContext = {\n addStream: (id, metadata) => {\n const downstream = createClientDownStream(metadata, () =>\n this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32')),\n )\n this.downStreams.set(id, downstream)\n return downstream.blob\n },\n getStream: (id) => this.downStreams.get(id),\n }\n }\n\n async connect() {\n // reset default autoreconnect value\n this.autoreconnect = this.options.autoreconnect ?? true\n await this.healthCheck()\n\n this.ws = this.wsFactory(\n this.getURL('api', 'ws', {\n ...this.queryParams,\n accept: this.client.format.contentType,\n 'content-type': this.client.format.contentType,\n services: this.client.services,\n }),\n )\n\n this.ws.binaryType = 'arraybuffer'\n\n this.ws.onmessage = (event) => {\n const buffer: ArrayBuffer = event.data\n const type = decodeNumber(buffer, 'Uint8')\n const handler = this[type]\n if (handler) {\n handler.call(this, buffer.slice(Uint8Array.BYTES_PER_ELEMENT), this.ws)\n }\n }\n this.ws.onopen = (event) => {\n this.isConnected = true\n this.emit('open')\n this.checkHealthAttempts = 0\n }\n this.ws.onclose = (event) => {\n this.isConnected = false\n this.isHealthy = false\n this.emit(\n 'close',\n event.code === 1000\n ? undefined\n : new Error(\n `Connection closed with code ${event.code}: ${event.reason}`,\n ),\n )\n // FIXME: cleanup calls, streams, subscriptions\n if (this.autoreconnect) this.connect()\n }\n this.ws.onerror = (event) => {\n this.isHealthy = false\n }\n await once(this, 'open')\n this.emit('connect')\n }\n\n async disconnect(): Promise<void> {\n this.autoreconnect = false\n this.ws?.close()\n await once(this, 'close')\n }\n\n async rpc(call: ClientTransportRpcCall): Promise<ClientTransportRpcResult> {\n const { signal, callId, payload, procedure, service } = call\n\n const data = this.client.format.encodeRpc(\n {\n callId,\n service,\n procedure,\n payload,\n },\n this.encodeRpcContext,\n )\n\n onAbort(signal, () => {\n const call = this.calls.get(callId)\n if (call) {\n const { reject } = call\n reject(new Error('Request aborted'))\n this.calls.delete(callId)\n this.send(MessageType.RpcAbort, encodeNumber(callId, 'Uint32'))\n }\n })\n\n if (!this.isConnected) await once(this, 'connect')\n\n return new Promise((resolve, reject) => {\n this.calls.set(callId, { resolve, reject })\n this.send(MessageType.Rpc, data)\n })\n }\n\n setQueryParams(params: Record<string, any>) {\n this.queryParams = params\n }\n\n protected send(type: MessageType, ...payload: ArrayBuffer[]) {\n this.ws?.send(concat(encodeNumber(type, 'Uint8'), ...payload))\n if (this.options.debug) {\n console.groupCollapsed(`[WS] Sent ${MessageTypeName[type]}`)\n console.trace()\n console.groupEnd()\n }\n }\n\n protected async clear(error: Error = new Error('Connection closed')) {\n for (const call of this.calls.values()) {\n const { reject } = call\n reject(error)\n }\n\n for (const stream of this.upStreams.values()) {\n stream.reader.cancel(error)\n }\n\n for (const stream of this.downStreams.values()) {\n stream.writer.abort(error)\n }\n\n for (const subscription of this.subscriptions.values()) {\n subscription.unsubscribe()\n }\n\n this.calls.clear()\n this.upStreams.clear()\n this.downStreams.clear()\n this.subscriptions.clear()\n }\n\n protected async healthCheck() {\n while (!this.isHealthy) {\n try {\n const signal = AbortSignal.timeout(10000)\n const url = this.getURL('healthy', 'http', {\n auth: this.client.auth,\n })\n const { ok } = await fetch(url, { signal })\n this.isHealthy = ok\n } catch (e) {}\n\n if (!this.isHealthy) {\n this.checkHealthAttempts++\n const seconds = Math.min(this.checkHealthAttempts, 15)\n await new Promise((r) => setTimeout(r, seconds * 1000))\n }\n }\n this.emit('healthy')\n }\n\n private getURL(\n path = '',\n protocol: 'ws' | 'http',\n params: Record<string, any> = {},\n ) {\n // TODO: add custom path support?\n const base = new URL(this.options.origin)\n const secure = base.protocol === 'https:'\n const url = new URL(\n `${secure ? protocol + 's' : protocol}://${base.host}/${path}`,\n )\n for (let [key, values] of Object.entries(params)) {\n if (!Array.isArray(values)) values = [values]\n for (const value of values) {\n url.searchParams.append(key, value)\n }\n }\n return url\n }\n\n protected resolveRpc(callId: number, value: ClientTransportRpcResult) {\n const call = this.calls.get(callId)\n if (call) {\n const { resolve } = call\n this.calls.delete(callId)\n resolve(value)\n }\n }\n\n protected async [MessageType.Event](buffer: ArrayBuffer) {\n const [service, event, payload] = this.client.format.decode(buffer)\n if (this.options.debug) {\n console.groupCollapsed(`[WS] Received \"Event\" ${service}/${event}`)\n console.log(payload)\n console.groupEnd()\n }\n this.emit('event', service, event, payload)\n }\n\n protected async [MessageType.Rpc](buffer: ArrayBuffer) {\n const { callId, error, payload } = this.client.format.decodeRpc(\n buffer,\n this.decodeRpcContext,\n )\n\n if (this.calls.has(callId)) {\n this.resolveRpc(\n callId,\n error ? { success: false, error } : { success: true, value: payload },\n )\n }\n }\n\n protected async [MessageType.UpStreamPull](buffer: ArrayBuffer) {\n const id = decodeNumber(buffer, 'Uint32')\n const size = decodeNumber(buffer, 'Uint32', Uint32Array.BYTES_PER_ELEMENT)\n\n if (this.options.debug) {\n console.log(`[WS] Received \"UpStreamPull\" ${id}`)\n }\n\n const stream = this.upStreams.get(id)\n if (stream) {\n const buf = new Uint8Array(new ArrayBuffer(size))\n const { done, value } = await stream.reader.read(buf)\n if (done) {\n this.send(MessageType.UpStreamEnd, encodeNumber(id, 'Uint32'))\n } else {\n this.send(\n MessageType.UpStreamPush,\n concat(\n encodeNumber(id, 'Uint32'),\n value.buffer.slice(0, value.byteLength),\n ),\n )\n }\n }\n }\n\n protected async [MessageType.UpStreamEnd](buffer: ArrayBuffer) {\n const id = decodeNumber(buffer, 'Uint32')\n if (this.options.debug) {\n console.log(`[WS] Received \"UpStreamEnd\" ${id}`)\n }\n const stream = this.upStreams.get(id)\n if (stream) {\n stream.reader.cancel()\n this.upStreams.delete(id)\n }\n }\n\n protected [MessageType.UpStreamAbort](buffer: ArrayBuffer) {\n const id = decodeNumber(buffer, 'Uint32')\n if (this.options.debug) {\n console.log(`[WS] Received \"UpStreamAbort\" ${id}`)\n }\n const stream = this.upStreams.get(id)\n if (stream) {\n try {\n stream.reader.cancel(new Error('Aborted by server'))\n } finally {\n this.upStreams.delete(id)\n }\n }\n }\n\n protected async [MessageType.DownStreamPush](buffer: ArrayBuffer) {\n const id = decodeNumber(buffer, 'Uint32')\n if (this.options.debug) {\n console.log(`[WS] Received \"DownStreamPush\" ${id}`)\n }\n const stream = this.downStreams.get(id)\n if (stream) {\n try {\n await stream.writer.ready\n const chunk = buffer.slice(Uint32Array.BYTES_PER_ELEMENT)\n await stream.writer.write(new Uint8Array(chunk))\n await stream.writer.ready\n this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32'))\n } catch (e) {\n this.send(MessageType.DownStreamAbort, encodeNumber(id, 'Uint32'))\n this.downStreams.delete(id)\n }\n }\n }\n\n protected async [MessageType.DownStreamEnd](buffer: ArrayBuffer) {\n const id = decodeNumber(buffer, 'Uint32')\n if (this.options.debug) {\n console.log(`[WS] Received \"DownStreamEnd\" ${id}`)\n }\n const stream = this.downStreams.get(id)\n if (stream) {\n this.downStreams.delete(id)\n stream.writer.close().catch(() => {})\n }\n }\n\n protected async [MessageType.DownStreamAbort](buffer: ArrayBuffer) {\n const id = decodeNumber(buffer, 'Uint32')\n if (this.options.debug) {\n console.log(`[WS] Received \"DownStreamAbort\" ${id}`)\n }\n const stream = this.downStreams.get(id)\n if (stream) {\n this.downStreams.delete(id)\n stream.writer.abort(new Error('Aborted by server')).catch(() => {})\n }\n }\n\n protected [MessageType.RpcSubscription](buffer: ArrayBuffer) {\n const {\n callId,\n payload: [key, payload],\n } = this.client.format.decodeRpc(buffer, this.decodeRpcContext)\n if (this.calls.has(callId)) {\n const subscription = new Subscription(key, () => {\n subscription.emit('end')\n this.subscriptions.delete(key)\n this.send(\n MessageType.ClientUnsubscribe,\n this.client.format.encode([key]),\n )\n })\n this.subscriptions.set(key, subscription)\n this.resolveRpc(callId, {\n success: true,\n value: { payload, subscription },\n })\n }\n }\n\n protected [MessageType.ServerSubscriptionEvent](buffer: ArrayBuffer) {\n const [key, event, payload] = this.client.format.decode(buffer)\n if (this.options.debug) {\n console.groupCollapsed(\n `[WS] Received \"ServerSubscriptionEvent\" ${key}/${event}`,\n )\n console.log(payload)\n console.groupEnd()\n }\n const subscription = this.subscriptions.get(key)\n if (subscription) subscription.emit(event, payload)\n }\n\n protected [MessageType.ServerUnsubscribe](buffer: ArrayBuffer) {\n const [key] = this.client.format.decode(buffer)\n if (this.options.debug) {\n console.log(`[WS] Received \"ServerUnsubscribe\" ${key}`)\n }\n const subscription = this.subscriptions.get(key)\n subscription?.emit('end')\n this.subscriptions.delete(key)\n }\n}\n"],"names":["ClientTransport","ClientUpStream","Subscription","createClientDownStream","onAbort","once","MessageType","MessageTypeName","TransportType","concat","decodeNumber","encodeNumber","WsClientTransport","type","calls","subscriptions","upStreams","downStreams","upStreamId","queryParams","ws","isHealthy","checkHealthAttempts","autoreconnect","isConnected","wsFactory","encodeRpcContext","decodeRpcContext","constructor","options","WS","Map","url","WebSocket","toString","on","error","clear","addStream","blob","id","upstream","set","metadata","getStream","get","downstream","send","DownStreamPull","connect","healthCheck","getURL","accept","client","format","contentType","services","binaryType","onmessage","event","buffer","data","handler","call","slice","Uint8Array","BYTES_PER_ELEMENT","onopen","emit","onclose","code","undefined","Error","reason","onerror","disconnect","close","rpc","signal","callId","payload","procedure","service","encodeRpc","reject","delete","RpcAbort","Promise","resolve","Rpc","setQueryParams","params","debug","console","groupCollapsed","trace","groupEnd","values","stream","reader","cancel","writer","abort","subscription","unsubscribe","AbortSignal","timeout","auth","ok","fetch","e","seconds","Math","min","r","setTimeout","path","protocol","base","URL","origin","secure","host","key","Object","entries","Array","isArray","value","searchParams","append","resolveRpc","Event","decode","log","decodeRpc","has","success","UpStreamPull","size","Uint32Array","buf","ArrayBuffer","done","read","UpStreamEnd","UpStreamPush","byteLength","UpStreamAbort","DownStreamPush","ready","chunk","write","DownStreamAbort","DownStreamEnd","catch","RpcSubscription","ClientUnsubscribe","encode","ServerSubscriptionEvent","ServerUnsubscribe"],"mappings":"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"}
|
package/index.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClientDownStreamWrapper,
|
|
3
|
+
ClientTransport,
|
|
4
|
+
type ClientTransportRpcCall,
|
|
5
|
+
type ClientTransportRpcResult,
|
|
6
|
+
ClientUpStream,
|
|
7
|
+
Subscription,
|
|
8
|
+
createClientDownStream,
|
|
9
|
+
onAbort,
|
|
10
|
+
once,
|
|
11
|
+
} from '@nmtjs/client'
|
|
12
|
+
import {
|
|
13
|
+
type DecodeRpcContext,
|
|
14
|
+
type EncodeRpcContext,
|
|
15
|
+
MessageType,
|
|
16
|
+
MessageTypeName,
|
|
17
|
+
TransportType,
|
|
18
|
+
concat,
|
|
19
|
+
decodeNumber,
|
|
20
|
+
encodeNumber,
|
|
21
|
+
} from '@nmtjs/common'
|
|
22
|
+
|
|
23
|
+
export type WsClientTransportOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* The origin of the server
|
|
26
|
+
* @example 'http://localhost:3000'
|
|
27
|
+
*/
|
|
28
|
+
origin: string
|
|
29
|
+
/**
|
|
30
|
+
* Whether to autoreconnect on close
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
autoreconnect?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Custom WebSocket class
|
|
36
|
+
* @default globalThis.WebSocket
|
|
37
|
+
*/
|
|
38
|
+
wsFactory?: (url: URL) => WebSocket
|
|
39
|
+
|
|
40
|
+
debug?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type WsCall = {
|
|
44
|
+
resolve: (value: ClientTransportRpcResult) => void
|
|
45
|
+
reject: (error: Error) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class WsClientTransport extends ClientTransport<{
|
|
49
|
+
open: []
|
|
50
|
+
close: [Error?]
|
|
51
|
+
connect: []
|
|
52
|
+
healthy: []
|
|
53
|
+
}> {
|
|
54
|
+
readonly type = TransportType.WS
|
|
55
|
+
|
|
56
|
+
protected readonly calls = new Map<number, WsCall>()
|
|
57
|
+
protected readonly subscriptions = new Map<number, Subscription>()
|
|
58
|
+
protected readonly upStreams = new Map<number, ClientUpStream>()
|
|
59
|
+
protected readonly downStreams = new Map<number, ClientDownStreamWrapper>()
|
|
60
|
+
protected upStreamId = 0
|
|
61
|
+
protected queryParams = {}
|
|
62
|
+
protected ws?: WebSocket
|
|
63
|
+
protected isHealthy = false
|
|
64
|
+
protected checkHealthAttempts = 0
|
|
65
|
+
protected autoreconnect: boolean
|
|
66
|
+
protected isConnected = false
|
|
67
|
+
|
|
68
|
+
private wsFactory!: (url: URL) => WebSocket
|
|
69
|
+
private encodeRpcContext: EncodeRpcContext
|
|
70
|
+
private decodeRpcContext: DecodeRpcContext
|
|
71
|
+
|
|
72
|
+
constructor(private readonly options: WsClientTransportOptions) {
|
|
73
|
+
super()
|
|
74
|
+
|
|
75
|
+
this.autoreconnect = this.options.autoreconnect ?? true
|
|
76
|
+
|
|
77
|
+
if (options.wsFactory) {
|
|
78
|
+
this.wsFactory = options.wsFactory
|
|
79
|
+
} else {
|
|
80
|
+
this.wsFactory = (url: URL) => new WebSocket(url.toString())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// TODO: wtf is this for?
|
|
84
|
+
this.on('close', (error) => this.clear(error))
|
|
85
|
+
|
|
86
|
+
this.encodeRpcContext = {
|
|
87
|
+
addStream: (blob) => {
|
|
88
|
+
const id = ++this.upStreamId
|
|
89
|
+
const upstream = new ClientUpStream(id, blob)
|
|
90
|
+
this.upStreams.set(id, upstream)
|
|
91
|
+
return { id, metadata: blob.metadata }
|
|
92
|
+
},
|
|
93
|
+
getStream: (id) => this.upStreams.get(id),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.decodeRpcContext = {
|
|
97
|
+
addStream: (id, metadata) => {
|
|
98
|
+
const downstream = createClientDownStream(metadata, () =>
|
|
99
|
+
this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32')),
|
|
100
|
+
)
|
|
101
|
+
this.downStreams.set(id, downstream)
|
|
102
|
+
return downstream.blob
|
|
103
|
+
},
|
|
104
|
+
getStream: (id) => this.downStreams.get(id),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async connect() {
|
|
109
|
+
// reset default autoreconnect value
|
|
110
|
+
this.autoreconnect = this.options.autoreconnect ?? true
|
|
111
|
+
await this.healthCheck()
|
|
112
|
+
|
|
113
|
+
this.ws = this.wsFactory(
|
|
114
|
+
this.getURL('api', 'ws', {
|
|
115
|
+
...this.queryParams,
|
|
116
|
+
accept: this.client.format.contentType,
|
|
117
|
+
'content-type': this.client.format.contentType,
|
|
118
|
+
services: this.client.services,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
this.ws.binaryType = 'arraybuffer'
|
|
123
|
+
|
|
124
|
+
this.ws.onmessage = (event) => {
|
|
125
|
+
const buffer: ArrayBuffer = event.data
|
|
126
|
+
const type = decodeNumber(buffer, 'Uint8')
|
|
127
|
+
const handler = this[type]
|
|
128
|
+
if (handler) {
|
|
129
|
+
handler.call(this, buffer.slice(Uint8Array.BYTES_PER_ELEMENT), this.ws)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
this.ws.onopen = (event) => {
|
|
133
|
+
this.isConnected = true
|
|
134
|
+
this.emit('open')
|
|
135
|
+
this.checkHealthAttempts = 0
|
|
136
|
+
}
|
|
137
|
+
this.ws.onclose = (event) => {
|
|
138
|
+
this.isConnected = false
|
|
139
|
+
this.isHealthy = false
|
|
140
|
+
this.emit(
|
|
141
|
+
'close',
|
|
142
|
+
event.code === 1000
|
|
143
|
+
? undefined
|
|
144
|
+
: new Error(
|
|
145
|
+
`Connection closed with code ${event.code}: ${event.reason}`,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
// FIXME: cleanup calls, streams, subscriptions
|
|
149
|
+
if (this.autoreconnect) this.connect()
|
|
150
|
+
}
|
|
151
|
+
this.ws.onerror = (event) => {
|
|
152
|
+
this.isHealthy = false
|
|
153
|
+
}
|
|
154
|
+
await once(this, 'open')
|
|
155
|
+
this.emit('connect')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async disconnect(): Promise<void> {
|
|
159
|
+
this.autoreconnect = false
|
|
160
|
+
this.ws?.close()
|
|
161
|
+
await once(this, 'close')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async rpc(call: ClientTransportRpcCall): Promise<ClientTransportRpcResult> {
|
|
165
|
+
const { signal, callId, payload, procedure, service } = call
|
|
166
|
+
|
|
167
|
+
const data = this.client.format.encodeRpc(
|
|
168
|
+
{
|
|
169
|
+
callId,
|
|
170
|
+
service,
|
|
171
|
+
procedure,
|
|
172
|
+
payload,
|
|
173
|
+
},
|
|
174
|
+
this.encodeRpcContext,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
onAbort(signal, () => {
|
|
178
|
+
const call = this.calls.get(callId)
|
|
179
|
+
if (call) {
|
|
180
|
+
const { reject } = call
|
|
181
|
+
reject(new Error('Request aborted'))
|
|
182
|
+
this.calls.delete(callId)
|
|
183
|
+
this.send(MessageType.RpcAbort, encodeNumber(callId, 'Uint32'))
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
if (!this.isConnected) await once(this, 'connect')
|
|
188
|
+
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
this.calls.set(callId, { resolve, reject })
|
|
191
|
+
this.send(MessageType.Rpc, data)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
setQueryParams(params: Record<string, any>) {
|
|
196
|
+
this.queryParams = params
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
protected send(type: MessageType, ...payload: ArrayBuffer[]) {
|
|
200
|
+
this.ws?.send(concat(encodeNumber(type, 'Uint8'), ...payload))
|
|
201
|
+
if (this.options.debug) {
|
|
202
|
+
console.groupCollapsed(`[WS] Sent ${MessageTypeName[type]}`)
|
|
203
|
+
console.trace()
|
|
204
|
+
console.groupEnd()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
protected async clear(error: Error = new Error('Connection closed')) {
|
|
209
|
+
for (const call of this.calls.values()) {
|
|
210
|
+
const { reject } = call
|
|
211
|
+
reject(error)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const stream of this.upStreams.values()) {
|
|
215
|
+
stream.reader.cancel(error)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const stream of this.downStreams.values()) {
|
|
219
|
+
stream.writer.abort(error)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const subscription of this.subscriptions.values()) {
|
|
223
|
+
subscription.unsubscribe()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.calls.clear()
|
|
227
|
+
this.upStreams.clear()
|
|
228
|
+
this.downStreams.clear()
|
|
229
|
+
this.subscriptions.clear()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected async healthCheck() {
|
|
233
|
+
while (!this.isHealthy) {
|
|
234
|
+
try {
|
|
235
|
+
const signal = AbortSignal.timeout(10000)
|
|
236
|
+
const url = this.getURL('healthy', 'http', {
|
|
237
|
+
auth: this.client.auth,
|
|
238
|
+
})
|
|
239
|
+
const { ok } = await fetch(url, { signal })
|
|
240
|
+
this.isHealthy = ok
|
|
241
|
+
} catch (e) {}
|
|
242
|
+
|
|
243
|
+
if (!this.isHealthy) {
|
|
244
|
+
this.checkHealthAttempts++
|
|
245
|
+
const seconds = Math.min(this.checkHealthAttempts, 15)
|
|
246
|
+
await new Promise((r) => setTimeout(r, seconds * 1000))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
this.emit('healthy')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private getURL(
|
|
253
|
+
path = '',
|
|
254
|
+
protocol: 'ws' | 'http',
|
|
255
|
+
params: Record<string, any> = {},
|
|
256
|
+
) {
|
|
257
|
+
// TODO: add custom path support?
|
|
258
|
+
const base = new URL(this.options.origin)
|
|
259
|
+
const secure = base.protocol === 'https:'
|
|
260
|
+
const url = new URL(
|
|
261
|
+
`${secure ? protocol + 's' : protocol}://${base.host}/${path}`,
|
|
262
|
+
)
|
|
263
|
+
for (let [key, values] of Object.entries(params)) {
|
|
264
|
+
if (!Array.isArray(values)) values = [values]
|
|
265
|
+
for (const value of values) {
|
|
266
|
+
url.searchParams.append(key, value)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return url
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
protected resolveRpc(callId: number, value: ClientTransportRpcResult) {
|
|
273
|
+
const call = this.calls.get(callId)
|
|
274
|
+
if (call) {
|
|
275
|
+
const { resolve } = call
|
|
276
|
+
this.calls.delete(callId)
|
|
277
|
+
resolve(value)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
protected async [MessageType.Event](buffer: ArrayBuffer) {
|
|
282
|
+
const [service, event, payload] = this.client.format.decode(buffer)
|
|
283
|
+
if (this.options.debug) {
|
|
284
|
+
console.groupCollapsed(`[WS] Received "Event" ${service}/${event}`)
|
|
285
|
+
console.log(payload)
|
|
286
|
+
console.groupEnd()
|
|
287
|
+
}
|
|
288
|
+
this.emit('event', service, event, payload)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
protected async [MessageType.Rpc](buffer: ArrayBuffer) {
|
|
292
|
+
const { callId, error, payload } = this.client.format.decodeRpc(
|
|
293
|
+
buffer,
|
|
294
|
+
this.decodeRpcContext,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if (this.calls.has(callId)) {
|
|
298
|
+
this.resolveRpc(
|
|
299
|
+
callId,
|
|
300
|
+
error ? { success: false, error } : { success: true, value: payload },
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
protected async [MessageType.UpStreamPull](buffer: ArrayBuffer) {
|
|
306
|
+
const id = decodeNumber(buffer, 'Uint32')
|
|
307
|
+
const size = decodeNumber(buffer, 'Uint32', Uint32Array.BYTES_PER_ELEMENT)
|
|
308
|
+
|
|
309
|
+
if (this.options.debug) {
|
|
310
|
+
console.log(`[WS] Received "UpStreamPull" ${id}`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const stream = this.upStreams.get(id)
|
|
314
|
+
if (stream) {
|
|
315
|
+
const buf = new Uint8Array(new ArrayBuffer(size))
|
|
316
|
+
const { done, value } = await stream.reader.read(buf)
|
|
317
|
+
if (done) {
|
|
318
|
+
this.send(MessageType.UpStreamEnd, encodeNumber(id, 'Uint32'))
|
|
319
|
+
} else {
|
|
320
|
+
this.send(
|
|
321
|
+
MessageType.UpStreamPush,
|
|
322
|
+
concat(
|
|
323
|
+
encodeNumber(id, 'Uint32'),
|
|
324
|
+
value.buffer.slice(0, value.byteLength),
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
protected async [MessageType.UpStreamEnd](buffer: ArrayBuffer) {
|
|
332
|
+
const id = decodeNumber(buffer, 'Uint32')
|
|
333
|
+
if (this.options.debug) {
|
|
334
|
+
console.log(`[WS] Received "UpStreamEnd" ${id}`)
|
|
335
|
+
}
|
|
336
|
+
const stream = this.upStreams.get(id)
|
|
337
|
+
if (stream) {
|
|
338
|
+
stream.reader.cancel()
|
|
339
|
+
this.upStreams.delete(id)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
protected [MessageType.UpStreamAbort](buffer: ArrayBuffer) {
|
|
344
|
+
const id = decodeNumber(buffer, 'Uint32')
|
|
345
|
+
if (this.options.debug) {
|
|
346
|
+
console.log(`[WS] Received "UpStreamAbort" ${id}`)
|
|
347
|
+
}
|
|
348
|
+
const stream = this.upStreams.get(id)
|
|
349
|
+
if (stream) {
|
|
350
|
+
try {
|
|
351
|
+
stream.reader.cancel(new Error('Aborted by server'))
|
|
352
|
+
} finally {
|
|
353
|
+
this.upStreams.delete(id)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
protected async [MessageType.DownStreamPush](buffer: ArrayBuffer) {
|
|
359
|
+
const id = decodeNumber(buffer, 'Uint32')
|
|
360
|
+
if (this.options.debug) {
|
|
361
|
+
console.log(`[WS] Received "DownStreamPush" ${id}`)
|
|
362
|
+
}
|
|
363
|
+
const stream = this.downStreams.get(id)
|
|
364
|
+
if (stream) {
|
|
365
|
+
try {
|
|
366
|
+
await stream.writer.ready
|
|
367
|
+
const chunk = buffer.slice(Uint32Array.BYTES_PER_ELEMENT)
|
|
368
|
+
await stream.writer.write(new Uint8Array(chunk))
|
|
369
|
+
await stream.writer.ready
|
|
370
|
+
this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32'))
|
|
371
|
+
} catch (e) {
|
|
372
|
+
this.send(MessageType.DownStreamAbort, encodeNumber(id, 'Uint32'))
|
|
373
|
+
this.downStreams.delete(id)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
protected async [MessageType.DownStreamEnd](buffer: ArrayBuffer) {
|
|
379
|
+
const id = decodeNumber(buffer, 'Uint32')
|
|
380
|
+
if (this.options.debug) {
|
|
381
|
+
console.log(`[WS] Received "DownStreamEnd" ${id}`)
|
|
382
|
+
}
|
|
383
|
+
const stream = this.downStreams.get(id)
|
|
384
|
+
if (stream) {
|
|
385
|
+
this.downStreams.delete(id)
|
|
386
|
+
stream.writer.close().catch(() => {})
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
protected async [MessageType.DownStreamAbort](buffer: ArrayBuffer) {
|
|
391
|
+
const id = decodeNumber(buffer, 'Uint32')
|
|
392
|
+
if (this.options.debug) {
|
|
393
|
+
console.log(`[WS] Received "DownStreamAbort" ${id}`)
|
|
394
|
+
}
|
|
395
|
+
const stream = this.downStreams.get(id)
|
|
396
|
+
if (stream) {
|
|
397
|
+
this.downStreams.delete(id)
|
|
398
|
+
stream.writer.abort(new Error('Aborted by server')).catch(() => {})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
protected [MessageType.RpcSubscription](buffer: ArrayBuffer) {
|
|
403
|
+
const {
|
|
404
|
+
callId,
|
|
405
|
+
payload: [key, payload],
|
|
406
|
+
} = this.client.format.decodeRpc(buffer, this.decodeRpcContext)
|
|
407
|
+
if (this.calls.has(callId)) {
|
|
408
|
+
const subscription = new Subscription(key, () => {
|
|
409
|
+
subscription.emit('end')
|
|
410
|
+
this.subscriptions.delete(key)
|
|
411
|
+
this.send(
|
|
412
|
+
MessageType.ClientUnsubscribe,
|
|
413
|
+
this.client.format.encode([key]),
|
|
414
|
+
)
|
|
415
|
+
})
|
|
416
|
+
this.subscriptions.set(key, subscription)
|
|
417
|
+
this.resolveRpc(callId, {
|
|
418
|
+
success: true,
|
|
419
|
+
value: { payload, subscription },
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
protected [MessageType.ServerSubscriptionEvent](buffer: ArrayBuffer) {
|
|
425
|
+
const [key, event, payload] = this.client.format.decode(buffer)
|
|
426
|
+
if (this.options.debug) {
|
|
427
|
+
console.groupCollapsed(
|
|
428
|
+
`[WS] Received "ServerSubscriptionEvent" ${key}/${event}`,
|
|
429
|
+
)
|
|
430
|
+
console.log(payload)
|
|
431
|
+
console.groupEnd()
|
|
432
|
+
}
|
|
433
|
+
const subscription = this.subscriptions.get(key)
|
|
434
|
+
if (subscription) subscription.emit(event, payload)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
protected [MessageType.ServerUnsubscribe](buffer: ArrayBuffer) {
|
|
438
|
+
const [key] = this.client.format.decode(buffer)
|
|
439
|
+
if (this.options.debug) {
|
|
440
|
+
console.log(`[WS] Received "ServerUnsubscribe" ${key}`)
|
|
441
|
+
}
|
|
442
|
+
const subscription = this.subscriptions.get(key)
|
|
443
|
+
subscription?.emit('end')
|
|
444
|
+
this.subscriptions.delete(key)
|
|
445
|
+
}
|
|
446
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nmtjs/ws-client",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"module": "./dist/index.js",
|
|
5
|
+
"peerDependencies": {
|
|
6
|
+
"@nmtjs/common": "^0.0.1",
|
|
7
|
+
"@nmtjs/client": "^0.0.1"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@nmtjs/common": "^0.0.1",
|
|
11
|
+
"@nmtjs/client": "^0.0.1"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.ts",
|
|
15
|
+
"dist",
|
|
16
|
+
"tsconfig.json",
|
|
17
|
+
"LICENSE.md",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"version": "0.0.1",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "neemata-build -p neutral ./index.ts",
|
|
23
|
+
"type-check": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"types": "./index.ts"
|
|
26
|
+
}
|