@nmtjs/ws-client 0.1.8 → 0.2.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/dist/index.js +52 -322
- package/dist/index.js.map +1 -1
- package/index.ts +72 -403
- package/package.json +5 -8
package/dist/index.js
CHANGED
|
@@ -1,336 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export class
|
|
1
|
+
import { EventEmitter } from '@nmtjs/protocol/client';
|
|
2
|
+
import { ServerMessageType, concat, decodeNumber, encodeNumber } from '@nmtjs/protocol/common';
|
|
3
|
+
export class WebSocketClientTransport extends EventEmitter {
|
|
4
4
|
options;
|
|
5
|
-
|
|
6
|
-
|
|
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;
|
|
5
|
+
#webSocket;
|
|
6
|
+
#connecting;
|
|
20
7
|
constructor(options){
|
|
21
|
-
super(), this.options = options, this
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
id,
|
|
36
|
-
metadata: blob.metadata
|
|
37
|
-
};
|
|
38
|
-
},
|
|
39
|
-
getStream: (id)=>this.upStreams.get(id)
|
|
40
|
-
};
|
|
41
|
-
this.decodeRpcContext = {
|
|
42
|
-
addStream: (id, metadata)=>{
|
|
43
|
-
const downstream = createClientDownStream(metadata, ()=>this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32')));
|
|
44
|
-
this.downStreams.set(id, downstream);
|
|
45
|
-
return downstream.blob;
|
|
46
|
-
},
|
|
47
|
-
getStream: (id)=>this.downStreams.get(id)
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
async connect() {
|
|
51
|
-
this.autoreconnect = this.options.autoreconnect ?? true;
|
|
52
|
-
await this.healthCheck();
|
|
53
|
-
this.ws = this.wsFactory(this.getURL('api', 'ws', {
|
|
54
|
-
...this.queryParams,
|
|
55
|
-
accept: this.client.format.contentType,
|
|
56
|
-
'content-type': this.client.format.contentType,
|
|
57
|
-
services: this.client.services
|
|
58
|
-
}));
|
|
59
|
-
this.ws.binaryType = 'arraybuffer';
|
|
60
|
-
this.ws.onmessage = (event)=>{
|
|
61
|
-
const buffer = event.data;
|
|
8
|
+
super(), this.options = options, this.#webSocket = null, this.#connecting = null;
|
|
9
|
+
}
|
|
10
|
+
connect(auth = undefined, contentType) {
|
|
11
|
+
const wsUrl = new URL(this.options.origin);
|
|
12
|
+
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
13
|
+
wsUrl.pathname = '/api';
|
|
14
|
+
wsUrl.searchParams.set('content-type', contentType);
|
|
15
|
+
wsUrl.searchParams.set('accept', contentType);
|
|
16
|
+
if (auth) wsUrl.searchParams.set('auth', auth);
|
|
17
|
+
const ws = this.options.wsFactory?.(wsUrl) ?? new WebSocket(wsUrl.toString());
|
|
18
|
+
ws.binaryType = 'arraybuffer';
|
|
19
|
+
ws.addEventListener('message', ({ data })=>{
|
|
20
|
+
const buffer = data;
|
|
62
21
|
const type = decodeNumber(buffer, 'Uint8');
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
handler.call(this, buffer.slice(Uint8Array.BYTES_PER_ELEMENT), this.ws);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
this.ws.onopen = (event)=>{
|
|
69
|
-
if (this.options.debug) console.log(`[WS] Connected to ${this.options.origin}`);
|
|
70
|
-
this.isConnected = true;
|
|
71
|
-
this.emit('open');
|
|
72
|
-
this.checkHealthAttempts = 0;
|
|
73
|
-
};
|
|
74
|
-
this.ws.onclose = (event)=>{
|
|
75
|
-
this.isConnected = false;
|
|
76
|
-
this.isHealthy = false;
|
|
77
|
-
this.emit('close', event.code === 1000 ? undefined : new Error(`Connection closed with code ${event.code}: ${event.reason}`));
|
|
78
|
-
if (this.autoreconnect) this.connect();
|
|
79
|
-
};
|
|
80
|
-
this.ws.onerror = (cause)=>{
|
|
81
|
-
this.isHealthy = false;
|
|
82
|
-
this.emit('close', new Error('Connection error', {
|
|
83
|
-
cause
|
|
84
|
-
}));
|
|
85
|
-
};
|
|
86
|
-
await once(this, 'open');
|
|
87
|
-
if (this.options.debug) console.log(`2[WS] Connected to ${this.options.origin}`);
|
|
88
|
-
this.emit('connect');
|
|
89
|
-
}
|
|
90
|
-
async disconnect() {
|
|
91
|
-
this.autoreconnect = false;
|
|
92
|
-
this.ws?.close();
|
|
93
|
-
await once(this, 'close');
|
|
94
|
-
}
|
|
95
|
-
async rpc(call) {
|
|
96
|
-
const { signal, callId, payload, procedure, service } = call;
|
|
97
|
-
const data = this.client.format.encodeRpc({
|
|
98
|
-
callId,
|
|
99
|
-
service,
|
|
100
|
-
procedure,
|
|
101
|
-
payload
|
|
102
|
-
}, this.encodeRpcContext);
|
|
103
|
-
onAbort(signal, ()=>{
|
|
104
|
-
const call = this.calls.get(callId);
|
|
105
|
-
if (call) {
|
|
106
|
-
const { reject } = call;
|
|
107
|
-
reject(new Error('Request aborted'));
|
|
108
|
-
this.calls.delete(callId);
|
|
109
|
-
this.send(MessageType.RpcAbort, encodeNumber(callId, 'Uint32'));
|
|
22
|
+
if (type in ServerMessageType) {
|
|
23
|
+
this.emit(`${type}`, buffer.slice(Uint8Array.BYTES_PER_ELEMENT));
|
|
110
24
|
}
|
|
111
25
|
});
|
|
112
|
-
|
|
26
|
+
this.#webSocket = ws;
|
|
113
27
|
return new Promise((resolve, reject)=>{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
28
|
+
ws.addEventListener('open', ()=>{
|
|
29
|
+
this.emit('connect');
|
|
30
|
+
resolve();
|
|
31
|
+
}, {
|
|
32
|
+
once: true
|
|
117
33
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.queryParams = params;
|
|
123
|
-
}
|
|
124
|
-
send(type, ...payload) {
|
|
125
|
-
this.ws?.send(concat(encodeNumber(type, 'Uint8'), ...payload));
|
|
126
|
-
if (this.options.debug) {
|
|
127
|
-
console.groupCollapsed(`[WS] Sent ${MessageTypeName[type]}`);
|
|
128
|
-
console.trace();
|
|
129
|
-
console.groupEnd();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
async clear(error = new Error('Connection closed')) {
|
|
133
|
-
for (const call of this.calls.values()){
|
|
134
|
-
const { reject } = call;
|
|
135
|
-
reject(error);
|
|
136
|
-
}
|
|
137
|
-
for (const stream of this.upStreams.values()){
|
|
138
|
-
stream.reader.cancel(error);
|
|
139
|
-
}
|
|
140
|
-
for (const stream of this.downStreams.values()){
|
|
141
|
-
stream.writer.abort(error);
|
|
142
|
-
}
|
|
143
|
-
for (const subscription of this.subscriptions.values()){
|
|
144
|
-
subscription.unsubscribe();
|
|
145
|
-
}
|
|
146
|
-
this.calls.clear();
|
|
147
|
-
this.upStreams.clear();
|
|
148
|
-
this.downStreams.clear();
|
|
149
|
-
this.subscriptions.clear();
|
|
150
|
-
}
|
|
151
|
-
async healthCheck() {
|
|
152
|
-
while(!this.isHealthy){
|
|
153
|
-
try {
|
|
154
|
-
const signal = AbortSignal.timeout(10000);
|
|
155
|
-
const url = this.getURL('healthy', 'http', {
|
|
156
|
-
auth: this.client.auth
|
|
157
|
-
});
|
|
158
|
-
const { ok } = await fetch(url, {
|
|
159
|
-
signal
|
|
160
|
-
});
|
|
161
|
-
this.isHealthy = ok;
|
|
162
|
-
} catch (e) {}
|
|
163
|
-
if (!this.isHealthy) {
|
|
164
|
-
this.checkHealthAttempts++;
|
|
165
|
-
const seconds = Math.min(this.checkHealthAttempts, 15);
|
|
166
|
-
await new Promise((r)=>setTimeout(r, seconds * 1000));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
this.emit('healthy');
|
|
170
|
-
}
|
|
171
|
-
getURL(path = '', protocol, params = {}) {
|
|
172
|
-
const base = new URL(this.options.origin);
|
|
173
|
-
const secure = base.protocol === 'https:';
|
|
174
|
-
const url = new URL(`${secure ? protocol + 's' : protocol}://${base.host}/${path}`);
|
|
175
|
-
for (let [key, values] of Object.entries(params)){
|
|
176
|
-
if (!Array.isArray(values)) values = [
|
|
177
|
-
values
|
|
178
|
-
];
|
|
179
|
-
for (const value of values){
|
|
180
|
-
url.searchParams.append(key, value);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return url;
|
|
184
|
-
}
|
|
185
|
-
resolveRpc(callId, value) {
|
|
186
|
-
const call = this.calls.get(callId);
|
|
187
|
-
if (call) {
|
|
188
|
-
const { resolve } = call;
|
|
189
|
-
this.calls.delete(callId);
|
|
190
|
-
resolve(value);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
async [MessageType.Event](buffer) {
|
|
194
|
-
const [service, event, payload] = this.client.format.decode(buffer);
|
|
195
|
-
if (this.options.debug) {
|
|
196
|
-
console.groupCollapsed(`[WS] Received "Event" ${service}/${event}`);
|
|
197
|
-
console.log(payload);
|
|
198
|
-
console.groupEnd();
|
|
199
|
-
}
|
|
200
|
-
this.emit('event', service, event, payload);
|
|
201
|
-
}
|
|
202
|
-
async [MessageType.Rpc](buffer) {
|
|
203
|
-
const { callId, error, payload } = this.client.format.decodeRpc(buffer, this.decodeRpcContext);
|
|
204
|
-
if (this.calls.has(callId)) {
|
|
205
|
-
this.resolveRpc(callId, error ? {
|
|
206
|
-
success: false,
|
|
207
|
-
error
|
|
208
|
-
} : {
|
|
209
|
-
success: true,
|
|
210
|
-
value: payload
|
|
34
|
+
ws.addEventListener('error', (event)=>reject(new Error('WebSocket error', {
|
|
35
|
+
cause: event
|
|
36
|
+
})), {
|
|
37
|
+
once: true
|
|
211
38
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (this.options.debug) {
|
|
218
|
-
console.log(`[WS] Received "UpStreamPull" ${id}`);
|
|
219
|
-
}
|
|
220
|
-
const stream = this.upStreams.get(id);
|
|
221
|
-
if (stream) {
|
|
222
|
-
const buf = new Uint8Array(new ArrayBuffer(size));
|
|
223
|
-
const { done, value } = await stream.reader.read(buf);
|
|
224
|
-
if (done) {
|
|
225
|
-
this.send(MessageType.UpStreamEnd, encodeNumber(id, 'Uint32'));
|
|
226
|
-
} else {
|
|
227
|
-
this.send(MessageType.UpStreamPush, concat(encodeNumber(id, 'Uint32'), value.buffer.slice(0, value.byteLength)));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
async [MessageType.UpStreamEnd](buffer) {
|
|
232
|
-
const id = decodeNumber(buffer, 'Uint32');
|
|
233
|
-
if (this.options.debug) {
|
|
234
|
-
console.log(`[WS] Received "UpStreamEnd" ${id}`);
|
|
235
|
-
}
|
|
236
|
-
const stream = this.upStreams.get(id);
|
|
237
|
-
if (stream) {
|
|
238
|
-
stream.reader.cancel();
|
|
239
|
-
this.upStreams.delete(id);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
[MessageType.UpStreamAbort](buffer) {
|
|
243
|
-
const id = decodeNumber(buffer, 'Uint32');
|
|
244
|
-
if (this.options.debug) {
|
|
245
|
-
console.log(`[WS] Received "UpStreamAbort" ${id}`);
|
|
246
|
-
}
|
|
247
|
-
const stream = this.upStreams.get(id);
|
|
248
|
-
if (stream) {
|
|
249
|
-
try {
|
|
250
|
-
stream.reader.cancel(new Error('Aborted by server'));
|
|
251
|
-
} finally{
|
|
252
|
-
this.upStreams.delete(id);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
async [MessageType.DownStreamPush](buffer) {
|
|
257
|
-
const id = decodeNumber(buffer, 'Uint32');
|
|
258
|
-
if (this.options.debug) {
|
|
259
|
-
console.log(`[WS] Received "DownStreamPush" ${id}`);
|
|
260
|
-
}
|
|
261
|
-
const stream = this.downStreams.get(id);
|
|
262
|
-
if (stream) {
|
|
263
|
-
try {
|
|
264
|
-
await stream.writer.ready;
|
|
265
|
-
const chunk = buffer.slice(Uint32Array.BYTES_PER_ELEMENT);
|
|
266
|
-
await stream.writer.write(new Uint8Array(chunk));
|
|
267
|
-
await stream.writer.ready;
|
|
268
|
-
this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32'));
|
|
269
|
-
} catch (e) {
|
|
270
|
-
this.send(MessageType.DownStreamAbort, encodeNumber(id, 'Uint32'));
|
|
271
|
-
this.downStreams.delete(id);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
async [MessageType.DownStreamEnd](buffer) {
|
|
276
|
-
const id = decodeNumber(buffer, 'Uint32');
|
|
277
|
-
if (this.options.debug) {
|
|
278
|
-
console.log(`[WS] Received "DownStreamEnd" ${id}`);
|
|
279
|
-
}
|
|
280
|
-
const stream = this.downStreams.get(id);
|
|
281
|
-
if (stream) {
|
|
282
|
-
this.downStreams.delete(id);
|
|
283
|
-
stream.writer.close().catch(()=>{});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
async [MessageType.DownStreamAbort](buffer) {
|
|
287
|
-
const id = decodeNumber(buffer, 'Uint32');
|
|
288
|
-
if (this.options.debug) {
|
|
289
|
-
console.log(`[WS] Received "DownStreamAbort" ${id}`);
|
|
290
|
-
}
|
|
291
|
-
const stream = this.downStreams.get(id);
|
|
292
|
-
if (stream) {
|
|
293
|
-
this.downStreams.delete(id);
|
|
294
|
-
stream.writer.abort(new Error('Aborted by server')).catch(()=>{});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
[MessageType.RpcSubscription](buffer) {
|
|
298
|
-
const { callId, payload: [key, payload] } = this.client.format.decodeRpc(buffer, this.decodeRpcContext);
|
|
299
|
-
if (this.calls.has(callId)) {
|
|
300
|
-
const subscription = new Subscription(key, ()=>{
|
|
301
|
-
subscription.emit('end');
|
|
302
|
-
this.subscriptions.delete(key);
|
|
303
|
-
this.send(MessageType.ClientUnsubscribe, this.client.format.encode([
|
|
304
|
-
key
|
|
305
|
-
]));
|
|
306
|
-
});
|
|
307
|
-
this.subscriptions.set(key, subscription);
|
|
308
|
-
this.resolveRpc(callId, {
|
|
309
|
-
success: true,
|
|
310
|
-
value: {
|
|
311
|
-
payload,
|
|
312
|
-
subscription
|
|
39
|
+
ws.addEventListener('close', (event)=>{
|
|
40
|
+
this.emit('disconnect');
|
|
41
|
+
this.#webSocket = null;
|
|
42
|
+
if (this.options.autoreconnect === true) {
|
|
43
|
+
setTimeout(()=>this.connect(auth, contentType), 1000);
|
|
313
44
|
}
|
|
45
|
+
}, {
|
|
46
|
+
once: true
|
|
314
47
|
});
|
|
315
|
-
}
|
|
48
|
+
});
|
|
316
49
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.log(payload);
|
|
322
|
-
console.groupEnd();
|
|
323
|
-
}
|
|
324
|
-
const subscription = this.subscriptions.get(key);
|
|
325
|
-
if (subscription) subscription.emit(event, payload);
|
|
50
|
+
async disconnect() {
|
|
51
|
+
if (this.#webSocket === null) return;
|
|
52
|
+
this.#webSocket.close();
|
|
53
|
+
return _once(this.#webSocket, 'close');
|
|
326
54
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
console.log(`[WS] Received "ServerUnsubscribe" ${key}`);
|
|
331
|
-
}
|
|
332
|
-
const subscription = this.subscriptions.get(key);
|
|
333
|
-
subscription?.emit('end');
|
|
334
|
-
this.subscriptions.delete(key);
|
|
55
|
+
async send(messageType, buffer) {
|
|
56
|
+
if (this.#connecting) await this.#connecting;
|
|
57
|
+
this.#webSocket.send(concat(encodeNumber(messageType, 'Uint8'), buffer));
|
|
335
58
|
}
|
|
336
59
|
}
|
|
60
|
+
function _once(target, event) {
|
|
61
|
+
return new Promise((resolve)=>{
|
|
62
|
+
target.addEventListener(event, ()=>resolve(), {
|
|
63
|
+
once: true
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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 if (this.options.debug)\n console.log(`[WS] Connected to ${this.options.origin}`)\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 = (cause) => {\n this.isHealthy = false\n this.emit('close', new Error('Connection error', { cause }))\n }\n await once(this, 'open')\n if (this.options.debug)\n console.log(`2[WS] Connected to ${this.options.origin}`)\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","debug","console","log","origin","emit","onclose","code","undefined","Error","reason","onerror","cause","disconnect","close","rpc","signal","callId","payload","procedure","service","encodeRpc","reject","delete","RpcAbort","Promise","resolve","Rpc","setQueryParams","params","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","secure","host","key","Object","entries","Array","isArray","value","searchParams","append","resolveRpc","Event","decode","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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"}
|
|
1
|
+
{"version":3,"sources":["../../index.ts"],"sourcesContent":["import {\n type BaseClientFormat,\n EventEmitter,\n type ProtocolTransport,\n} from '@nmtjs/protocol/client'\nimport {\n type ClientMessageType,\n ServerMessageType,\n concat,\n decodeNumber,\n encodeNumber,\n} from '@nmtjs/protocol/common'\n\nexport type WebSocketClientTransportOptions = {\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\nexport class WebSocketClientTransport\n extends EventEmitter\n implements ProtocolTransport\n{\n #webSocket: WebSocket | null = null\n #connecting: Promise<void> | null = null\n\n constructor(private readonly options: WebSocketClientTransportOptions) {\n super()\n }\n\n connect(\n auth: string | undefined = undefined,\n contentType: BaseClientFormat['contentType'],\n ): Promise<void> {\n const wsUrl = new URL(this.options.origin)\n wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'\n wsUrl.pathname = '/api'\n wsUrl.searchParams.set('content-type', contentType)\n wsUrl.searchParams.set('accept', contentType)\n if (auth) wsUrl.searchParams.set('auth', auth)\n\n const ws =\n this.options.wsFactory?.(wsUrl) ?? new WebSocket(wsUrl.toString())\n\n ws.binaryType = 'arraybuffer'\n\n ws.addEventListener('message', ({ data }) => {\n const buffer: ArrayBuffer = data\n const type = decodeNumber(buffer, 'Uint8')\n if (type in ServerMessageType) {\n this.emit(`${type}`, buffer.slice(Uint8Array.BYTES_PER_ELEMENT))\n }\n })\n\n this.#webSocket = ws\n\n return new Promise((resolve, reject) => {\n ws.addEventListener(\n 'open',\n () => {\n this.emit('connect')\n resolve()\n },\n { once: true },\n )\n\n ws.addEventListener(\n 'error',\n (event) => reject(new Error('WebSocket error', { cause: event })),\n { once: true },\n )\n\n ws.addEventListener(\n 'close',\n (event) => {\n this.emit('disconnect')\n this.#webSocket = null\n if (this.options.autoreconnect === true) {\n setTimeout(() => this.connect(auth, contentType), 1000)\n }\n },\n { once: true },\n )\n })\n }\n\n async disconnect(): Promise<void> {\n if (this.#webSocket === null) return\n this.#webSocket!.close()\n return _once(this.#webSocket, 'close')\n }\n\n async send(\n messageType: ClientMessageType,\n buffer: ArrayBuffer,\n ): Promise<void> {\n if (this.#connecting) await this.#connecting\n this.#webSocket!.send(concat(encodeNumber(messageType, 'Uint8'), buffer))\n }\n}\n\nfunction _once(target: EventTarget, event: string) {\n return new Promise<void>((resolve) => {\n target.addEventListener(event, () => resolve(), { once: true })\n })\n}\n"],"names":["EventEmitter","ServerMessageType","concat","decodeNumber","encodeNumber","WebSocketClientTransport","constructor","options","connect","auth","undefined","contentType","wsUrl","URL","origin","protocol","pathname","searchParams","set","ws","wsFactory","WebSocket","toString","binaryType","addEventListener","data","buffer","type","emit","slice","Uint8Array","BYTES_PER_ELEMENT","Promise","resolve","reject","once","event","Error","cause","autoreconnect","setTimeout","disconnect","close","_once","send","messageType","target"],"mappings":"AAAA,SAEEA,YAAY,QAEP,yBAAwB;AAC/B,SAEEC,iBAAiB,EACjBC,MAAM,EACNC,YAAY,EACZC,YAAY,QACP,yBAAwB;AAsB/B,OAAO,MAAMC,iCACHL;;IAGR,CAAA,SAAU,CAAyB;IACnC,CAAA,UAAW,CAA6B;IAExCM,YAAY,AAAiBC,OAAwC,CAAE;QACrE,KAAK,SADsBA,UAAAA,cAH7B,CAAA,SAAU,GAAqB,WAC/B,CAAA,UAAW,GAAyB;IAIpC;IAEAC,QACEC,OAA2BC,SAAS,EACpCC,WAA4C,EAC7B;QACf,MAAMC,QAAQ,IAAIC,IAAI,IAAI,CAACN,OAAO,CAACO,MAAM;QACzCF,MAAMG,QAAQ,GAAGH,MAAMG,QAAQ,KAAK,WAAW,SAAS;QACxDH,MAAMI,QAAQ,GAAG;QACjBJ,MAAMK,YAAY,CAACC,GAAG,CAAC,gBAAgBP;QACvCC,MAAMK,YAAY,CAACC,GAAG,CAAC,UAAUP;QACjC,IAAIF,MAAMG,MAAMK,YAAY,CAACC,GAAG,CAAC,QAAQT;QAEzC,MAAMU,KACJ,IAAI,CAACZ,OAAO,CAACa,SAAS,GAAGR,UAAU,IAAIS,UAAUT,MAAMU,QAAQ;QAEjEH,GAAGI,UAAU,GAAG;QAEhBJ,GAAGK,gBAAgB,CAAC,WAAW,CAAC,EAAEC,IAAI,EAAE;YACtC,MAAMC,SAAsBD;YAC5B,MAAME,OAAOxB,aAAauB,QAAQ;YAClC,IAAIC,QAAQ1B,mBAAmB;gBAC7B,IAAI,CAAC2B,IAAI,CAAC,CAAC,EAAED,KAAK,CAAC,EAAED,OAAOG,KAAK,CAACC,WAAWC,iBAAiB;YAChE;QACF;QAEA,IAAI,CAAC,CAAA,SAAU,GAAGZ;QAElB,OAAO,IAAIa,QAAQ,CAACC,SAASC;YAC3Bf,GAAGK,gBAAgB,CACjB,QACA;gBACE,IAAI,CAACI,IAAI,CAAC;gBACVK;YACF,GACA;gBAAEE,MAAM;YAAK;YAGfhB,GAAGK,gBAAgB,CACjB,SACA,CAACY,QAAUF,OAAO,IAAIG,MAAM,mBAAmB;oBAAEC,OAAOF;gBAAM,KAC9D;gBAAED,MAAM;YAAK;YAGfhB,GAAGK,gBAAgB,CACjB,SACA,CAACY;gBACC,IAAI,CAACR,IAAI,CAAC;gBACV,IAAI,CAAC,CAAA,SAAU,GAAG;gBAClB,IAAI,IAAI,CAACrB,OAAO,CAACgC,aAAa,KAAK,MAAM;oBACvCC,WAAW,IAAM,IAAI,CAAChC,OAAO,CAACC,MAAME,cAAc;gBACpD;YACF,GACA;gBAAEwB,MAAM;YAAK;QAEjB;IACF;IAEA,MAAMM,aAA4B;QAChC,IAAI,IAAI,CAAC,CAAA,SAAU,KAAK,MAAM;QAC9B,IAAI,CAAC,CAAA,SAAU,CAAEC,KAAK;QACtB,OAAOC,MAAM,IAAI,CAAC,CAAA,SAAU,EAAE;IAChC;IAEA,MAAMC,KACJC,WAA8B,EAC9BnB,MAAmB,EACJ;QACf,IAAI,IAAI,CAAC,CAAA,UAAW,EAAE,MAAM,IAAI,CAAC,CAAA,UAAW;QAC5C,IAAI,CAAC,CAAA,SAAU,CAAEkB,IAAI,CAAC1C,OAAOE,aAAayC,aAAa,UAAUnB;IACnE;AACF;AAEA,SAASiB,MAAMG,MAAmB,EAAEV,KAAa;IAC/C,OAAO,IAAIJ,QAAc,CAACC;QACxBa,OAAOtB,gBAAgB,CAACY,OAAO,IAAMH,WAAW;YAAEE,MAAM;QAAK;IAC/D;AACF"}
|
package/index.ts
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type
|
|
3
|
-
|
|
4
|
-
type
|
|
5
|
-
|
|
6
|
-
ClientUpStream,
|
|
7
|
-
Subscription,
|
|
8
|
-
createClientDownStream,
|
|
9
|
-
onAbort,
|
|
10
|
-
once,
|
|
11
|
-
} from '@nmtjs/client'
|
|
2
|
+
type BaseClientFormat,
|
|
3
|
+
EventEmitter,
|
|
4
|
+
type ProtocolTransport,
|
|
5
|
+
} from '@nmtjs/protocol/client'
|
|
12
6
|
import {
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
MessageType,
|
|
16
|
-
MessageTypeName,
|
|
17
|
-
TransportType,
|
|
7
|
+
type ClientMessageType,
|
|
8
|
+
ServerMessageType,
|
|
18
9
|
concat,
|
|
19
10
|
decodeNumber,
|
|
20
11
|
encodeNumber,
|
|
21
|
-
} from '@nmtjs/common'
|
|
12
|
+
} from '@nmtjs/protocol/common'
|
|
22
13
|
|
|
23
|
-
export type
|
|
14
|
+
export type WebSocketClientTransportOptions = {
|
|
24
15
|
/**
|
|
25
16
|
* The origin of the server
|
|
26
17
|
* @example 'http://localhost:3000'
|
|
@@ -40,412 +31,90 @@ export type WsClientTransportOptions = {
|
|
|
40
31
|
debug?: boolean
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
34
|
+
export class WebSocketClientTransport
|
|
35
|
+
extends EventEmitter
|
|
36
|
+
implements ProtocolTransport
|
|
37
|
+
{
|
|
38
|
+
#webSocket: WebSocket | null = null
|
|
39
|
+
#connecting: Promise<void> | null = null
|
|
67
40
|
|
|
68
|
-
private
|
|
69
|
-
private encodeRpcContext: EncodeRpcContext
|
|
70
|
-
private decodeRpcContext: DecodeRpcContext
|
|
71
|
-
|
|
72
|
-
constructor(private readonly options: WsClientTransportOptions) {
|
|
41
|
+
constructor(private readonly options: WebSocketClientTransportOptions) {
|
|
73
42
|
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
43
|
}
|
|
107
44
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
45
|
+
connect(
|
|
46
|
+
auth: string | undefined = undefined,
|
|
47
|
+
contentType: BaseClientFormat['contentType'],
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const wsUrl = new URL(this.options.origin)
|
|
50
|
+
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
51
|
+
wsUrl.pathname = '/api'
|
|
52
|
+
wsUrl.searchParams.set('content-type', contentType)
|
|
53
|
+
wsUrl.searchParams.set('accept', contentType)
|
|
54
|
+
if (auth) wsUrl.searchParams.set('auth', auth)
|
|
112
55
|
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
...this.queryParams,
|
|
116
|
-
accept: this.client.format.contentType,
|
|
117
|
-
'content-type': this.client.format.contentType,
|
|
118
|
-
services: this.client.services,
|
|
119
|
-
}),
|
|
120
|
-
)
|
|
56
|
+
const ws =
|
|
57
|
+
this.options.wsFactory?.(wsUrl) ?? new WebSocket(wsUrl.toString())
|
|
121
58
|
|
|
122
|
-
|
|
59
|
+
ws.binaryType = 'arraybuffer'
|
|
123
60
|
|
|
124
|
-
|
|
125
|
-
const buffer: ArrayBuffer =
|
|
61
|
+
ws.addEventListener('message', ({ data }) => {
|
|
62
|
+
const buffer: ArrayBuffer = data
|
|
126
63
|
const type = decodeNumber(buffer, 'Uint8')
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
handler.call(this, buffer.slice(Uint8Array.BYTES_PER_ELEMENT), this.ws)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
this.ws.onopen = (event) => {
|
|
133
|
-
if (this.options.debug)
|
|
134
|
-
console.log(`[WS] Connected to ${this.options.origin}`)
|
|
135
|
-
this.isConnected = true
|
|
136
|
-
this.emit('open')
|
|
137
|
-
this.checkHealthAttempts = 0
|
|
138
|
-
}
|
|
139
|
-
this.ws.onclose = (event) => {
|
|
140
|
-
this.isConnected = false
|
|
141
|
-
this.isHealthy = false
|
|
142
|
-
this.emit(
|
|
143
|
-
'close',
|
|
144
|
-
event.code === 1000
|
|
145
|
-
? undefined
|
|
146
|
-
: new Error(
|
|
147
|
-
`Connection closed with code ${event.code}: ${event.reason}`,
|
|
148
|
-
),
|
|
149
|
-
)
|
|
150
|
-
// FIXME: cleanup calls, streams, subscriptions
|
|
151
|
-
if (this.autoreconnect) this.connect()
|
|
152
|
-
}
|
|
153
|
-
this.ws.onerror = (cause) => {
|
|
154
|
-
this.isHealthy = false
|
|
155
|
-
this.emit('close', new Error('Connection error', { cause }))
|
|
156
|
-
}
|
|
157
|
-
await once(this, 'open')
|
|
158
|
-
if (this.options.debug)
|
|
159
|
-
console.log(`2[WS] Connected to ${this.options.origin}`)
|
|
160
|
-
this.emit('connect')
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async disconnect(): Promise<void> {
|
|
164
|
-
this.autoreconnect = false
|
|
165
|
-
this.ws?.close()
|
|
166
|
-
await once(this, 'close')
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async rpc(call: ClientTransportRpcCall): Promise<ClientTransportRpcResult> {
|
|
170
|
-
const { signal, callId, payload, procedure, service } = call
|
|
171
|
-
|
|
172
|
-
const data = this.client.format.encodeRpc(
|
|
173
|
-
{
|
|
174
|
-
callId,
|
|
175
|
-
service,
|
|
176
|
-
procedure,
|
|
177
|
-
payload,
|
|
178
|
-
},
|
|
179
|
-
this.encodeRpcContext,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
onAbort(signal, () => {
|
|
183
|
-
const call = this.calls.get(callId)
|
|
184
|
-
if (call) {
|
|
185
|
-
const { reject } = call
|
|
186
|
-
reject(new Error('Request aborted'))
|
|
187
|
-
this.calls.delete(callId)
|
|
188
|
-
this.send(MessageType.RpcAbort, encodeNumber(callId, 'Uint32'))
|
|
64
|
+
if (type in ServerMessageType) {
|
|
65
|
+
this.emit(`${type}`, buffer.slice(Uint8Array.BYTES_PER_ELEMENT))
|
|
189
66
|
}
|
|
190
67
|
})
|
|
191
68
|
|
|
192
|
-
|
|
69
|
+
this.#webSocket = ws
|
|
193
70
|
|
|
194
71
|
return new Promise((resolve, reject) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
protected send(type: MessageType, ...payload: ArrayBuffer[]) {
|
|
205
|
-
this.ws?.send(concat(encodeNumber(type, 'Uint8'), ...payload))
|
|
206
|
-
if (this.options.debug) {
|
|
207
|
-
console.groupCollapsed(`[WS] Sent ${MessageTypeName[type]}`)
|
|
208
|
-
console.trace()
|
|
209
|
-
console.groupEnd()
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
protected async clear(error: Error = new Error('Connection closed')) {
|
|
214
|
-
for (const call of this.calls.values()) {
|
|
215
|
-
const { reject } = call
|
|
216
|
-
reject(error)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
for (const stream of this.upStreams.values()) {
|
|
220
|
-
stream.reader.cancel(error)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
for (const stream of this.downStreams.values()) {
|
|
224
|
-
stream.writer.abort(error)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
for (const subscription of this.subscriptions.values()) {
|
|
228
|
-
subscription.unsubscribe()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
this.calls.clear()
|
|
232
|
-
this.upStreams.clear()
|
|
233
|
-
this.downStreams.clear()
|
|
234
|
-
this.subscriptions.clear()
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
protected async healthCheck() {
|
|
238
|
-
while (!this.isHealthy) {
|
|
239
|
-
try {
|
|
240
|
-
const signal = AbortSignal.timeout(10000)
|
|
241
|
-
const url = this.getURL('healthy', 'http', {
|
|
242
|
-
auth: this.client.auth,
|
|
243
|
-
})
|
|
244
|
-
const { ok } = await fetch(url, { signal })
|
|
245
|
-
this.isHealthy = ok
|
|
246
|
-
} catch (e) {}
|
|
247
|
-
|
|
248
|
-
if (!this.isHealthy) {
|
|
249
|
-
this.checkHealthAttempts++
|
|
250
|
-
const seconds = Math.min(this.checkHealthAttempts, 15)
|
|
251
|
-
await new Promise((r) => setTimeout(r, seconds * 1000))
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
this.emit('healthy')
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private getURL(
|
|
258
|
-
path = '',
|
|
259
|
-
protocol: 'ws' | 'http',
|
|
260
|
-
params: Record<string, any> = {},
|
|
261
|
-
) {
|
|
262
|
-
// TODO: add custom path support?
|
|
263
|
-
const base = new URL(this.options.origin)
|
|
264
|
-
const secure = base.protocol === 'https:'
|
|
265
|
-
const url = new URL(
|
|
266
|
-
`${secure ? protocol + 's' : protocol}://${base.host}/${path}`,
|
|
267
|
-
)
|
|
268
|
-
for (let [key, values] of Object.entries(params)) {
|
|
269
|
-
if (!Array.isArray(values)) values = [values]
|
|
270
|
-
for (const value of values) {
|
|
271
|
-
url.searchParams.append(key, value)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return url
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
protected resolveRpc(callId: number, value: ClientTransportRpcResult) {
|
|
278
|
-
const call = this.calls.get(callId)
|
|
279
|
-
if (call) {
|
|
280
|
-
const { resolve } = call
|
|
281
|
-
this.calls.delete(callId)
|
|
282
|
-
resolve(value)
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
protected async [MessageType.Event](buffer: ArrayBuffer) {
|
|
287
|
-
const [service, event, payload] = this.client.format.decode(buffer)
|
|
288
|
-
if (this.options.debug) {
|
|
289
|
-
console.groupCollapsed(`[WS] Received "Event" ${service}/${event}`)
|
|
290
|
-
console.log(payload)
|
|
291
|
-
console.groupEnd()
|
|
292
|
-
}
|
|
293
|
-
this.emit('event', service, event, payload)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
protected async [MessageType.Rpc](buffer: ArrayBuffer) {
|
|
297
|
-
const { callId, error, payload } = this.client.format.decodeRpc(
|
|
298
|
-
buffer,
|
|
299
|
-
this.decodeRpcContext,
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
if (this.calls.has(callId)) {
|
|
303
|
-
this.resolveRpc(
|
|
304
|
-
callId,
|
|
305
|
-
error ? { success: false, error } : { success: true, value: payload },
|
|
72
|
+
ws.addEventListener(
|
|
73
|
+
'open',
|
|
74
|
+
() => {
|
|
75
|
+
this.emit('connect')
|
|
76
|
+
resolve()
|
|
77
|
+
},
|
|
78
|
+
{ once: true },
|
|
306
79
|
)
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
protected async [MessageType.UpStreamPull](buffer: ArrayBuffer) {
|
|
311
|
-
const id = decodeNumber(buffer, 'Uint32')
|
|
312
|
-
const size = decodeNumber(buffer, 'Uint32', Uint32Array.BYTES_PER_ELEMENT)
|
|
313
|
-
|
|
314
|
-
if (this.options.debug) {
|
|
315
|
-
console.log(`[WS] Received "UpStreamPull" ${id}`)
|
|
316
|
-
}
|
|
317
80
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
this.send(MessageType.UpStreamEnd, encodeNumber(id, 'Uint32'))
|
|
324
|
-
} else {
|
|
325
|
-
this.send(
|
|
326
|
-
MessageType.UpStreamPush,
|
|
327
|
-
concat(
|
|
328
|
-
encodeNumber(id, 'Uint32'),
|
|
329
|
-
value.buffer.slice(0, value.byteLength),
|
|
330
|
-
),
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
protected async [MessageType.UpStreamEnd](buffer: ArrayBuffer) {
|
|
337
|
-
const id = decodeNumber(buffer, 'Uint32')
|
|
338
|
-
if (this.options.debug) {
|
|
339
|
-
console.log(`[WS] Received "UpStreamEnd" ${id}`)
|
|
340
|
-
}
|
|
341
|
-
const stream = this.upStreams.get(id)
|
|
342
|
-
if (stream) {
|
|
343
|
-
stream.reader.cancel()
|
|
344
|
-
this.upStreams.delete(id)
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
protected [MessageType.UpStreamAbort](buffer: ArrayBuffer) {
|
|
349
|
-
const id = decodeNumber(buffer, 'Uint32')
|
|
350
|
-
if (this.options.debug) {
|
|
351
|
-
console.log(`[WS] Received "UpStreamAbort" ${id}`)
|
|
352
|
-
}
|
|
353
|
-
const stream = this.upStreams.get(id)
|
|
354
|
-
if (stream) {
|
|
355
|
-
try {
|
|
356
|
-
stream.reader.cancel(new Error('Aborted by server'))
|
|
357
|
-
} finally {
|
|
358
|
-
this.upStreams.delete(id)
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
protected async [MessageType.DownStreamPush](buffer: ArrayBuffer) {
|
|
364
|
-
const id = decodeNumber(buffer, 'Uint32')
|
|
365
|
-
if (this.options.debug) {
|
|
366
|
-
console.log(`[WS] Received "DownStreamPush" ${id}`)
|
|
367
|
-
}
|
|
368
|
-
const stream = this.downStreams.get(id)
|
|
369
|
-
if (stream) {
|
|
370
|
-
try {
|
|
371
|
-
await stream.writer.ready
|
|
372
|
-
const chunk = buffer.slice(Uint32Array.BYTES_PER_ELEMENT)
|
|
373
|
-
await stream.writer.write(new Uint8Array(chunk))
|
|
374
|
-
await stream.writer.ready
|
|
375
|
-
this.send(MessageType.DownStreamPull, encodeNumber(id, 'Uint32'))
|
|
376
|
-
} catch (e) {
|
|
377
|
-
this.send(MessageType.DownStreamAbort, encodeNumber(id, 'Uint32'))
|
|
378
|
-
this.downStreams.delete(id)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
protected async [MessageType.DownStreamEnd](buffer: ArrayBuffer) {
|
|
384
|
-
const id = decodeNumber(buffer, 'Uint32')
|
|
385
|
-
if (this.options.debug) {
|
|
386
|
-
console.log(`[WS] Received "DownStreamEnd" ${id}`)
|
|
387
|
-
}
|
|
388
|
-
const stream = this.downStreams.get(id)
|
|
389
|
-
if (stream) {
|
|
390
|
-
this.downStreams.delete(id)
|
|
391
|
-
stream.writer.close().catch(() => {})
|
|
392
|
-
}
|
|
393
|
-
}
|
|
81
|
+
ws.addEventListener(
|
|
82
|
+
'error',
|
|
83
|
+
(event) => reject(new Error('WebSocket error', { cause: event })),
|
|
84
|
+
{ once: true },
|
|
85
|
+
)
|
|
394
86
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
87
|
+
ws.addEventListener(
|
|
88
|
+
'close',
|
|
89
|
+
(event) => {
|
|
90
|
+
this.emit('disconnect')
|
|
91
|
+
this.#webSocket = null
|
|
92
|
+
if (this.options.autoreconnect === true) {
|
|
93
|
+
setTimeout(() => this.connect(auth, contentType), 1000)
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{ once: true },
|
|
97
|
+
)
|
|
98
|
+
})
|
|
405
99
|
}
|
|
406
100
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
} = this.client.format.decodeRpc(buffer, this.decodeRpcContext)
|
|
412
|
-
if (this.calls.has(callId)) {
|
|
413
|
-
const subscription = new Subscription(key, () => {
|
|
414
|
-
subscription.emit('end')
|
|
415
|
-
this.subscriptions.delete(key)
|
|
416
|
-
this.send(
|
|
417
|
-
MessageType.ClientUnsubscribe,
|
|
418
|
-
this.client.format.encode([key]),
|
|
419
|
-
)
|
|
420
|
-
})
|
|
421
|
-
this.subscriptions.set(key, subscription)
|
|
422
|
-
this.resolveRpc(callId, {
|
|
423
|
-
success: true,
|
|
424
|
-
value: { payload, subscription },
|
|
425
|
-
})
|
|
426
|
-
}
|
|
101
|
+
async disconnect(): Promise<void> {
|
|
102
|
+
if (this.#webSocket === null) return
|
|
103
|
+
this.#webSocket!.close()
|
|
104
|
+
return _once(this.#webSocket, 'close')
|
|
427
105
|
}
|
|
428
106
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
console.log(payload)
|
|
436
|
-
console.groupEnd()
|
|
437
|
-
}
|
|
438
|
-
const subscription = this.subscriptions.get(key)
|
|
439
|
-
if (subscription) subscription.emit(event, payload)
|
|
107
|
+
async send(
|
|
108
|
+
messageType: ClientMessageType,
|
|
109
|
+
buffer: ArrayBuffer,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
if (this.#connecting) await this.#connecting
|
|
112
|
+
this.#webSocket!.send(concat(encodeNumber(messageType, 'Uint8'), buffer))
|
|
440
113
|
}
|
|
114
|
+
}
|
|
441
115
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
const subscription = this.subscriptions.get(key)
|
|
448
|
-
subscription?.emit('end')
|
|
449
|
-
this.subscriptions.delete(key)
|
|
450
|
-
}
|
|
116
|
+
function _once(target: EventTarget, event: string) {
|
|
117
|
+
return new Promise<void>((resolve) => {
|
|
118
|
+
target.addEventListener(event, () => resolve(), { once: true })
|
|
119
|
+
})
|
|
451
120
|
}
|
package/package.json
CHANGED
|
@@ -7,13 +7,10 @@
|
|
|
7
7
|
"types": "./index.ts"
|
|
8
8
|
}
|
|
9
9
|
},
|
|
10
|
-
"
|
|
11
|
-
"@nmtjs/client": "^0.
|
|
12
|
-
"@nmtjs/common": "^0.
|
|
13
|
-
|
|
14
|
-
"devDependencies": {
|
|
15
|
-
"@nmtjs/client": "^0.4.2",
|
|
16
|
-
"@nmtjs/common": "^0.4.2"
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@nmtjs/client": "^0.6.1",
|
|
12
|
+
"@nmtjs/common": "^0.6.1",
|
|
13
|
+
"@nmtjs/protocol": "^0.6.1"
|
|
17
14
|
},
|
|
18
15
|
"files": [
|
|
19
16
|
"index.ts",
|
|
@@ -22,7 +19,7 @@
|
|
|
22
19
|
"LICENSE.md",
|
|
23
20
|
"README.md"
|
|
24
21
|
],
|
|
25
|
-
"version": "0.
|
|
22
|
+
"version": "0.2.0",
|
|
26
23
|
"scripts": {
|
|
27
24
|
"build": "neemata-build -p neutral ./index.ts",
|
|
28
25
|
"type-check": "tsc --noEmit"
|