relayx-webjs 1.0.5 → 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 -94
- package/realtime/utils.js +113 -0
- package/tests/test_kv.js +679 -0
- package/tests/test_queue.js +568 -0
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,71 +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 (staging !== undefined || staging !== null){
|
|
119
|
-
this.#baseUrl = staging ? [
|
|
120
|
-
"nats://0.0.0.0:4421",
|
|
121
|
-
"nats://0.0.0.0:4422",
|
|
122
|
-
"nats://0.0.0.0:4423"
|
|
123
|
-
] :
|
|
124
|
-
[
|
|
125
|
-
`wss://api.relay-x.io:4421`,
|
|
126
|
-
`wss://api.relay-x.io:4422`,
|
|
127
|
-
`wss://api.relay-x.io:4423`
|
|
128
|
-
];
|
|
129
|
-
}else{
|
|
130
|
-
this.#baseUrl = [
|
|
131
|
-
`wss://api.relay-x.io:4421`,
|
|
132
|
-
`wss://api.relay-x.io:4422`,
|
|
133
|
-
`wss://api.relay-x.io:4423`
|
|
134
|
-
];
|
|
135
|
-
}
|
|
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
|
+
];
|
|
136
97
|
|
|
137
98
|
this.#log(this.#baseUrl);
|
|
138
|
-
this.#log(opts);
|
|
99
|
+
this.#log(this.opts);
|
|
139
100
|
}
|
|
140
101
|
|
|
141
102
|
/**
|
|
@@ -199,8 +160,16 @@ export class Realtime {
|
|
|
199
160
|
this.connected = true;
|
|
200
161
|
this.#connectCalled = true;
|
|
201
162
|
}catch(err){
|
|
202
|
-
this.#
|
|
203
|
-
|
|
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
|
+
}
|
|
204
173
|
|
|
205
174
|
this.connected = false;
|
|
206
175
|
}
|
|
@@ -255,7 +224,11 @@ export class Realtime {
|
|
|
255
224
|
this.#publishMessagesOnReconnect();
|
|
256
225
|
break;
|
|
257
226
|
case Events.Error:
|
|
258
|
-
|
|
227
|
+
if(s.data == "NATS_PROTOCOL_ERR"){
|
|
228
|
+
console.log("User kicked off network by account admin!")
|
|
229
|
+
|
|
230
|
+
await this.#natsClient.close();
|
|
231
|
+
}
|
|
259
232
|
break;
|
|
260
233
|
case DebugEvents.Reconnecting:
|
|
261
234
|
this.#log("client is attempting to reconnect");
|
|
@@ -270,8 +243,6 @@ export class Realtime {
|
|
|
270
243
|
case DebugEvents.StaleConnection:
|
|
271
244
|
this.#log("client has a stale connection");
|
|
272
245
|
break;
|
|
273
|
-
default:
|
|
274
|
-
this.#log(`got an unknown status ${s.type}`);
|
|
275
246
|
}
|
|
276
247
|
}
|
|
277
248
|
})().then();
|
|
@@ -283,7 +254,7 @@ export class Realtime {
|
|
|
283
254
|
// Callback on client side
|
|
284
255
|
if (CONNECTED in this.#event_func){
|
|
285
256
|
if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
|
|
286
|
-
this.#event_func[CONNECTED]()
|
|
257
|
+
this.#event_func[CONNECTED](true)
|
|
287
258
|
}
|
|
288
259
|
}
|
|
289
260
|
}
|
|
@@ -314,7 +285,15 @@ export class Realtime {
|
|
|
314
285
|
async #subscribeToTopics(){
|
|
315
286
|
this.#topicMap.forEach(async (topic) => {
|
|
316
287
|
// Subscribe to stream
|
|
317
|
-
|
|
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
|
+
}
|
|
318
297
|
});
|
|
319
298
|
}
|
|
320
299
|
|
|
@@ -389,7 +368,15 @@ export class Realtime {
|
|
|
389
368
|
|
|
390
369
|
if(this.connected){
|
|
391
370
|
// Connected we need to create a topic in a stream
|
|
392
|
-
|
|
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
|
+
}
|
|
393
380
|
}
|
|
394
381
|
}
|
|
395
382
|
|
|
@@ -441,12 +428,22 @@ export class Realtime {
|
|
|
441
428
|
|
|
442
429
|
this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
|
|
443
430
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
447
|
|
|
451
448
|
return ack !== null && ack !== undefined;
|
|
452
449
|
}else{
|
|
@@ -553,6 +550,51 @@ export class Realtime {
|
|
|
553
550
|
return history;
|
|
554
551
|
}
|
|
555
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
|
+
|
|
556
598
|
/**
|
|
557
599
|
* Method resends messages when the client successfully connects to the
|
|
558
600
|
* server again
|
|
@@ -595,7 +637,7 @@ export class Realtime {
|
|
|
595
637
|
|
|
596
638
|
var opts = {
|
|
597
639
|
name: consumerName,
|
|
598
|
-
filter_subjects:
|
|
640
|
+
filter_subjects: this.#getStreamTopic(topic),
|
|
599
641
|
replay_policy: ReplayPolicy.Instant,
|
|
600
642
|
opt_start_time: new Date(),
|
|
601
643
|
ack_policy: AckPolicy.Explicit,
|
|
@@ -603,7 +645,6 @@ export class Realtime {
|
|
|
603
645
|
}
|
|
604
646
|
|
|
605
647
|
const consumer = await this.#jetstream.consumers.get(this.#getStreamName(), opts);
|
|
606
|
-
this.#log(this.#topicMap)
|
|
607
648
|
|
|
608
649
|
this.#consumerMap[topic] = consumer;
|
|
609
650
|
|
|
@@ -641,7 +682,7 @@ export class Realtime {
|
|
|
641
682
|
}
|
|
642
683
|
}
|
|
643
684
|
});
|
|
644
|
-
this.#log(
|
|
685
|
+
this.#log(`Consumer is consuming for => ${topic}`);
|
|
645
686
|
}
|
|
646
687
|
|
|
647
688
|
/**
|
|
@@ -714,6 +755,10 @@ export class Realtime {
|
|
|
714
755
|
return this.#natsClient?.info?.client_id
|
|
715
756
|
}
|
|
716
757
|
|
|
758
|
+
#checkVarOk(variable){
|
|
759
|
+
return variable !== null && variable !== undefined
|
|
760
|
+
}
|
|
761
|
+
|
|
717
762
|
async #pushLatencyData(data){
|
|
718
763
|
this.#isSendingLatency = true;
|
|
719
764
|
|
|
@@ -853,20 +898,7 @@ export class Realtime {
|
|
|
853
898
|
while (i < a.length || j < b.length) {
|
|
854
899
|
const tokA = a[i];
|
|
855
900
|
const tokB = b[j];
|
|
856
|
-
|
|
857
|
-
/*──────────── literal match or single‑token wildcard on either side ────────────*/
|
|
858
|
-
const singleWildcard =
|
|
859
|
-
(tokA === "*" && j < b.length) ||
|
|
860
|
-
(tokB === "*" && i < a.length);
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
(tokA !== undefined && tokA === tokB) ||
|
|
864
|
-
singleWildcard
|
|
865
|
-
) {
|
|
866
|
-
i++; j++;
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
|
|
901
|
+
|
|
870
902
|
/*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
|
|
871
903
|
if (tokA === ">") {
|
|
872
904
|
if (i !== a.length - 1) return false; // '>' not in last position → invalid
|
|
@@ -883,6 +915,19 @@ export class Realtime {
|
|
|
883
915
|
continue;
|
|
884
916
|
}
|
|
885
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
|
+
|
|
886
931
|
/*───────────────────────────── back‑track using last '>' ───────────────────────*/
|
|
887
932
|
if (starAi !== -1) { // let patternA's '>' absorb one more token of B
|
|
888
933
|
j = ++starAj;
|
|
@@ -1046,6 +1091,14 @@ ${secret}
|
|
|
1046
1091
|
return null;
|
|
1047
1092
|
}
|
|
1048
1093
|
}
|
|
1094
|
+
|
|
1095
|
+
testGetJetstream(){
|
|
1096
|
+
if(process.env.NODE_ENV == "test"){
|
|
1097
|
+
return this.#jetstream;
|
|
1098
|
+
}else{
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1049
1102
|
}
|
|
1050
1103
|
|
|
1051
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
|
+
}
|