@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 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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "lib": ["ESNext", "DOM"]
5
+ }
6
+ }