@mojaloop/central-services-shared 18.25.0 → 18.26.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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/util/redis/pubSub.js +26 -9
- package/test/unit/util/redis/pubSub.test.js +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [18.26.0](https://github.com/mojaloop/central-services-shared/compare/v18.25.0...v18.26.0) (2025-05-13)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add sharded pubsub support ([#450](https://github.com/mojaloop/central-services-shared/issues/450)) ([d98675c](https://github.com/mojaloop/central-services-shared/commit/d98675c4d403ad835f462affc06cbab55344958d))
|
|
11
|
+
|
|
5
12
|
## [18.25.0](https://github.com/mojaloop/central-services-shared/compare/v18.24.0...v18.25.0) (2025-05-13)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
package/src/util/redis/pubSub.js
CHANGED
|
@@ -54,7 +54,7 @@ class PubSub {
|
|
|
54
54
|
createRedisClient () {
|
|
55
55
|
this.config.lazyConnect ??= true
|
|
56
56
|
return this.isCluster
|
|
57
|
-
? new Redis.Cluster(this.config.cluster, this.config)
|
|
57
|
+
? new Redis.Cluster(this.config.cluster, { ...this.config, shardedSubscribers: true })
|
|
58
58
|
: new Redis(this.config)
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -113,7 +113,11 @@ class PubSub {
|
|
|
113
113
|
|
|
114
114
|
async publish (channel, message) {
|
|
115
115
|
try {
|
|
116
|
-
|
|
116
|
+
if (this.isCluster) {
|
|
117
|
+
await this.publisherClient.spublish(channel, JSON.stringify(message))
|
|
118
|
+
} else {
|
|
119
|
+
await this.publisherClient.publish(channel, JSON.stringify(message))
|
|
120
|
+
}
|
|
117
121
|
this.log.info(`Message published to channel: ${channel}`)
|
|
118
122
|
} catch (err) {
|
|
119
123
|
this.log.error('Error publishing message:', err)
|
|
@@ -123,12 +127,21 @@ class PubSub {
|
|
|
123
127
|
|
|
124
128
|
async subscribe (channel, callback) {
|
|
125
129
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
if (this.isCluster) {
|
|
131
|
+
await this.subscriberClient.ssubscribe(channel)
|
|
132
|
+
this.subscriberClient.on('smessage', (subscribedChannel, message) => {
|
|
133
|
+
if (subscribedChannel === channel) {
|
|
134
|
+
callback(JSON.parse(message))
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
} else {
|
|
138
|
+
await this.subscriberClient.subscribe(channel)
|
|
139
|
+
this.subscriberClient.on('message', (subscribedChannel, message) => {
|
|
140
|
+
if (subscribedChannel === channel) {
|
|
141
|
+
callback(JSON.parse(message))
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
132
145
|
this.log.info(`Subscribed to channel: ${channel}`)
|
|
133
146
|
return channel
|
|
134
147
|
} catch (err) {
|
|
@@ -139,7 +152,11 @@ class PubSub {
|
|
|
139
152
|
|
|
140
153
|
async unsubscribe (channel) {
|
|
141
154
|
try {
|
|
142
|
-
|
|
155
|
+
if (this.isCluster) {
|
|
156
|
+
await this.subscriberClient.sunsubscribe(channel)
|
|
157
|
+
} else {
|
|
158
|
+
await this.subscriberClient.unsubscribe(channel)
|
|
159
|
+
}
|
|
143
160
|
this.log.info(`Unsubscribed from channel: ${channel}`)
|
|
144
161
|
} catch (err) {
|
|
145
162
|
this.log.error('Error unsubscribing from channel:', err)
|
|
@@ -371,5 +371,153 @@ Test('PubSub', (t) => {
|
|
|
371
371
|
t.deepEqual(connectionStatus, { publisherConnected: false, subscriberConnected: false }, 'isConnected returns false statuses when clients are not connected')
|
|
372
372
|
t.end()
|
|
373
373
|
})
|
|
374
|
+
t.test('should publish a message to a channel using spublish when isCluster is true', async (t) => {
|
|
375
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
376
|
+
const pubSub = new PubSub(config)
|
|
377
|
+
const channel = 'cluster-channel'
|
|
378
|
+
const message = { key: 'cluster-value' }
|
|
379
|
+
|
|
380
|
+
sandbox.stub(pubSub.publisherClient, 'spublish').resolves()
|
|
381
|
+
|
|
382
|
+
await pubSub.publish(channel, message)
|
|
383
|
+
|
|
384
|
+
t.ok(pubSub.publisherClient.spublish.calledWith(channel, JSON.stringify(message)), 'spublish called with correct arguments')
|
|
385
|
+
t.end()
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
t.test('should handle error when publishing a message with spublish in cluster mode', async (t) => {
|
|
389
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
390
|
+
const pubSub = new PubSub(config)
|
|
391
|
+
const channel = 'cluster-channel'
|
|
392
|
+
const message = { key: 'cluster-value' }
|
|
393
|
+
const error = new Error('Cluster spublish error')
|
|
394
|
+
|
|
395
|
+
sandbox.stub(pubSub.publisherClient, 'spublish').rejects(error)
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
await pubSub.publish(channel, message)
|
|
399
|
+
t.fail('Should have thrown an error')
|
|
400
|
+
} catch (err) {
|
|
401
|
+
t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
|
|
402
|
+
}
|
|
403
|
+
t.end()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
t.test('should subscribe to a channel and handle smessage in cluster mode', async (t) => {
|
|
407
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
408
|
+
const pubSub = new PubSub(config)
|
|
409
|
+
const channel = 'cluster-channel'
|
|
410
|
+
const callback = sinon.stub()
|
|
411
|
+
const message = JSON.stringify({ key: 'cluster-value' })
|
|
412
|
+
|
|
413
|
+
sandbox.stub(pubSub.subscriberClient, 'ssubscribe').resolves()
|
|
414
|
+
pubSub.subscriberClient.on.withArgs('smessage').yields(channel, message)
|
|
415
|
+
|
|
416
|
+
await pubSub.subscribe(channel, callback)
|
|
417
|
+
|
|
418
|
+
t.ok(pubSub.subscriberClient.ssubscribe.calledWith(channel), 'ssubscribe called with correct channel')
|
|
419
|
+
t.ok(callback.calledWith(JSON.parse(message)), 'callback called with parsed message')
|
|
420
|
+
t.end()
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
t.test('should not call callback if smessage subscribedChannel does not match channel in cluster mode', async (t) => {
|
|
424
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
425
|
+
const pubSub = new PubSub(config)
|
|
426
|
+
const channel = 'cluster-channel'
|
|
427
|
+
const callback = sinon.stub()
|
|
428
|
+
const message = JSON.stringify({ key: 'cluster-value' })
|
|
429
|
+
const otherChannel = 'other-cluster-channel'
|
|
430
|
+
|
|
431
|
+
sandbox.stub(pubSub.subscriberClient, 'ssubscribe').resolves()
|
|
432
|
+
pubSub.subscriberClient.on.withArgs('smessage').yields(otherChannel, message)
|
|
433
|
+
|
|
434
|
+
await pubSub.subscribe(channel, callback)
|
|
435
|
+
|
|
436
|
+
t.ok(pubSub.subscriberClient.ssubscribe.calledWith(channel), 'ssubscribe called with correct channel')
|
|
437
|
+
t.notOk(callback.called, 'callback not called when smessage channel does not match')
|
|
438
|
+
t.end()
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
t.test('should handle error when subscribing to a channel in cluster mode', async (t) => {
|
|
442
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
443
|
+
const pubSub = new PubSub(config)
|
|
444
|
+
const channel = 'cluster-channel'
|
|
445
|
+
const callback = sinon.stub()
|
|
446
|
+
const error = new Error('Cluster subscribe error')
|
|
447
|
+
|
|
448
|
+
sandbox.stub(pubSub.subscriberClient, 'ssubscribe').rejects(error)
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
await pubSub.subscribe(channel, callback)
|
|
452
|
+
t.fail('Should have thrown an error')
|
|
453
|
+
} catch (err) {
|
|
454
|
+
t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
|
|
455
|
+
}
|
|
456
|
+
t.end()
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
t.test('should unsubscribe from a channel using sunsubscribe in cluster mode', async (t) => {
|
|
460
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
461
|
+
const pubSub = new PubSub(config)
|
|
462
|
+
const channel = 'cluster-channel'
|
|
463
|
+
|
|
464
|
+
sandbox.stub(pubSub.subscriberClient, 'sunsubscribe').resolves()
|
|
465
|
+
|
|
466
|
+
await pubSub.unsubscribe(channel)
|
|
467
|
+
|
|
468
|
+
t.ok(pubSub.subscriberClient.sunsubscribe.calledWith(channel), 'sunsubscribe called with correct channel')
|
|
469
|
+
t.end()
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
t.test('should handle error when unsubscribing from a channel in cluster mode', async (t) => {
|
|
473
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
474
|
+
const pubSub = new PubSub(config)
|
|
475
|
+
const channel = 'cluster-channel'
|
|
476
|
+
const error = new Error('Cluster unsubscribe error')
|
|
477
|
+
|
|
478
|
+
sandbox.stub(pubSub.subscriberClient, 'sunsubscribe').rejects(error)
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
await pubSub.unsubscribe(channel)
|
|
482
|
+
t.fail('Should have thrown an error')
|
|
483
|
+
} catch (err) {
|
|
484
|
+
t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
|
|
485
|
+
}
|
|
486
|
+
t.end()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
t.test('should broadcast a message to multiple channels using spublish in cluster mode', async (t) => {
|
|
490
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
491
|
+
const pubSub = new PubSub(config)
|
|
492
|
+
const channels = ['cluster1', 'cluster2']
|
|
493
|
+
const message = { key: 'cluster-broadcast' }
|
|
494
|
+
|
|
495
|
+
sandbox.stub(pubSub.publisherClient, 'spublish').resolves()
|
|
496
|
+
|
|
497
|
+
await pubSub.broadcast(channels, message)
|
|
498
|
+
|
|
499
|
+
t.ok(pubSub.publisherClient.spublish.calledTwice, 'spublish called twice')
|
|
500
|
+
t.ok(pubSub.publisherClient.spublish.firstCall.calledWith(channels[0], JSON.stringify(message)), 'spublish called with first channel and message')
|
|
501
|
+
t.ok(pubSub.publisherClient.spublish.secondCall.calledWith(channels[1], JSON.stringify(message)), 'spublish called with second channel and message')
|
|
502
|
+
t.end()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
t.test('should handle error when broadcasting a message in cluster mode', async (t) => {
|
|
506
|
+
const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
|
|
507
|
+
const pubSub = new PubSub(config)
|
|
508
|
+
const channels = ['cluster1', 'cluster2']
|
|
509
|
+
const message = { key: 'cluster-broadcast' }
|
|
510
|
+
const error = new Error('Cluster broadcast error')
|
|
511
|
+
|
|
512
|
+
sandbox.stub(pubSub.publisherClient, 'spublish').onFirstCall().rejects(error)
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
await pubSub.broadcast(channels, message)
|
|
516
|
+
t.fail('Should have thrown an error')
|
|
517
|
+
} catch (err) {
|
|
518
|
+
t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
|
|
519
|
+
}
|
|
520
|
+
t.end()
|
|
521
|
+
})
|
|
374
522
|
t.end()
|
|
375
523
|
})
|