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
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import { connect, JSONCodec, Events, DebugEvents, AckPolicy, ReplayPolicy, credsAuthenticator } from "nats.ws";
|
|
2
|
+
import { DeliverPolicy, jetstream, jetstreamManager } from "@nats-io/jetstream";
|
|
3
|
+
import { encode, decode } from "@msgpack/msgpack";
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import { ErrorLogging } from "./utils.js";
|
|
6
|
+
import Message from "./models/message.js";
|
|
7
|
+
|
|
8
|
+
export class Queue {
|
|
9
|
+
|
|
10
|
+
#queueID = null;
|
|
11
|
+
#api_key = null;
|
|
12
|
+
|
|
13
|
+
#baseUrl = "";
|
|
14
|
+
|
|
15
|
+
#natsClient = null;
|
|
16
|
+
#codec = JSONCodec();
|
|
17
|
+
#jetstream = null;
|
|
18
|
+
#jetStreamManager = null;
|
|
19
|
+
#consumerMap = {};
|
|
20
|
+
|
|
21
|
+
#event_func = {};
|
|
22
|
+
#topicMap = [];
|
|
23
|
+
|
|
24
|
+
#errorLogging = null;
|
|
25
|
+
#debug = false;
|
|
26
|
+
|
|
27
|
+
// Status Codes
|
|
28
|
+
#RECONNECTING = "RECONNECTING";
|
|
29
|
+
#RECONNECTED = "RECONNECTED";
|
|
30
|
+
#RECONN_FAIL = "RECONN_FAIL";
|
|
31
|
+
|
|
32
|
+
CONNECTED = "CONNECTED";
|
|
33
|
+
RECONNECT = "RECONNECT";
|
|
34
|
+
MESSAGE_RESEND = "MESSAGE_RESEND";
|
|
35
|
+
DISCONNECTED = "DISCONNECTED";
|
|
36
|
+
SERVER_DISCONNECT = "SERVER_DISCONNECT";
|
|
37
|
+
|
|
38
|
+
#reservedSystemTopics = [this.CONNECTED, this.DISCONNECTED, this.RECONNECT, this.#RECONNECTED, this.#RECONNECTING, this.#RECONN_FAIL, this.MESSAGE_RESEND, this.SERVER_DISCONNECT];
|
|
39
|
+
|
|
40
|
+
setRemoteUserAttempts = 0;
|
|
41
|
+
setRemoteUserRetries = 5;
|
|
42
|
+
|
|
43
|
+
// Retry attempts end
|
|
44
|
+
reconnected = false;
|
|
45
|
+
disconnected = true;
|
|
46
|
+
reconnecting = false;
|
|
47
|
+
connected = true;
|
|
48
|
+
|
|
49
|
+
// Offline messages
|
|
50
|
+
#offlineMessageBuffer = [];
|
|
51
|
+
|
|
52
|
+
#connectCalled = false;
|
|
53
|
+
|
|
54
|
+
constructor(config){
|
|
55
|
+
this.#jetstream = config.jetstream;
|
|
56
|
+
this.#natsClient = config.nats_client;
|
|
57
|
+
|
|
58
|
+
this.#api_key = config.api_key;
|
|
59
|
+
|
|
60
|
+
this.#debug = config.debug;
|
|
61
|
+
|
|
62
|
+
this.#errorLogging = new ErrorLogging();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async init(queueID){
|
|
66
|
+
this.#queueID = queueID;
|
|
67
|
+
|
|
68
|
+
this.#jetStreamManager = await this.#jetstream.jetstreamManager()
|
|
69
|
+
|
|
70
|
+
var result = await this.#getQueueNamespace();
|
|
71
|
+
|
|
72
|
+
this.#initConnectionListener();
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get namespace to start subscribing and publishing in the queue
|
|
79
|
+
*/
|
|
80
|
+
async #getQueueNamespace(){
|
|
81
|
+
this.#log("Getting queue namespace data...")
|
|
82
|
+
var data = null;
|
|
83
|
+
|
|
84
|
+
try{
|
|
85
|
+
var res = await this.#natsClient.request("accounts.user.get_queue_namespace",
|
|
86
|
+
JSONCodec().encode({
|
|
87
|
+
api_key: this.#api_key,
|
|
88
|
+
queue_id: this.#queueID
|
|
89
|
+
}),
|
|
90
|
+
{
|
|
91
|
+
timeout: 5000
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
data = res.json()
|
|
96
|
+
|
|
97
|
+
this.#log(data)
|
|
98
|
+
}catch(err){
|
|
99
|
+
console.log("-------------------------")
|
|
100
|
+
console.log("Error fetching queue namespace!")
|
|
101
|
+
console.log(err);
|
|
102
|
+
console.log("-------------------------")
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if(data.status == "NAMESPACE_RETRIEVE_SUCCESS"){
|
|
108
|
+
this.namespace = data.data.namespace
|
|
109
|
+
this.topicHash = data.data.hash
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
}else{
|
|
113
|
+
this.namespace = null;
|
|
114
|
+
this.topicHash = null;
|
|
115
|
+
|
|
116
|
+
var code = data.code;
|
|
117
|
+
|
|
118
|
+
if(code == "QUEUE_NOT_FOUND"){
|
|
119
|
+
console.log("-------------------------------")
|
|
120
|
+
console.log(`Code: ${code}`)
|
|
121
|
+
console.log(`Description: The queue does not exist OR has been disabled`)
|
|
122
|
+
console.log(`Queue ID: ${this.#queueID}`)
|
|
123
|
+
console.log(`Docs Link To Resolve Problem: <>`)
|
|
124
|
+
console.log("-------------------------------")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Connection listener to handle queue
|
|
133
|
+
*/
|
|
134
|
+
async #initConnectionListener(){
|
|
135
|
+
(async () => {
|
|
136
|
+
for await (const s of this.#natsClient.status()) {
|
|
137
|
+
switch (s.type) {
|
|
138
|
+
case Events.Disconnect:
|
|
139
|
+
this.#log(`client disconnected - ${s.data}`);
|
|
140
|
+
|
|
141
|
+
this.connected = false;
|
|
142
|
+
break;
|
|
143
|
+
case Events.Reconnect:
|
|
144
|
+
this.#log(`client reconnected -`);
|
|
145
|
+
this.#log(s.data)
|
|
146
|
+
|
|
147
|
+
this.reconnecting = false;
|
|
148
|
+
this.connected = true;
|
|
149
|
+
|
|
150
|
+
// Resend any messages sent while client was offline
|
|
151
|
+
this.#publishMessagesOnReconnect();
|
|
152
|
+
break;
|
|
153
|
+
case DebugEvents.Reconnecting:
|
|
154
|
+
this.#log("client is attempting to reconnect");
|
|
155
|
+
|
|
156
|
+
this.reconnecting = true;
|
|
157
|
+
this.connected = false;
|
|
158
|
+
break;
|
|
159
|
+
case DebugEvents.StaleConnection:
|
|
160
|
+
this.#log("client has a stale connection");
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})().then();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A method to send a message to a queue topic.
|
|
169
|
+
* Retry methods included. Stores messages in an array if offline.
|
|
170
|
+
* @param {string} topic - Name of the event
|
|
171
|
+
* @param {object} data - Data to send
|
|
172
|
+
* @returns
|
|
173
|
+
*/
|
|
174
|
+
async publish(topic, data){
|
|
175
|
+
if(topic == null || topic == undefined){
|
|
176
|
+
throw new Error("$topic is null or undefined");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if(topic == ""){
|
|
180
|
+
throw new Error("$topic cannot be an empty string")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if(typeof topic !== "string"){
|
|
184
|
+
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if(!this.isTopicValid(topic)){
|
|
188
|
+
throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if(!this.isMessageValid(data)){
|
|
192
|
+
throw new Error("$message must be JSON, string or number")
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
var start = Date.now()
|
|
196
|
+
var messageId = crypto.randomUUID();
|
|
197
|
+
|
|
198
|
+
var message = {
|
|
199
|
+
"client_id": this.#getClientId(),
|
|
200
|
+
"id": messageId,
|
|
201
|
+
"room": topic,
|
|
202
|
+
"message": data,
|
|
203
|
+
"start": Date.now()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if(this.connected){
|
|
207
|
+
this.#log("Encoding message via msg pack...")
|
|
208
|
+
var encodedMessage = encode(message);
|
|
209
|
+
|
|
210
|
+
this.#log(`Publishing to topic => ${this.#getStreamTopic(topic)}`)
|
|
211
|
+
|
|
212
|
+
var ack = null;
|
|
213
|
+
|
|
214
|
+
try{
|
|
215
|
+
ack = await this.#jetstream.publish(this.#getStreamTopic(topic), encodedMessage);
|
|
216
|
+
this.#log(`Publish Ack =>`)
|
|
217
|
+
this.#log(ack)
|
|
218
|
+
|
|
219
|
+
var latency = Date.now() - start;
|
|
220
|
+
this.#log(`Latency => ${latency} ms`);
|
|
221
|
+
}catch(err){
|
|
222
|
+
this.#errorLogging.logError({
|
|
223
|
+
err: err,
|
|
224
|
+
topic: topic,
|
|
225
|
+
op: "publish"
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return ack !== null && ack !== undefined;
|
|
230
|
+
}else{
|
|
231
|
+
this.#offlineMessageBuffer.push({
|
|
232
|
+
topic: topic,
|
|
233
|
+
message: data
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Subscribes to a topic
|
|
242
|
+
* @param {string} topic - Name of the event
|
|
243
|
+
* @param {function} func - Callback function to call on user thread
|
|
244
|
+
* @returns {boolean} - To check if topic subscription was successful
|
|
245
|
+
*/
|
|
246
|
+
async consume(data, func){
|
|
247
|
+
var topic = data.topic;
|
|
248
|
+
|
|
249
|
+
if(!this.isTopicValid(topic) && this.#reservedSystemTopics.includes(topic)){
|
|
250
|
+
throw new Error(`Invalid Topic!`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if ((typeof func !== "function")){
|
|
254
|
+
throw new Error(`Expected $listener type -> function. Instead receieved -> ${typeof func}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if(typeof topic !== "string"){
|
|
258
|
+
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if(topic in this.#event_func || this.#topicMap.includes(topic)){
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.#event_func[topic] = func;
|
|
266
|
+
|
|
267
|
+
if (!this.#reservedSystemTopics.includes(topic)){
|
|
268
|
+
if(!this.isTopicValid(topic)){
|
|
269
|
+
throw new Error("Invalid topic, use isTopicValid($topic) to validate topic")
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.#topicMap.push(topic);
|
|
273
|
+
|
|
274
|
+
if(this.connected){
|
|
275
|
+
// Connected we need to create a topic in a stream
|
|
276
|
+
try{
|
|
277
|
+
await this.#startConsumer(data);
|
|
278
|
+
}catch(err){
|
|
279
|
+
this.#errorLogging.logError({
|
|
280
|
+
err: err,
|
|
281
|
+
topic: topic,
|
|
282
|
+
op: "subscribe"
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Deletes reference to user defined event callback.
|
|
291
|
+
* This will stop listening to a topic
|
|
292
|
+
* @param {string} topic
|
|
293
|
+
* @returns {boolean} - To check if topic unsubscribe was successful
|
|
294
|
+
*/
|
|
295
|
+
async detachConsumer(topic){
|
|
296
|
+
if(topic == null || topic == undefined){
|
|
297
|
+
throw new Error("$topic is null / undefined")
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if(typeof topic !== "string"){
|
|
301
|
+
throw new Error(`Expected $topic type -> string. Instead receieved -> ${typeof topic}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.#topicMap = this.#topicMap.filter(item => item !== topic);
|
|
305
|
+
|
|
306
|
+
delete this.#event_func[topic];
|
|
307
|
+
|
|
308
|
+
delete this.#consumerMap[topic];
|
|
309
|
+
|
|
310
|
+
this.#log(`Consumer closed => ${topic}`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Start consumers for topics initialized by user
|
|
315
|
+
*/
|
|
316
|
+
async #subscribeToTopics(){
|
|
317
|
+
this.#topicMap.forEach(async (topic) => {
|
|
318
|
+
// Subscribe to stream
|
|
319
|
+
await this.#startConsumer(topic);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Method resends messages when the client successfully connects to the
|
|
325
|
+
* server again
|
|
326
|
+
* @returns - Array of success and failure messages
|
|
327
|
+
*/
|
|
328
|
+
async #publishMessagesOnReconnect(){
|
|
329
|
+
var messageSentStatus = [];
|
|
330
|
+
|
|
331
|
+
for(let i = 0; i < this.#offlineMessageBuffer.length; i++){
|
|
332
|
+
let data = this.#offlineMessageBuffer[i];
|
|
333
|
+
|
|
334
|
+
const topic = data.topic;
|
|
335
|
+
const message = data.message;
|
|
336
|
+
|
|
337
|
+
const output = await this.publish(topic, message);
|
|
338
|
+
|
|
339
|
+
messageSentStatus.push({
|
|
340
|
+
topic: topic,
|
|
341
|
+
message: message,
|
|
342
|
+
resent: output
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Clearing out offline messages
|
|
347
|
+
this.#offlineMessageBuffer.length = 0;
|
|
348
|
+
|
|
349
|
+
// Send to client
|
|
350
|
+
if(this.MESSAGE_RESEND in this.#event_func && messageSentStatus.length > 0){
|
|
351
|
+
this.#event_func[this.MESSAGE_RESEND](messageSentStatus);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Room functions
|
|
356
|
+
/**
|
|
357
|
+
* Starts consumer for particular topic if stream exists
|
|
358
|
+
* @param {string} topic
|
|
359
|
+
*/
|
|
360
|
+
async #startConsumer(config){
|
|
361
|
+
this.#validateConsumerConfig(config);
|
|
362
|
+
|
|
363
|
+
var name = config.name;
|
|
364
|
+
var topic = config.topic;
|
|
365
|
+
|
|
366
|
+
var opts = {
|
|
367
|
+
name: name,
|
|
368
|
+
durable_name: name,
|
|
369
|
+
delivery_group: config.group,
|
|
370
|
+
delivery_policy: DeliverPolicy.New,
|
|
371
|
+
replay_policy: ReplayPolicy.Instant,
|
|
372
|
+
filter_subject: this.#getStreamTopic(topic),
|
|
373
|
+
ack_policy: AckPolicy.Explicit,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if(this.#checkVarOk(config.ack_wait) && !(config.ack_wait < 0) && (typeof config.ack_wait === "number")){
|
|
377
|
+
opts.ack_wait = config.ack_wait * 1_000_000_000 // Seconds to nano seconds
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if(this.#checkVarOk(config.backoff) && Array.isArray(config.backoff)){
|
|
381
|
+
var backoffNanos = [];
|
|
382
|
+
|
|
383
|
+
for(let bo of config.backoff){
|
|
384
|
+
backoffNanos.push(bo * 1_000_000_000) // Seconds to nano seconds
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
opts.backoff = backoffNanos
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if(this.#checkVarOk(config.max_deliver) && !(config.max_deliver < 0) && (typeof config.max_deliver === "number")){
|
|
391
|
+
opts.max_deliver = config.max_deliver
|
|
392
|
+
}else{
|
|
393
|
+
opts.max_deliver = -1
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if(this.#checkVarOk(config.max_ack_pending) && !(config.max_ack_pending < 0) && (typeof config.max_ack_pending === "number")){
|
|
397
|
+
opts.max_ack_pending = config.max_ack_pending
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
var consumer = null;
|
|
401
|
+
|
|
402
|
+
try{
|
|
403
|
+
consumer = await this.#jetStreamManager.consumers.info(this.#getQueueName(), name);
|
|
404
|
+
}catch(err){
|
|
405
|
+
consumer = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if(consumer == null){
|
|
409
|
+
this.#log("Consumer not found, creating...")
|
|
410
|
+
|
|
411
|
+
await this.#jetStreamManager.consumers.add(this.#getQueueName(), opts);
|
|
412
|
+
|
|
413
|
+
this.#log(`Consumer created: ${name}`)
|
|
414
|
+
}else{
|
|
415
|
+
this.#log("Consumer found, updating...")
|
|
416
|
+
|
|
417
|
+
await this.#jetStreamManager.consumers.update(this.#getQueueName(), name, opts);
|
|
418
|
+
|
|
419
|
+
this.#log(`Consumer updated: ${name}`)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
consumer = await this.#jetstream.consumers.get(this.#getQueueName(), name)
|
|
423
|
+
|
|
424
|
+
this.#consumerMap[topic] = consumer;
|
|
425
|
+
|
|
426
|
+
while(true){
|
|
427
|
+
var msg = await consumer.next({
|
|
428
|
+
expires: 1000
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if(!this.#checkVarOk(this.#consumerMap[topic])){
|
|
432
|
+
// consumerMap has no callback function because
|
|
433
|
+
// we called detachConsumer(). We did that because
|
|
434
|
+
// we did not want to consumer messages anymore
|
|
435
|
+
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if(msg == null){
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try{
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
msg.working()
|
|
446
|
+
this.#log("Decoding msgpack message...")
|
|
447
|
+
var data = decode(msg.data);
|
|
448
|
+
|
|
449
|
+
var msgTopic = this.#stripStreamHash(msg.subject);
|
|
450
|
+
|
|
451
|
+
this.#log(data);
|
|
452
|
+
|
|
453
|
+
// Push topic message to main thread
|
|
454
|
+
if (data.client_id != this.#getClientId()){
|
|
455
|
+
var topicMatch = this.#topicPatternMatcher(topic, msgTopic)
|
|
456
|
+
|
|
457
|
+
if(topicMatch){
|
|
458
|
+
var fMsg = {
|
|
459
|
+
id: data.id,
|
|
460
|
+
topic: msgTopic,
|
|
461
|
+
message: data.message,
|
|
462
|
+
msg: msg
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
var msgObj = new Message(fMsg)
|
|
466
|
+
|
|
467
|
+
this.#event_func[topic](msgObj);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}catch(err){
|
|
471
|
+
this.#log("Consumer err " + err);
|
|
472
|
+
msg.nak(5000);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
this.#log(`Consumer done => ${topic}`)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async deleteConsumer(topic){
|
|
480
|
+
this.#log(topic)
|
|
481
|
+
const consumer = this.#consumerMap[topic]
|
|
482
|
+
|
|
483
|
+
var del = false;
|
|
484
|
+
|
|
485
|
+
if (consumer != null && consumer != undefined){
|
|
486
|
+
del = await consumer.delete();
|
|
487
|
+
}else{
|
|
488
|
+
del = false
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
delete this.#consumerMap[topic];
|
|
492
|
+
|
|
493
|
+
return del;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Utility functions
|
|
497
|
+
#getClientId(){
|
|
498
|
+
return this.#natsClient?.info?.client_id
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Checks if a topic can be used to send messages to.
|
|
503
|
+
* @param {string} topic - Name of event
|
|
504
|
+
* @returns {boolean} - If topic is valid or not
|
|
505
|
+
*/
|
|
506
|
+
isTopicValid(topic){
|
|
507
|
+
if(topic !== null && topic !== undefined && (typeof topic) == "string"){
|
|
508
|
+
var arrayCheck = !this.#reservedSystemTopics.includes(topic);
|
|
509
|
+
|
|
510
|
+
const TOPIC_REGEX = /^(?!.*\$)(?:[A-Za-z0-9_*~-]+(?:\.[A-Za-z0-9_*~-]+)*(?:\.>)?|>)$/u;
|
|
511
|
+
|
|
512
|
+
var spaceStarCheck = !topic.includes(" ") && TOPIC_REGEX.test(topic);
|
|
513
|
+
|
|
514
|
+
return arrayCheck && spaceStarCheck;
|
|
515
|
+
}else{
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
isMessageValid(message){
|
|
521
|
+
if(message == null || message == undefined){
|
|
522
|
+
throw new Error("$message cannot be null / undefined")
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if(typeof message == "string"){
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if(typeof message == "number"){
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if(this.#isJSON(message)){
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#validateConsumerConfig(config){
|
|
541
|
+
if(config === null || config === undefined){
|
|
542
|
+
throw new Error("$config (subscribe config) cannot be null / undefined")
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if(config.name === null || config.name === undefined || config.name == ""){
|
|
546
|
+
throw new Error("$config.name (subscribe config) cannot be null / undefined / Empty")
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if(config.topic === null || config.topic === undefined || config.topic == ""){
|
|
550
|
+
throw new Error("$config.topic (subscribe config) cannot be null / undefined")
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if(config.group === null || config.group === undefined || config.group == ""){
|
|
554
|
+
throw new Error("$config.group (subscribe config) cannot be null / undefined")
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#isJSON(data){
|
|
559
|
+
try{
|
|
560
|
+
JSON.stringify(data?.toString())
|
|
561
|
+
return true;
|
|
562
|
+
}catch(err){
|
|
563
|
+
return false
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#checkVarOk(variable){
|
|
568
|
+
return variable !== null && variable !== undefined
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#getQueueName(){
|
|
572
|
+
if(this.namespace != null){
|
|
573
|
+
return `Q_${this.namespace}`
|
|
574
|
+
}else{
|
|
575
|
+
this.close();
|
|
576
|
+
throw new Error("$namespace is null. Cannot initialize program with null $namespace")
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#getStreamTopic(topic){
|
|
581
|
+
if(this.topicHash != null){
|
|
582
|
+
return `${this.topicHash}.${topic}`;
|
|
583
|
+
}else{
|
|
584
|
+
throw new Error("$topicHash is null. Cannot initialize program with null $topicHash")
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
#stripStreamHash(topic){
|
|
589
|
+
return topic.replace(`${this.topicHash}.`, "")
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#topicPatternMatcher(patternA, patternB) {
|
|
593
|
+
const a = patternA.split(".");
|
|
594
|
+
const b = patternB.split(".");
|
|
595
|
+
|
|
596
|
+
let i = 0, j = 0; // cursors in a & b
|
|
597
|
+
let starAi = -1, starAj = -1; // last '>' position in A and the token count it has consumed
|
|
598
|
+
let starBi = -1, starBj = -1; // same for pattern B
|
|
599
|
+
|
|
600
|
+
while (i < a.length || j < b.length) {
|
|
601
|
+
const tokA = a[i];
|
|
602
|
+
const tokB = b[j];
|
|
603
|
+
|
|
604
|
+
/*────────────────── multi‑token wildcard ">" — must be **final** ───────────────*/
|
|
605
|
+
if (tokA === ">") {
|
|
606
|
+
if (i !== a.length - 1) return false; // '>' not in last position → invalid
|
|
607
|
+
if (j >= b.length) return false; // must consume at least one token
|
|
608
|
+
starAi = i++; // remember where '>' is
|
|
609
|
+
starAj = ++j; // gobble one token from B
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (tokB === ">") {
|
|
613
|
+
if (j !== b.length - 1) return false; // same rule for patternB
|
|
614
|
+
if (i >= a.length) return false;
|
|
615
|
+
starBi = j++;
|
|
616
|
+
starBj = ++i;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/*──────────── literal match or single‑token wildcard on either side ────────────*/
|
|
621
|
+
const singleWildcard =
|
|
622
|
+
(tokA === "*" && j < b.length) ||
|
|
623
|
+
(tokB === "*" && i < a.length);
|
|
624
|
+
|
|
625
|
+
if (
|
|
626
|
+
(tokA !== undefined && tokA === tokB) ||
|
|
627
|
+
singleWildcard
|
|
628
|
+
) {
|
|
629
|
+
i++; j++;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/*───────────────────────────── back‑track using last '>' ───────────────────────*/
|
|
634
|
+
if (starAi !== -1) { // let patternA's '>' absorb one more token of B
|
|
635
|
+
j = ++starAj;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (starBi !== -1) { // let patternB's '>' absorb one more token of A
|
|
639
|
+
i = ++starBj;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/*────────────────────────────────── dead‑end ───────────────────────────────────*/
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
sleep(ms) {
|
|
651
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
#log(msg){
|
|
655
|
+
if(this.#debug){
|
|
656
|
+
console.log(msg);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
}
|