relayx-js 1.0.18 → 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.
@@ -1,8 +1,11 @@
1
1
  import { connect, JSONCodec, Events, DebugEvents, AckPolicy, ReplayPolicy, credsAuthenticator } from "nats";
2
- import { DeliverPolicy, jetstream } from "@nats-io/jetstream";
2
+ import { DeliverPolicy, jetstream, jetstreamManager } from "@nats-io/jetstream";
3
3
  import { encode, decode } from "@msgpack/msgpack";
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
  import { initDNSSpoof } from "./dns_change.js";
6
+ import { Queue } from "./queue.js";
7
+ import { ErrorLogging } from "./utils.js";
8
+ import { KVStore } from "./kv_storage.js";
6
9
 
7
10
  export class Realtime {
8
11
 
@@ -11,11 +14,16 @@ export class Realtime {
11
14
  #natsClient = null;
12
15
  #codec = JSONCodec();
13
16
  #jetstream = null;
17
+ #jetStreamManager = null;
14
18
  #consumerMap = {};
15
19
 
20
+ #kvStore = null;
21
+
16
22
  #event_func = {};
17
23
  #topicMap = [];
18
24
 
25
+ #errorLogging = null;
26
+
19
27
  // Status Codes
20
28
  #RECONNECTING = "RECONNECTING";
21
29
  #RECONNECTED = "RECONNECTED";
@@ -71,56 +79,17 @@ export class Realtime {
71
79
  /*
72
80
  Initializes library with configuration options.
73
81
  */
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
- }
82
+ async init(data){
83
+ this.#errorLogging = new ErrorLogging();
85
84
 
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;
85
+ this.staging = this.#checkVarOk(data.staging) && typeof data.staging == "boolean" ? data.staging : false;
86
+ this.opts = data.opts;
117
87
 
118
88
  if(process.env.PROXY){
119
89
  this.#baseUrl = ["tls://api2.relay-x.io:8666"];
120
90
  initDNSSpoof();
121
91
  }else{
122
- if (staging !== undefined || staging !== null){
123
- this.#baseUrl = staging ? [
92
+ this.#baseUrl = this.staging ? [
124
93
  "nats://0.0.0.0:4221",
125
94
  "nats://0.0.0.0:4222",
126
95
  "nats://0.0.0.0:4223",
@@ -130,17 +99,10 @@ export class Realtime {
130
99
  `tls://api.relay-x.io:4222`,
131
100
  `tls://api.relay-x.io:4223`
132
101
  ];
133
- }else{
134
- this.#baseUrl = [
135
- `tls://api.relay-x.io:4221`,
136
- `tls://api.relay-x.io:4222`,
137
- `tls://api.relay-x.io:4223`
138
- ];
139
- }
140
102
  }
141
103
 
142
104
  this.#log(this.#baseUrl);
143
- this.#log(opts);
105
+ this.#log(this.opts);
144
106
  }
145
107
 
146
108
  /**
@@ -199,14 +161,23 @@ export class Realtime {
199
161
  });
200
162
 
201
163
  this.#jetstream = await jetstream(this.#natsClient);
164
+ this.#jetStreamManager = await jetstreamManager(this.#natsClient)
202
165
 
203
166
  await this.#getNameSpace()
204
167
 
205
168
  this.connected = true;
206
169
  this.#connectCalled = true;
207
170
  }catch(err){
208
- this.#log("ERR")
209
- this.#log(err);
171
+ this.#errorLogging.logError({
172
+ err: err
173
+ })
174
+
175
+ // Callback on client side
176
+ if (CONNECTED in this.#event_func){
177
+ if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
178
+ this.#event_func[CONNECTED](false)
179
+ }
180
+ }
210
181
 
211
182
  this.connected = false;
212
183
  }
@@ -231,54 +202,56 @@ export class Realtime {
231
202
 
232
203
  (async () => {
233
204
  for await (const s of this.#natsClient.status()) {
234
- this.#log(s.type)
235
-
236
- switch (s.type) {
237
- case Events.Disconnect:
238
- this.#log(`client disconnected - ${s.data}`);
239
-
240
- this.connected = false;
241
- break;
242
- case Events.LDM:
243
- this.#log("client has been requested to reconnect");
244
- break;
245
- case Events.Update:
246
- this.#log(`client received a cluster update - `);
247
- this.#log(s.data)
248
- break;
249
- case Events.Reconnect:
250
- this.#log(`client reconnected -`);
251
- this.#log(s.data)
252
-
253
- this.reconnecting = false;
254
- this.connected = true;
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
- this.connected = false;
271
-
272
- if(RECONNECT in this.#event_func && this.reconnecting){
273
- this.#event_func[RECONNECT](this.#RECONNECTING);
274
- }
275
- break;
276
- case DebugEvents.StaleConnection:
277
- this.#log("client has a stale connection");
278
- break;
279
- default:
280
- this.#log(`got an unknown status ${s.type}`);
281
- }
205
+ switch (s.type) {
206
+ case Events.Disconnect:
207
+ this.#log(`client disconnected - ${s.data}`);
208
+
209
+ this.connected = false;
210
+ break;
211
+ case Events.LDM:
212
+ this.#log("client has been requested to reconnect");
213
+ break;
214
+ case Events.Update:
215
+ this.#log(`client received a cluster update - `);
216
+ this.#log(s.data)
217
+ break;
218
+ case Events.Reconnect:
219
+ this.#log(`client reconnected -`);
220
+ this.#log(s.data)
221
+
222
+ this.reconnecting = false;
223
+ this.connected = true;
224
+
225
+ if(RECONNECT in this.#event_func){
226
+ this.#event_func[RECONNECT](this.#RECONNECTED);
227
+ }
228
+
229
+ // Resend any messages sent while client was offline
230
+ this.#publishMessagesOnReconnect();
231
+ break;
232
+ case Events.Error:
233
+ if(s.data == "NATS_PROTOCOL_ERR"){
234
+ console.log("User kicked off network by account admin!")
235
+
236
+ await this.#natsClient.close();
237
+ }
238
+ break;
239
+ case DebugEvents.Reconnecting:
240
+ this.#log("client is attempting to reconnect");
241
+
242
+ this.reconnecting = true;
243
+ this.connected = false;
244
+
245
+ if(RECONNECT in this.#event_func && this.reconnecting){
246
+ this.#event_func[RECONNECT](this.#RECONNECTING);
247
+ }
248
+ break;
249
+ case DebugEvents.StaleConnection:
250
+ this.#log("client has a stale connection");
251
+ break;
252
+ // default:
253
+ // this.#log(`got an unknown status ${s.type}`);
254
+ }
282
255
  }
283
256
  })().then();
284
257
 
@@ -289,7 +262,7 @@ export class Realtime {
289
262
  // Callback on client side
290
263
  if (CONNECTED in this.#event_func){
291
264
  if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
292
- this.#event_func[CONNECTED]()
265
+ this.#event_func[CONNECTED](true)
293
266
  }
294
267
  }
295
268
  }
@@ -320,7 +293,15 @@ export class Realtime {
320
293
  async #subscribeToTopics(){
321
294
  this.#topicMap.forEach(async (topic) => {
322
295
  // Subscribe to stream
323
- await this.#startConsumer(topic);
296
+ try{
297
+ await this.#startConsumer(topic);
298
+ }catch(err){
299
+ this.#errorLogging.logError({
300
+ err: err,
301
+ topic: topic,
302
+ op: "subscribe"
303
+ })
304
+ }
324
305
  });
325
306
  }
326
307
 
@@ -395,7 +376,15 @@ export class Realtime {
395
376
 
396
377
  if(this.connected){
397
378
  // Connected we need to create a topic in a stream
398
- await this.#startConsumer(topic);
379
+ try{
380
+ await this.#startConsumer(topic);
381
+ }catch(err){
382
+ this.#errorLogging.logError({
383
+ err: err,
384
+ topic: topic,
385
+ op: "subscribe"
386
+ })
387
+ }
399
388
  }
400
389
  }
401
390
 
@@ -434,7 +423,6 @@ export class Realtime {
434
423
  var messageId = crypto.randomUUID();
435
424
 
436
425
  var message = {
437
- "client_id": this.#getClientId(),
438
426
  "id": messageId,
439
427
  "room": topic,
440
428
  "message": data,
@@ -447,12 +435,22 @@ export class Realtime {
447
435
 
448
436
  this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
449
437
 
450
- const ack = await this.#jetstream.publish(this.#getStreamTopic(topic), encodedMessage);
451
- this.#log(`Publish Ack =>`)
452
- this.#log(ack)
453
-
454
- var latency = Date.now() - start;
455
- this.#log(`Latency => ${latency} ms`);
438
+ var ack = null;
439
+
440
+ try{
441
+ ack = await this.#jetstream.publish(this.#getStreamTopic(topic), encodedMessage);
442
+ this.#log(`Publish Ack =>`)
443
+ this.#log(ack)
444
+
445
+ var latency = Date.now() - start;
446
+ this.#log(`Latency => ${latency} ms`);
447
+ }catch(err){
448
+ this.#errorLogging.logError({
449
+ err: err,
450
+ topic: topic,
451
+ op: "publish"
452
+ })
453
+ }
456
454
 
457
455
  return ack !== null && ack !== undefined;
458
456
  }else{
@@ -511,7 +509,7 @@ export class Realtime {
511
509
  }
512
510
 
513
511
  var opts = {
514
- name: `${topic}_${uuidv4()}_history`,
512
+ name: `nodejs_${topic}_${uuidv4()}_history_consumer`,
515
513
  filter_subjects: [this.#getStreamTopic(topic)],
516
514
  replay_policy: ReplayPolicy.Instant,
517
515
  opt_start_time: start,
@@ -597,11 +595,11 @@ export class Realtime {
597
595
  * @param {string} topic
598
596
  */
599
597
  async #startConsumer(topic){
600
- const consumerName = `${topic}_${uuidv4()}_consumer`;
598
+ const consumerName = `nodejs_${uuidv4()}_consumer`;
601
599
 
602
600
  var opts = {
603
601
  name: consumerName,
604
- filter_subjects: [this.#getStreamTopic(topic)],
602
+ filter_subjects: this.#getStreamTopic(topic),
605
603
  replay_policy: ReplayPolicy.Instant,
606
604
  opt_start_time: new Date(),
607
605
  ack_policy: AckPolicy.Explicit,
@@ -609,7 +607,6 @@ export class Realtime {
609
607
  }
610
608
 
611
609
  const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
612
- this.#log(this.#topicMap)
613
610
 
614
611
  this.#consumerMap[topic] = consumer;
615
612
 
@@ -626,16 +623,14 @@ export class Realtime {
626
623
  this.#log(data);
627
624
 
628
625
  // Push topic message to main thread
629
- if (data.client_id != this.#getClientId()){
630
- var topicMatch = this.#topicPatternMatcher(topic, msgTopic)
631
-
632
- if(topicMatch){
633
- this.#event_func[topic]({
634
- "id": data.id,
635
- "topic": msgTopic,
636
- "data": data.message
637
- });
638
- }
626
+ var topicMatch = this.#topicPatternMatcher(topic, msgTopic)
627
+
628
+ if(topicMatch){
629
+ this.#event_func[topic]({
630
+ "id": data.id,
631
+ "topic": msgTopic,
632
+ "data": data.message
633
+ });
639
634
  }
640
635
 
641
636
  msg.ack();
@@ -647,7 +642,8 @@ export class Realtime {
647
642
  }
648
643
  }
649
644
  });
650
- this.#log("Consumer is consuming");
645
+
646
+ this.#log("Consumer is consuming => " + topic);
651
647
  }
652
648
 
653
649
  async #deleteConsumer(topic){
@@ -711,6 +707,51 @@ export class Realtime {
711
707
  }
712
708
  }
713
709
 
710
+ // Queue Functions
711
+ async initQueue(queueID){
712
+ if(!this.connected){
713
+ this.#log("Not connected to relayX network. Skipping queue init")
714
+
715
+ return;
716
+ }
717
+
718
+ this.#log("Validating queue ID...")
719
+ if(queueID == undefined || queueID == null || queueID == ""){
720
+ throw new Error("$queueID cannot be null / undefined / empty!")
721
+ }
722
+
723
+ var queue = new Queue({
724
+ jetstream: this.#jetstream,
725
+ nats_client: this.#natsClient,
726
+ api_key: this.api_key,
727
+ debug: this.opts?.debug ? this.opts?.debug : false
728
+ });
729
+
730
+ var initResult = await queue.init(queueID);
731
+
732
+ return initResult ? queue : null;
733
+ }
734
+
735
+ // KV Functions
736
+ async initKVStore(){
737
+
738
+ if(this.#kvStore == null){
739
+ var debugCheck = this.opts.debug !== null && this.opts.debug !== undefined && typeof this.opts.debug == "boolean"
740
+
741
+ this.#kvStore = new KVStore({
742
+ namespace: this.namespace,
743
+ jetstream: this.#jetstream,
744
+ debug: debugCheck ? this.opts.debug : false
745
+ })
746
+
747
+ var init = await this.#kvStore.init()
748
+
749
+ return init ? this.#kvStore : null;
750
+ }else{
751
+ return this.#kvStore
752
+ }
753
+ }
754
+
714
755
  // Utility functions
715
756
  #getClientId(){
716
757
  return this.#natsClient?.info?.client_id
@@ -801,7 +842,7 @@ export class Realtime {
801
842
 
802
843
  #getStreamName(){
803
844
  if(this.namespace != null){
804
- return this.namespace + "_stream"
845
+ return `${this.namespace}_stream`
805
846
  }else{
806
847
  this.close();
807
848
  throw new Error("$namespace is null. Cannot initialize program with null $namespace")
@@ -856,19 +897,6 @@ export class Realtime {
856
897
  const tokA = a[i];
857
898
  const tokB = b[j];
858
899
 
859
- /*──────────── literal match or single‑token wildcard on either side ────────────*/
860
- const singleWildcard =
861
- (tokA === "*" && j < b.length) ||
862
- (tokB === "*" && i < a.length);
863
-
864
- if (
865
- (tokA !== undefined && tokA === tokB) ||
866
- singleWildcard
867
- ) {
868
- i++; j++;
869
- continue;
870
- }
871
-
872
900
  /*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
873
901
  if (tokA === ">") {
874
902
  if (i !== a.length - 1) return false; // '>' not in last position → invalid
@@ -885,6 +913,19 @@ export class Realtime {
885
913
  continue;
886
914
  }
887
915
 
916
+ /*──────────── literal match or single‑token wildcard on either side ────────────*/
917
+ const singleWildcard =
918
+ (tokA === "*" && j < b.length) ||
919
+ (tokB === "*" && i < a.length);
920
+
921
+ if (
922
+ (tokA !== undefined && tokA === tokB) ||
923
+ singleWildcard
924
+ ) {
925
+ i++; j++;
926
+ continue;
927
+ }
928
+
888
929
  /*───────────────────────────── back‑track using last '>' ───────────────────────*/
889
930
  if (starAi !== -1) { // let patternA's '>' absorb one more token of B
890
931
  j = ++starAj;
@@ -899,7 +940,7 @@ export class Realtime {
899
940
  return false;
900
941
  }
901
942
 
902
- return true;
943
+ return true;
903
944
  }
904
945
 
905
946
  sleep(ms) {
@@ -984,6 +1025,10 @@ ${secret}
984
1025
  *************************************************************`
985
1026
  }
986
1027
 
1028
+ #checkVarOk(variable){
1029
+ return variable !== null && variable !== undefined
1030
+ }
1031
+
987
1032
  // Exposure for tests
988
1033
  testRetryTillSuccess(){
989
1034
  if(process.env.NODE_ENV == "test"){
@@ -1048,6 +1093,14 @@ ${secret}
1048
1093
  return null;
1049
1094
  }
1050
1095
  }
1096
+
1097
+ testGetJetstream(){
1098
+ if(process.env.NODE_ENV == "test"){
1099
+ return this.#jetstream;
1100
+ }else{
1101
+ return null;
1102
+ }
1103
+ }
1051
1104
  }
1052
1105
 
1053
1106
  export const CONNECTED = "CONNECTED";
@@ -0,0 +1,114 @@
1
+ import { JetStreamApiError, JetStreamError } from "@nats-io/jetstream";
2
+ import NatsError from "nats"
3
+
4
+ export class ErrorLogging {
5
+
6
+ logError(data){
7
+ var err = data.err;
8
+
9
+ if(err instanceof JetStreamApiError){
10
+ var code = err.code;
11
+
12
+ if(code == 10077){
13
+ // Code 10077 is for message limit exceeded
14
+ console.table({
15
+ Event: "Message Limit Exceeded",
16
+ Description: "Current message count for account exceeds plan defined limits. Upgrade plan to remove limits",
17
+ Link: "https://console.relay-x.io/billing"
18
+ })
19
+
20
+ throw new Error("Message limit exceeded!")
21
+ }
22
+ }
23
+
24
+ if(err instanceof JetStreamError){
25
+ var code = err.code;
26
+
27
+ if(code == 409){
28
+ // Consumer deleted
29
+
30
+ console.table({
31
+ Event: "Consumer Manually Deleted!",
32
+ Description: "Consumer was manually deleted by user using deleteConsumer() or the library equivalent",
33
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/detailed_doc/NodeJS/queue_consume#deleting-a-consumer"
34
+ })
35
+ }
36
+ }
37
+
38
+ if(err.name == "NatsError"){
39
+ var code = err.code;
40
+ var chainedError = err.chainedError;
41
+ var permissionContext = err.permissionContext;
42
+ var userOp = data.op;
43
+
44
+ if(code == "PERMISSIONS_VIOLATION"){
45
+ if(userOp == "publish"){
46
+ console.table({
47
+ Event: "Publish Permissions Violation",
48
+ Description: `User is not permitted to publish on '${data.topic}'`,
49
+ Topic: data.topic,
50
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#messaging--publish-permissions"
51
+ })
52
+
53
+ throw new Error(`User is not permitted to publish on '${data.topic}'`)
54
+ }else if(userOp == "subscribe"){
55
+ console.table({
56
+ Event: "Subscribe Permissions Violation",
57
+ Description: `User is not permitted to subscribe to '${data.topic}'`,
58
+ Topic: data.topic,
59
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#messaging--subscribe-permissions"
60
+ })
61
+
62
+ throw new Error(`User is not permitted to subscribe to '${data.topic}'`)
63
+ }else if(userOp == "kv_write"){
64
+ console.table({
65
+ Event: "KV Write Failure",
66
+ Description: `User is not permitted to write to KV Store`,
67
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#write-permission"
68
+ })
69
+
70
+ throw new Error(`User is not permitted to write to KV Store`)
71
+ }else if(userOp == "kv_read"){
72
+ console.table({
73
+ Event: "KV Read Failure",
74
+ Description: `User is not permitted to read from KV Store`,
75
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#read-permission"
76
+ })
77
+
78
+ throw new Error(`User is not permitted to read from KV Store`)
79
+ }else if(userOp == "kv_delete"){
80
+ console.table({
81
+ Event: "KV Key Delete Failure",
82
+ Description: `User is not permitted to delete key from KV Store`,
83
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#write-permission"
84
+ })
85
+
86
+ throw new Error(`User is not permitted to delete key from KV Store`)
87
+ }
88
+ }else if(code == "AUTHORIZATION_VIOLATION"){
89
+ console.table({
90
+ Event: "Authentication Failure",
91
+ Description: `User failed to authenticate. Check if API key exists & if it is enabled`,
92
+ "Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#enabling-and-disabling-keys"
93
+ })
94
+ }
95
+ }
96
+ }
97
+
98
+ }
99
+
100
+ export class Logging {
101
+
102
+ #debug = false;
103
+
104
+ constructor(debug){
105
+ this.#debug = debug !== null && debug !== undefined && typeof debug == "boolean" ? debug : false;
106
+ }
107
+
108
+ log(...msg){
109
+ if(this.#debug){
110
+ console.log(...msg)
111
+ }
112
+ }
113
+
114
+ }