relayx-js 1.0.16 → 1.0.18

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 CHANGED
@@ -1,3 +1,7 @@
1
+ V1.0.16
2
+ - Wildcard topics for pub / sub fixes
3
+ - Unit tests added for wildcard pattern matching
4
+
1
5
  V1.0.16
2
6
  - URL Fix
3
7
 
@@ -32,10 +32,22 @@ async function run(){
32
32
  console.log("power-telemetry", data);
33
33
  });
34
34
 
35
- await realtime.on("hello.*", (data) => {
36
- console.log("hello.*", data);
35
+ // await realtime.on("hello.*", (data) => {
36
+ // console.log("hello.*", data);
37
+ // });
38
+
39
+ await realtime.on("hello.>", async (data) => {
40
+ console.log("hello.>", data);
37
41
  });
38
42
 
43
+ // await realtime.on("hello.hey.*", (data) => {
44
+ // console.log("hell.hey.*", data);
45
+ // });
46
+
47
+ // await realtime.on("hello.hey.>", (data) => {
48
+ // console.log("hello.hey.>", data);
49
+ // });
50
+
39
51
  realtime.on(MESSAGE_RESEND, (data) => {
40
52
  console.log(`[MSG RESEND] => ${data}`)
41
53
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayx-js",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "main": "realtime/realtime.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,13 +16,13 @@ export class Realtime {
16
16
  #event_func = {};
17
17
  #topicMap = [];
18
18
 
19
- #config = "CiAgICAgICAgLS0tLS1CRUdJTiBOQVRTIFVTRVIgSldULS0tLS0KICAgICAgICBKV1RfS0VZCiAgICAgICAgLS0tLS0tRU5EIE5BVFMgVVNFUiBKV1QtLS0tLS0KCiAgICAgICAgKioqKioqKioqKioqKioqKioqKioqKioqKiBJTVBPUlRBTlQgKioqKioqKioqKioqKioqKioqKioqKioqKgogICAgICAgIE5LRVkgU2VlZCBwcmludGVkIGJlbG93IGNhbiBiZSB1c2VkIHRvIHNpZ24gYW5kIHByb3ZlIGlkZW50aXR5LgogICAgICAgIE5LRVlzIGFyZSBzZW5zaXRpdmUgYW5kIHNob3VsZCBiZSB0cmVhdGVkIGFzIHNlY3JldHMuCgogICAgICAgIC0tLS0tQkVHSU4gVVNFUiBOS0VZIFNFRUQtLS0tLQogICAgICAgIFNFQ1JFVF9LRVkKICAgICAgICAtLS0tLS1FTkQgVVNFUiBOS0VZIFNFRUQtLS0tLS0KCiAgICAgICAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKgogICAgICAgIA=="
20
-
21
19
  // Status Codes
22
20
  #RECONNECTING = "RECONNECTING";
23
21
  #RECONNECTED = "RECONNECTED";
24
22
  #RECONN_FAIL = "RECONN_FAIL";
25
23
 
24
+ #reservedSystemTopics = [CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED, this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT];
25
+
26
26
  setRemoteUserAttempts = 0;
27
27
  setRemoteUserRetries = 5;
28
28
 
@@ -42,6 +42,8 @@ export class Realtime {
42
42
 
43
43
  #maxPublishRetries = 5;
44
44
 
45
+ #connectCalled = false;
46
+
45
47
  constructor(config){
46
48
  if(typeof config != "object"){
47
49
  throw new Error("Realtime($config). $config not object => {}")
@@ -97,10 +99,13 @@ export class Realtime {
97
99
  if(arguments[0] instanceof Object){
98
100
  opts = arguments[0];
99
101
  staging = false;
100
- }else{
102
+ }else if(typeof arguments[0] == "boolean"){
101
103
  opts = {};
102
104
  staging = arguments[0];
103
105
  this.#log(staging)
106
+ }else{
107
+ opts = {};
108
+ staging = false
104
109
  }
105
110
  }else{
106
111
  staging = false;
@@ -171,6 +176,10 @@ export class Realtime {
171
176
  * Connects to the relay network
172
177
  */
173
178
  async connect(){
179
+ if(this.#connectCalled){
180
+ return;
181
+ }
182
+
174
183
  this.SEVER_URL = this.#baseUrl;
175
184
 
176
185
  var credsFile = this.#getUserCreds(this.api_key, this.secret)
@@ -194,6 +203,7 @@ export class Realtime {
194
203
  await this.#getNameSpace()
195
204
 
196
205
  this.connected = true;
206
+ this.#connectCalled = true;
197
207
  }catch(err){
198
208
  this.#log("ERR")
199
209
  this.#log(err);
@@ -204,17 +214,13 @@ export class Realtime {
204
214
  if (this.connected == true){
205
215
  this.#log("Connected to server!");
206
216
 
207
- // Callback on client side
208
- if (CONNECTED in this.#event_func){
209
- if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
210
- this.#event_func[CONNECTED]()
211
- }
212
- }
213
-
214
217
  this.#natsClient.closed().then(() => {
215
218
  this.#log("the connection closed!");
216
219
 
217
220
  this.#offlineMessageBuffer.length = 0;
221
+ this.connected = false;
222
+ this.reconnecting = false;
223
+ this.#connectCalled = false;
218
224
 
219
225
  if (DISCONNECTED in this.#event_func){
220
226
  if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
@@ -232,13 +238,6 @@ export class Realtime {
232
238
  this.#log(`client disconnected - ${s.data}`);
233
239
 
234
240
  this.connected = false;
235
- this.#consumerMap = {};
236
-
237
- if (DISCONNECTED in this.#event_func){
238
- if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
239
- this.#event_func[DISCONNECTED]()
240
- }
241
- }
242
241
  break;
243
242
  case Events.LDM:
244
243
  this.#log("client has been requested to reconnect");
@@ -268,6 +267,7 @@ export class Realtime {
268
267
  this.#log("client is attempting to reconnect");
269
268
 
270
269
  this.reconnecting = true;
270
+ this.connected = false;
271
271
 
272
272
  if(RECONNECT in this.#event_func && this.reconnecting){
273
273
  this.#event_func[RECONNECT](this.#RECONNECTING);
@@ -285,19 +285,29 @@ export class Realtime {
285
285
  // Subscribe to topics
286
286
  this.#subscribeToTopics();
287
287
  this.#log("Subscribed to topics");
288
+
289
+ // Callback on client side
290
+ if (CONNECTED in this.#event_func){
291
+ if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
292
+ this.#event_func[CONNECTED]()
293
+ }
294
+ }
288
295
  }
289
296
  }
290
297
 
291
298
  /**
292
299
  * Closes connection
293
300
  */
294
- close(){
301
+ async close(){
295
302
  if(this.#natsClient !== null){
296
303
  this.reconnected = false;
297
304
  this.disconnected = true;
305
+ this.#connectCalled = false;
298
306
 
299
307
  this.#offlineMessageBuffer.length = 0;
300
308
 
309
+ await this.#deleteAllConsumers();
310
+
301
311
  this.#natsClient.close();
302
312
  }else{
303
313
  this.#log("Null / undefined socket, cannot close connection");
@@ -314,6 +324,17 @@ export class Realtime {
314
324
  });
315
325
  }
316
326
 
327
+ /**
328
+ * Delete consumers for topics initialized by user
329
+ */
330
+ async #deleteAllConsumers(){
331
+ for(let i = 0; i < this.#topicMap.length; i++){
332
+ let topic = this.#topicMap[i];
333
+
334
+ await this.#deleteConsumer(topic);
335
+ }
336
+ }
337
+
317
338
  /**
318
339
  * Deletes reference to user defined event callback.
319
340
  * This will stop listening to a topic
@@ -359,31 +380,23 @@ export class Realtime {
359
380
  throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
360
381
  }
361
382
 
362
- if(!(topic in this.#event_func)){
363
- this.#event_func[topic] = func;
364
- }else{
383
+ if(topic in this.#event_func || this.#topicMap.includes(topic)){
365
384
  return false
366
385
  }
367
386
 
368
- if (![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
369
- this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic)){
370
- if(!this.isTopicValid(topic)){
371
- // We have an invalid topic, lets remove it
372
- if(topic in this.#event_func){
373
- delete this.#event_func[topic];
374
- }
387
+ this.#event_func[topic] = func;
375
388
 
376
- throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
377
- }
389
+ if (!this.#reservedSystemTopics.includes(topic)){
390
+ if(!this.isTopicValid(topic)){
391
+ throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
392
+ }
378
393
 
379
- if(!this.#topicMap.includes(topic)){
380
- this.#topicMap.push(topic);
381
- }
394
+ this.#topicMap.push(topic);
382
395
 
383
- if(this.connected){
384
- // Connected we need to create a topic in a stream
385
- await this.#startConsumer(topic);
386
- }
396
+ if(this.connected){
397
+ // Connected we need to create a topic in a stream
398
+ await this.#startConsumer(topic);
399
+ }
387
400
  }
388
401
 
389
402
  return true;
@@ -413,7 +426,7 @@ export class Realtime {
413
426
  throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
414
427
  }
415
428
 
416
- if(!this.#isMessageValid(data)){
429
+ if(!this.isMessageValid(data)){
417
430
  throw new Error("$message must be JSON, string or number")
418
431
  }
419
432
 
@@ -428,15 +441,9 @@ export class Realtime {
428
441
  "start": Date.now()
429
442
  }
430
443
 
431
- this.#log("Encoding message via msg pack...")
432
- var encodedMessage = encode(message);
433
-
434
444
  if(this.connected){
435
- if(!this.#topicMap.includes(topic)){
436
- this.#topicMap.push(topic);
437
- }else{
438
- this.#log(`${topic} exists locally, moving on...`)
439
- }
445
+ this.#log("Encoding message via msg pack...")
446
+ var encodedMessage = encode(message);
440
447
 
441
448
  this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
442
449
 
@@ -463,7 +470,6 @@ export class Realtime {
463
470
  * @param {string} topic
464
471
  */
465
472
  async history(topic, start, end){
466
- this.#log(start)
467
473
  if(topic == null || topic == undefined){
468
474
  throw new Error("$topic is null or undefined");
469
475
  }
@@ -476,6 +482,10 @@ export class Realtime {
476
482
  throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
477
483
  }
478
484
 
485
+ if(!this.isTopicValid(topic)){
486
+ throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
487
+ }
488
+
479
489
  if(start == undefined || start == null){
480
490
  throw new Error(`$start must be provided. $start is => ${start}`)
481
491
  }
@@ -496,11 +506,16 @@ export class Realtime {
496
506
  end = end.toISOString();
497
507
  }
498
508
 
509
+ if(!this.connected){
510
+ return [];
511
+ }
512
+
499
513
  var opts = {
500
514
  name: `${topic}_${uuidv4()}_history`,
501
515
  filter_subjects: [this.#getStreamTopic(topic)],
502
516
  replay_policy: ReplayPolicy.Instant,
503
517
  opt_start_time: start,
518
+ delivery_policy: DeliverPolicy.StartTime,
504
519
  ack_policy: AckPolicy.Explicit,
505
520
  }
506
521
 
@@ -529,7 +544,12 @@ export class Realtime {
529
544
  var data = decode(msg.data);
530
545
  this.#log(data);
531
546
 
532
- history.push(data.message);
547
+ history.push({
548
+ "id": data.id,
549
+ "topic": data.room,
550
+ "message": data.message,
551
+ "timestamp": msg.timestamp
552
+ });
533
553
  }
534
554
 
535
555
  var del = await consumer.delete();
@@ -577,10 +597,10 @@ export class Realtime {
577
597
  * @param {string} topic
578
598
  */
579
599
  async #startConsumer(topic){
580
- this.#log(`Starting consumer for topic: ${topic}_${uuidv4()}`)
600
+ const consumerName = `${topic}_${uuidv4()}_consumer`;
581
601
 
582
602
  var opts = {
583
- name: `${topic}_${uuidv4()}`,
603
+ name: consumerName,
584
604
  filter_subjects: [this.#getStreamTopic(topic)],
585
605
  replay_policy: ReplayPolicy.Instant,
586
606
  opt_start_time: new Date(),
@@ -597,19 +617,25 @@ export class Realtime {
597
617
  callback: async (msg) => {
598
618
  try{
599
619
  const now = Date.now();
620
+ msg.working()
600
621
  this.#log("Decoding msgpack message...")
601
622
  var data = decode(msg.data);
602
623
 
603
- var room = data.room;
624
+ var msgTopic = this.#stripStreamHash(msg.subject);
604
625
 
605
626
  this.#log(data);
606
627
 
607
628
  // Push topic message to main thread
608
- if (room in this.#event_func && data.client_id != this.#getClientId()){
609
- this.#event_func[room]({
610
- "id": data.id,
611
- "data": data.message
612
- });
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
+ }
613
639
  }
614
640
 
615
641
  msg.ack();
@@ -617,18 +643,15 @@ export class Realtime {
617
643
  await this.#logLatency(now, data);
618
644
  }catch(err){
619
645
  this.#log("Consumer err " + err);
620
- msg.nack(5000);
646
+ msg.nak(5000);
621
647
  }
622
648
  }
623
649
  });
624
650
  this.#log("Consumer is consuming");
625
651
  }
626
652
 
627
- /**
628
- * Deletes consumer
629
- * @param {string} topic
630
- */
631
653
  async #deleteConsumer(topic){
654
+ this.#log(topic)
632
655
  const consumer = this.#consumerMap[topic]
633
656
 
634
657
  var del = false;
@@ -650,11 +673,6 @@ export class Realtime {
650
673
  return;
651
674
  }
652
675
 
653
- if(this.#latency.length >= 100){
654
- this.#log("Latency array is full, skipping log");
655
- return;
656
- }
657
-
658
676
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
659
677
 
660
678
  this.#log(`Timezone: ${timeZone}`);
@@ -671,7 +689,7 @@ export class Realtime {
671
689
  this.#latencyPush = setTimeout(async () => {
672
690
  this.#log("setTimeout called");
673
691
 
674
- if(this.#latency.length > 0 && this.connected){
692
+ if(this.#latency.length > 0 && this.connected && !this.#isSendingLatency){
675
693
  this.#log("Push from setTimeout")
676
694
  await this.#pushLatencyData({
677
695
  timezone: timeZone,
@@ -684,7 +702,7 @@ export class Realtime {
684
702
  }, 30000);
685
703
  }
686
704
 
687
- if(this.#latency.length == 100 && !this.#isSendingLatency){
705
+ if(this.#latency.length >= 100 && !this.#isSendingLatency){
688
706
  this.#log("Push from Length Check: " + this.#latency.length);
689
707
  await this.#pushLatencyData({
690
708
  timezone: timeZone,
@@ -740,10 +758,9 @@ export class Realtime {
740
758
  */
741
759
  isTopicValid(topic){
742
760
  if(topic !== null && topic !== undefined && (typeof topic) == "string"){
743
- var arrayCheck = ![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
744
- this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic);
761
+ var arrayCheck = !this.#reservedSystemTopics.includes(topic);
745
762
 
746
- const TOPIC_REGEX = /^(?!\$)[A-Za-z0-9_,.*>\$-]+$/;
763
+ const TOPIC_REGEX = /^(?!.*\$)(?:[A-Za-z0-9_*~-]+(?:\.[A-Za-z0-9_*~-]+)*(?:\.>)?|>)$/u;
747
764
 
748
765
  var spaceStarCheck = !topic.includes(" ") && TOPIC_REGEX.test(topic);
749
766
 
@@ -753,7 +770,7 @@ export class Realtime {
753
770
  }
754
771
  }
755
772
 
756
- #isMessageValid(message){
773
+ isMessageValid(message){
757
774
  if(message == null || message == undefined){
758
775
  throw new Error("$message cannot be null / undefined")
759
776
  }
@@ -800,6 +817,91 @@ export class Realtime {
800
817
  }
801
818
  }
802
819
 
820
+ #stripStreamHash(topic){
821
+ return topic.replace(`${this.topicHash}.`, "")
822
+ }
823
+
824
+ #getCallbackTopics(topic){
825
+ var validTopics = [];
826
+
827
+ var topicPatterns = Object.keys(this.#event_func);
828
+
829
+ for(let i = 0; i < topicPatterns.length; i++){
830
+ var pattern = topicPatterns[i];
831
+
832
+ if([CONNECTED, RECONNECT, MESSAGE_RESEND, DISCONNECTED, SERVER_DISCONNECT].includes(pattern)){
833
+ continue;
834
+ }
835
+
836
+ var match = this.#topicPatternMatcher(pattern, topic);
837
+
838
+ if(match){
839
+ validTopics.push(pattern)
840
+ }
841
+ }
842
+
843
+ return validTopics;
844
+
845
+ }
846
+
847
+ #topicPatternMatcher(patternA, patternB) {
848
+ const a = patternA.split(".");
849
+ const b = patternB.split(".");
850
+
851
+ let i = 0, j = 0; // cursors in a & b
852
+ let starAi = -1, starAj = -1; // last '>' position in A and the token count it has consumed
853
+ let starBi = -1, starBj = -1; // same for pattern B
854
+
855
+ while (i < a.length || j < b.length) {
856
+ const tokA = a[i];
857
+ const tokB = b[j];
858
+
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
+ /*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
873
+ if (tokA === ">") {
874
+ if (i !== a.length - 1) return false; // '>' not in last position → invalid
875
+ if (j >= b.length) return false; // must consume at least one token
876
+ starAi = i++; // remember where '>' is
877
+ starAj = ++j; // gobble one token from B
878
+ continue;
879
+ }
880
+ if (tokB === ">") {
881
+ if (j !== b.length - 1) return false; // same rule for patternB
882
+ if (i >= a.length) return false;
883
+ starBi = j++;
884
+ starBj = ++i;
885
+ continue;
886
+ }
887
+
888
+ /*───────────────────────────── back‑track using last '>' ───────────────────────*/
889
+ if (starAi !== -1) { // let patternA's '>' absorb one more token of B
890
+ j = ++starAj;
891
+ continue;
892
+ }
893
+ if (starBi !== -1) { // let patternB's '>' absorb one more token of A
894
+ i = ++starBj;
895
+ continue;
896
+ }
897
+
898
+ /*────────────────────────────────── dead‑end ───────────────────────────────────*/
899
+ return false;
900
+ }
901
+
902
+ return true;
903
+ }
904
+
803
905
  sleep(ms) {
804
906
  return new Promise(resolve => setTimeout(resolve, ms));
805
907
  }
@@ -865,13 +967,21 @@ export class Realtime {
865
967
  return methodDataOutput;
866
968
  }
867
969
 
868
- #getUserCreds(jwt, secret){
869
- var template = Buffer.from(this.#config, "base64").toString("utf8")
970
+ #getUserCreds(jwt, secret){
971
+ return `
972
+ -----BEGIN NATS USER JWT-----
973
+ ${jwt}
974
+ ------END NATS USER JWT------
975
+
976
+ ************************* IMPORTANT *************************
977
+ NKEY Seed printed below can be used to sign and prove identity.
978
+ NKEYs are sensitive and should be treated as secrets.
870
979
 
871
- var creds = template.replace("JWT_KEY", jwt);
872
- creds = creds.replace("SECRET_KEY", secret)
980
+ -----BEGIN USER NKEY SEED-----
981
+ ${secret}
982
+ ------END USER NKEY SEED------
873
983
 
874
- return creds
984
+ *************************************************************`
875
985
  }
876
986
 
877
987
  // Exposure for tests
@@ -930,6 +1040,14 @@ export class Realtime {
930
1040
  return null;
931
1041
  }
932
1042
  }
1043
+
1044
+ testPatternMatcher(){
1045
+ if(process.env.NODE_ENV == "test"){
1046
+ return this.#topicPatternMatcher.bind(this)
1047
+ }else{
1048
+ return null;
1049
+ }
1050
+ }
933
1051
  }
934
1052
 
935
1053
  export const CONNECTED = "CONNECTED";
package/tests/test.js CHANGED
@@ -476,36 +476,38 @@ test("Test isTopicValidMethod()", () => {
476
476
  });
477
477
 
478
478
  unreservedInvalidTopics = [
479
- '$internal', // starts with $
480
- 'hello world', // space
481
- 'topic/', // slash
482
- 'name?', // ?
483
- 'foo#bar', // #
484
- 'bar.baz!', // !
485
- ' space', // leading space
486
- 'tab\tchar', // tab
487
- 'line\nbreak', // newline
488
- 'comma ,', // space + comma
489
- '', // empty string
490
- 'bad|pipe', // |
491
- 'semi;colon', // ;
492
- 'colon:here', // :
493
- "quote's", // '
494
- '"doublequote"', // "
495
- 'brackets[]', // []
496
- 'brace{}', // {}
497
- 'paren()', // ()
498
- 'plus+sign', // +
499
- 'eq=val', // =
500
- 'gt>lt<', // < mixed with >
501
- 'percent%', // %
502
- 'caret^', // ^
503
- 'ampersand&', // &
504
- 'back\\slash', // backslash
505
- '中文字符', // non‑ASCII
506
- '👍emoji', // emoji
507
- 'foo\rbar', // carriage return
508
- 'end ' // trailing space
479
+ "$foo",
480
+ "foo$",
481
+ "foo.$.bar",
482
+ "foo..bar",
483
+ ".foo",
484
+ "foo.",
485
+ "foo.>.bar",
486
+ ">foo",
487
+ "foo>bar",
488
+ "foo.>bar",
489
+ "foo.bar.>.",
490
+ "foo bar",
491
+ "foo/bar",
492
+ "foo#bar",
493
+ "",
494
+ " ",
495
+ "..",
496
+ ".>",
497
+ "foo..",
498
+ ".",
499
+ ">.",
500
+ "foo,baz",
501
+ "αbeta",
502
+ "foo|bar",
503
+ "foo;bar",
504
+ "foo:bar",
505
+ "foo%bar",
506
+ "foo.*.>.bar",
507
+ "foo.*.>.",
508
+ "foo.*..bar",
509
+ "foo.>.bar",
510
+ "foo>"
509
511
  ];
510
512
 
511
513
  unreservedInvalidTopics.forEach(topic => {
@@ -514,40 +516,41 @@ test("Test isTopicValidMethod()", () => {
514
516
  });
515
517
 
516
518
  var unreservedValidTopics = [
517
- 'Orders',
518
- 'customer_123',
519
- 'foo-bar',
520
- 'a,b,c',
521
- '*',
522
- 'foo>*',
523
- 'hello$world',
524
- 'topic.123',
525
- 'ABC_def-ghi',
526
- 'data_stream_2025',
527
- 'NODE*',
528
- 'pubsub>events',
529
- 'log,metric,error',
530
- 'X123_Y456',
531
- 'multi.step.topic',
532
- 'batch-process',
533
- 'sensor1_data',
534
- 'finance$Q2',
535
- 'alpha,beta,gamma',
536
- 'Z9_Y8-X7',
537
- 'config>*',
538
- 'route-map',
539
- 'STATS_2025-07',
540
- 'msg_queue*',
541
- 'update>patch',
542
- 'pipeline_v2',
543
- 'FOO$BAR$BAZ',
544
- 'user.profile',
545
- 'id_001-xyz',
546
- 'event_queue>'
519
+ "foo",
520
+ "foo.bar",
521
+ "foo.bar.baz",
522
+ "*",
523
+ "foo.*",
524
+ "*.bar",
525
+ "foo.*.baz",
526
+ ">",
527
+ "foo.>",
528
+ "foo.bar.>",
529
+ "*.*.>",
530
+ "alpha_beta",
531
+ "alpha-beta",
532
+ "alpha~beta",
533
+ "abc123",
534
+ "123abc",
535
+ "~",
536
+ "alpha.*.>",
537
+ "alpha.*",
538
+ "alpha.*.*",
539
+ "-foo",
540
+ "foo_bar-baz~qux",
541
+ "A.B.C",
542
+ "sensor.temperature",
543
+ "metric.cpu.load",
544
+ "foo.*.*",
545
+ "foo.*.>",
546
+ "foo_bar.*",
547
+ "*.*",
548
+ "metrics.>"
547
549
  ];
548
550
 
549
551
  unreservedValidTopics.forEach(topic => {
550
552
  var valid = realTimeEnabled.isTopicValid(topic);
553
+ console.log(topic)
551
554
  assert.strictEqual(valid, true);
552
555
  });
553
556
  });
@@ -606,4 +609,77 @@ test("History test", async () => {
606
609
  },
607
610
  new Error("$start must be a Date object"),
608
611
  "Expected error was not thrown");
609
- })
612
+ })
613
+
614
+ test("Pattern matcher test", async () => {
615
+ var cases = [
616
+ ["foo", "foo", true], // 1
617
+ ["foo", "bar", false], // 2
618
+ ["foo.*", "foo.bar", true], // 3
619
+ ["foo.bar", "foo.*", true], // 4
620
+ ["*", "token", true], // 5
621
+ ["*", "*", true], // 6
622
+ ["foo.*", "foo.bar.baz", false], // 7
623
+ ["foo.>", "foo.bar.baz", true], // 8
624
+ ["foo.>", "foo", false], // 9 (zero‑token '>' now invalid)
625
+ ["foo.bar.baz", "foo.>", true], // 10
626
+ ["foo.bar.>", "foo.bar", false], // 11
627
+ ["foo", "foo.>", false], // 12
628
+ ["foo.*.>", "foo.bar.baz.qux", true], // 13
629
+ ["foo.*.baz", "foo.bar.>", true], // 14
630
+ ["alpha.*", "beta.gamma", false], // 15
631
+ ["alpha.beta", "alpha.*.*", false], // 16
632
+ ["foo.>.bar", "foo.any.bar", false], // 17 ('>' mid‑pattern)
633
+ [">", "foo.bar", true], // 18
634
+ [">", ">", true], // 19
635
+ ["*", ">", true], // 20
636
+ ["*.>", "foo.bar", true], // 21
637
+ ["*.*.*", "a.b.c", true], // 22
638
+ ["*.*.*", "a.b", false], // 23
639
+ ["a.b.c.d.e", "a.b.c.d.e", true], // 24
640
+ ["a.b.c.d.e", "a.b.c.d.f", false], // 25
641
+ ["a.b.*.d", "a.b.c.d", true], // 26
642
+ ["a.b.*.d", "a.b.c.e", false], // 27
643
+ ["a.b.>", "a.b", false], // 28
644
+ ["a.b", "a.b.c.d.>", false], // 29
645
+ ["a.b.>.c", "a.b.x.c", false], // 30
646
+ ["a.*.*", "a.b", false], // 31
647
+ ["a.*", "a.b.c", false], // 32
648
+ ["metrics.cpu.load", "metrics.*.load", true], // 33
649
+ ["metrics.cpu.load", "metrics.cpu.*", true], // 34
650
+ ["metrics.cpu.load", "metrics.>.load", false], // 35
651
+ ["metrics.>", "metrics", false], // 36
652
+ ["metrics.>", "othermetrics.cpu", false], // 37
653
+ ["*.*.>", "a.b", false], // 38
654
+ ["*.*.>", "a.b.c.d", true], // 39
655
+ ["a.b.c", "*.*.*", true], // 40
656
+ ["a.b.c", "*.*", false], // 41
657
+ ["alpha.*.>", "alpha", false], // 42
658
+ ["alpha.*.>", "alpha.beta", false], // 43
659
+ ["alpha.*.>", "alpha.beta.gamma", true], // 44
660
+ ["alpha.*.>", "beta.alpha.gamma", false], // 45
661
+ ["foo-bar_baz", "foo-bar_baz", true], // 46
662
+ ["foo-bar_*", "foo-bar_123", false], // 47 ( '*' here is literal )
663
+ ["foo-bar_*", "foo-bar_*", true], // 48
664
+ ["order-*", "order-123", false], // 49
665
+ ["hello.hey.*", "hello.hey.>", true] // 50
666
+ ];
667
+
668
+ var realtime = new Realtime({
669
+ api_key: process.env.AUTH_JWT,
670
+ secret: process.env.AUTH_SECRET
671
+ });
672
+
673
+ var patternMatcher = realtime.testPatternMatcher();
674
+
675
+ cases.forEach(testCase => {
676
+ var tokenA = testCase[0];
677
+ var tokenB = testCase[1];
678
+ var expectedResult = testCase[2];
679
+
680
+ console.log(`${tokenA} ⇆ ${tokenB} → ${expectedResult}`)
681
+
682
+ var result = patternMatcher(tokenA, tokenB)
683
+ assert.strictEqual(expectedResult, result)
684
+ });
685
+ })