relayx-webjs 1.0.0 → 1.0.2

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,6 +1,7 @@
1
- V1.0.8
2
- - History API fetch() to consume()
1
+ V1.0.2
2
+ - Wildcard topics for pub / sub fixes
3
+ - Unit tests added for wildcard pattern matching
3
4
 
4
- V1.0.7
5
- - History API added
6
- - README updated to explain history API usage
5
+ V1.0.1
6
+ - Wildcard topics for pub / sub
7
+ - Message replay on reconnection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayx-webjs",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A powerful library for integrating real-time communication into your webapps, powered by the Relay Network.",
5
5
  "main": "realtime/realtime.js",
6
6
  "type": "module",
@@ -11,6 +11,7 @@ export class Realtime {
11
11
  #codec = JSONCodec();
12
12
  #jetstream = null;
13
13
  #consumerMap = {};
14
+ #consumer = null;
14
15
 
15
16
  #event_func = {};
16
17
  #topicMap = [];
@@ -223,7 +224,6 @@ export class Realtime {
223
224
  this.#log(`client disconnected - ${s.data}`);
224
225
 
225
226
  this.connected = false;
226
- this.#consumerMap = {};
227
227
 
228
228
  if (DISCONNECTED in this.#event_func){
229
229
  if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
@@ -282,13 +282,15 @@ export class Realtime {
282
282
  /**
283
283
  * Closes connection
284
284
  */
285
- close(){
285
+ async close(){
286
286
  if(this.#natsClient !== null){
287
287
  this.reconnected = false;
288
288
  this.disconnected = true;
289
289
 
290
290
  this.#offlineMessageBuffer.length = 0;
291
291
 
292
+ await this.#deleteConsumer();
293
+
292
294
  this.#natsClient.close();
293
295
  }else{
294
296
  this.#log("Null / undefined socket, cannot close connection");
@@ -299,10 +301,9 @@ export class Realtime {
299
301
  * Start consumers for topics initialized by user
300
302
  */
301
303
  async #subscribeToTopics(){
302
- this.#topicMap.forEach(async (topic) => {
303
- // Subscribe to stream
304
- await this.#startConsumer(topic);
305
- });
304
+ if(this.#topicMap.length > 0){
305
+ await this.#startConsumer();
306
+ }
306
307
  }
307
308
 
308
309
  /**
@@ -323,8 +324,6 @@ export class Realtime {
323
324
  this.#topicMap = this.#topicMap.filter(item => item !== topic);
324
325
 
325
326
  delete this.#event_func[topic];
326
-
327
- return await this.#deleteConsumer(topic);
328
327
  }
329
328
 
330
329
  /**
@@ -467,6 +466,10 @@ export class Realtime {
467
466
  throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
468
467
  }
469
468
 
469
+ if(!this.isTopicValid(topic)){
470
+ throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
471
+ }
472
+
470
473
  if(start == undefined || start == null){
471
474
  throw new Error(`$start must be provided. $start is => ${start}`)
472
475
  }
@@ -568,39 +571,49 @@ export class Realtime {
568
571
  * @param {string} topic
569
572
  */
570
573
  async #startConsumer(topic){
574
+ if(this.#consumer != null){
575
+ return;
576
+ }
577
+
571
578
  this.#log(`Starting consumer for topic: ${topic}_${uuidv4()}`)
572
579
 
573
580
  var opts = {
574
- name: `${topic}_${uuidv4()}`,
575
- filter_subjects: [this.#getStreamTopic(topic), this.#getStreamTopic(topic) + "_presence"],
581
+ name: `${uuidv4()}`,
582
+ filter_subjects: [this.#getStreamTopic(">")],
576
583
  replay_policy: ReplayPolicy.Instant,
577
584
  opt_start_time: new Date(),
578
585
  ack_policy: AckPolicy.Explicit,
579
586
  delivery_policy: DeliverPolicy.New
580
587
  }
581
588
 
582
- const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
589
+ this.#consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
583
590
  this.#log(this.#topicMap)
584
591
 
585
- this.#consumerMap[topic] = consumer;
586
-
587
- await consumer.consume({
592
+ await this.#consumer.consume({
588
593
  callback: async (msg) => {
589
594
  try{
590
595
  const now = Date.now();
591
596
  this.#log("Decoding msgpack message...")
592
597
  var data = decode(msg.data);
593
598
 
594
- var room = data.room;
599
+ var room = this.#stripStreamHash(msg.subject);
595
600
 
596
601
  this.#log(data);
597
602
 
598
603
  // 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
+ if (data.client_id != this.#getClientId()){
605
+ var topics = this.#getCallbackTopics(room);
606
+ this.#log(topics)
607
+
608
+ for(let i = 0; i < topics.length; i++){
609
+ var top = topics[i];
610
+
611
+ this.#event_func[top]({
612
+ "id": data.id,
613
+ "topic": room,
614
+ "data": data.message
615
+ });
616
+ }
604
617
  }
605
618
 
606
619
  msg.ack();
@@ -608,7 +621,7 @@ export class Realtime {
608
621
  await this.#logLatency(now, data);
609
622
  }catch(err){
610
623
  this.#log("Consumer err " + err);
611
- msg.nack(5000);
624
+ msg.nak(5000);
612
625
  }
613
626
  }
614
627
  });
@@ -619,19 +632,15 @@ export class Realtime {
619
632
  * Deletes consumer
620
633
  * @param {string} topic
621
634
  */
622
- async #deleteConsumer(topic){
623
- const consumer = this.#consumerMap[topic]
624
-
635
+ async #deleteConsumer(){
625
636
  var del = false;
626
637
 
627
- if (consumer != null && consumer != undefined){
628
- del = await consumer.delete();
638
+ if (this.#consumer != null && this.#consumer != undefined){
639
+ del = await this.#consumer.delete();
629
640
  }else{
630
641
  del = false
631
642
  }
632
643
 
633
- delete this.#consumerMap[topic];
634
-
635
644
  return del;
636
645
  }
637
646
 
@@ -734,7 +743,9 @@ export class Realtime {
734
743
  var arrayCheck = ![CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED,
735
744
  this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic);
736
745
 
737
- var spaceStarCheck = !topic.includes(" ") && !topic.includes("*") && !topic.includes(".");
746
+ const TOPIC_REGEX = /^(?!.*\$)(?:[A-Za-z0-9_*~-]+(?:\.[A-Za-z0-9_*~-]+)*(?:\.>)?|>)$/u;
747
+
748
+ var spaceStarCheck = !topic.includes(" ") && TOPIC_REGEX.test(topic);
738
749
 
739
750
  return arrayCheck && spaceStarCheck;
740
751
  }else{
@@ -789,6 +800,91 @@ export class Realtime {
789
800
  }
790
801
  }
791
802
 
803
+ #stripStreamHash(topic){
804
+ return topic.replace(`${this.topicHash}.`, "")
805
+ }
806
+
807
+ #getCallbackTopics(topic){
808
+ var validTopics = [];
809
+
810
+ var topicPatterns = Object.keys(this.#event_func);
811
+
812
+ for(let i = 0; i < topicPatterns.length; i++){
813
+ var pattern = topicPatterns[i];
814
+
815
+ if([CONNECTED, RECONNECT, MESSAGE_RESEND, DISCONNECTED, SERVER_DISCONNECT].includes(pattern)){
816
+ continue;
817
+ }
818
+
819
+ var match = this.#topicPatternMatcher(pattern, topic);
820
+
821
+ if(match){
822
+ validTopics.push(pattern)
823
+ }
824
+ }
825
+
826
+ return validTopics;
827
+
828
+ }
829
+
830
+ #topicPatternMatcher(patternA, patternB) {
831
+ const a = patternA.split(".");
832
+ const b = patternB.split(".");
833
+
834
+ let i = 0, j = 0; // cursors in a & b
835
+ let starAi = -1, starAj = -1; // last '>' position in A and the token count it has consumed
836
+ let starBi = -1, starBj = -1; // same for pattern B
837
+
838
+ while (i < a.length || j < b.length) {
839
+ const tokA = a[i];
840
+ const tokB = b[j];
841
+
842
+ /*──────────── literal match or single‑token wildcard on either side ────────────*/
843
+ const singleWildcard =
844
+ (tokA === "*" && j < b.length) ||
845
+ (tokB === "*" && i < a.length);
846
+
847
+ if (
848
+ (tokA !== undefined && tokA === tokB) ||
849
+ singleWildcard
850
+ ) {
851
+ i++; j++;
852
+ continue;
853
+ }
854
+
855
+ /*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
856
+ if (tokA === ">") {
857
+ if (i !== a.length - 1) return false; // '>' not in last position → invalid
858
+ if (j >= b.length) return false; // must consume at least one token
859
+ starAi = i++; // remember where '>' is
860
+ starAj = ++j; // gobble one token from B
861
+ continue;
862
+ }
863
+ if (tokB === ">") {
864
+ if (j !== b.length - 1) return false; // same rule for patternB
865
+ if (i >= a.length) return false;
866
+ starBi = j++;
867
+ starBj = ++i;
868
+ continue;
869
+ }
870
+
871
+ /*───────────────────────────── back‑track using last '>' ───────────────────────*/
872
+ if (starAi !== -1) { // let patternA's '>' absorb one more token of B
873
+ j = ++starAj;
874
+ continue;
875
+ }
876
+ if (starBi !== -1) { // let patternB's '>' absorb one more token of A
877
+ i = ++starBj;
878
+ continue;
879
+ }
880
+
881
+ /*────────────────────────────────── dead‑end ───────────────────────────────────*/
882
+ return false;
883
+ }
884
+
885
+ return true;
886
+ }
887
+
792
888
  sleep(ms) {
793
889
  return new Promise(resolve => setTimeout(resolve, ms));
794
890
  }
@@ -927,6 +1023,14 @@ ${secret}
927
1023
  return null;
928
1024
  }
929
1025
  }
1026
+
1027
+ testPatternMatcher(){
1028
+ if(process.env.NODE_ENV == "test"){
1029
+ return this.#topicPatternMatcher.bind(this)
1030
+ }else{
1031
+ return null;
1032
+ }
1033
+ }
930
1034
  }
931
1035
 
932
1036
  export const CONNECTED = "CONNECTED";
package/tests/test.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Realtime, CONNECTED, RECONNECT, DISCONNECTED, MESSAGE_RESEND } from "../realtime/realtime.js";
2
- import axios from "axios";
3
2
  import { test, before, after } from 'node:test';
4
3
  import assert from 'node:assert';
5
4
 
@@ -8,8 +7,8 @@ let realTimeEnabled;
8
7
  before(async () => {
9
8
  // Start server for testing. Run local server!!
10
9
  realTimeEnabled = new Realtime({
11
- api_key: process.env.user_key,
12
- secret: process.env.secret
10
+ api_key: process.env.AUTH_JWT,
11
+ secret: process.env.AUTH_SECRET
13
12
  });
14
13
  await realTimeEnabled.init(false, {
15
14
  debug: true
@@ -55,8 +54,8 @@ test("No creds in constructor", async () => {
55
54
 
56
55
  test('init() function test', async () => {
57
56
  var realtime = new Realtime({
58
- api_key: process.env.user_key,
59
- secret: process.env.secret
57
+ api_key: process.env.AUTH_JWT,
58
+ secret: process.env.AUTH_SECRET
60
59
  });
61
60
  await realtime.init(true);
62
61
 
@@ -148,8 +147,8 @@ test("Retry method test", async () => {
148
147
 
149
148
  test("get publish retry count test based in init()", async () => {
150
149
  var realtime = new Realtime({
151
- api_key: process.env.user_key,
152
- secret: process.env.secret
150
+ api_key: process.env.AUTH_JWT,
151
+ secret: process.env.AUTH_SECRET
153
152
  });
154
153
 
155
154
  await realtime.init({
@@ -271,8 +270,8 @@ test("Testing publish(topic, data) with invalid inputs", async () => {
271
270
 
272
271
  test("on() test", async () => {
273
272
  var realtime = new Realtime({
274
- api_key: process.env.user_key,
275
- secret: process.env.secret
273
+ api_key: process.env.AUTH_JWT,
274
+ secret: process.env.AUTH_SECRET
276
275
  });
277
276
 
278
277
  await assert.rejects(async () => {
@@ -349,8 +348,8 @@ test("on() test", async () => {
349
348
 
350
349
  test("off() test", async () => {
351
350
  var realtime = new Realtime({
352
- api_key: process.env.user_key,
353
- secret: process.env.secret
351
+ api_key: process.env.AUTH_JWT,
352
+ secret: process.env.AUTH_SECRET
354
353
  });
355
354
 
356
355
  await assert.rejects(async () => {
@@ -397,8 +396,8 @@ test("off() test", async () => {
397
396
 
398
397
  test("Get stream name test", () => {
399
398
  var realtime = new Realtime({
400
- api_key: process.env.user_key,
401
- secret: process.env.secret
399
+ api_key: process.env.AUTH_JWT,
400
+ secret: process.env.AUTH_SECRET
402
401
  });
403
402
 
404
403
  realtime.namespace = "spacex-dragon-program"
@@ -449,10 +448,82 @@ test("Test isTopicValidMethod()", () => {
449
448
  assert.strictEqual(valid, false);
450
449
  });
451
450
 
452
- var unreservedValidTopics = ["hello", "test-room", "heyyyyy", "room-connect"];
451
+ unreservedInvalidTopics = [
452
+ "$foo",
453
+ "foo$",
454
+ "foo.$.bar",
455
+ "foo..bar",
456
+ ".foo",
457
+ "foo.",
458
+ "foo.>.bar",
459
+ ">foo",
460
+ "foo>bar",
461
+ "foo.>bar",
462
+ "foo.bar.>.",
463
+ "foo bar",
464
+ "foo/bar",
465
+ "foo#bar",
466
+ "",
467
+ " ",
468
+ "..",
469
+ ".>",
470
+ "foo..",
471
+ ".",
472
+ ">.",
473
+ "foo,baz",
474
+ "αbeta",
475
+ "foo|bar",
476
+ "foo;bar",
477
+ "foo:bar",
478
+ "foo%bar",
479
+ "foo.*.>.bar",
480
+ "foo.*.>.",
481
+ "foo.*..bar",
482
+ "foo.>.bar",
483
+ "foo>"
484
+ ];
485
+
486
+ unreservedInvalidTopics.forEach(topic => {
487
+ var valid = realTimeEnabled.isTopicValid(topic);
488
+ assert.strictEqual(valid, false);
489
+ });
490
+
491
+ var unreservedValidTopics = [
492
+ "foo",
493
+ "foo.bar",
494
+ "foo.bar.baz",
495
+ "*",
496
+ "foo.*",
497
+ "*.bar",
498
+ "foo.*.baz",
499
+ ">",
500
+ "foo.>",
501
+ "foo.bar.>",
502
+ "*.*.>",
503
+ "alpha_beta",
504
+ "alpha-beta",
505
+ "alpha~beta",
506
+ "abc123",
507
+ "123abc",
508
+ "~",
509
+ "alpha.*.>",
510
+ "alpha.*",
511
+ "alpha.*.*",
512
+ "-foo",
513
+ "foo_bar-baz~qux",
514
+ "A.B.C",
515
+ "sensor.temperature",
516
+ "metric.cpu.load",
517
+ "foo.*.*",
518
+ "foo.*.>",
519
+ "foo_bar.*",
520
+ "*.*",
521
+ "metrics.>"
522
+ ];
453
523
 
454
524
  unreservedValidTopics.forEach(topic => {
455
525
  var valid = realTimeEnabled.isTopicValid(topic);
526
+ console.log(topic)
456
527
  assert.strictEqual(valid, true);
457
528
  });
458
529
  });
@@ -511,4 +582,77 @@ test("History test", async () => {
511
582
  },
512
583
  new Error("$start must be a Date object"),
513
584
  "Expected error was not thrown");
585
+ })
586
+
587
+ test("Pattern matcher test", async () => {
588
+ var cases = [
589
+ ["foo", "foo", true], // 1
590
+ ["foo", "bar", false], // 2
591
+ ["foo.*", "foo.bar", true], // 3
592
+ ["foo.bar", "foo.*", true], // 4
593
+ ["*", "token", true], // 5
594
+ ["*", "*", true], // 6
595
+ ["foo.*", "foo.bar.baz", false], // 7
596
+ ["foo.>", "foo.bar.baz", true], // 8
597
+ ["foo.>", "foo", false], // 9 (zero‑token '>' now invalid)
598
+ ["foo.bar.baz", "foo.>", true], // 10
599
+ ["foo.bar.>", "foo.bar", false], // 11
600
+ ["foo", "foo.>", false], // 12
601
+ ["foo.*.>", "foo.bar.baz.qux", true], // 13
602
+ ["foo.*.baz", "foo.bar.>", true], // 14
603
+ ["alpha.*", "beta.gamma", false], // 15
604
+ ["alpha.beta", "alpha.*.*", false], // 16
605
+ ["foo.>.bar", "foo.any.bar", false], // 17 ('>' mid‑pattern)
606
+ [">", "foo.bar", true], // 18
607
+ [">", ">", true], // 19
608
+ ["*", ">", true], // 20
609
+ ["*.>", "foo.bar", true], // 21
610
+ ["*.*.*", "a.b.c", true], // 22
611
+ ["*.*.*", "a.b", false], // 23
612
+ ["a.b.c.d.e", "a.b.c.d.e", true], // 24
613
+ ["a.b.c.d.e", "a.b.c.d.f", false], // 25
614
+ ["a.b.*.d", "a.b.c.d", true], // 26
615
+ ["a.b.*.d", "a.b.c.e", false], // 27
616
+ ["a.b.>", "a.b", false], // 28
617
+ ["a.b", "a.b.c.d.>", false], // 29
618
+ ["a.b.>.c", "a.b.x.c", false], // 30
619
+ ["a.*.*", "a.b", false], // 31
620
+ ["a.*", "a.b.c", false], // 32
621
+ ["metrics.cpu.load", "metrics.*.load", true], // 33
622
+ ["metrics.cpu.load", "metrics.cpu.*", true], // 34
623
+ ["metrics.cpu.load", "metrics.>.load", false], // 35
624
+ ["metrics.>", "metrics", false], // 36
625
+ ["metrics.>", "othermetrics.cpu", false], // 37
626
+ ["*.*.>", "a.b", false], // 38
627
+ ["*.*.>", "a.b.c.d", true], // 39
628
+ ["a.b.c", "*.*.*", true], // 40
629
+ ["a.b.c", "*.*", false], // 41
630
+ ["alpha.*.>", "alpha", false], // 42
631
+ ["alpha.*.>", "alpha.beta", false], // 43
632
+ ["alpha.*.>", "alpha.beta.gamma", true], // 44
633
+ ["alpha.*.>", "beta.alpha.gamma", false], // 45
634
+ ["foo-bar_baz", "foo-bar_baz", true], // 46
635
+ ["foo-bar_*", "foo-bar_123", false], // 47 ( '*' here is literal )
636
+ ["foo-bar_*", "foo-bar_*", true], // 48
637
+ ["order-*", "order-123", false], // 49
638
+ ["hello.hey.*", "hello.hey.>", true] // 50
639
+ ];
640
+
641
+ var realtime = new Realtime({
642
+ api_key: process.env.AUTH_JWT,
643
+ secret: process.env.AUTH_SECRET
644
+ });
645
+
646
+ var patternMatcher = realtime.testPatternMatcher();
647
+
648
+ cases.forEach(testCase => {
649
+ var tokenA = testCase[0];
650
+ var tokenB = testCase[1];
651
+ var expectedResult = testCase[2];
652
+
653
+ console.log(`${tokenA} ⇆ ${tokenB} → ${expectedResult}`)
654
+
655
+ var result = patternMatcher(tokenA, tokenB)
656
+ assert.strictEqual(expectedResult, result)
657
+ });
514
658
  })