relayx-webjs 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.
@@ -0,0 +1,936 @@
1
+ import { connect, JSONCodec, Events, DebugEvents, AckPolicy, ReplayPolicy, credsAuthenticator } from "nats.ws";
2
+ import { DeliverPolicy, jetstream } from "@nats-io/jetstream";
3
+ import { encode, decode } from "@msgpack/msgpack";
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ export class Realtime {
7
+
8
+ #baseUrl = "";
9
+
10
+ #natsClient = null;
11
+ #codec = JSONCodec();
12
+ #jetstream = null;
13
+ #consumerMap = {};
14
+
15
+ #event_func = {};
16
+ #topicMap = [];
17
+
18
+ // Status Codes
19
+ #RECONNECTING = "RECONNECTING";
20
+ #RECONNECTED = "RECONNECTED";
21
+ #RECONN_FAIL = "RECONN_FAIL";
22
+
23
+ setRemoteUserAttempts = 0;
24
+ setRemoteUserRetries = 5;
25
+
26
+ // Retry attempts end
27
+ reconnected = false;
28
+ disconnected = true;
29
+ reconnecting = false;
30
+ connected = false;
31
+
32
+ // Offline messages
33
+ #offlineMessageBuffer = [];
34
+
35
+ // Latency
36
+ #latency = [];
37
+ #latencyPush = null;
38
+ #isSendingLatency = false;
39
+
40
+ #maxPublishRetries = 5;
41
+
42
+ constructor(config){
43
+ if(typeof config != "object"){
44
+ throw new Error("Realtime($config). $config not object => {}")
45
+ }
46
+
47
+ if(config != null && config != undefined){
48
+ this.api_key = config.api_key != undefined ? config.api_key : null;
49
+ this.secret = config.secret != undefined ? config.secret : null;
50
+
51
+ if(this.api_key == null){
52
+ throw new Error("api_key value null")
53
+ }
54
+
55
+ if(this.secret == null){
56
+ throw new Error("secret value null")
57
+ }
58
+ }else{
59
+ throw new Error("{api_key: <value>, secret: <value>} not passed in constructor")
60
+ }
61
+
62
+ this.namespace = null;
63
+ this.topicHash = null;
64
+ }
65
+
66
+ /*
67
+ Initializes library with configuration options.
68
+ */
69
+ async init(staging, opts){
70
+ /**
71
+ * Method can take in 2 variables
72
+ * @param{boolean} staging - Sets URL to staging or production URL
73
+ * @param{Object} opts - Library configuration options
74
+ */
75
+ var len = arguments.length;
76
+
77
+ if (len > 2){
78
+ new Error("Method takes only 2 variables, " + len + " given");
79
+ }
80
+
81
+ if (len == 2){
82
+ if(typeof arguments[0] == "boolean"){
83
+ staging = arguments[0];
84
+ }else{
85
+ staging = false;
86
+ }
87
+
88
+ if(arguments[1] instanceof Object){
89
+ opts = arguments[1];
90
+ }else{
91
+ opts = {};
92
+ }
93
+ }else if(len == 1){
94
+ if(arguments[0] instanceof Object){
95
+ opts = arguments[0];
96
+ staging = false;
97
+ }else{
98
+ opts = {};
99
+ staging = arguments[0];
100
+ this.#log(staging)
101
+ }
102
+ }else{
103
+ staging = false;
104
+ opts = {};
105
+ }
106
+
107
+ this.staging = staging;
108
+ this.opts = opts;
109
+
110
+ if (staging !== undefined || staging !== null){
111
+ this.#baseUrl = staging ? [
112
+ "nats://0.0.0.0:4421",
113
+ "nats://0.0.0.0:4422",
114
+ "nats://0.0.0.0:4423"
115
+ ] :
116
+ [
117
+ `wss://api.relay-x.io:4421`,
118
+ `wss://api.relay-x.io:4422`,
119
+ `wss://api.relay-x.io:4423`
120
+ ];
121
+ }else{
122
+ this.#baseUrl = [
123
+ `wss://api.relay-x.io:4421`,
124
+ `wss://api.relay-x.io:4422`,
125
+ `wss://api.relay-x.io:4423`
126
+ ];
127
+ }
128
+
129
+ this.#log(this.#baseUrl);
130
+ this.#log(opts);
131
+ }
132
+
133
+ /**
134
+ * Gets the namespace of the user using a micro service
135
+ * @returns {string} namespace value. Null if failed to retreive
136
+ */
137
+ async #getNameSpace() {
138
+ var res = await this.#natsClient.request("accounts.user.get_namespace",
139
+ this.#codec.encode({
140
+ "api_key": this.api_key
141
+ }),
142
+ {
143
+ timeout: 5000
144
+ }
145
+ )
146
+
147
+ var data = res.json()
148
+
149
+ this.#log(data)
150
+
151
+ if(data["status"] == "NAMESPACE_RETRIEVE_SUCCESS"){
152
+ this.namespace = data["data"]["namespace"]
153
+ this.topicHash = data["data"]["hash"]
154
+ }else{
155
+ this.namespace = null;
156
+ this.topicHash = null;
157
+ return
158
+ }
159
+ }
160
+
161
+
162
+ /**
163
+ * Connects to the relay network
164
+ */
165
+ async connect(){
166
+ this.SEVER_URL = this.#baseUrl;
167
+
168
+ var credsFile = this.#getUserCreds(this.api_key, this.secret)
169
+ credsFile = new TextEncoder().encode(credsFile);
170
+ var credsAuth = credsAuthenticator(credsFile);
171
+
172
+ try{
173
+ this.#natsClient = await connect({
174
+ servers: this.SEVER_URL,
175
+ noEcho: true,
176
+ reconnect: true,
177
+ maxReconnectAttempts: 1200,
178
+ reconnectTimeWait: 1000,
179
+ authenticator: credsAuth,
180
+ token: this.api_key,
181
+ });
182
+
183
+ this.#jetstream = await jetstream(this.#natsClient);
184
+
185
+ await this.#getNameSpace()
186
+
187
+ this.connected = true;
188
+ }catch(err){
189
+ this.#log("ERR")
190
+ this.#log(err);
191
+
192
+ this.connected = false;
193
+ }
194
+
195
+ if (this.connected == true){
196
+ this.#log("Connected to server!");
197
+
198
+ // Callback on client side
199
+ if (CONNECTED in this.#event_func){
200
+ if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
201
+ this.#event_func[CONNECTED]()
202
+ }
203
+ }
204
+
205
+ this.#natsClient.closed().then(() => {
206
+ this.#log("the connection closed!");
207
+
208
+ this.#offlineMessageBuffer.length = 0;
209
+
210
+ if (DISCONNECTED in this.#event_func){
211
+ if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
212
+ this.#event_func[DISCONNECTED]()
213
+ }
214
+ }
215
+ });
216
+
217
+ (async () => {
218
+ for await (const s of this.#natsClient.status()) {
219
+ this.#log(s.type)
220
+
221
+ switch (s.type) {
222
+ case Events.Disconnect:
223
+ this.#log(`client disconnected - ${s.data}`);
224
+
225
+ this.connected = false;
226
+ this.#consumerMap = {};
227
+
228
+ if (DISCONNECTED in this.#event_func){
229
+ if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
230
+ this.#event_func[DISCONNECTED]()
231
+ }
232
+ }
233
+ break;
234
+ case Events.LDM:
235
+ this.#log("client has been requested to reconnect");
236
+ break;
237
+ case Events.Update:
238
+ this.#log(`client received a cluster update - `);
239
+ this.#log(s.data)
240
+ break;
241
+ case Events.Reconnect:
242
+ this.#log(`client reconnected -`);
243
+ this.#log(s.data)
244
+
245
+ this.reconnecting = false;
246
+ this.connected = true;
247
+
248
+ if(RECONNECT in this.#event_func){
249
+ this.#event_func[RECONNECT](this.#RECONNECTED);
250
+ }
251
+
252
+ // Resend any messages sent while client was offline
253
+ this.#publishMessagesOnReconnect();
254
+ break;
255
+ case Events.Error:
256
+ this.#log("client got a permissions error");
257
+ break;
258
+ case DebugEvents.Reconnecting:
259
+ this.#log("client is attempting to reconnect");
260
+
261
+ this.reconnecting = true;
262
+
263
+ if(RECONNECT in this.#event_func && this.reconnecting){
264
+ this.#event_func[RECONNECT](this.#RECONNECTING);
265
+ }
266
+ break;
267
+ case DebugEvents.StaleConnection:
268
+ this.#log("client has a stale connection");
269
+ break;
270
+ default:
271
+ this.#log(`got an unknown status ${s.type}`);
272
+ }
273
+ }
274
+ })().then();
275
+
276
+ // Subscribe to topics
277
+ this.#subscribeToTopics();
278
+ this.#log("Subscribed to topics");
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Closes connection
284
+ */
285
+ close(){
286
+ if(this.#natsClient !== null){
287
+ this.reconnected = false;
288
+ this.disconnected = true;
289
+
290
+ this.#offlineMessageBuffer.length = 0;
291
+
292
+ this.#natsClient.close();
293
+ }else{
294
+ this.#log("Null / undefined socket, cannot close connection");
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Start consumers for topics initialized by user
300
+ */
301
+ async #subscribeToTopics(){
302
+ this.#topicMap.forEach(async (topic) => {
303
+ // Subscribe to stream
304
+ await this.#startConsumer(topic);
305
+ });
306
+ }
307
+
308
+ /**
309
+ * Deletes reference to user defined event callback.
310
+ * This will stop listening to a topic
311
+ * @param {string} topic
312
+ * @returns {boolean} - To check if topic unsubscribe was successful
313
+ */
314
+ async off(topic){
315
+ if(topic == null || topic == undefined){
316
+ throw new Error("$topic is null / undefined")
317
+ }
318
+
319
+ if(typeof topic !== "string"){
320
+ throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
321
+ }
322
+
323
+ this.#topicMap = this.#topicMap.filter(item => item !== topic);
324
+
325
+ delete this.#event_func[topic];
326
+
327
+ return await this.#deleteConsumer(topic);
328
+ }
329
+
330
+ /**
331
+ * Subscribes to a topic
332
+ * @param {string} topic - Name of the event
333
+ * @param {function} func - Callback function to call on user thread
334
+ * @returns {boolean} - To check if topic subscription was successful
335
+ */
336
+ async on(topic, func){
337
+ if(topic == null || topic == undefined){
338
+ throw new Error("$topic is null / undefined")
339
+ }
340
+
341
+ if(func == null || func == undefined){
342
+ throw new Error("$func is null / undefined")
343
+ }
344
+
345
+ if ((typeof func !== "function")){
346
+ throw new Error(`Expected $listener type -> function. Instead receieved -> ${typeof func}`);
347
+ }
348
+
349
+ if(typeof topic !== "string"){
350
+ throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
351
+ }
352
+
353
+ if(!(topic in this.#event_func)){
354
+ this.#event_func[topic] = func;
355
+ }else{
356
+ return false
357
+ }
358
+
359
+ if (![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
360
+ this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic)){
361
+ if(!this.isTopicValid(topic)){
362
+ // We have an invalid topic, lets remove it
363
+ if(topic in this.#event_func){
364
+ delete this.#event_func[topic];
365
+ }
366
+
367
+ throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
368
+ }
369
+
370
+ if(!this.#topicMap.includes(topic)){
371
+ this.#topicMap.push(topic);
372
+ }
373
+
374
+ if(this.connected){
375
+ // Connected we need to create a topic in a stream
376
+ await this.#startConsumer(topic);
377
+ }
378
+ }
379
+
380
+ return true;
381
+ }
382
+
383
+ /**
384
+ * A method to send a message to a topic.
385
+ * Retry methods included. Stores messages in an array if offline.
386
+ * @param {string} topic - Name of the event
387
+ * @param {object} data - Data to send
388
+ * @returns
389
+ */
390
+ async publish(topic, data){
391
+ if(topic == null || topic == undefined){
392
+ throw new Error("$topic is null or undefined");
393
+ }
394
+
395
+ if(topic == ""){
396
+ throw new Error("$topic cannot be an empty string")
397
+ }
398
+
399
+ if(typeof topic !== "string"){
400
+ throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
401
+ }
402
+
403
+ if(!this.isTopicValid(topic)){
404
+ throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
405
+ }
406
+
407
+ if(!this.#isMessageValid(data)){
408
+ throw new Error("$message must be JSON, string or number")
409
+ }
410
+
411
+ var start = Date.now()
412
+ var messageId = crypto.randomUUID();
413
+
414
+ var message = {
415
+ "client_id": this.#getClientId(),
416
+ "id": messageId,
417
+ "room": topic,
418
+ "message": data,
419
+ "start": Date.now()
420
+ }
421
+
422
+ this.#log("Encoding message via msg pack...")
423
+ var encodedMessage = encode(message);
424
+
425
+ if(this.connected){
426
+ if(!this.#topicMap.includes(topic)){
427
+ this.#topicMap.push(topic);
428
+ }else{
429
+ this.#log(`${topic} exists locally, moving on...`)
430
+ }
431
+
432
+ this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
433
+
434
+ const ack = await this.#jetstream.publish(this.#getStreamTopic(topic), encodedMessage);
435
+ this.#log(`Publish Ack =>`)
436
+ this.#log(ack)
437
+
438
+ var latency = Date.now() - start;
439
+ this.#log(`Latency => ${latency} ms`);
440
+
441
+ return ack !== null && ack !== undefined;
442
+ }else{
443
+ this.#offlineMessageBuffer.push({
444
+ topic: topic,
445
+ message: data
446
+ });
447
+
448
+ return false;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Starts consumer for particular topic if stream exists
454
+ * @param {string} topic
455
+ */
456
+ async history(topic, start, end){
457
+ this.#log(start)
458
+ if(topic == null || topic == undefined){
459
+ throw new Error("$topic is null or undefined");
460
+ }
461
+
462
+ if(topic == ""){
463
+ throw new Error("$topic cannot be an empty string")
464
+ }
465
+
466
+ if(typeof topic !== "string"){
467
+ throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
468
+ }
469
+
470
+ if(start == undefined || start == null){
471
+ throw new Error(`$start must be provided. $start is => ${start}`)
472
+ }
473
+
474
+ if(!(start instanceof Date)){
475
+ throw new Error(`$start must be a Date object`)
476
+ }
477
+
478
+ if(end != undefined && end != null){
479
+ if(!(end instanceof Date)){
480
+ throw new Error(`$end must be a Date object`)
481
+ }
482
+
483
+ if(start > end){
484
+ throw new Error("$start is greater than $end, must be before $end")
485
+ }
486
+
487
+ end = end.toISOString();
488
+ }
489
+
490
+ var opts = {
491
+ name: `${topic}_${uuidv4()}_history`,
492
+ filter_subjects: [this.#getStreamTopic(topic)],
493
+ replay_policy: ReplayPolicy.Instant,
494
+ opt_start_time: start,
495
+ ack_policy: AckPolicy.Explicit,
496
+ }
497
+
498
+ const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
499
+ this.#log(this.#topicMap)
500
+ this.#log("Consumer is consuming");
501
+
502
+ var history = [];
503
+
504
+ while(true){
505
+ var msg = await consumer.next({
506
+ expires: 1000
507
+ });
508
+
509
+ if(msg == null){
510
+ break;
511
+ }
512
+
513
+ if(end != null || end != undefined){
514
+ if(msg.timestamp > end){
515
+ break
516
+ }
517
+ }
518
+
519
+ this.#log("Decoding msgpack message...")
520
+ var data = decode(msg.data);
521
+ this.#log(data);
522
+
523
+ history.push(data.message);
524
+ }
525
+
526
+ var del = await consumer.delete();
527
+
528
+ this.#log("History pull done: " + del);
529
+
530
+ return history;
531
+ }
532
+
533
+ /**
534
+ * Method resends messages when the client successfully connects to the
535
+ * server again
536
+ * @returns - Array of success and failure messages
537
+ */
538
+ async #publishMessagesOnReconnect(){
539
+ var messageSentStatus = [];
540
+
541
+ for(let i = 0; i < this.#offlineMessageBuffer.length; i++){
542
+ let data = this.#offlineMessageBuffer[i];
543
+
544
+ const topic = data.topic;
545
+ const message = data.message;
546
+
547
+ const output = await this.publish(topic, message);
548
+
549
+ messageSentStatus.push({
550
+ topic: topic,
551
+ message: message,
552
+ resent: output
553
+ });
554
+ }
555
+
556
+ // Clearing out offline messages
557
+ this.#offlineMessageBuffer.length = 0;
558
+
559
+ // Send to client
560
+ if(MESSAGE_RESEND in this.#event_func && messageSentStatus.length > 0){
561
+ this.#event_func[MESSAGE_RESEND](messageSentStatus);
562
+ }
563
+ }
564
+
565
+ // Room functions
566
+ /**
567
+ * Starts consumer for particular topic if stream exists
568
+ * @param {string} topic
569
+ */
570
+ async #startConsumer(topic){
571
+ this.#log(`Starting consumer for topic: ${topic}_${uuidv4()}`)
572
+
573
+ var opts = {
574
+ name: `${topic}_${uuidv4()}`,
575
+ filter_subjects: [this.#getStreamTopic(topic), this.#getStreamTopic(topic) + "_presence"],
576
+ replay_policy: ReplayPolicy.Instant,
577
+ opt_start_time: new Date(),
578
+ ack_policy: AckPolicy.Explicit,
579
+ delivery_policy: DeliverPolicy.New
580
+ }
581
+
582
+ const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
583
+ this.#log(this.#topicMap)
584
+
585
+ this.#consumerMap[topic] = consumer;
586
+
587
+ await consumer.consume({
588
+ callback: async (msg) => {
589
+ try{
590
+ const now = Date.now();
591
+ this.#log("Decoding msgpack message...")
592
+ var data = decode(msg.data);
593
+
594
+ var room = data.room;
595
+
596
+ this.#log(data);
597
+
598
+ // Push topic message to main thread
599
+ if (room in this.#event_func && data.client_id != this.#getClientId()){
600
+ this.#event_func[room]({
601
+ "id": data.id,
602
+ "data": data.message
603
+ });
604
+ }
605
+
606
+ msg.ack();
607
+
608
+ await this.#logLatency(now, data);
609
+ }catch(err){
610
+ this.#log("Consumer err " + err);
611
+ msg.nack(5000);
612
+ }
613
+ }
614
+ });
615
+ this.#log("Consumer is consuming");
616
+ }
617
+
618
+ /**
619
+ * Deletes consumer
620
+ * @param {string} topic
621
+ */
622
+ async #deleteConsumer(topic){
623
+ const consumer = this.#consumerMap[topic]
624
+
625
+ var del = false;
626
+
627
+ if (consumer != null && consumer != undefined){
628
+ del = await consumer.delete();
629
+ }else{
630
+ del = false
631
+ }
632
+
633
+ delete this.#consumerMap[topic];
634
+
635
+ return del;
636
+ }
637
+
638
+ async #logLatency(now, data){
639
+ if(data.client_id == this.#getClientId()){
640
+ this.#log("Skipping latency log for own message");
641
+ return;
642
+ }
643
+
644
+ if(this.#latency.length >= 100){
645
+ this.#log("Latency array is full, skipping log");
646
+ return;
647
+ }
648
+
649
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
650
+
651
+ this.#log(`Timezone: ${timeZone}`);
652
+
653
+ const latency = now - data.start
654
+ this.#log(`Latency => ${latency}`)
655
+
656
+ this.#latency.push({
657
+ latency: latency,
658
+ timestamp: now
659
+ });
660
+
661
+ if(this.#latencyPush == null){
662
+ this.#latencyPush = setTimeout(async () => {
663
+ this.#log("setTimeout called");
664
+
665
+ if(this.#latency.length > 0){
666
+ this.#log("Push from setTimeout")
667
+ await this.#pushLatencyData({
668
+ timezone: timeZone,
669
+ history: this.#latency,
670
+ });
671
+ }else{
672
+ this.#log("No latency data to push");
673
+ }
674
+
675
+ }, 30000);
676
+ }
677
+
678
+ if(this.#latency.length == 100 && !this.#isSendingLatency){
679
+ this.#log("Push from Length Check: " + this.#latency.length);
680
+ await this.#pushLatencyData({
681
+ timezone: timeZone,
682
+ history: this.#latency,
683
+ });
684
+ }
685
+ }
686
+
687
+ // Utility functions
688
+ #getClientId(){
689
+ return this.#natsClient?.info?.client_id
690
+ }
691
+
692
+ async #pushLatencyData(data){
693
+ this.#isSendingLatency = true;
694
+
695
+ try{
696
+ var res = await this.#natsClient.request("accounts.user.log_latency",
697
+ JSONCodec().encode({
698
+ api_key: this.api_key,
699
+ payload: data
700
+ }),
701
+ {
702
+ timeout: 5000
703
+ }
704
+ )
705
+
706
+ var data = res.json()
707
+
708
+ this.#log(data)
709
+ this.#resetLatencyTracker();
710
+ }catch(err){
711
+ this.#log("Error getting pushing latency data")
712
+ this.#log(err);
713
+ }
714
+
715
+ this.#isSendingLatency = false;
716
+ }
717
+
718
+ #resetLatencyTracker(){
719
+ this.#latency = [];
720
+
721
+ if(this.#latencyPush != null){
722
+ clearTimeout(this.#latencyPush);
723
+ this.#latencyPush = null;
724
+ }
725
+ }
726
+
727
+ /**
728
+ * Checks if a topic can be used to send messages to.
729
+ * @param {string} topic - Name of event
730
+ * @returns {boolean} - If topic is valid or not
731
+ */
732
+ isTopicValid(topic){
733
+ if(topic !== null && topic !== undefined && (typeof topic) == "string"){
734
+ var arrayCheck = ![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
735
+ this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic);
736
+
737
+ var spaceStarCheck = !topic.includes(" ") && !topic.includes("*") && !topic.includes(".");
738
+
739
+ return arrayCheck && spaceStarCheck;
740
+ }else{
741
+ return false;
742
+ }
743
+ }
744
+
745
+ #isMessageValid(message){
746
+ if(message == null || message == undefined){
747
+ throw new Error("$message cannot be null / undefined")
748
+ }
749
+
750
+ if(typeof message == "string"){
751
+ return true;
752
+ }
753
+
754
+ if(typeof message == "number"){
755
+ return true;
756
+ }
757
+
758
+ if(this.#isJSON(message)){
759
+ return true;
760
+ }
761
+
762
+ return false;
763
+ }
764
+
765
+ #isJSON(data){
766
+ try{
767
+ JSON.stringify(data?.toString())
768
+ return true;
769
+ }catch(err){
770
+ return false
771
+ }
772
+ }
773
+
774
+ #getStreamName(){
775
+ if(this.namespace != null){
776
+ return this.namespace + "_stream"
777
+ }else{
778
+ this.close();
779
+ throw new Error("$namespace is null. Cannot initialize program with null $namespace")
780
+ }
781
+ }
782
+
783
+ #getStreamTopic(topic){
784
+ if(this.topicHash != null){
785
+ return this.topicHash + "." + topic;
786
+ }else{
787
+ this.close();
788
+ throw new Error("$topicHash is null. Cannot initialize program with null $topicHash")
789
+ }
790
+ }
791
+
792
+ sleep(ms) {
793
+ return new Promise(resolve => setTimeout(resolve, ms));
794
+ }
795
+
796
+ #log(msg){
797
+ if(this.opts?.debug){
798
+ console.log(msg);
799
+ }
800
+ }
801
+
802
+ #getPublishRetry(){
803
+ this.#log(this.opts)
804
+ if(this.opts !== null && this.opts !== undefined){
805
+ if(this.opts.max_retries !== null && this.opts.max_retries !== undefined){
806
+ if (this.opts.max_retries <= 0){
807
+ return this.#maxPublishRetries;
808
+ }else{
809
+ return this.opts.max_retries;
810
+ }
811
+ }else{
812
+ return this.#maxPublishRetries;
813
+ }
814
+ }else{
815
+ return this.#maxPublishRetries;
816
+ }
817
+ }
818
+
819
+ /**
820
+ *
821
+ * @param {function} func - Function to execute under retry
822
+ * @param {int} count - Number of times to retry
823
+ * @param {int} delay - Delay between each retry
824
+ * @param {...any} args - Args to pass to func
825
+ * @returns {any} - Output of the func method
826
+ */
827
+ async #retryTillSuccess(func, count, delay, ...args){
828
+ func = func.bind(this);
829
+
830
+ var output = null;
831
+ var success = false;
832
+ var methodDataOutput = null;
833
+
834
+ for(let i = 1; i <= count; i++){
835
+ this.#log(`Attempt ${i} at executing ${func.name}()`)
836
+
837
+ await this.sleep(delay)
838
+
839
+ output = await func(...args);
840
+ success = output.success;
841
+
842
+ methodDataOutput = output.output;
843
+
844
+ if (success){
845
+ this.#log(`Successfully called ${func.name}`)
846
+ break;
847
+ }
848
+ }
849
+
850
+ if(!success){
851
+ this.#log(`${func.name} executed ${count} times BUT not a success`);
852
+ }
853
+
854
+ return methodDataOutput;
855
+ }
856
+
857
+ #getUserCreds(jwt, secret){
858
+ return `
859
+ -----BEGIN NATS USER JWT-----
860
+ ${jwt}
861
+ ------END NATS USER JWT------
862
+
863
+ ************************* IMPORTANT *************************
864
+ NKEY Seed printed below can be used to sign and prove identity.
865
+ NKEYs are sensitive and should be treated as secrets.
866
+
867
+ -----BEGIN USER NKEY SEED-----
868
+ ${secret}
869
+ ------END USER NKEY SEED------
870
+
871
+ *************************************************************`
872
+ }
873
+
874
+ // Exposure for tests
875
+ testRetryTillSuccess(){
876
+ if(process.env.NODE_ENV == "test"){
877
+ return this.#retryTillSuccess.bind(this);
878
+ }else{
879
+ return null;
880
+ }
881
+ }
882
+
883
+ testGetPublishRetry(){
884
+ if(process.env.NODE_ENV == "test"){
885
+ return this.#getPublishRetry.bind(this);
886
+ }else{
887
+ return null;
888
+ }
889
+ }
890
+
891
+ testGetStreamName(){
892
+ if(process.env.NODE_ENV == "test"){
893
+ return this.#getStreamName.bind(this);
894
+ }else{
895
+ return null;
896
+ }
897
+ }
898
+
899
+ testGetStreamTopic(){
900
+ if(process.env.NODE_ENV == "test"){
901
+ return this.#getStreamTopic.bind(this);
902
+ }else{
903
+ return null;
904
+ }
905
+ }
906
+
907
+ testGetTopicMap(){
908
+ if(process.env.NODE_ENV == "test"){
909
+ return this.#topicMap
910
+ }else{
911
+ return null;
912
+ }
913
+ }
914
+
915
+ testGetEventMap(){
916
+ if(process.env.NODE_ENV == "test"){
917
+ return this.#event_func
918
+ }else{
919
+ return null;
920
+ }
921
+ }
922
+
923
+ testGetConsumerMap(){
924
+ if(process.env.NODE_ENV == "test"){
925
+ return this.#consumerMap
926
+ }else{
927
+ return null;
928
+ }
929
+ }
930
+ }
931
+
932
+ export const CONNECTED = "CONNECTED";
933
+ export const RECONNECT = "RECONNECT";
934
+ export const MESSAGE_RESEND = "MESSAGE_RESEND";
935
+ export const DISCONNECTED = "DISCONNECTED";
936
+ export const SERVER_DISCONNECT = "SERVER_DISCONNECT";