relayx-webjs 1.0.1 → 1.0.3
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/example/chat-app/package.json +2 -1
- package/example/stand-alone/example_chat.js +111 -0
- package/package.json +1 -1
- package/realtime/dns_change.js +20 -0
- package/realtime/realtime.js +205 -82
- package/tests/test.js +136 -60
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Realtime, CONNECTED, RECONNECT, DISCONNECTED, MESSAGE_RESEND } from "../../realtime/realtime.js"
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
async function run(){
|
|
10
|
+
var realtime = new Realtime({
|
|
11
|
+
api_key: process.env.AUTH_JWT,
|
|
12
|
+
secret: process.env.AUTH_SECRET
|
|
13
|
+
});
|
|
14
|
+
await realtime.init(false, {
|
|
15
|
+
max_retries: 2,
|
|
16
|
+
debug: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
realtime.on(CONNECTED, async () => {
|
|
20
|
+
console.log("[IMPL] => CONNECTED!");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
realtime.on(RECONNECT, (status) => {
|
|
24
|
+
console.log(`[IMPL] RECONNECT => ${status}`)
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
realtime.on(DISCONNECTED, () => {
|
|
28
|
+
console.log(`[IMPL] DISONNECT`)
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await realtime.on("power-telemetry", (data) => {
|
|
32
|
+
console.log("power-telemetry", data);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await realtime.on("hello.>", async (data) => {
|
|
36
|
+
console.log("hello.>", data);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// await realtime.on("hello.hey.*", (data) => {
|
|
40
|
+
// console.log("hell.hey.*", data);
|
|
41
|
+
// });
|
|
42
|
+
|
|
43
|
+
// await realtime.on("hello.hey.>", (data) => {
|
|
44
|
+
// console.log("hello.hey.>", data);
|
|
45
|
+
// });
|
|
46
|
+
|
|
47
|
+
realtime.on(MESSAGE_RESEND, (data) => {
|
|
48
|
+
console.log(`[MSG RESEND] => ${data}`)
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
rl.on('line', async (input) => {
|
|
52
|
+
console.log(`You entered: ${input}`);
|
|
53
|
+
|
|
54
|
+
if(input == "exit"){
|
|
55
|
+
var output = await realtime.off("hello");
|
|
56
|
+
console.log(output);
|
|
57
|
+
|
|
58
|
+
realtime.close();
|
|
59
|
+
|
|
60
|
+
process.exit();
|
|
61
|
+
}else if(input == "history"){
|
|
62
|
+
rl.question("topic: ", async (topic) => {
|
|
63
|
+
var start = new Date();
|
|
64
|
+
var past = start.setDate(start.getDate() - 4)
|
|
65
|
+
var pastDate = new Date(past)
|
|
66
|
+
|
|
67
|
+
var end = new Date();
|
|
68
|
+
var past = end.setDate(end.getDate())
|
|
69
|
+
var endDate = new Date(past)
|
|
70
|
+
|
|
71
|
+
var history = await realtime.history(topic, pastDate)
|
|
72
|
+
console.log(history)
|
|
73
|
+
})
|
|
74
|
+
}else if(input == "off"){
|
|
75
|
+
rl.question("topic to off(): ", async (topic) => {
|
|
76
|
+
await realtime.off(topic);
|
|
77
|
+
console.log("off() executed")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
}else if(input == "close"){
|
|
82
|
+
realtime.close();
|
|
83
|
+
console.log("Connection closed");
|
|
84
|
+
}else if(input == "init"){
|
|
85
|
+
await realtime.connect()
|
|
86
|
+
}else if(input == "on"){
|
|
87
|
+
rl.question("topic: ", async (topic) => {
|
|
88
|
+
await realtime.on(topic, (data) => {
|
|
89
|
+
console.log(topic, data);
|
|
90
|
+
});
|
|
91
|
+
})
|
|
92
|
+
}else{
|
|
93
|
+
rl.question("topic: ", async (topic) => {
|
|
94
|
+
var output = await realtime.publish(topic, input);
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
realtime.connect();
|
|
100
|
+
|
|
101
|
+
process.on('SIGINT', async () => {
|
|
102
|
+
console.log('Keyboard interrupt detected (Ctrl+C). Cleaning up...');
|
|
103
|
+
// Perform any necessary cleanup here
|
|
104
|
+
|
|
105
|
+
// Exit the process
|
|
106
|
+
process.exit();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await run();
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import dns from 'node:dns';
|
|
2
|
+
|
|
3
|
+
export function initDNSSpoof(){
|
|
4
|
+
const originalLookup = dns.lookup;
|
|
5
|
+
|
|
6
|
+
// Override for the whole process
|
|
7
|
+
dns.lookup = function patchedLookup(hostname, options, callback) {
|
|
8
|
+
|
|
9
|
+
// ── Our one special case ──────────────────────────────────
|
|
10
|
+
if (hostname === 'api2.relay-x.io') {
|
|
11
|
+
// Map to loop‑back; family 4 avoids ::1
|
|
12
|
+
return process.nextTick(() =>
|
|
13
|
+
callback(null, [{address: '127.0.0.1', family: 4}])
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Anything else → real DNS
|
|
18
|
+
return originalLookup.call(dns, hostname, options, callback);
|
|
19
|
+
};
|
|
20
|
+
}
|
package/realtime/realtime.js
CHANGED
|
@@ -2,6 +2,7 @@ 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 { initDNSSpoof } from "./dns_change.js";
|
|
5
6
|
|
|
6
7
|
export class Realtime {
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ export class Realtime {
|
|
|
11
12
|
#codec = JSONCodec();
|
|
12
13
|
#jetstream = null;
|
|
13
14
|
#consumerMap = {};
|
|
15
|
+
#consumer = null;
|
|
14
16
|
|
|
15
17
|
#event_func = {};
|
|
16
18
|
#topicMap = [];
|
|
@@ -20,6 +22,8 @@ export class Realtime {
|
|
|
20
22
|
#RECONNECTED = "RECONNECTED";
|
|
21
23
|
#RECONN_FAIL = "RECONN_FAIL";
|
|
22
24
|
|
|
25
|
+
#reservedSystemTopics = [CONNECTED, DISCONNECTED, RECONNECT, this.#RECONNECTED, this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT];
|
|
26
|
+
|
|
23
27
|
setRemoteUserAttempts = 0;
|
|
24
28
|
setRemoteUserRetries = 5;
|
|
25
29
|
|
|
@@ -39,6 +43,8 @@ export class Realtime {
|
|
|
39
43
|
|
|
40
44
|
#maxPublishRetries = 5;
|
|
41
45
|
|
|
46
|
+
#connectCalled = false;
|
|
47
|
+
|
|
42
48
|
constructor(config){
|
|
43
49
|
if(typeof config != "object"){
|
|
44
50
|
throw new Error("Realtime($config). $config not object => {}")
|
|
@@ -94,10 +100,13 @@ export class Realtime {
|
|
|
94
100
|
if(arguments[0] instanceof Object){
|
|
95
101
|
opts = arguments[0];
|
|
96
102
|
staging = false;
|
|
97
|
-
}else{
|
|
103
|
+
}else if(typeof arguments[0] == "boolean"){
|
|
98
104
|
opts = {};
|
|
99
105
|
staging = arguments[0];
|
|
100
106
|
this.#log(staging)
|
|
107
|
+
}else{
|
|
108
|
+
opts = {};
|
|
109
|
+
staging = false
|
|
101
110
|
}
|
|
102
111
|
}else{
|
|
103
112
|
staging = false;
|
|
@@ -107,23 +116,28 @@ export class Realtime {
|
|
|
107
116
|
this.staging = staging;
|
|
108
117
|
this.opts = opts;
|
|
109
118
|
|
|
110
|
-
if
|
|
111
|
-
this.#baseUrl =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
if(process.env.PROXY){
|
|
120
|
+
this.#baseUrl = ["wss://api2.relay-x.io:8666"];
|
|
121
|
+
initDNSSpoof();
|
|
122
|
+
}else{
|
|
123
|
+
if (staging !== undefined || staging !== null){
|
|
124
|
+
this.#baseUrl = staging ? [
|
|
125
|
+
"nats://0.0.0.0:4421",
|
|
126
|
+
"nats://0.0.0.0:4422",
|
|
127
|
+
"nats://0.0.0.0:4423"
|
|
128
|
+
] :
|
|
129
|
+
[
|
|
130
|
+
`wss://api.relay-x.io:4421`,
|
|
131
|
+
`wss://api.relay-x.io:4422`,
|
|
132
|
+
`wss://api.relay-x.io:4423`
|
|
133
|
+
];
|
|
134
|
+
}else{
|
|
135
|
+
this.#baseUrl = [
|
|
117
136
|
`wss://api.relay-x.io:4421`,
|
|
118
137
|
`wss://api.relay-x.io:4422`,
|
|
119
138
|
`wss://api.relay-x.io:4423`
|
|
120
139
|
];
|
|
121
|
-
|
|
122
|
-
this.#baseUrl = [
|
|
123
|
-
`wss://api.relay-x.io:4421`,
|
|
124
|
-
`wss://api.relay-x.io:4422`,
|
|
125
|
-
`wss://api.relay-x.io:4423`
|
|
126
|
-
];
|
|
140
|
+
}
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
this.#log(this.#baseUrl);
|
|
@@ -163,6 +177,10 @@ export class Realtime {
|
|
|
163
177
|
* Connects to the relay network
|
|
164
178
|
*/
|
|
165
179
|
async connect(){
|
|
180
|
+
if(this.#connectCalled){
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
this.SEVER_URL = this.#baseUrl;
|
|
167
185
|
|
|
168
186
|
var credsFile = this.#getUserCreds(this.api_key, this.secret)
|
|
@@ -177,7 +195,7 @@ export class Realtime {
|
|
|
177
195
|
maxReconnectAttempts: 1200,
|
|
178
196
|
reconnectTimeWait: 1000,
|
|
179
197
|
authenticator: credsAuth,
|
|
180
|
-
token: this.api_key
|
|
198
|
+
token: this.api_key
|
|
181
199
|
});
|
|
182
200
|
|
|
183
201
|
this.#jetstream = await jetstream(this.#natsClient);
|
|
@@ -185,6 +203,7 @@ export class Realtime {
|
|
|
185
203
|
await this.#getNameSpace()
|
|
186
204
|
|
|
187
205
|
this.connected = true;
|
|
206
|
+
this.#connectCalled = true;
|
|
188
207
|
}catch(err){
|
|
189
208
|
this.#log("ERR")
|
|
190
209
|
this.#log(err);
|
|
@@ -195,17 +214,13 @@ export class Realtime {
|
|
|
195
214
|
if (this.connected == true){
|
|
196
215
|
this.#log("Connected to server!");
|
|
197
216
|
|
|
198
|
-
// Callback on client side
|
|
199
|
-
if (CONNECTED in this.#event_func){
|
|
200
|
-
if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
|
|
201
|
-
this.#event_func[CONNECTED]()
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
217
|
this.#natsClient.closed().then(() => {
|
|
206
218
|
this.#log("the connection closed!");
|
|
207
219
|
|
|
208
220
|
this.#offlineMessageBuffer.length = 0;
|
|
221
|
+
this.connected = false;
|
|
222
|
+
this.reconnecting = false;
|
|
223
|
+
this.#connectCalled = false;
|
|
209
224
|
|
|
210
225
|
if (DISCONNECTED in this.#event_func){
|
|
211
226
|
if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
|
|
@@ -223,13 +238,6 @@ export class Realtime {
|
|
|
223
238
|
this.#log(`client disconnected - ${s.data}`);
|
|
224
239
|
|
|
225
240
|
this.connected = false;
|
|
226
|
-
this.#consumerMap = {};
|
|
227
|
-
|
|
228
|
-
if (DISCONNECTED in this.#event_func){
|
|
229
|
-
if (this.#event_func[DISCONNECTED] !== null || this.#event_func[DISCONNECTED] !== undefined){
|
|
230
|
-
this.#event_func[DISCONNECTED]()
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
241
|
break;
|
|
234
242
|
case Events.LDM:
|
|
235
243
|
this.#log("client has been requested to reconnect");
|
|
@@ -259,6 +267,7 @@ export class Realtime {
|
|
|
259
267
|
this.#log("client is attempting to reconnect");
|
|
260
268
|
|
|
261
269
|
this.reconnecting = true;
|
|
270
|
+
this.connected = false;
|
|
262
271
|
|
|
263
272
|
if(RECONNECT in this.#event_func && this.reconnecting){
|
|
264
273
|
this.#event_func[RECONNECT](this.#RECONNECTING);
|
|
@@ -276,19 +285,29 @@ export class Realtime {
|
|
|
276
285
|
// Subscribe to topics
|
|
277
286
|
this.#subscribeToTopics();
|
|
278
287
|
this.#log("Subscribed to topics");
|
|
288
|
+
|
|
289
|
+
// Callback on client side
|
|
290
|
+
if (CONNECTED in this.#event_func){
|
|
291
|
+
if (this.#event_func[CONNECTED] !== null || this.#event_func[CONNECTED] !== undefined){
|
|
292
|
+
this.#event_func[CONNECTED]()
|
|
293
|
+
}
|
|
294
|
+
}
|
|
279
295
|
}
|
|
280
296
|
}
|
|
281
297
|
|
|
282
298
|
/**
|
|
283
299
|
* Closes connection
|
|
284
300
|
*/
|
|
285
|
-
close(){
|
|
301
|
+
async close(){
|
|
286
302
|
if(this.#natsClient !== null){
|
|
287
303
|
this.reconnected = false;
|
|
288
304
|
this.disconnected = true;
|
|
305
|
+
this.#connectCalled = false;
|
|
289
306
|
|
|
290
307
|
this.#offlineMessageBuffer.length = 0;
|
|
291
308
|
|
|
309
|
+
await this.#deleteAllConsumers();
|
|
310
|
+
|
|
292
311
|
this.#natsClient.close();
|
|
293
312
|
}else{
|
|
294
313
|
this.#log("Null / undefined socket, cannot close connection");
|
|
@@ -304,6 +323,17 @@ export class Realtime {
|
|
|
304
323
|
await this.#startConsumer(topic);
|
|
305
324
|
});
|
|
306
325
|
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Delete consumers for topics initialized by user
|
|
329
|
+
*/
|
|
330
|
+
async #deleteAllConsumers(){
|
|
331
|
+
for(let i = 0; i < this.#topicMap.length; i++){
|
|
332
|
+
let topic = this.#topicMap[i];
|
|
333
|
+
|
|
334
|
+
await this.#deleteConsumer(topic);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
307
337
|
|
|
308
338
|
/**
|
|
309
339
|
* Deletes reference to user defined event callback.
|
|
@@ -350,31 +380,23 @@ export class Realtime {
|
|
|
350
380
|
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
351
381
|
}
|
|
352
382
|
|
|
353
|
-
if(
|
|
354
|
-
this.#event_func[topic] = func;
|
|
355
|
-
}else{
|
|
383
|
+
if(topic in this.#event_func || this.#topicMap.includes(topic)){
|
|
356
384
|
return false
|
|
357
385
|
}
|
|
358
386
|
|
|
359
|
-
|
|
360
|
-
this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic)){
|
|
361
|
-
if(!this.isTopicValid(topic)){
|
|
362
|
-
// We have an invalid topic, lets remove it
|
|
363
|
-
if(topic in this.#event_func){
|
|
364
|
-
delete this.#event_func[topic];
|
|
365
|
-
}
|
|
387
|
+
this.#event_func[topic] = func;
|
|
366
388
|
|
|
367
|
-
|
|
368
|
-
|
|
389
|
+
if (!this.#reservedSystemTopics.includes(topic)){
|
|
390
|
+
if(!this.isTopicValid(topic)){
|
|
391
|
+
throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
|
|
392
|
+
}
|
|
369
393
|
|
|
370
|
-
|
|
371
|
-
this.#topicMap.push(topic);
|
|
372
|
-
}
|
|
394
|
+
this.#topicMap.push(topic);
|
|
373
395
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
if(this.connected){
|
|
397
|
+
// Connected we need to create a topic in a stream
|
|
398
|
+
await this.#startConsumer(topic);
|
|
399
|
+
}
|
|
378
400
|
}
|
|
379
401
|
|
|
380
402
|
return true;
|
|
@@ -404,7 +426,7 @@ export class Realtime {
|
|
|
404
426
|
throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
|
|
405
427
|
}
|
|
406
428
|
|
|
407
|
-
if(!this
|
|
429
|
+
if(!this.isMessageValid(data)){
|
|
408
430
|
throw new Error("$message must be JSON, string or number")
|
|
409
431
|
}
|
|
410
432
|
|
|
@@ -419,15 +441,9 @@ export class Realtime {
|
|
|
419
441
|
"start": Date.now()
|
|
420
442
|
}
|
|
421
443
|
|
|
422
|
-
this.#log("Encoding message via msg pack...")
|
|
423
|
-
var encodedMessage = encode(message);
|
|
424
|
-
|
|
425
444
|
if(this.connected){
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}else{
|
|
429
|
-
this.#log(`${topic} exists locally, moving on...`)
|
|
430
|
-
}
|
|
445
|
+
this.#log("Encoding message via msg pack...")
|
|
446
|
+
var encodedMessage = encode(message);
|
|
431
447
|
|
|
432
448
|
this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
|
|
433
449
|
|
|
@@ -454,7 +470,6 @@ export class Realtime {
|
|
|
454
470
|
* @param {string} topic
|
|
455
471
|
*/
|
|
456
472
|
async history(topic, start, end){
|
|
457
|
-
this.#log(start)
|
|
458
473
|
if(topic == null || topic == undefined){
|
|
459
474
|
throw new Error("$topic is null or undefined");
|
|
460
475
|
}
|
|
@@ -467,6 +482,10 @@ export class Realtime {
|
|
|
467
482
|
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
468
483
|
}
|
|
469
484
|
|
|
485
|
+
if(!this.isTopicValid(topic)){
|
|
486
|
+
throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
|
|
487
|
+
}
|
|
488
|
+
|
|
470
489
|
if(start == undefined || start == null){
|
|
471
490
|
throw new Error(`$start must be provided. $start is => ${start}`)
|
|
472
491
|
}
|
|
@@ -487,11 +506,16 @@ export class Realtime {
|
|
|
487
506
|
end = end.toISOString();
|
|
488
507
|
}
|
|
489
508
|
|
|
509
|
+
if(!this.connected){
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
|
|
490
513
|
var opts = {
|
|
491
|
-
name:
|
|
514
|
+
name: `webjs_${topic}_${uuidv4()}_history_consumer`,
|
|
492
515
|
filter_subjects: [this.#getStreamTopic(topic)],
|
|
493
516
|
replay_policy: ReplayPolicy.Instant,
|
|
494
517
|
opt_start_time: start,
|
|
518
|
+
delivery_policy: DeliverPolicy.StartTime,
|
|
495
519
|
ack_policy: AckPolicy.Explicit,
|
|
496
520
|
}
|
|
497
521
|
|
|
@@ -520,7 +544,12 @@ export class Realtime {
|
|
|
520
544
|
var data = decode(msg.data);
|
|
521
545
|
this.#log(data);
|
|
522
546
|
|
|
523
|
-
history.push(
|
|
547
|
+
history.push({
|
|
548
|
+
"id": data.id,
|
|
549
|
+
"topic": data.room,
|
|
550
|
+
"message": data.message,
|
|
551
|
+
"timestamp": msg.timestamp
|
|
552
|
+
});
|
|
524
553
|
}
|
|
525
554
|
|
|
526
555
|
var del = await consumer.delete();
|
|
@@ -568,11 +597,11 @@ export class Realtime {
|
|
|
568
597
|
* @param {string} topic
|
|
569
598
|
*/
|
|
570
599
|
async #startConsumer(topic){
|
|
571
|
-
|
|
600
|
+
const consumerName = `webjs_${topic}_${uuidv4()}_consumer`;
|
|
572
601
|
|
|
573
602
|
var opts = {
|
|
574
|
-
name:
|
|
575
|
-
filter_subjects: [this.#getStreamTopic(topic)
|
|
603
|
+
name: consumerName,
|
|
604
|
+
filter_subjects: [this.#getStreamTopic(topic)],
|
|
576
605
|
replay_policy: ReplayPolicy.Instant,
|
|
577
606
|
opt_start_time: new Date(),
|
|
578
607
|
ack_policy: AckPolicy.Explicit,
|
|
@@ -588,19 +617,25 @@ export class Realtime {
|
|
|
588
617
|
callback: async (msg) => {
|
|
589
618
|
try{
|
|
590
619
|
const now = Date.now();
|
|
620
|
+
msg.working()
|
|
591
621
|
this.#log("Decoding msgpack message...")
|
|
592
622
|
var data = decode(msg.data);
|
|
593
623
|
|
|
594
|
-
var
|
|
624
|
+
var msgTopic = this.#stripStreamHash(msg.subject);
|
|
595
625
|
|
|
596
626
|
this.#log(data);
|
|
597
627
|
|
|
598
628
|
// Push topic message to main thread
|
|
599
|
-
if (
|
|
600
|
-
this.#
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
629
|
+
if (data.client_id != this.#getClientId()){
|
|
630
|
+
var topicMatch = this.#topicPatternMatcher(topic, msgTopic)
|
|
631
|
+
|
|
632
|
+
if(topicMatch){
|
|
633
|
+
this.#event_func[topic]({
|
|
634
|
+
"id": data.id,
|
|
635
|
+
"topic": msgTopic,
|
|
636
|
+
"data": data.message
|
|
637
|
+
});
|
|
638
|
+
}
|
|
604
639
|
}
|
|
605
640
|
|
|
606
641
|
msg.ack();
|
|
@@ -608,7 +643,7 @@ export class Realtime {
|
|
|
608
643
|
await this.#logLatency(now, data);
|
|
609
644
|
}catch(err){
|
|
610
645
|
this.#log("Consumer err " + err);
|
|
611
|
-
msg.
|
|
646
|
+
msg.nak(5000);
|
|
612
647
|
}
|
|
613
648
|
}
|
|
614
649
|
});
|
|
@@ -620,6 +655,7 @@ export class Realtime {
|
|
|
620
655
|
* @param {string} topic
|
|
621
656
|
*/
|
|
622
657
|
async #deleteConsumer(topic){
|
|
658
|
+
this.#log(topic)
|
|
623
659
|
const consumer = this.#consumerMap[topic]
|
|
624
660
|
|
|
625
661
|
var del = false;
|
|
@@ -641,11 +677,6 @@ export class Realtime {
|
|
|
641
677
|
return;
|
|
642
678
|
}
|
|
643
679
|
|
|
644
|
-
if(this.#latency.length >= 100){
|
|
645
|
-
this.#log("Latency array is full, skipping log");
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
680
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
650
681
|
|
|
651
682
|
this.#log(`Timezone: ${timeZone}`);
|
|
@@ -662,7 +693,7 @@ export class Realtime {
|
|
|
662
693
|
this.#latencyPush = setTimeout(async () => {
|
|
663
694
|
this.#log("setTimeout called");
|
|
664
695
|
|
|
665
|
-
if(this.#latency.length > 0){
|
|
696
|
+
if(this.#latency.length > 0 && this.connected && !this.#isSendingLatency){
|
|
666
697
|
this.#log("Push from setTimeout")
|
|
667
698
|
await this.#pushLatencyData({
|
|
668
699
|
timezone: timeZone,
|
|
@@ -675,7 +706,7 @@ export class Realtime {
|
|
|
675
706
|
}, 30000);
|
|
676
707
|
}
|
|
677
708
|
|
|
678
|
-
if(this.#latency.length
|
|
709
|
+
if(this.#latency.length >= 100 && !this.#isSendingLatency){
|
|
679
710
|
this.#log("Push from Length Check: " + this.#latency.length);
|
|
680
711
|
await this.#pushLatencyData({
|
|
681
712
|
timezone: timeZone,
|
|
@@ -731,10 +762,9 @@ export class Realtime {
|
|
|
731
762
|
*/
|
|
732
763
|
isTopicValid(topic){
|
|
733
764
|
if(topic !== null && topic !== undefined && (typeof topic) == "string"){
|
|
734
|
-
var arrayCheck = !
|
|
735
|
-
this.#RECONNECTING, this.#RECONN_FAIL, MESSAGE_RESEND, SERVER_DISCONNECT].includes(topic);
|
|
765
|
+
var arrayCheck = !this.#reservedSystemTopics.includes(topic);
|
|
736
766
|
|
|
737
|
-
const TOPIC_REGEX = /^(
|
|
767
|
+
const TOPIC_REGEX = /^(?!.*\$)(?:[A-Za-z0-9_*~-]+(?:\.[A-Za-z0-9_*~-]+)*(?:\.>)?|>)$/u;
|
|
738
768
|
|
|
739
769
|
var spaceStarCheck = !topic.includes(" ") && TOPIC_REGEX.test(topic);
|
|
740
770
|
|
|
@@ -744,7 +774,7 @@ export class Realtime {
|
|
|
744
774
|
}
|
|
745
775
|
}
|
|
746
776
|
|
|
747
|
-
|
|
777
|
+
isMessageValid(message){
|
|
748
778
|
if(message == null || message == undefined){
|
|
749
779
|
throw new Error("$message cannot be null / undefined")
|
|
750
780
|
}
|
|
@@ -791,6 +821,91 @@ export class Realtime {
|
|
|
791
821
|
}
|
|
792
822
|
}
|
|
793
823
|
|
|
824
|
+
#stripStreamHash(topic){
|
|
825
|
+
return topic.replace(`${this.topicHash}.`, "")
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
#getCallbackTopics(topic){
|
|
829
|
+
var validTopics = [];
|
|
830
|
+
|
|
831
|
+
var topicPatterns = Object.keys(this.#event_func);
|
|
832
|
+
|
|
833
|
+
for(let i = 0; i < topicPatterns.length; i++){
|
|
834
|
+
var pattern = topicPatterns[i];
|
|
835
|
+
|
|
836
|
+
if([CONNECTED, RECONNECT, MESSAGE_RESEND, DISCONNECTED, SERVER_DISCONNECT].includes(pattern)){
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
var match = this.#topicPatternMatcher(pattern, topic);
|
|
841
|
+
|
|
842
|
+
if(match){
|
|
843
|
+
validTopics.push(pattern)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return validTopics;
|
|
848
|
+
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
#topicPatternMatcher(patternA, patternB) {
|
|
852
|
+
const a = patternA.split(".");
|
|
853
|
+
const b = patternB.split(".");
|
|
854
|
+
|
|
855
|
+
let i = 0, j = 0; // cursors in a & b
|
|
856
|
+
let starAi = -1, starAj = -1; // last '>' position in A and the token count it has consumed
|
|
857
|
+
let starBi = -1, starBj = -1; // same for pattern B
|
|
858
|
+
|
|
859
|
+
while (i < a.length || j < b.length) {
|
|
860
|
+
const tokA = a[i];
|
|
861
|
+
const tokB = b[j];
|
|
862
|
+
|
|
863
|
+
/*──────────── literal match or single‑token wildcard on either side ────────────*/
|
|
864
|
+
const singleWildcard =
|
|
865
|
+
(tokA === "*" && j < b.length) ||
|
|
866
|
+
(tokB === "*" && i < a.length);
|
|
867
|
+
|
|
868
|
+
if (
|
|
869
|
+
(tokA !== undefined && tokA === tokB) ||
|
|
870
|
+
singleWildcard
|
|
871
|
+
) {
|
|
872
|
+
i++; j++;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
|
|
877
|
+
if (tokA === ">") {
|
|
878
|
+
if (i !== a.length - 1) return false; // '>' not in last position → invalid
|
|
879
|
+
if (j >= b.length) return false; // must consume at least one token
|
|
880
|
+
starAi = i++; // remember where '>' is
|
|
881
|
+
starAj = ++j; // gobble one token from B
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (tokB === ">") {
|
|
885
|
+
if (j !== b.length - 1) return false; // same rule for patternB
|
|
886
|
+
if (i >= a.length) return false;
|
|
887
|
+
starBi = j++;
|
|
888
|
+
starBj = ++i;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/*───────────────────────────── back‑track using last '>' ───────────────────────*/
|
|
893
|
+
if (starAi !== -1) { // let patternA's '>' absorb one more token of B
|
|
894
|
+
j = ++starAj;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (starBi !== -1) { // let patternB's '>' absorb one more token of A
|
|
898
|
+
i = ++starBj;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/*────────────────────────────────── dead‑end ───────────────────────────────────*/
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
|
|
794
909
|
sleep(ms) {
|
|
795
910
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
796
911
|
}
|
|
@@ -929,6 +1044,14 @@ ${secret}
|
|
|
929
1044
|
return null;
|
|
930
1045
|
}
|
|
931
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
testPatternMatcher(){
|
|
1049
|
+
if(process.env.NODE_ENV == "test"){
|
|
1050
|
+
return this.#topicPatternMatcher.bind(this)
|
|
1051
|
+
}else{
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
932
1055
|
}
|
|
933
1056
|
|
|
934
1057
|
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
|
})
|