relayx-js 1.0.15 → 1.0.17

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,10 @@
1
+ V1.0.16
2
+ - Wildcard topics for pub / sub fixes
3
+ - Unit tests added for wildcard pattern matching
4
+
5
+ V1.0.16
6
+ - URL Fix
7
+
1
8
  V1.0.15
2
9
  - Wildcard topic pub / sub
3
10
  - Message replay on reconnect
@@ -28,13 +28,26 @@ async function run(){
28
28
  console.log(`[IMPL] DISONNECT`)
29
29
  });
30
30
 
31
- await realtime.on("power-telemetry", (data) => {
32
- console.log("power-telemetry", data);
33
- });
34
-
35
- await realtime.on("hello.*", (data) => {
36
- console.log("hello.*", data);
37
- });
31
+ // await realtime.on("power-telemetry", (data) => {
32
+ // console.log("power-telemetry", data);
33
+ // });
34
+
35
+ // await realtime.on("hello.*", (data) => {
36
+ // console.log("hello.*", data);
37
+ // });
38
+
39
+ // await realtime.on("hello.>", async (data) => {
40
+ // await realtime.sleep(10000)
41
+ // console.log("hello.>", data);
42
+ // });
43
+
44
+ // await realtime.on("hello.hey.*", (data) => {
45
+ // console.log("hell.hey.*", data);
46
+ // });
47
+
48
+ // await realtime.on("hello.hey.>", (data) => {
49
+ // console.log("hello.hey.>", data);
50
+ // });
38
51
 
39
52
  realtime.on(MESSAGE_RESEND, (data) => {
40
53
  console.log(`[MSG RESEND] => ${data}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayx-js",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "main": "realtime/realtime.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -12,6 +12,7 @@ export class Realtime {
12
12
  #codec = JSONCodec();
13
13
  #jetstream = null;
14
14
  #consumerMap = {};
15
+ #consumer = null;
15
16
 
16
17
  #event_func = {};
17
18
  #topicMap = [];
@@ -121,9 +122,9 @@ export class Realtime {
121
122
  "nats://0.0.0.0:4223",
122
123
  ] :
123
124
  [
124
- `tls://api2.relay-x.io:4221`,
125
- `tls://api2.relay-x.io:4222`,
126
- `tls://api2.relay-x.io:4223`
125
+ `tls://api.relay-x.io:4221`,
126
+ `tls://api.relay-x.io:4222`,
127
+ `tls://api.relay-x.io:4223`
127
128
  ];
128
129
  }else{
129
130
  this.#baseUrl = [
@@ -232,7 +233,6 @@ export class Realtime {
232
233
  this.#log(`client disconnected - ${s.data}`);
233
234
 
234
235
  this.connected = false;
235
- this.#consumerMap = {};
236
236
 
237
237
  if (DISCONNECTED in this.#event_func){
238
238
  if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
@@ -291,13 +291,17 @@ export class Realtime {
291
291
  /**
292
292
  * Closes connection
293
293
  */
294
- close(){
294
+ async close(){
295
295
  if(this.#natsClient !== null){
296
296
  this.reconnected = false;
297
297
  this.disconnected = true;
298
298
 
299
+ this.#consumer?.delete()
300
+
299
301
  this.#offlineMessageBuffer.length = 0;
300
302
 
303
+ await this.#deleteConsumer();
304
+
301
305
  this.#natsClient.close();
302
306
  }else{
303
307
  this.#log("Null / undefined socket, cannot close connection");
@@ -308,10 +312,9 @@ export class Realtime {
308
312
  * Start consumers for topics initialized by user
309
313
  */
310
314
  async #subscribeToTopics(){
311
- this.#topicMap.forEach(async (topic) => {
312
- // Subscribe to stream
313
- await this.#startConsumer(topic);
314
- });
315
+ if(this.#topicMap.length > 0){
316
+ await this.#startConsumer();
317
+ }
315
318
  }
316
319
 
317
320
  /**
@@ -332,8 +335,6 @@ export class Realtime {
332
335
  this.#topicMap = this.#topicMap.filter(item => item !== topic);
333
336
 
334
337
  delete this.#event_func[topic];
335
-
336
- return await this.#deleteConsumer(topic);
337
338
  }
338
339
 
339
340
  /**
@@ -476,6 +477,10 @@ export class Realtime {
476
477
  throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
477
478
  }
478
479
 
480
+ if(!this.isTopicValid(topic)){
481
+ throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
482
+ }
483
+
479
484
  if(start == undefined || start == null){
480
485
  throw new Error(`$start must be provided. $start is => ${start}`)
481
486
  }
@@ -576,40 +581,48 @@ export class Realtime {
576
581
  * Starts consumer for particular topic if stream exists
577
582
  * @param {string} topic
578
583
  */
579
- async #startConsumer(topic){
580
- this.#log(`Starting consumer for topic: ${topic}_${uuidv4()}`)
584
+ async #startConsumer(){
585
+ if(this.#consumer != null){
586
+ return;
587
+ }
581
588
 
582
589
  var opts = {
583
- name: `${topic}_${uuidv4()}`,
584
- filter_subjects: [this.#getStreamTopic(topic)],
590
+ name: `${uuidv4()}`,
591
+ filter_subjects: [this.#getStreamTopic(">")],
585
592
  replay_policy: ReplayPolicy.Instant,
586
593
  opt_start_time: new Date(),
587
594
  ack_policy: AckPolicy.Explicit,
588
595
  delivery_policy: DeliverPolicy.New
589
596
  }
590
597
 
591
- const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
598
+ this.#consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
592
599
  this.#log(this.#topicMap)
593
600
 
594
- this.#consumerMap[topic] = consumer;
595
-
596
- await consumer.consume({
601
+ await this.#consumer.consume({
597
602
  callback: async (msg) => {
598
603
  try{
599
604
  const now = Date.now();
600
605
  this.#log("Decoding msgpack message...")
601
606
  var data = decode(msg.data);
602
607
 
603
- var room = data.room;
608
+ var room = this.#stripStreamHash(msg.subject);
604
609
 
605
610
  this.#log(data);
606
611
 
607
612
  // 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
- });
613
+ if (data.client_id != this.#getClientId()){
614
+ var topics = this.#getCallbackTopics(room);
615
+ this.#log(topics)
616
+
617
+ for(let i = 0; i < topics.length; i++){
618
+ var top = topics[i];
619
+
620
+ this.#event_func[top]({
621
+ "id": data.id,
622
+ "topic": room,
623
+ "data": data.message
624
+ });
625
+ }
613
626
  }
614
627
 
615
628
  msg.ack();
@@ -617,30 +630,22 @@ export class Realtime {
617
630
  await this.#logLatency(now, data);
618
631
  }catch(err){
619
632
  this.#log("Consumer err " + err);
620
- msg.nack(5000);
633
+ msg.nak(5000);
621
634
  }
622
635
  }
623
636
  });
624
637
  this.#log("Consumer is consuming");
625
638
  }
626
639
 
627
- /**
628
- * Deletes consumer
629
- * @param {string} topic
630
- */
631
- async #deleteConsumer(topic){
632
- const consumer = this.#consumerMap[topic]
633
-
640
+ async #deleteConsumer(){
634
641
  var del = false;
635
642
 
636
- if (consumer != null && consumer != undefined){
637
- del = await consumer.delete();
643
+ if (this.#consumer != null && this.#consumer != undefined){
644
+ del = await this.#consumer.delete();
638
645
  }else{
639
646
  del = false
640
647
  }
641
648
 
642
- delete this.#consumerMap[topic];
643
-
644
649
  return del;
645
650
  }
646
651
 
@@ -743,7 +748,7 @@ export class Realtime {
743
748
  var arrayCheck = ![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
744
749
  this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic);
745
750
 
746
- const TOPIC_REGEX = /^(?!\$)[A-Za-z0-9_,.*>\$-]+$/;
751
+ const TOPIC_REGEX = /^(?!.*\$)(?:[A-Za-z0-9_*~-]+(?:\.[A-Za-z0-9_*~-]+)*(?:\.>)?|>)$/u;
747
752
 
748
753
  var spaceStarCheck = !topic.includes(" ") && TOPIC_REGEX.test(topic);
749
754
 
@@ -800,6 +805,91 @@ export class Realtime {
800
805
  }
801
806
  }
802
807
 
808
+ #stripStreamHash(topic){
809
+ return topic.replace(`${this.topicHash}.`, "")
810
+ }
811
+
812
+ #getCallbackTopics(topic){
813
+ var validTopics = [];
814
+
815
+ var topicPatterns = Object.keys(this.#event_func);
816
+
817
+ for(let i = 0; i < topicPatterns.length; i++){
818
+ var pattern = topicPatterns[i];
819
+
820
+ if([CONNECTED, RECONNECT, MESSAGE_RESEND, DISCONNECTED, SERVER_DISCONNECT].includes(pattern)){
821
+ continue;
822
+ }
823
+
824
+ var match = this.#topicPatternMatcher(pattern, topic);
825
+
826
+ if(match){
827
+ validTopics.push(pattern)
828
+ }
829
+ }
830
+
831
+ return validTopics;
832
+
833
+ }
834
+
835
+ #topicPatternMatcher(patternA, patternB) {
836
+ const a = patternA.split(".");
837
+ const b = patternB.split(".");
838
+
839
+ let i = 0, j = 0; // cursors in a & b
840
+ let starAi = -1, starAj = -1; // last '>' position in A and the token count it has consumed
841
+ let starBi = -1, starBj = -1; // same for pattern B
842
+
843
+ while (i < a.length || j < b.length) {
844
+ const tokA = a[i];
845
+ const tokB = b[j];
846
+
847
+ /*──────────── literal match or single‑token wildcard on either side ────────────*/
848
+ const singleWildcard =
849
+ (tokA === "*" && j < b.length) ||
850
+ (tokB === "*" && i < a.length);
851
+
852
+ if (
853
+ (tokA !== undefined && tokA === tokB) ||
854
+ singleWildcard
855
+ ) {
856
+ i++; j++;
857
+ continue;
858
+ }
859
+
860
+ /*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
861
+ if (tokA === ">") {
862
+ if (i !== a.length - 1) return false; // '>' not in last position → invalid
863
+ if (j >= b.length) return false; // must consume at least one token
864
+ starAi = i++; // remember where '>' is
865
+ starAj = ++j; // gobble one token from B
866
+ continue;
867
+ }
868
+ if (tokB === ">") {
869
+ if (j !== b.length - 1) return false; // same rule for patternB
870
+ if (i >= a.length) return false;
871
+ starBi = j++;
872
+ starBj = ++i;
873
+ continue;
874
+ }
875
+
876
+ /*───────────────────────────── back‑track using last '>' ───────────────────────*/
877
+ if (starAi !== -1) { // let patternA's '>' absorb one more token of B
878
+ j = ++starAj;
879
+ continue;
880
+ }
881
+ if (starBi !== -1) { // let patternB's '>' absorb one more token of A
882
+ i = ++starBj;
883
+ continue;
884
+ }
885
+
886
+ /*────────────────────────────────── dead‑end ───────────────────────────────────*/
887
+ return false;
888
+ }
889
+
890
+ return true;
891
+ }
892
+
803
893
  sleep(ms) {
804
894
  return new Promise(resolve => setTimeout(resolve, ms));
805
895
  }
@@ -930,6 +1020,14 @@ export class Realtime {
930
1020
  return null;
931
1021
  }
932
1022
  }
1023
+
1024
+ testPatternMatcher(){
1025
+ if(process.env.NODE_ENV == "test"){
1026
+ return this.#topicPatternMatcher.bind(this)
1027
+ }else{
1028
+ return null;
1029
+ }
1030
+ }
933
1031
  }
934
1032
 
935
1033
  export const CONNECTED = "CONNECTED";
package/tests/test.js CHANGED
@@ -475,17 +475,82 @@ test("Test isTopicValidMethod()", () => {
475
475
  assert.strictEqual(valid, false);
476
476
  });
477
477
 
478
- unreservedInvalidTopics = ["$hey.hey", "orders created", ""];
478
+ unreservedInvalidTopics = [
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>"
511
+ ];
479
512
 
480
513
  unreservedInvalidTopics.forEach(topic => {
481
514
  var valid = realTimeEnabled.isTopicValid(topic);
482
515
  assert.strictEqual(valid, false);
483
516
  });
484
517
 
485
- var unreservedValidTopics = ["hello", "test-room", "heyyyyy", "room-connect", "hey$"];
518
+ var unreservedValidTopics = [
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.>"
549
+ ];
486
550
 
487
551
  unreservedValidTopics.forEach(topic => {
488
552
  var valid = realTimeEnabled.isTopicValid(topic);
553
+ console.log(topic)
489
554
  assert.strictEqual(valid, true);
490
555
  });
491
556
  });
@@ -544,4 +609,77 @@ test("History test", async () => {
544
609
  },
545
610
  new Error("$start must be a Date object"),
546
611
  "Expected error was not thrown");
547
- })
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
+ })
@@ -1,51 +0,0 @@
1
- # Sample workflow for building and deploying a Jekyll site to GitHub Pages
2
- name: Deploy Jekyll with GitHub Pages dependencies preinstalled
3
-
4
- on:
5
- # Runs on pushes targeting the default branch
6
- push:
7
- branches: ["main"]
8
-
9
- # Allows you to run this workflow manually from the Actions tab
10
- workflow_dispatch:
11
-
12
- # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13
- permissions:
14
- contents: read
15
- pages: write
16
- id-token: write
17
-
18
- # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19
- # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20
- concurrency:
21
- group: "pages"
22
- cancel-in-progress: false
23
-
24
- jobs:
25
- # Build job
26
- build:
27
- runs-on: ubuntu-latest
28
- steps:
29
- - name: Checkout
30
- uses: actions/checkout@v4
31
- - name: Setup Pages
32
- uses: actions/configure-pages@v5
33
- - name: Build with Jekyll
34
- uses: actions/jekyll-build-pages@v1
35
- with:
36
- source: ./
37
- destination: ./_site
38
- - name: Upload artifact
39
- uses: actions/upload-pages-artifact@v3
40
-
41
- # Deployment job
42
- deploy:
43
- environment:
44
- name: github-pages
45
- url: ${{ steps.deployment.outputs.page_url }}
46
- runs-on: ubuntu-latest
47
- needs: build
48
- steps:
49
- - name: Deploy to GitHub Pages
50
- id: deployment
51
- uses: actions/deploy-pages@v4