relayx-webjs 1.0.4 → 1.1.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.
@@ -2,6 +2,9 @@ import { connect, JSONCodec, Events, DebugEvents, AckPolicy, ReplayPolicy, creds
2
2
  import { DeliverPolicy, jetstream } from "@nats-io/jetstream";
3
3
  import { encode, decode } from "@msgpack/msgpack";
4
4
  import { v4 as uuidv4 } from 'uuid';
5
+ import { Queue } from "./queue.js";
6
+ import { ErrorLogging } from "./utils.js";
7
+ import { KVStore } from "./kv_storage.js";
5
8
 
6
9
  export class Realtime {
7
10
 
@@ -13,6 +16,10 @@ export class Realtime {
13
16
  #consumerMap = {};
14
17
  #consumer = null;
15
18
 
19
+ #kvStore = null;
20
+
21
+ #errorLogging = null;
22
+
16
23
  #event_func = {};
17
24
  #topicMap = [];
18
25
 
@@ -71,75 +78,25 @@ export class Realtime {
71
78
  /*
72
79
  Initializes library with configuration options.
73
80
  */
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 if(typeof arguments[0] == "boolean"){
103
- opts = {};
104
- staging = arguments[0];
105
- this.#log(staging)
106
- }else{
107
- opts = {};
108
- staging = false
109
- }
110
- }else{
111
- staging = false;
112
- opts = {};
113
- }
114
-
115
- this.staging = staging;
116
- this.opts = opts;
117
-
118
- if(process.env.PROXY){
119
- this.#baseUrl = ["wss://api2.relay-x.io:8666"];
120
- }else{
121
- if (staging !== undefined || staging !== null){
122
- this.#baseUrl = staging ? [
123
- "nats://0.0.0.0:4421",
124
- "nats://0.0.0.0:4422",
125
- "nats://0.0.0.0:4423"
126
- ] :
127
- [
128
- `wss://api.relay-x.io:4421`,
129
- `wss://api.relay-x.io:4422`,
130
- `wss://api.relay-x.io:4423`
131
- ];
132
- }else{
133
- this.#baseUrl = [
134
- `wss://api.relay-x.io:4421`,
135
- `wss://api.relay-x.io:4422`,
136
- `wss://api.relay-x.io:4423`
137
- ];
138
- }
139
- }
81
+ async init(data){
82
+ this.#errorLogging = new ErrorLogging();
83
+
84
+ this.staging = this.#checkVarOk(data.staging) && typeof data.staging == "boolean" ? data.staging : false;
85
+ this.opts = data.opts;
86
+
87
+ this.#baseUrl = this.staging ? [
88
+ "nats://0.0.0.0:4421",
89
+ "nats://0.0.0.0:4422",
90
+ "nats://0.0.0.0:4423"
91
+ ] :
92
+ [
93
+ `wss://api.relay-x.io:4421`,
94
+ `wss://api.relay-x.io:4422`,
95
+ `wss://api.relay-x.io:4423`
96
+ ];
140
97
 
141
98
  this.#log(this.#baseUrl);
142
- this.#log(opts);
99
+ this.#log(this.opts);
143
100
  }
144
101
 
145
102
  /**
@@ -203,8 +160,16 @@ export class Realtime {
203
160
  this.connected = true;
204
161
  this.#connectCalled = true;
205
162
  }catch(err){
206
- this.#log("ERR")
207
- this.#log(err);
163
+ this.#errorLogging.logError({
164
+ err: err
165
+ })
166
+
167
+ // Callback on client side
168
+ if (CONNECTED in this.#event_func){
169
+ if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
170
+ this.#event_func[CONNECTED](false)
171
+ }
172
+ }
208
173
 
209
174
  this.connected = false;
210
175
  }
@@ -259,7 +224,11 @@ export class Realtime {
259
224
  this.#publishMessagesOnReconnect();
260
225
  break;
261
226
  case Events.Error:
262
- this.#log("client got a permissions error");
227
+ if(s.data == "NATS_PROTOCOL_ERR"){
228
+ console.log("User kicked off network by account admin!")
229
+
230
+ await this.#natsClient.close();
231
+ }
263
232
  break;
264
233
  case DebugEvents.Reconnecting:
265
234
  this.#log("client is attempting to reconnect");
@@ -274,8 +243,6 @@ export class Realtime {
274
243
  case DebugEvents.StaleConnection:
275
244
  this.#log("client has a stale connection");
276
245
  break;
277
- default:
278
- this.#log(`got an unknown status ${s.type}`);
279
246
  }
280
247
  }
281
248
  })().then();
@@ -287,7 +254,7 @@ export class Realtime {
287
254
  // Callback on client side
288
255
  if (CONNECTED in this.#event_func){
289
256
  if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
290
- this.#event_func[CONNECTED]()
257
+ this.#event_func[CONNECTED](true)
291
258
  }
292
259
  }
293
260
  }
@@ -318,7 +285,15 @@ export class Realtime {
318
285
  async #subscribeToTopics(){
319
286
  this.#topicMap.forEach(async (topic) => {
320
287
  // Subscribe to stream
321
- await this.#startConsumer(topic);
288
+ try{
289
+ await this.#startConsumer(topic);
290
+ }catch(err){
291
+ this.#errorLogging.logError({
292
+ err: err,
293
+ topic: topic,
294
+ op: "subscribe"
295
+ })
296
+ }
322
297
  });
323
298
  }
324
299
 
@@ -393,7 +368,15 @@ export class Realtime {
393
368
 
394
369
  if(this.connected){
395
370
  // Connected we need to create a topic in a stream
396
- await this.#startConsumer(topic);
371
+ try{
372
+ await this.#startConsumer(topic);
373
+ }catch(err){
374
+ this.#errorLogging.logError({
375
+ err: err,
376
+ topic: topic,
377
+ op: "subscribe"
378
+ })
379
+ }
397
380
  }
398
381
  }
399
382
 
@@ -445,12 +428,22 @@ export class Realtime {
445
428
 
446
429
  this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
447
430
 
448
- const ack = await this.#jetstream.publish(this.#getStreamTopic(topic), encodedMessage);
449
- this.#log(`Publish Ack =>`)
450
- this.#log(ack)
451
-
452
- var latency = Date.now() - start;
453
- this.#log(`Latency => ${latency} ms`);
431
+ var ack = null;
432
+
433
+ try{
434
+ 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
+ }catch(err){
441
+ this.#errorLogging.logError({
442
+ err: err,
443
+ topic: topic,
444
+ op: "publish"
445
+ })
446
+ }
454
447
 
455
448
  return ack !== null && ack !== undefined;
456
449
  }else{
@@ -557,6 +550,51 @@ export class Realtime {
557
550
  return history;
558
551
  }
559
552
 
553
+ // Queue Functions
554
+ async initQueue(queueID){
555
+ if(!this.connected){
556
+ this.#log("Not connected to relayX network. Skipping queue init")
557
+
558
+ return null;
559
+ }
560
+
561
+ this.#log("Validating queue ID...")
562
+ if(queueID == undefined || queueID == null || queueID == ""){
563
+ throw new Error("$queueID cannot be null / undefined / empty!")
564
+ }
565
+
566
+ var queue = new Queue({
567
+ jetstream: this.#jetstream,
568
+ nats_client: this.#natsClient,
569
+ api_key: this.api_key,
570
+ debug: this.opts?.debug ? this.opts?.debug : false
571
+ });
572
+
573
+ var initResult = await queue.init(queueID);
574
+
575
+ return initResult ? queue : null;
576
+ }
577
+
578
+ // KV Functions
579
+ async initKVStore(){
580
+
581
+ if(this.#kvStore == null){
582
+ var debugCheck = this.opts.debug !== null && this.opts.debug !== undefined && typeof this.opts.debug == "boolean"
583
+
584
+ this.#kvStore = new KVStore({
585
+ namespace: this.namespace,
586
+ jetstream: this.#jetstream,
587
+ debug: debugCheck ? this.opts.debug : false
588
+ })
589
+
590
+ var init = await this.#kvStore.init()
591
+
592
+ return init ? this.#kvStore : null;
593
+ }else{
594
+ return this.#kvStore
595
+ }
596
+ }
597
+
560
598
  /**
561
599
  * Method resends messages when the client successfully connects to the
562
600
  * server again
@@ -599,7 +637,7 @@ export class Realtime {
599
637
 
600
638
  var opts = {
601
639
  name: consumerName,
602
- filter_subjects: [this.#getStreamTopic(topic)],
640
+ filter_subjects: this.#getStreamTopic(topic),
603
641
  replay_policy: ReplayPolicy.Instant,
604
642
  opt_start_time: new Date(),
605
643
  ack_policy: AckPolicy.Explicit,
@@ -607,7 +645,6 @@ export class Realtime {
607
645
  }
608
646
 
609
647
  const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
610
- this.#log(this.#topicMap)
611
648
 
612
649
  this.#consumerMap[topic] = consumer;
613
650
 
@@ -645,7 +682,7 @@ export class Realtime {
645
682
  }
646
683
  }
647
684
  });
648
- this.#log("Consumer is consuming");
685
+ this.#log(`Consumer is consuming for => ${topic}`);
649
686
  }
650
687
 
651
688
  /**
@@ -718,6 +755,10 @@ export class Realtime {
718
755
  return this.#natsClient?.info?.client_id
719
756
  }
720
757
 
758
+ #checkVarOk(variable){
759
+ return variable !== null && variable !== undefined
760
+ }
761
+
721
762
  async #pushLatencyData(data){
722
763
  this.#isSendingLatency = true;
723
764
 
@@ -857,20 +898,7 @@ export class Realtime {
857
898
  while (i < a.length || j < b.length) {
858
899
  const tokA = a[i];
859
900
  const tokB = b[j];
860
-
861
- /*──────────── literal match or single‑token wildcard on either side ────────────*/
862
- const singleWildcard =
863
- (tokA === "*" && j < b.length) ||
864
- (tokB === "*" && i < a.length);
865
-
866
- if (
867
- (tokA !== undefined && tokA === tokB) ||
868
- singleWildcard
869
- ) {
870
- i++; j++;
871
- continue;
872
- }
873
-
901
+
874
902
  /*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
875
903
  if (tokA === ">") {
876
904
  if (i !== a.length - 1) return false; // '>' not in last position → invalid
@@ -887,6 +915,19 @@ export class Realtime {
887
915
  continue;
888
916
  }
889
917
 
918
+ /*──────────── literal match or single‑token wildcard on either side ────────────*/
919
+ const singleWildcard =
920
+ (tokA === "*" && j < b.length) ||
921
+ (tokB === "*" && i < a.length);
922
+
923
+ if (
924
+ (tokA !== undefined && tokA === tokB) ||
925
+ singleWildcard
926
+ ) {
927
+ i++; j++;
928
+ continue;
929
+ }
930
+
890
931
  /*───────────────────────────── back‑track using last '>' ───────────────────────*/
891
932
  if (starAi !== -1) { // let patternA's '>' absorb one more token of B
892
933
  j = ++starAj;
@@ -1050,6 +1091,14 @@ ${secret}
1050
1091
  return null;
1051
1092
  }
1052
1093
  }
1094
+
1095
+ testGetJetstream(){
1096
+ if(process.env.NODE_ENV == "test"){
1097
+ return this.#jetstream;
1098
+ }else{
1099
+ return null;
1100
+ }
1101
+ }
1053
1102
  }
1054
1103
 
1055
1104
  export const CONNECTED = "CONNECTED";
@@ -0,0 +1,113 @@
1
+ import { JetStreamApiError, JetStreamError } from "@nats-io/jetstream";
2
+
3
+ export class ErrorLogging {
4
+
5
+ logError(data){
6
+ var err = data.err;
7
+
8
+ if(err instanceof JetStreamApiError){
9
+ var code = err.code;
10
+
11
+ if(code == 10077){
12
+ // Code 10077 is for message limit exceeded
13
+ console.table({
14
+ Event: "Message Limit Exceeded",
15
+ Description: "Current message count for account exceeds plan defined limits. Upgrade plan to remove limits",
16
+ Link: "https://console.relay-x.io/billing"
17
+ })
18
+
19
+ throw new Error("Message limit exceeded!")
20
+ }
21
+ }
22
+
23
+ if(err instanceof JetStreamError){
24
+ var code = err.code;
25
+
26
+ if(code == 409){
27
+ // Consumer deleted
28
+
29
+ console.table({
30
+ Event: "Consumer Manually Deleted!",
31
+ Description: "Consumer was manually deleted by user using deleteConsumer() or the library equivalent",
32
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/detailed_doc/NodeJS/queue_consume#deleting-a-consumer"
33
+ })
34
+ }
35
+ }
36
+
37
+ if(err.name == "NatsError"){
38
+ var code = err.code;
39
+ var chainedError = err.chainedError;
40
+ var permissionContext = err.permissionContext;
41
+ var userOp = data.op;
42
+
43
+ if(code == "PERMISSIONS_VIOLATION"){
44
+ if(userOp == "publish"){
45
+ console.table({
46
+ Event: "Publish Permissions Violation",
47
+ Description: `User is not permitted to publish on '${data.topic}'`,
48
+ Topic: data.topic,
49
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#messaging--publish-permissions"
50
+ })
51
+
52
+ throw new Error(`User is not permitted to publish on '${data.topic}'`)
53
+ }else if(userOp == "subscribe"){
54
+ console.table({
55
+ Event: "Subscribe Permissions Violation",
56
+ Description: `User is not permitted to subscribe to '${data.topic}'`,
57
+ Topic: data.topic,
58
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#messaging--subscribe-permissions"
59
+ })
60
+
61
+ throw new Error(`User is not permitted to subscribe to '${data.topic}'`)
62
+ }else if(userOp == "kv_write"){
63
+ console.table({
64
+ Event: "KV Write Failure",
65
+ Description: `User is not permitted to write to KV Store`,
66
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#write-permission"
67
+ })
68
+
69
+ throw new Error(`User is not permitted to write to KV Store`)
70
+ }else if(userOp == "kv_read"){
71
+ console.table({
72
+ Event: "KV Read Failure",
73
+ Description: `User is not permitted to read from KV Store`,
74
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#read-permission"
75
+ })
76
+
77
+ throw new Error(`User is not permitted to read from KV Store`)
78
+ }else if(userOp == "kv_delete"){
79
+ console.table({
80
+ Event: "KV Key Delete Failure",
81
+ Description: `User is not permitted to delete key from KV Store`,
82
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#write-permission"
83
+ })
84
+
85
+ throw new Error(`User is not permitted to delete key from KV Store`)
86
+ }
87
+ }else if(code == "AUTHORIZATION_VIOLATION"){
88
+ console.table({
89
+ Event: "Authentication Failure",
90
+ Description: `User failed to authenticate. Check if API key exists & if it is enabled`,
91
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#enabling-and-disabling-keys"
92
+ })
93
+ }
94
+ }
95
+ }
96
+
97
+ }
98
+
99
+ export class Logging {
100
+
101
+ #debug = false;
102
+
103
+ constructor(debug){
104
+ this.#debug = debug !== null && debug !== undefined && typeof debug == "boolean" ? debug : false;
105
+ }
106
+
107
+ log(...msg){
108
+ if(this.#debug){
109
+ console.log(...msg)
110
+ }
111
+ }
112
+
113
+ }