relayx-webjs 1.0.1 → 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 +4 -0
- package/package.json +1 -1
- package/realtime/realtime.js +131 -29
- package/tests/test.js +136 -60
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/realtime/realtime.js
CHANGED
|
@@ -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.
|
|
303
|
-
|
|
304
|
-
|
|
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: `${
|
|
575
|
-
filter_subjects: [this.#getStreamTopic(
|
|
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
|
-
|
|
589
|
+
this.#consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
|
|
583
590
|
this.#log(this.#topicMap)
|
|
584
591
|
|
|
585
|
-
this.#
|
|
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 =
|
|
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 (
|
|
600
|
-
this.#
|
|
601
|
-
|
|
602
|
-
|
|
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.
|
|
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(
|
|
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,7 @@ 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
|
-
const TOPIC_REGEX = /^(
|
|
746
|
+
const TOPIC_REGEX = /^(?!.*\$)(?:[A-Za-z0-9_*~-]+(?:\.[A-Za-z0-9_*~-]+)*(?:\.>)?|>)$/u;
|
|
738
747
|
|
|
739
748
|
var spaceStarCheck = !topic.includes(" ") && TOPIC_REGEX.test(topic);
|
|
740
749
|
|
|
@@ -791,6 +800,91 @@ export class Realtime {
|
|
|
791
800
|
}
|
|
792
801
|
}
|
|
793
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
|
+
|
|
794
888
|
sleep(ms) {
|
|
795
889
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
796
890
|
}
|
|
@@ -929,6 +1023,14 @@ ${secret}
|
|
|
929
1023
|
return null;
|
|
930
1024
|
}
|
|
931
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
testPatternMatcher(){
|
|
1028
|
+
if(process.env.NODE_ENV == "test"){
|
|
1029
|
+
return this.#topicPatternMatcher.bind(this)
|
|
1030
|
+
}else{
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
932
1034
|
}
|
|
933
1035
|
|
|
934
1036
|
export const CONNECTED = "CONNECTED";
|
package/tests/test.js
CHANGED
|
@@ -449,36 +449,38 @@ test("Test isTopicValidMethod()", () => {
|
|
|
449
449
|
});
|
|
450
450
|
|
|
451
451
|
unreservedInvalidTopics = [
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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>"
|
|
482
484
|
];
|
|
483
485
|
|
|
484
486
|
unreservedInvalidTopics.forEach(topic => {
|
|
@@ -487,40 +489,41 @@ test("Test isTopicValidMethod()", () => {
|
|
|
487
489
|
});
|
|
488
490
|
|
|
489
491
|
var unreservedValidTopics = [
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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.>"
|
|
520
522
|
];
|
|
521
523
|
|
|
522
524
|
unreservedValidTopics.forEach(topic => {
|
|
523
525
|
var valid = realTimeEnabled.isTopicValid(topic);
|
|
526
|
+
console.log(topic)
|
|
524
527
|
assert.strictEqual(valid, true);
|
|
525
528
|
});
|
|
526
529
|
});
|
|
@@ -579,4 +582,77 @@ test("History test", async () => {
|
|
|
579
582
|
},
|
|
580
583
|
new Error("$start must be a Date object"),
|
|
581
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
|
+
});
|
|
582
658
|
})
|