relayx-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -0
- package/README.md +156 -0
- package/examples/example_chat.js +97 -0
- package/examples/example_send_data_on_connect.js +46 -0
- package/package.json +25 -0
- package/realtime/history.js +160 -0
- package/realtime/http.js +0 -0
- package/realtime/realtime.js +769 -0
- package/tests/load.js +101 -0
- package/tests/test.js +433 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
import { History } from "./history.js";
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { connect, JSONCodec, Events, DebugEvents, AckPolicy, ReplayPolicy, credsAuthenticator } from "nats";
|
|
4
|
+
import { DeliverPolicy, jetstream, jetstreamManager } from "@nats-io/jetstream";
|
|
5
|
+
import { readFileSync } from "fs"
|
|
6
|
+
|
|
7
|
+
export class Realtime {
|
|
8
|
+
|
|
9
|
+
#baseUrl = "";
|
|
10
|
+
|
|
11
|
+
#natsClient = null;
|
|
12
|
+
#codec = JSONCodec();
|
|
13
|
+
#jetstream = null;
|
|
14
|
+
#jsManager = null;
|
|
15
|
+
#streamTracker = [];
|
|
16
|
+
#consumerMap = {};
|
|
17
|
+
|
|
18
|
+
#event_func = {};
|
|
19
|
+
#topicMap = [];
|
|
20
|
+
|
|
21
|
+
#config = "CiAgICAgICAgLS0tLS1CRUdJTiBOQVRTIFVTRVIgSldULS0tLS0KICAgICAgICBKV1RfS0VZCiAgICAgICAgLS0tLS0tRU5EIE5BVFMgVVNFUiBKV1QtLS0tLS0KCiAgICAgICAgKioqKioqKioqKioqKioqKioqKioqKioqKiBJTVBPUlRBTlQgKioqKioqKioqKioqKioqKioqKioqKioqKgogICAgICAgIE5LRVkgU2VlZCBwcmludGVkIGJlbG93IGNhbiBiZSB1c2VkIHRvIHNpZ24gYW5kIHByb3ZlIGlkZW50aXR5LgogICAgICAgIE5LRVlzIGFyZSBzZW5zaXRpdmUgYW5kIHNob3VsZCBiZSB0cmVhdGVkIGFzIHNlY3JldHMuCgogICAgICAgIC0tLS0tQkVHSU4gVVNFUiBOS0VZIFNFRUQtLS0tLQogICAgICAgIFNFQ1JFVF9LRVkKICAgICAgICAtLS0tLS1FTkQgVVNFUiBOS0VZIFNFRUQtLS0tLS0KCiAgICAgICAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKgogICAgICAgIA=="
|
|
22
|
+
|
|
23
|
+
// Status Codes
|
|
24
|
+
#RECONNECTING = "RECONNECTING";
|
|
25
|
+
#RECONNECTED = "RECONNECTED";
|
|
26
|
+
#RECONN_FAIL = "RECONN_FAIL";
|
|
27
|
+
|
|
28
|
+
setRemoteUserAttempts = 0;
|
|
29
|
+
setRemoteUserRetries = 5;
|
|
30
|
+
|
|
31
|
+
// Retry attempts end
|
|
32
|
+
reconnected = false;
|
|
33
|
+
disconnected = true;
|
|
34
|
+
reconnecting = false;
|
|
35
|
+
connected = false;
|
|
36
|
+
|
|
37
|
+
// Offline messages
|
|
38
|
+
#offlineMessageBuffer = [];
|
|
39
|
+
|
|
40
|
+
// History API
|
|
41
|
+
history = null;
|
|
42
|
+
|
|
43
|
+
// Test Variables
|
|
44
|
+
#timeout = 1000;
|
|
45
|
+
|
|
46
|
+
#maxPublishRetries = 5;
|
|
47
|
+
|
|
48
|
+
constructor(config){
|
|
49
|
+
if(typeof config != "object"){
|
|
50
|
+
throw new Error("Realtime($config). $config not object => {}")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if(config != null && config != undefined){
|
|
54
|
+
this.api_key = config.api_key != undefined ? config.api_key : null;
|
|
55
|
+
this.secret = config.secret != undefined ? config.secret : null;
|
|
56
|
+
|
|
57
|
+
if(this.api_key == null){
|
|
58
|
+
throw new Error("api_key value null")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if(this.secret == null){
|
|
62
|
+
throw new Error("secret value null")
|
|
63
|
+
}
|
|
64
|
+
}else{
|
|
65
|
+
throw new Error("{api_key: <value>, secret: <value>} not passed in constructor")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.namespace = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
Initialized library with configuration options.
|
|
73
|
+
*/
|
|
74
|
+
async init(staging, opts){
|
|
75
|
+
/**
|
|
76
|
+
* Method can take in 2 variables
|
|
77
|
+
* @param{boolean} staging - Sets URL to staging or production URL
|
|
78
|
+
* @param{Object} opts - Library configuration options
|
|
79
|
+
*/
|
|
80
|
+
var len = arguments.length;
|
|
81
|
+
|
|
82
|
+
if (len > 2){
|
|
83
|
+
new Error("Method takes only 2 variables, " + len + " given");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (len == 2){
|
|
87
|
+
if(typeof arguments[0] == "boolean"){
|
|
88
|
+
staging = arguments[0];
|
|
89
|
+
}else{
|
|
90
|
+
staging = false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if(arguments[1] instanceof Object){
|
|
94
|
+
opts = arguments[1];
|
|
95
|
+
}else{
|
|
96
|
+
opts = {};
|
|
97
|
+
}
|
|
98
|
+
}else if(len == 1){
|
|
99
|
+
if(arguments[0] instanceof Object){
|
|
100
|
+
opts = arguments[0];
|
|
101
|
+
staging = false;
|
|
102
|
+
}else{
|
|
103
|
+
opts = {};
|
|
104
|
+
staging = arguments[0];
|
|
105
|
+
this.#log(staging)
|
|
106
|
+
}
|
|
107
|
+
}else{
|
|
108
|
+
staging = false;
|
|
109
|
+
opts = {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.staging = staging;
|
|
113
|
+
|
|
114
|
+
if (staging !== undefined || staging !== null){
|
|
115
|
+
this.#baseUrl = staging ? [
|
|
116
|
+
"nats://0.0.0.0:4221",
|
|
117
|
+
"nats://0.0.0.0:4222",
|
|
118
|
+
"nats://0.0.0.0:4223",
|
|
119
|
+
"nats://0.0.0.0:4224",
|
|
120
|
+
"nats://0.0.0.0:4225",
|
|
121
|
+
"nats://0.0.0.0:4226"] :
|
|
122
|
+
[
|
|
123
|
+
"nats://api.relay-x.io:4221",
|
|
124
|
+
"nats://api.relay-x.io:4222",
|
|
125
|
+
"nats://api.relay-x.io:4223",
|
|
126
|
+
"nats://api.relay-x.io:4224",
|
|
127
|
+
"nats://api.relay-x.io:4225",
|
|
128
|
+
"nats://api.relay-x.io:4226",
|
|
129
|
+
];
|
|
130
|
+
}else{
|
|
131
|
+
this.#baseUrl = [
|
|
132
|
+
"nats://api.relay-x.io:4221",
|
|
133
|
+
"nats://api.relay-x.io:4222",
|
|
134
|
+
"nats://api.relay-x.io:4223",
|
|
135
|
+
"nats://api.relay-x.io:4224",
|
|
136
|
+
"nats://api.relay-x.io:4225",
|
|
137
|
+
"nats://api.relay-x.io:4226",
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.#log(this.#baseUrl);
|
|
142
|
+
this.#log(opts);
|
|
143
|
+
|
|
144
|
+
this.opts = opts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gets the namespace of the user using a REST API
|
|
149
|
+
* @returns {string} namespace value. Null if failed to retreive
|
|
150
|
+
*/
|
|
151
|
+
async #getNameSpace() {
|
|
152
|
+
var res = await this.#natsClient.request("accounts.user.get_namespace",
|
|
153
|
+
this.#codec.encode({
|
|
154
|
+
"api_key": this.api_key
|
|
155
|
+
}),
|
|
156
|
+
{
|
|
157
|
+
timeout: 5000
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
var data = res.json()
|
|
162
|
+
|
|
163
|
+
this.#log(data)
|
|
164
|
+
|
|
165
|
+
if(data["status"] == "NAMESPACE_RETRIEVE_SUCCESS"){
|
|
166
|
+
this.namespace = data["data"]["namespace"]
|
|
167
|
+
}else{
|
|
168
|
+
this.namespace = null;
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Connects to the relay network
|
|
176
|
+
*/
|
|
177
|
+
async connect(){
|
|
178
|
+
this.SEVER_URL = this.#baseUrl;
|
|
179
|
+
|
|
180
|
+
var credsFile = this.#getUserCreds(this.api_key, this.secret)
|
|
181
|
+
credsFile = new TextEncoder().encode(credsFile);
|
|
182
|
+
var credsAuth = credsAuthenticator(credsFile);
|
|
183
|
+
|
|
184
|
+
try{
|
|
185
|
+
this.#natsClient = await connect({
|
|
186
|
+
servers: this.SEVER_URL,
|
|
187
|
+
noEcho: true,
|
|
188
|
+
maxReconnectAttempts: 1200,
|
|
189
|
+
reconnect: true,
|
|
190
|
+
reconnectTimeWait: 1000,
|
|
191
|
+
authenticator: credsAuth,
|
|
192
|
+
token: this.api_key
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.#jsManager = await jetstreamManager(this.#natsClient);
|
|
196
|
+
this.#jetstream = await jetstream(this.#natsClient);
|
|
197
|
+
|
|
198
|
+
await this.#getNameSpace()
|
|
199
|
+
|
|
200
|
+
this.connected = true;
|
|
201
|
+
}catch(err){
|
|
202
|
+
this.#log("ERR")
|
|
203
|
+
this.#log(err);
|
|
204
|
+
|
|
205
|
+
this.connected = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.connected == true){
|
|
209
|
+
this.#log("Connected to server!");
|
|
210
|
+
|
|
211
|
+
// Callback on client side
|
|
212
|
+
if (CONNECTED in this.#event_func){
|
|
213
|
+
if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
|
|
214
|
+
this.#event_func[CONNECTED]()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.#natsClient.closed().then(() => {
|
|
219
|
+
this.#log("the connection closed!");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
(async () => {
|
|
223
|
+
for await (const s of this.#natsClient.status()) {
|
|
224
|
+
this.#log(s.type)
|
|
225
|
+
|
|
226
|
+
switch (s.type) {
|
|
227
|
+
case Events.Disconnect:
|
|
228
|
+
this.#log(`client disconnected - ${s.data}`);
|
|
229
|
+
|
|
230
|
+
this.connected = false;
|
|
231
|
+
this.#streamTracker = [];
|
|
232
|
+
this.#consumerMap = {};
|
|
233
|
+
|
|
234
|
+
if (DISCONNECTED in this.#event_func){
|
|
235
|
+
if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
|
|
236
|
+
this.#event_func[DISCONNECTED]()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
case Events.LDM:
|
|
241
|
+
this.#log("client has been requested to reconnect");
|
|
242
|
+
break;
|
|
243
|
+
case Events.Update:
|
|
244
|
+
this.#log(`client received a cluster update - `);
|
|
245
|
+
this.#log(s.data)
|
|
246
|
+
break;
|
|
247
|
+
case Events.Reconnect:
|
|
248
|
+
this.#log(`client reconnected -`);
|
|
249
|
+
this.#log(s.data)
|
|
250
|
+
|
|
251
|
+
this.reconnecting = false;
|
|
252
|
+
this.connected = true;
|
|
253
|
+
|
|
254
|
+
this.#subscribeToTopics();
|
|
255
|
+
|
|
256
|
+
if(RECONNECT in this.#event_func){
|
|
257
|
+
this.#event_func[RECONNECT](this.#RECONNECTED);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Resend any messages sent while client was offline
|
|
261
|
+
this.#publishMessagesOnReconnect();
|
|
262
|
+
break;
|
|
263
|
+
case Events.Error:
|
|
264
|
+
this.#log("client got a permissions error");
|
|
265
|
+
break;
|
|
266
|
+
case DebugEvents.Reconnecting:
|
|
267
|
+
this.#log("client is attempting to reconnect");
|
|
268
|
+
|
|
269
|
+
this.reconnecting = true;
|
|
270
|
+
|
|
271
|
+
if(RECONNECT in this.#event_func && this.reconnecting){
|
|
272
|
+
this.#event_func[RECONNECT](this.#RECONNECTING);
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
case DebugEvents.StaleConnection:
|
|
276
|
+
this.#log("client has a stale connection");
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
this.#log(`got an unknown status ${s.type}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
})().then();
|
|
283
|
+
|
|
284
|
+
// Subscribe to topics
|
|
285
|
+
this.#subscribeToTopics();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Closes connection
|
|
291
|
+
*/
|
|
292
|
+
close(){
|
|
293
|
+
if(this.#natsClient !== null){
|
|
294
|
+
this.reconnected = false;
|
|
295
|
+
this.disconnected = true;
|
|
296
|
+
|
|
297
|
+
this.#natsClient.close();
|
|
298
|
+
}else{
|
|
299
|
+
this.#log("Null / undefined socket, cannot close connection");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Start consumers for topics initialized by user
|
|
305
|
+
*/
|
|
306
|
+
async #subscribeToTopics(){
|
|
307
|
+
this.#topicMap.forEach(async (topic) => {
|
|
308
|
+
// Subscribe to stream
|
|
309
|
+
await this.#startConsumer(topic);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Deletes reference to user defined event callback.
|
|
315
|
+
* This will stop listening to a topic
|
|
316
|
+
* @param {string} topic
|
|
317
|
+
* @returns {boolean} - To check if topic unsubscribe was successful
|
|
318
|
+
*/
|
|
319
|
+
async off(topic){
|
|
320
|
+
if(topic == null || topic == undefined){
|
|
321
|
+
throw new Error("$topic is null / undefined")
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if(typeof topic !== "string"){
|
|
325
|
+
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this.#topicMap = this.#topicMap.filter(item => item !== topic);
|
|
329
|
+
|
|
330
|
+
delete this.#event_func[topic];
|
|
331
|
+
|
|
332
|
+
return await this.#deleteConsumer(topic);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Subscribes to a topic
|
|
337
|
+
* @param {string} topic - Name of the event
|
|
338
|
+
* @param {function} func - Callback function to call on user thread
|
|
339
|
+
* @returns {boolean} - To check if topic subscription was successful
|
|
340
|
+
*/
|
|
341
|
+
async on(topic, func){
|
|
342
|
+
if(topic == null || topic == undefined){
|
|
343
|
+
throw new Error("$topic is null / undefined")
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if(func == null || func == undefined){
|
|
347
|
+
throw new Error("$func is null / undefined")
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if ((typeof func !== "function")){
|
|
351
|
+
throw new Error(`Expected $listener type -> function. Instead receieved -> ${typeof func}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if(typeof topic !== "string"){
|
|
355
|
+
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if(!(topic in this.#event_func)){
|
|
359
|
+
this.#event_func[topic] = func;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
|
|
363
|
+
this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND].includes(topic)){
|
|
364
|
+
if(!this.#topicMap.includes(topic)){
|
|
365
|
+
this.#topicMap.push(topic);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if(this.connected){
|
|
369
|
+
// Connected we need to create a topic in a stream
|
|
370
|
+
await this.#startConsumer(topic);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* A method to send a message to a topic.
|
|
377
|
+
* Retry methods included. Stores messages in an array if offline.
|
|
378
|
+
* @param {string} topic - Name of the event
|
|
379
|
+
* @param {object} data - Data to send
|
|
380
|
+
* @returns
|
|
381
|
+
*/
|
|
382
|
+
async publish(topic, data){
|
|
383
|
+
if(topic == null || topic == undefined){
|
|
384
|
+
throw new Error("$topic is null or undefined");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if(topic == ""){
|
|
388
|
+
throw new Error("$topic cannot be an empty string")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if(typeof topic !== "string"){
|
|
392
|
+
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if(!this.isTopicValid(topic)){
|
|
396
|
+
throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
var start = Date.now()
|
|
400
|
+
var messageId = crypto.randomUUID();
|
|
401
|
+
|
|
402
|
+
var message = {
|
|
403
|
+
"client_id": this.#getClientId(),
|
|
404
|
+
"id": messageId,
|
|
405
|
+
"room": topic,
|
|
406
|
+
"message": data,
|
|
407
|
+
"start": Date.now()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
var encodedMessage = this.#codec.encode(message)
|
|
411
|
+
|
|
412
|
+
if(this.connected){
|
|
413
|
+
if(!this.#topicMap.includes(topic)){
|
|
414
|
+
this.#topicMap.push(topic);
|
|
415
|
+
|
|
416
|
+
await this.#createOrGetStream();
|
|
417
|
+
}else{
|
|
418
|
+
this.#log(`${topic} exists locally, moving on...`)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
|
|
422
|
+
|
|
423
|
+
const ack = await this.#jetstream.publish(this.#getStreamTopic(topic), encodedMessage);
|
|
424
|
+
this.#log(`Publish Ack =>`)
|
|
425
|
+
this.#log(ack)
|
|
426
|
+
|
|
427
|
+
var latency = Date.now() - start;
|
|
428
|
+
this.#log(`Latency => ${latency} ms`);
|
|
429
|
+
|
|
430
|
+
return ack !== null && ack !== undefined;
|
|
431
|
+
}else{
|
|
432
|
+
this.#offlineMessageBuffer.push({
|
|
433
|
+
topic: topic,
|
|
434
|
+
message: data
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Method resends messages when the client successfully connects to the
|
|
443
|
+
* server again
|
|
444
|
+
* @returns - Array of success and failure messages
|
|
445
|
+
*/
|
|
446
|
+
async #publishMessagesOnReconnect(){
|
|
447
|
+
var messageSentStatus = [];
|
|
448
|
+
|
|
449
|
+
for(let i = 0; i < this.#offlineMessageBuffer.length; i++){
|
|
450
|
+
let data = this.#offlineMessageBuffer[i];
|
|
451
|
+
|
|
452
|
+
const topic = data.topic;
|
|
453
|
+
const message = data.message;
|
|
454
|
+
|
|
455
|
+
const output = await this.publish(topic, message);
|
|
456
|
+
|
|
457
|
+
messageSentStatus.push({
|
|
458
|
+
topic: topic,
|
|
459
|
+
message: message,
|
|
460
|
+
resent: output
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Clearing out offline messages
|
|
465
|
+
this.#offlineMessageBuffer.length = 0;
|
|
466
|
+
|
|
467
|
+
// Send to client
|
|
468
|
+
if(MESSAGE_RESEND in this.#event_func && messageSentStatus.length > 0){
|
|
469
|
+
this.#event_func[MESSAGE_RESEND](messageSentStatus);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Room functions
|
|
474
|
+
/**
|
|
475
|
+
* Starts consumer for particular topic if stream exists
|
|
476
|
+
* @param {string} topic
|
|
477
|
+
*/
|
|
478
|
+
async #startConsumer(topic){
|
|
479
|
+
await this.#createOrGetStream();
|
|
480
|
+
|
|
481
|
+
var opts = {
|
|
482
|
+
name: topic,
|
|
483
|
+
filter_subjects: [this.#getStreamTopic(topic), this.#getStreamTopic(topic) + "_presence"],
|
|
484
|
+
replay_policy: ReplayPolicy.Instant,
|
|
485
|
+
opt_start_time: new Date(),
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
|
|
489
|
+
this.#log(this.#topicMap)
|
|
490
|
+
this.#log("Consumer is consuming");
|
|
491
|
+
|
|
492
|
+
this.#consumerMap[topic] = consumer;
|
|
493
|
+
|
|
494
|
+
await consumer.consume({
|
|
495
|
+
callback: (msg) => {
|
|
496
|
+
|
|
497
|
+
msg.ack();
|
|
498
|
+
|
|
499
|
+
try{
|
|
500
|
+
var data = this.#codec.decode(msg.data);
|
|
501
|
+
var room = data.room;
|
|
502
|
+
|
|
503
|
+
this.#log(data);
|
|
504
|
+
const latency = Date.now() - data.start
|
|
505
|
+
this.#log(`Latency => ${latency}`)
|
|
506
|
+
|
|
507
|
+
// Push topic message to main thread
|
|
508
|
+
if (room in this.#event_func && data.client_id != this.#getClientId()){
|
|
509
|
+
this.#event_func[room]({
|
|
510
|
+
"id": data.id,
|
|
511
|
+
"data": data.message
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}catch(err){
|
|
515
|
+
this.#log("Consumer err " + err);
|
|
516
|
+
msg.nack();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Deletes consumer
|
|
524
|
+
* @param {string} topic
|
|
525
|
+
*/
|
|
526
|
+
async #deleteConsumer(topic){
|
|
527
|
+
const consumer = this.#consumerMap[topic]
|
|
528
|
+
|
|
529
|
+
var del = false;
|
|
530
|
+
|
|
531
|
+
if (consumer != null && consumer != undefined){
|
|
532
|
+
del = await consumer.delete();
|
|
533
|
+
}else{
|
|
534
|
+
del = true
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
delete this.#consumerMap[topic];
|
|
538
|
+
|
|
539
|
+
return del;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Gets stream if it exists or creates one
|
|
544
|
+
* @param {string} streamName
|
|
545
|
+
*/
|
|
546
|
+
async #createOrGetStream(){
|
|
547
|
+
const streamName = this.#getStreamName();
|
|
548
|
+
var stream = null;
|
|
549
|
+
|
|
550
|
+
try{
|
|
551
|
+
stream = await this.#jsManager.streams.info(streamName);
|
|
552
|
+
}catch(err){
|
|
553
|
+
stream = null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
this.#log(`STREAM => ${stream}`)
|
|
557
|
+
|
|
558
|
+
if (stream == null){
|
|
559
|
+
// Stream does not exist, create one
|
|
560
|
+
await this.#jsManager.streams.add({
|
|
561
|
+
name: streamName,
|
|
562
|
+
subjects: [...this.#getStreamTopicList(), ...this.#getPresenceTopics()],
|
|
563
|
+
ack_policy: AckPolicy.Explicit,
|
|
564
|
+
delivery_policy: DeliverPolicy.New
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
this.#log(`${streamName} created`);
|
|
568
|
+
}else{
|
|
569
|
+
stream.config.subjects = [...this.#getStreamTopicList(), ...this.#getPresenceTopics()];
|
|
570
|
+
await this.#jsManager.streams.update(streamName, stream.config);
|
|
571
|
+
|
|
572
|
+
this.#log(`${streamName} exists, updating and moving on...`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Utility functions
|
|
577
|
+
#getClientId(){
|
|
578
|
+
return this.#natsClient?.info?.client_id
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Checks if a topic can be used to send messages to.
|
|
583
|
+
* @param {string} topic - Name of event
|
|
584
|
+
* @returns {boolean} - If topic is valid or not
|
|
585
|
+
*/
|
|
586
|
+
isTopicValid(topic){
|
|
587
|
+
if(topic !== null && topic !== undefined && (typeof topic) == "string"){
|
|
588
|
+
return ![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
|
|
589
|
+
this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND].includes(topic);
|
|
590
|
+
}else{
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#getStreamName(){
|
|
596
|
+
if(this.namespace != null){
|
|
597
|
+
return this.namespace + "_stream"
|
|
598
|
+
}else{
|
|
599
|
+
this.close();
|
|
600
|
+
throw new Error("$namespace is null. Cannot initialize program with null $namespace")
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#getStreamTopic(topic){
|
|
605
|
+
if(this.namespace != null){
|
|
606
|
+
return this.namespace + "_stream_" + topic;
|
|
607
|
+
}else{
|
|
608
|
+
this.close();
|
|
609
|
+
throw new Error("$namespace is null. Cannot initialize program with null $namespace")
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#getStreamTopicList(){
|
|
614
|
+
var topics = [];
|
|
615
|
+
|
|
616
|
+
this.#topicMap.forEach((topic) => {
|
|
617
|
+
topics.push(this.#getStreamTopic(topic))
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
return topics
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#getPresenceTopics(){
|
|
624
|
+
var presence = [];
|
|
625
|
+
|
|
626
|
+
this.#topicMap.forEach((topic) => {
|
|
627
|
+
presence.push(this.#getStreamTopic(topic) + "_presence")
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
return presence
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
sleep(ms) {
|
|
634
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
#log(msg){
|
|
638
|
+
if(this.opts?.debug){
|
|
639
|
+
console.log(msg);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#getPublishRetry(){
|
|
644
|
+
this.#log(this.opts)
|
|
645
|
+
if(this.opts !== null && this.opts !== undefined){
|
|
646
|
+
if(this.opts.max_retries !== null && this.opts.max_retries !== undefined){
|
|
647
|
+
if (this.opts.max_retries <= 0){
|
|
648
|
+
return this.#maxPublishRetries;
|
|
649
|
+
}else{
|
|
650
|
+
return this.opts.max_retries;
|
|
651
|
+
}
|
|
652
|
+
}else{
|
|
653
|
+
return this.#maxPublishRetries;
|
|
654
|
+
}
|
|
655
|
+
}else{
|
|
656
|
+
return this.#maxPublishRetries;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
*
|
|
662
|
+
* @param {function} func - Function to execute under retry
|
|
663
|
+
* @param {int} count - Number of times to retry
|
|
664
|
+
* @param {int} delay - Delay between each retry
|
|
665
|
+
* @param {...any} args - Args to pass to func
|
|
666
|
+
* @returns {any} - Output of the func method
|
|
667
|
+
*/
|
|
668
|
+
async #retryTillSuccess(func, count, delay, ...args){
|
|
669
|
+
func = func.bind(this);
|
|
670
|
+
|
|
671
|
+
var output = null;
|
|
672
|
+
var success = false;
|
|
673
|
+
var methodDataOutput = null;
|
|
674
|
+
|
|
675
|
+
for(let i = 1; i <= count; i++){
|
|
676
|
+
this.#log(`Attempt ${i} at executing ${func.name}()`)
|
|
677
|
+
|
|
678
|
+
await this.sleep(delay)
|
|
679
|
+
|
|
680
|
+
output = await func(...args);
|
|
681
|
+
success = output.success;
|
|
682
|
+
// this.#log(output);
|
|
683
|
+
|
|
684
|
+
methodDataOutput = output.output;
|
|
685
|
+
|
|
686
|
+
if (success){
|
|
687
|
+
this.#log(`Successfully called ${func.name}`)
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if(!success){
|
|
693
|
+
this.#log(`${func.name} executed ${count} times BUT not a success`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return methodDataOutput;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#getUserCreds(jwt, secret){
|
|
700
|
+
var template = Buffer.from(this.#config, "base64").toString("utf8")
|
|
701
|
+
|
|
702
|
+
var creds = template.replace("JWT_KEY", jwt);
|
|
703
|
+
creds = creds.replace("SECRET_KEY", secret)
|
|
704
|
+
|
|
705
|
+
return creds
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Exposure for tests
|
|
709
|
+
testRetryTillSuccess(){
|
|
710
|
+
if(process.env.NODE_ENV == "test"){
|
|
711
|
+
return this.#retryTillSuccess.bind(this);
|
|
712
|
+
}else{
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
testGetPublishRetry(){
|
|
718
|
+
if(process.env.NODE_ENV == "test"){
|
|
719
|
+
return this.#getPublishRetry.bind(this);
|
|
720
|
+
}else{
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
testGetStreamName(){
|
|
726
|
+
if(process.env.NODE_ENV == "test"){
|
|
727
|
+
return this.#getStreamName.bind(this);
|
|
728
|
+
}else{
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
testGetStreamTopic(){
|
|
734
|
+
if(process.env.NODE_ENV == "test"){
|
|
735
|
+
return this.#getStreamTopic.bind(this);
|
|
736
|
+
}else{
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
testGetTopicMap(){
|
|
742
|
+
if(process.env.NODE_ENV == "test"){
|
|
743
|
+
return this.#topicMap
|
|
744
|
+
}else{
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
testGetEventMap(){
|
|
750
|
+
if(process.env.NODE_ENV == "test"){
|
|
751
|
+
return this.#event_func
|
|
752
|
+
}else{
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
testGetConsumerMap(){
|
|
758
|
+
if(process.env.NODE_ENV == "test"){
|
|
759
|
+
return this.#consumerMap
|
|
760
|
+
}else{
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export const CONNECTED = "CONNECTED";
|
|
767
|
+
export const RECONNECT = "RECONNECT";
|
|
768
|
+
export const MESSAGE_RESEND = "MESSAGE_RESEND";
|
|
769
|
+
export const DISCONNECTED = "DISCONNECTED";
|