@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-services-shared",
3
- "version": "18.25.0",
3
+ "version": "18.26.0",
4
4
  "description": "Shared code for mojaloop central services",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
@@ -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
- await this.publisherClient.publish(channel, JSON.stringify(message))
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
- await this.subscriberClient.subscribe(channel)
127
- this.subscriberClient.on('message', (subscribedChannel, message) => {
128
- if (subscribedChannel === channel) {
129
- callback(JSON.parse(message))
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
- await this.subscriberClient.unsubscribe(channel)
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
  })