relayx-webjs 1.0.4 → 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.
- package/.claude/settings.local.json +10 -0
- package/CHANGELOG.md +3 -0
- package/LICENSE +1 -12
- package/README.md +133 -183
- package/example/stand-alone/example_chat.js +6 -3
- package/package.json +6 -3
- package/realtime/kv_storage.js +195 -0
- package/realtime/models/message.js +26 -0
- package/realtime/queue.js +660 -0
- package/realtime/realtime.js +147 -98
- package/realtime/utils.js +113 -0
- package/tests/test_kv.js +679 -0
- package/tests/test_queue.js +568 -0
- package/tests/test.js +0 -658
package/realtime/realtime.js
CHANGED
|
@@ -2,6 +2,9 @@ import { connect, JSONCodec, Events, DebugEvents, AckPolicy, ReplayPolicy, creds
|
|
|
2
2
|
import { DeliverPolicy, jetstream } from "@nats-io/jetstream";
|
|
3
3
|
import { encode, decode } from "@msgpack/msgpack";
|
|
4
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import { Queue } from "./queue.js";
|
|
6
|
+
import { ErrorLogging } from "./utils.js";
|
|
7
|
+
import { KVStore } from "./kv_storage.js";
|
|
5
8
|
|
|
6
9
|
export class Realtime {
|
|
7
10
|
|
|
@@ -13,6 +16,10 @@ export class Realtime {
|
|
|
13
16
|
#consumerMap = {};
|
|
14
17
|
#consumer = null;
|
|
15
18
|
|
|
19
|
+
#kvStore = null;
|
|
20
|
+
|
|
21
|
+
#errorLogging = null;
|
|
22
|
+
|
|
16
23
|
#event_func = {};
|
|
17
24
|
#topicMap = [];
|
|
18
25
|
|
|
@@ -71,75 +78,25 @@ export class Realtime {
|
|
|
71
78
|
/*
|
|
72
79
|
Initializes library with configuration options.
|
|
73
80
|
*/
|
|
74
|
-
async init(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
117
|
-
|
|
118
|
-
if(process.env.PROXY){
|
|
119
|
-
this.#baseUrl = ["wss://api2.relay-x.io:8666"];
|
|
120
|
-
}else{
|
|
121
|
-
if (staging !== undefined || staging !== null){
|
|
122
|
-
this.#baseUrl = staging ? [
|
|
123
|
-
"nats://0.0.0.0:4421",
|
|
124
|
-
"nats://0.0.0.0:4422",
|
|
125
|
-
"nats://0.0.0.0:4423"
|
|
126
|
-
] :
|
|
127
|
-
[
|
|
128
|
-
`wss://api.relay-x.io:4421`,
|
|
129
|
-
`wss://api.relay-x.io:4422`,
|
|
130
|
-
`wss://api.relay-x.io:4423`
|
|
131
|
-
];
|
|
132
|
-
}else{
|
|
133
|
-
this.#baseUrl = [
|
|
134
|
-
`wss://api.relay-x.io:4421`,
|
|
135
|
-
`wss://api.relay-x.io:4422`,
|
|
136
|
-
`wss://api.relay-x.io:4423`
|
|
137
|
-
];
|
|
138
|
-
}
|
|
139
|
-
}
|
|
81
|
+
async init(data){
|
|
82
|
+
this.#errorLogging = new ErrorLogging();
|
|
83
|
+
|
|
84
|
+
this.staging = this.#checkVarOk(data.staging) && typeof data.staging == "boolean" ? data.staging : false;
|
|
85
|
+
this.opts = data.opts;
|
|
86
|
+
|
|
87
|
+
this.#baseUrl = this.staging ? [
|
|
88
|
+
"nats://0.0.0.0:4421",
|
|
89
|
+
"nats://0.0.0.0:4422",
|
|
90
|
+
"nats://0.0.0.0:4423"
|
|
91
|
+
] :
|
|
92
|
+
[
|
|
93
|
+
`wss://api.relay-x.io:4421`,
|
|
94
|
+
`wss://api.relay-x.io:4422`,
|
|
95
|
+
`wss://api.relay-x.io:4423`
|
|
96
|
+
];
|
|
140
97
|
|
|
141
98
|
this.#log(this.#baseUrl);
|
|
142
|
-
this.#log(opts);
|
|
99
|
+
this.#log(this.opts);
|
|
143
100
|
}
|
|
144
101
|
|
|
145
102
|
/**
|
|
@@ -203,8 +160,16 @@ export class Realtime {
|
|
|
203
160
|
this.connected = true;
|
|
204
161
|
this.#connectCalled = true;
|
|
205
162
|
}catch(err){
|
|
206
|
-
this.#
|
|
207
|
-
|
|
163
|
+
this.#errorLogging.logError({
|
|
164
|
+
err: err
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Callback on client side
|
|
168
|
+
if (CONNECTED in this.#event_func){
|
|
169
|
+
if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
|
|
170
|
+
this.#event_func[CONNECTED](false)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
208
173
|
|
|
209
174
|
this.connected = false;
|
|
210
175
|
}
|
|
@@ -259,7 +224,11 @@ export class Realtime {
|
|
|
259
224
|
this.#publishMessagesOnReconnect();
|
|
260
225
|
break;
|
|
261
226
|
case Events.Error:
|
|
262
|
-
|
|
227
|
+
if(s.data == "NATS_PROTOCOL_ERR"){
|
|
228
|
+
console.log("User kicked off network by account admin!")
|
|
229
|
+
|
|
230
|
+
await this.#natsClient.close();
|
|
231
|
+
}
|
|
263
232
|
break;
|
|
264
233
|
case DebugEvents.Reconnecting:
|
|
265
234
|
this.#log("client is attempting to reconnect");
|
|
@@ -274,8 +243,6 @@ export class Realtime {
|
|
|
274
243
|
case DebugEvents.StaleConnection:
|
|
275
244
|
this.#log("client has a stale connection");
|
|
276
245
|
break;
|
|
277
|
-
default:
|
|
278
|
-
this.#log(`got an unknown status ${s.type}`);
|
|
279
246
|
}
|
|
280
247
|
}
|
|
281
248
|
})().then();
|
|
@@ -287,7 +254,7 @@ export class Realtime {
|
|
|
287
254
|
// Callback on client side
|
|
288
255
|
if (CONNECTED in this.#event_func){
|
|
289
256
|
if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
|
|
290
|
-
this.#event_func[CONNECTED]()
|
|
257
|
+
this.#event_func[CONNECTED](true)
|
|
291
258
|
}
|
|
292
259
|
}
|
|
293
260
|
}
|
|
@@ -318,7 +285,15 @@ export class Realtime {
|
|
|
318
285
|
async #subscribeToTopics(){
|
|
319
286
|
this.#topicMap.forEach(async (topic) => {
|
|
320
287
|
// Subscribe to stream
|
|
321
|
-
|
|
288
|
+
try{
|
|
289
|
+
await this.#startConsumer(topic);
|
|
290
|
+
}catch(err){
|
|
291
|
+
this.#errorLogging.logError({
|
|
292
|
+
err: err,
|
|
293
|
+
topic: topic,
|
|
294
|
+
op: "subscribe"
|
|
295
|
+
})
|
|
296
|
+
}
|
|
322
297
|
});
|
|
323
298
|
}
|
|
324
299
|
|
|
@@ -393,7 +368,15 @@ export class Realtime {
|
|
|
393
368
|
|
|
394
369
|
if(this.connected){
|
|
395
370
|
// Connected we need to create a topic in a stream
|
|
396
|
-
|
|
371
|
+
try{
|
|
372
|
+
await this.#startConsumer(topic);
|
|
373
|
+
}catch(err){
|
|
374
|
+
this.#errorLogging.logError({
|
|
375
|
+
err: err,
|
|
376
|
+
topic: topic,
|
|
377
|
+
op: "subscribe"
|
|
378
|
+
})
|
|
379
|
+
}
|
|
397
380
|
}
|
|
398
381
|
}
|
|
399
382
|
|
|
@@ -445,12 +428,22 @@ export class Realtime {
|
|
|
445
428
|
|
|
446
429
|
this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
|
|
447
430
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
431
|
+
var ack = null;
|
|
432
|
+
|
|
433
|
+
try{
|
|
434
|
+
ack = await this.#jetstream.publish(this.#getStreamTopic(topic), encodedMessage);
|
|
435
|
+
this.#log(`Publish Ack =>`)
|
|
436
|
+
this.#log(ack)
|
|
437
|
+
|
|
438
|
+
var latency = Date.now() - start;
|
|
439
|
+
this.#log(`Latency => ${latency} ms`);
|
|
440
|
+
}catch(err){
|
|
441
|
+
this.#errorLogging.logError({
|
|
442
|
+
err: err,
|
|
443
|
+
topic: topic,
|
|
444
|
+
op: "publish"
|
|
445
|
+
})
|
|
446
|
+
}
|
|
454
447
|
|
|
455
448
|
return ack !== null && ack !== undefined;
|
|
456
449
|
}else{
|
|
@@ -557,6 +550,51 @@ export class Realtime {
|
|
|
557
550
|
return history;
|
|
558
551
|
}
|
|
559
552
|
|
|
553
|
+
// Queue Functions
|
|
554
|
+
async initQueue(queueID){
|
|
555
|
+
if(!this.connected){
|
|
556
|
+
this.#log("Not connected to relayX network. Skipping queue init")
|
|
557
|
+
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
this.#log("Validating queue ID...")
|
|
562
|
+
if(queueID == undefined || queueID == null || queueID == ""){
|
|
563
|
+
throw new Error("$queueID cannot be null / undefined / empty!")
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
var queue = new Queue({
|
|
567
|
+
jetstream: this.#jetstream,
|
|
568
|
+
nats_client: this.#natsClient,
|
|
569
|
+
api_key: this.api_key,
|
|
570
|
+
debug: this.opts?.debug ? this.opts?.debug : false
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
var initResult = await queue.init(queueID);
|
|
574
|
+
|
|
575
|
+
return initResult ? queue : null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// KV Functions
|
|
579
|
+
async initKVStore(){
|
|
580
|
+
|
|
581
|
+
if(this.#kvStore == null){
|
|
582
|
+
var debugCheck = this.opts.debug !== null && this.opts.debug !== undefined && typeof this.opts.debug == "boolean"
|
|
583
|
+
|
|
584
|
+
this.#kvStore = new KVStore({
|
|
585
|
+
namespace: this.namespace,
|
|
586
|
+
jetstream: this.#jetstream,
|
|
587
|
+
debug: debugCheck ? this.opts.debug : false
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
var init = await this.#kvStore.init()
|
|
591
|
+
|
|
592
|
+
return init ? this.#kvStore : null;
|
|
593
|
+
}else{
|
|
594
|
+
return this.#kvStore
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
560
598
|
/**
|
|
561
599
|
* Method resends messages when the client successfully connects to the
|
|
562
600
|
* server again
|
|
@@ -599,7 +637,7 @@ export class Realtime {
|
|
|
599
637
|
|
|
600
638
|
var opts = {
|
|
601
639
|
name: consumerName,
|
|
602
|
-
filter_subjects:
|
|
640
|
+
filter_subjects: this.#getStreamTopic(topic),
|
|
603
641
|
replay_policy: ReplayPolicy.Instant,
|
|
604
642
|
opt_start_time: new Date(),
|
|
605
643
|
ack_policy: AckPolicy.Explicit,
|
|
@@ -607,7 +645,6 @@ export class Realtime {
|
|
|
607
645
|
}
|
|
608
646
|
|
|
609
647
|
const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
|
|
610
|
-
this.#log(this.#topicMap)
|
|
611
648
|
|
|
612
649
|
this.#consumerMap[topic] = consumer;
|
|
613
650
|
|
|
@@ -645,7 +682,7 @@ export class Realtime {
|
|
|
645
682
|
}
|
|
646
683
|
}
|
|
647
684
|
});
|
|
648
|
-
this.#log(
|
|
685
|
+
this.#log(`Consumer is consuming for => ${topic}`);
|
|
649
686
|
}
|
|
650
687
|
|
|
651
688
|
/**
|
|
@@ -718,6 +755,10 @@ export class Realtime {
|
|
|
718
755
|
return this.#natsClient?.info?.client_id
|
|
719
756
|
}
|
|
720
757
|
|
|
758
|
+
#checkVarOk(variable){
|
|
759
|
+
return variable !== null && variable !== undefined
|
|
760
|
+
}
|
|
761
|
+
|
|
721
762
|
async #pushLatencyData(data){
|
|
722
763
|
this.#isSendingLatency = true;
|
|
723
764
|
|
|
@@ -857,20 +898,7 @@ export class Realtime {
|
|
|
857
898
|
while (i < a.length || j < b.length) {
|
|
858
899
|
const tokA = a[i];
|
|
859
900
|
const tokB = b[j];
|
|
860
|
-
|
|
861
|
-
/*──────────── literal match or single‑token wildcard on either side ────────────*/
|
|
862
|
-
const singleWildcard =
|
|
863
|
-
(tokA === "*" && j < b.length) ||
|
|
864
|
-
(tokB === "*" && i < a.length);
|
|
865
|
-
|
|
866
|
-
if (
|
|
867
|
-
(tokA !== undefined && tokA === tokB) ||
|
|
868
|
-
singleWildcard
|
|
869
|
-
) {
|
|
870
|
-
i++; j++;
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
|
|
901
|
+
|
|
874
902
|
/*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
|
|
875
903
|
if (tokA === ">") {
|
|
876
904
|
if (i !== a.length - 1) return false; // '>' not in last position → invalid
|
|
@@ -887,6 +915,19 @@ export class Realtime {
|
|
|
887
915
|
continue;
|
|
888
916
|
}
|
|
889
917
|
|
|
918
|
+
/*──────────── literal match or single‑token wildcard on either side ────────────*/
|
|
919
|
+
const singleWildcard =
|
|
920
|
+
(tokA === "*" && j < b.length) ||
|
|
921
|
+
(tokB === "*" && i < a.length);
|
|
922
|
+
|
|
923
|
+
if (
|
|
924
|
+
(tokA !== undefined && tokA === tokB) ||
|
|
925
|
+
singleWildcard
|
|
926
|
+
) {
|
|
927
|
+
i++; j++;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
890
931
|
/*───────────────────────────── back‑track using last '>' ───────────────────────*/
|
|
891
932
|
if (starAi !== -1) { // let patternA's '>' absorb one more token of B
|
|
892
933
|
j = ++starAj;
|
|
@@ -1050,6 +1091,14 @@ ${secret}
|
|
|
1050
1091
|
return null;
|
|
1051
1092
|
}
|
|
1052
1093
|
}
|
|
1094
|
+
|
|
1095
|
+
testGetJetstream(){
|
|
1096
|
+
if(process.env.NODE_ENV == "test"){
|
|
1097
|
+
return this.#jetstream;
|
|
1098
|
+
}else{
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1053
1102
|
}
|
|
1054
1103
|
|
|
1055
1104
|
export const CONNECTED = "CONNECTED";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { JetStreamApiError, JetStreamError } from "@nats-io/jetstream";
|
|
2
|
+
|
|
3
|
+
export class ErrorLogging {
|
|
4
|
+
|
|
5
|
+
logError(data){
|
|
6
|
+
var err = data.err;
|
|
7
|
+
|
|
8
|
+
if(err instanceof JetStreamApiError){
|
|
9
|
+
var code = err.code;
|
|
10
|
+
|
|
11
|
+
if(code == 10077){
|
|
12
|
+
// Code 10077 is for message limit exceeded
|
|
13
|
+
console.table({
|
|
14
|
+
Event: "Message Limit Exceeded",
|
|
15
|
+
Description: "Current message count for account exceeds plan defined limits. Upgrade plan to remove limits",
|
|
16
|
+
Link: "https://console.relay-x.io/billing"
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
throw new Error("Message limit exceeded!")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if(err instanceof JetStreamError){
|
|
24
|
+
var code = err.code;
|
|
25
|
+
|
|
26
|
+
if(code == 409){
|
|
27
|
+
// Consumer deleted
|
|
28
|
+
|
|
29
|
+
console.table({
|
|
30
|
+
Event: "Consumer Manually Deleted!",
|
|
31
|
+
Description: "Consumer was manually deleted by user using deleteConsumer() or the library equivalent",
|
|
32
|
+
"Docs to Solve Issue": "https://docs.relay-x.io/docs/detailed_doc/NodeJS/queue_consume#deleting-a-consumer"
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if(err.name == "NatsError"){
|
|
38
|
+
var code = err.code;
|
|
39
|
+
var chainedError = err.chainedError;
|
|
40
|
+
var permissionContext = err.permissionContext;
|
|
41
|
+
var userOp = data.op;
|
|
42
|
+
|
|
43
|
+
if(code == "PERMISSIONS_VIOLATION"){
|
|
44
|
+
if(userOp == "publish"){
|
|
45
|
+
console.table({
|
|
46
|
+
Event: "Publish Permissions Violation",
|
|
47
|
+
Description: `User is not permitted to publish on '${data.topic}'`,
|
|
48
|
+
Topic: data.topic,
|
|
49
|
+
"Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#messaging--publish-permissions"
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
throw new Error(`User is not permitted to publish on '${data.topic}'`)
|
|
53
|
+
}else if(userOp == "subscribe"){
|
|
54
|
+
console.table({
|
|
55
|
+
Event: "Subscribe Permissions Violation",
|
|
56
|
+
Description: `User is not permitted to subscribe to '${data.topic}'`,
|
|
57
|
+
Topic: data.topic,
|
|
58
|
+
"Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#messaging--subscribe-permissions"
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
throw new Error(`User is not permitted to subscribe to '${data.topic}'`)
|
|
62
|
+
}else if(userOp == "kv_write"){
|
|
63
|
+
console.table({
|
|
64
|
+
Event: "KV Write Failure",
|
|
65
|
+
Description: `User is not permitted to write to KV Store`,
|
|
66
|
+
"Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#write-permission"
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
throw new Error(`User is not permitted to write to KV Store`)
|
|
70
|
+
}else if(userOp == "kv_read"){
|
|
71
|
+
console.table({
|
|
72
|
+
Event: "KV Read Failure",
|
|
73
|
+
Description: `User is not permitted to read from KV Store`,
|
|
74
|
+
"Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#read-permission"
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
throw new Error(`User is not permitted to read from KV Store`)
|
|
78
|
+
}else if(userOp == "kv_delete"){
|
|
79
|
+
console.table({
|
|
80
|
+
Event: "KV Key Delete Failure",
|
|
81
|
+
Description: `User is not permitted to delete key from KV Store`,
|
|
82
|
+
"Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#write-permission"
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
throw new Error(`User is not permitted to delete key from KV Store`)
|
|
86
|
+
}
|
|
87
|
+
}else if(code == "AUTHORIZATION_VIOLATION"){
|
|
88
|
+
console.table({
|
|
89
|
+
Event: "Authentication Failure",
|
|
90
|
+
Description: `User failed to authenticate. Check if API key exists & if it is enabled`,
|
|
91
|
+
"Docs to Solve Issue": "https://docs.relay-x.io/docs/setup/api_key_permissions#enabling-and-disabling-keys"
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class Logging {
|
|
100
|
+
|
|
101
|
+
#debug = false;
|
|
102
|
+
|
|
103
|
+
constructor(debug){
|
|
104
|
+
this.#debug = debug !== null && debug !== undefined && typeof debug == "boolean" ? debug : false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
log(...msg){
|
|
108
|
+
if(this.#debug){
|
|
109
|
+
console.log(...msg)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
}
|