woonplan-packages-redishelper 1.0.93 → 2.0.1

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/.gitattributes CHANGED
@@ -1,2 +1,2 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/jest.config.js ADDED
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ roots: ['<rootDir>/src'
3
+ ],
4
+ testMatch: [
5
+ "**/__tests__/**/*.+(ts|tsx|js)",
6
+ "**/?(*.)+(spec|test).+(ts|tsx|js)"
7
+ ],
8
+ transform: {
9
+ "^.+\\.(ts|tsx)$": "ts-jest"
10
+ },
11
+ testTimeout: 500,
12
+ globals: {
13
+ 'ts-jest': {
14
+ isolatedModules: true
15
+ }
16
+ },
17
+ preset: 'ts-jest/presets/js-with-ts'
18
+ }
package/package.json CHANGED
@@ -1,25 +1,39 @@
1
1
  {
2
2
  "name": "woonplan-packages-redishelper",
3
- "version": "1.0.93",
3
+ "version": "2.0.1",
4
4
  "description": "",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "scripts": {
8
- "prepublish": "npm run build",
9
- "build": "tsc",
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- },
5
+ "author": "Jasper Denk <jasper@e-trias.nl>",
6
+ "repository": "https://github.com/e-line-websolutions/woonplan-micro-energyio",
7
+ "license": "MIT",
12
8
  "keywords": [],
13
- "author": "Jasper Denk",
14
- "license": "ISC",
15
- "devDependencies": {
16
- "@types/node": "^16.11.13",
17
- "@types/uuid": "^8.3.4",
18
- "typescript": "^4.2.3"
9
+ "main": "./dist/tsc/main.js",
10
+ "types": "./dist/tsc/main.d.ts",
11
+ "scripts": {
12
+ "dev": "ts-node --esm src/main.ts",
13
+ "lint": "eslint src/ --ext .js,.jsx,.ts,.tsx",
14
+ "test": "jest",
15
+ "clean": "rm -rf dist build package node_modules package-lock.json && npm i && jest",
16
+ "ts-node": "ts-node",
17
+ "docs": "typedoc --entryPoints src/main.ts",
18
+ "build": "tsc -p tsconfig.json",
19
+ "start": "npm run build && node dist/tsc/main.js"
19
20
  },
20
21
  "dependencies": {
21
- "@types/ioredis": "5.0.4",
22
- "ioredis": "^5.0.4",
23
- "uuid": "^8.3.2"
22
+ "@e-trias/woonplan": "1.0.82",
23
+ "@types/ioredis": "^4.28.8",
24
+ "@types/jest": "^27.4.0",
25
+ "@types/node": "^17.0.18",
26
+ "@types/pg": "^8.6.5",
27
+ "@typescript-eslint/eslint-plugin": "^5.12.0",
28
+ "@typescript-eslint/parser": "^5.12.0",
29
+ "class-validator": "^0.13.2",
30
+ "dotenv": "^16.0.0",
31
+ "ioredis": "^5.2.3",
32
+ "ioredis-mock": "^8.2.2",
33
+ "jest": "^27.5.1",
34
+ "rollbar": "^2.25.1",
35
+ "ts-jest": "^27.1.3",
36
+ "ts-node": "^10.5.0",
37
+ "typescript": "^4.5.5"
24
38
  }
25
39
  }
@@ -0,0 +1,223 @@
1
+
2
+
3
+ import Broker from '../classes/Broker'
4
+ import Listener from '../classes/Listener'
5
+ import { DecypheredMessage, RedisConfig, RollbarConfig } from '../types'
6
+ import { stringifyMap } from "../services/utils"
7
+ import Redis from "ioredis-mock"
8
+
9
+ jest.mock( 'ioredis', () => require("ioredis-mock") )
10
+ // jest.mock( '../classes/Listener' )
11
+
12
+ const cb = () => {}
13
+
14
+
15
+ const rollbarConfig:RollbarConfig = {
16
+ environment : '',
17
+ accessToken : ''
18
+ }
19
+ const redisConfig:RedisConfig = {
20
+ REDISURL : ''
21
+ }
22
+
23
+ const service = "testservice",
24
+ consumer = "testconsumer"
25
+
26
+ xdescribe("addListener", () => {
27
+ afterEach(()=>jest.clearAllMocks())
28
+ it("should create a Listener class", () => {
29
+ new Broker(
30
+ redisConfig,
31
+ rollbarConfig,
32
+ service,
33
+ consumer
34
+ ).addListener( '', cb )
35
+
36
+ expect( Listener ).toHaveBeenCalled( )
37
+ })
38
+ it("should be chainable", () => {
39
+ new Broker(
40
+ redisConfig,
41
+ rollbarConfig,
42
+ service,
43
+ consumer
44
+ ).addListener( '', cb ).addListener( '', cb ).addListener( '', cb )
45
+
46
+ expect( Listener ).toHaveBeenCalledTimes( 3 )
47
+ })
48
+ })
49
+
50
+ describe("sanitize",()=>{
51
+ const broker = new Broker( redisConfig, rollbarConfig,
52
+ service,
53
+ consumer )
54
+ it("should return a json map if a map is given" ,() => {
55
+ expect( broker.sanitizeValue( new Map() ) ).toStrictEqual( stringifyMap(new Map()))
56
+ })
57
+ it("should return a number if a number is given" ,() => {
58
+ expect( broker.sanitizeValue( 1 )).toStrictEqual( 1 )
59
+ })
60
+ it("should return a string if a string is given" ,() => {
61
+ expect( broker.sanitizeValue( 'foo' ) ).toStrictEqual( 'foo' )
62
+ })
63
+ it("should return a JSON array if an array is given" ,() => {
64
+ expect( broker.sanitizeValue( ['foo'] ) ).toStrictEqual( JSON.stringify( ['foo']))
65
+ })
66
+ it("should return a JSON object if an object is given" ,() => {
67
+ expect( broker.sanitizeValue( {'foo':'bar'} ) ).toStrictEqual( JSON.stringify( {'foo':'bar'} ))
68
+ })
69
+ })
70
+
71
+ describe("createRedisMessage", () => {
72
+ const broker = new Broker( redisConfig, rollbarConfig,
73
+ service,
74
+ consumer )
75
+ it("should create a redis message array from an object ", () => {
76
+ expect( broker.createRedisMessage({
77
+ foo : 'bar',
78
+ bar : ['baz'],
79
+ hello : new Map( [['world','foo']])
80
+ })).toStrictEqual(
81
+ ['foo','bar','bar',JSON.stringify( ['baz']),'hello', stringifyMap(new Map( [['world','foo']]))]
82
+ )
83
+ })
84
+ })
85
+
86
+ jest.setTimeout( 2500 )
87
+
88
+ describe("getRequest", () => {
89
+ it("should add a subscription" , async () => {
90
+ const broker = new Broker( redisConfig, rollbarConfig,
91
+ service,
92
+ consumer )
93
+
94
+ broker.getRequest( 'foo', 'bar', {})
95
+ const mockresult = new Redis()
96
+ mockresult.publish( broker.subscriptions[0], 'baz')
97
+ expect( broker.subscriptions.length > 0 ).toBe( true )
98
+ await broker
99
+ })
100
+ it("should return a response" , async () => {
101
+ const broker = new Broker( redisConfig, rollbarConfig,
102
+ service,
103
+ consumer )
104
+
105
+ const result = broker.getRequest( 'foo', 'bar', {})
106
+
107
+ const mockresult = new Redis()
108
+ mockresult.publish( broker.subscriptions[0], 'baz')
109
+ expect( await result ).toStrictEqual( 'baz' )
110
+ })
111
+ it("should return a timeout and null if no response is given within timeout" , async () => {
112
+ const broker = new Broker( redisConfig, rollbarConfig,
113
+ service,
114
+ consumer )
115
+
116
+ const result = broker.getRequest( 'foo', 'bar', {})
117
+ expect( await result ).toStrictEqual( null )
118
+
119
+ })
120
+ })
121
+
122
+ // Unable to test this untill ioredis-mock has a xreadgroup
123
+ xdescribe("requester",()=>{
124
+ it("should send a request and be able to receive", async () => {
125
+ const receiver = new Broker( redisConfig, rollbarConfig, 'foo', consumer )
126
+
127
+ const requestEndpoint = jest.fn(() => {
128
+ console.log( 'requestendpoint hit')
129
+ return 'baz'
130
+ })
131
+ receiver.setRequestEndpoint( requestEndpoint )
132
+
133
+ const requester = new Broker( redisConfig, rollbarConfig, 'bar', consumer )
134
+
135
+ const request = await requester.getRequest( 'foo', 'key', {} )
136
+
137
+ expect( request ).toStrictEqual( 'baz' )
138
+
139
+
140
+ })
141
+ })
142
+
143
+ xdescribe( "getRequest", () => {
144
+ afterEach(() => jest.clearAllMocks())
145
+ it("should subscribe to a channel", async ()=>{
146
+ const spy = jest.spyOn( Broker.prototype, 'subscribe' ).mockResolvedValueOnce( null )
147
+ const broker = new Broker( redisConfig, rollbarConfig, 'foo', consumer )
148
+
149
+
150
+ jest.spyOn( Broker.prototype, 'requestMessageResponse' ).mockImplementation((resolve, timeout) => {
151
+ resolve()
152
+ clearTimeout( timeout )
153
+ })
154
+ broker.getRequest( 'foo', 'bar', {} )
155
+
156
+ expect( spy ).toBeCalled( )
157
+ })
158
+ it("should setup a message response", async ()=>{
159
+ jest.spyOn( Broker.prototype, 'subscribe' ).mockResolvedValueOnce( null )
160
+
161
+
162
+ const spy = jest.spyOn( Broker.prototype, 'requestMessageResponse' ).mockImplementation((resolve, timeout) => {
163
+ console.log( 1 )
164
+ resolve()
165
+ clearTimeout( timeout )
166
+ })
167
+
168
+ const broker = new Broker( redisConfig, rollbarConfig, 'foo', consumer )
169
+ broker.getRequest( 'foo', 'bar', {} )
170
+ expect( spy ).toBeCalled( )
171
+ })
172
+ it("should setup a timeout", async ()=>{
173
+ jest.spyOn( Broker.prototype, 'subscribe' ).mockResolvedValueOnce( null )
174
+
175
+ jest.spyOn( Broker.prototype, 'requestMessageResponse' ).mockImplementation((resolve, timeout) => {
176
+ resolve()
177
+ clearTimeout( timeout )
178
+ })
179
+
180
+ const spy = jest.spyOn( Broker.prototype, 'setupTimeout' ).mockImplementation((_resolve, channel) => {
181
+ return setTimeout(() => {
182
+
183
+ }, 200);
184
+ })
185
+
186
+ const broker = new Broker( redisConfig, rollbarConfig, 'foo', consumer )
187
+ broker.getRequest( 'foo', 'bar', {} )
188
+ expect( spy ).toBeCalled( )
189
+ })
190
+ })
191
+
192
+
193
+
194
+ const responses = [['foo',[['bar',['id','baz']],['bar',['id','baz1']],['bar',['id','baz2']]]]]
195
+
196
+ describe("filterStream", () => {
197
+ it("should filter the stream and only give back the messages with the request parameters", async () => {
198
+ jest.spyOn( Broker.prototype, 'getStreamInfo' ).mockResolvedValue( [0,1,2,3,4,5,6,7,8,responses] )
199
+ const broker = new Broker( redisConfig, rollbarConfig, 'foo', consumer )
200
+
201
+ expect( await broker.filterStream( 'foo', 'id', 'baz') ).toStrictEqual([{
202
+ id : 'bar',
203
+ parameters : {
204
+ id : 'baz'
205
+ }
206
+
207
+ }])
208
+
209
+ })
210
+ })
211
+
212
+ describe("addListListener", () => {
213
+
214
+ const broker = new Broker( redisConfig, rollbarConfig, 'foo', consumer )
215
+
216
+ const listcb = ( id:string ) => {
217
+ console.log( id )
218
+ }
219
+ const messagecb = ( message:DecypheredMessage ) => listcb( message.id )
220
+
221
+
222
+ broker.addListListener('measuredataschanged', messagecb )
223
+ })
@@ -0,0 +1,89 @@
1
+ import Broker from "../classes/Broker"
2
+ import Listener from "../classes/Listener"
3
+ import { RedisConfig, RollbarConfig } from "../types"
4
+
5
+ const cb = jest.fn()
6
+
7
+
8
+ const rollbarConfig:RollbarConfig = {
9
+ environment : '',
10
+ accessToken : ''
11
+ }
12
+
13
+ const redisConfig:RedisConfig = {
14
+ REDISURL : ''
15
+ }
16
+
17
+ const responses = [['foo',[['bar',['id','baz']]]]]
18
+
19
+ jest.mock( 'ioredis', () => jest.fn().mockImplementation(() => {
20
+ return {
21
+ xread: jest.fn(( _a,_b,_c,_d,e) => {
22
+ if( e == 'bar' ) return null
23
+ return responses
24
+ }),
25
+
26
+ xreadgroup: jest.fn(() => {
27
+ return null
28
+ })
29
+
30
+ }
31
+ }) )
32
+
33
+ describe("listenToStream", () => {
34
+ afterEach(() => jest.clearAllMocks())
35
+ it("should call xreadgroup if a groupname is given", async () => {
36
+ const broker = new Broker(
37
+ redisConfig,
38
+ rollbarConfig
39
+ ).addListener( 'stream', cb, 'groupname' )
40
+
41
+ await new Promise((r) => setTimeout(r, 100))
42
+ expect( broker.listeners.get( 'stream')?.client.xreadgroup ).toHaveBeenCalled()
43
+ expect( broker.listeners.get( 'stream')?.client.xread ).not.toHaveBeenCalled()
44
+ })
45
+ it("should call xread if a groupname is not given", async () => {
46
+ const broker = new Broker(
47
+ redisConfig,
48
+ rollbarConfig
49
+ ).addListener( 'stream', cb )
50
+
51
+ await new Promise((r) => setTimeout(r, 100))
52
+
53
+ expect( broker.listeners.get( 'stream')?.client.xreadgroup ).not.toHaveBeenCalled()
54
+ expect( broker.listeners.get( 'stream')?.client.xread ).toHaveBeenCalled()
55
+ })
56
+
57
+ it("should call the callback if a message comes in from the stream ", async ()=>{
58
+
59
+ new Broker(
60
+ redisConfig,
61
+ rollbarConfig
62
+ ).addListener( '', cb )
63
+
64
+ await new Promise((r) => setTimeout(r, 100))
65
+
66
+ expect( cb ).toHaveBeenCalledWith( 'bar', {
67
+ id : 'baz'
68
+ })
69
+
70
+ })
71
+
72
+
73
+ it("should report an error", async () => {
74
+
75
+ const errormock = jest.fn( () => {
76
+ throw new Error('foo')
77
+ })
78
+
79
+ const spy = jest.spyOn( Broker.prototype, 'throwError')
80
+ new Broker(
81
+ redisConfig,
82
+ rollbarConfig
83
+ ).addListener( 'stream', errormock )
84
+
85
+ await new Promise((r) => setTimeout(r, 100))
86
+
87
+ expect( spy ).toHaveBeenCalled( )
88
+ })
89
+ })
@@ -0,0 +1,224 @@
1
+ import { isArray, isObject } from "class-validator"
2
+ import IORedis, { Redis } from "ioredis"
3
+ import Rollbar from "rollbar"
4
+ import { isMap } from "util/types"
5
+ import { capitalizeFirstLetter, stringifyMap } from "../services/utils"
6
+ import Listener from "./Listener"
7
+ import ListListener from "./ListListener"
8
+ import { v4 as uuidv4 } from 'uuid'
9
+
10
+ export default class Broker{
11
+ redisConfig : RedisConfig
12
+ rollbar ?: Rollbar
13
+ consumername : string = ''
14
+ listeners: Map<string,Listener> = new Map()
15
+ writer : Redis
16
+ reader : Redis
17
+ listprefix : string = 'listUpdated'
18
+ service: string
19
+ subscriptions : string[] = []
20
+
21
+ requestendpoint ?: Function
22
+
23
+ constructor( redisConfig:RedisConfig, rollbarConfig:RollbarConfig, service:string, consumer : string ){
24
+ this.redisConfig = redisConfig
25
+ this.rollbar = new Rollbar({
26
+ accessToken: rollbarConfig.accessToken,
27
+ environment: rollbarConfig.environment,
28
+ })
29
+ this.writer = new IORedis( redisConfig.REDISURL as string )
30
+ this.reader = new IORedis( redisConfig.REDISURL as string )
31
+ this.consumername = consumer
32
+ this.service = service
33
+ }
34
+
35
+ get requeststream(){
36
+ return this.getRequestStream( this.service )
37
+ }
38
+
39
+ getRequestStream( service:string ){
40
+ return `keyRequestedFrom${capitalizeFirstLetter(service)}Service`
41
+ }
42
+
43
+ setRequestEndpoint( callback:Function ){
44
+ return this.addListener( this.requeststream, this.getRequestCallback( callback ), this.service )
45
+ }
46
+
47
+ getRequestCallback( callback:Function ){
48
+ return async ( id:string, parameters:DecypheredParameters ) => {
49
+ const result = await callback( id, parameters )
50
+ if( !parameters.messageid ) return null
51
+ const channel = this.getRequestSubscriptionName(parameters.messageid)
52
+ return this.publish( channel, result )
53
+ }
54
+ }
55
+
56
+ publish( channel:string, result:string ){
57
+ return this.writer.publish( channel, result )
58
+ }
59
+
60
+ addListener( stream:string, callback:Function, group ?: string){
61
+ const client = new IORedis( this.redisConfig.REDISURL as string )
62
+ this.listeners.set( stream, new Listener( this, client, stream, callback, group ))
63
+ return this
64
+ }
65
+
66
+ addListListener( event:string, callback:Function ){
67
+ const client = new IORedis( this.redisConfig.REDISURL as string )
68
+ const channel = this.getListChannel( event )
69
+ this.listeners.set( channel, new ListListener( this, client, channel, callback ))
70
+
71
+ return this
72
+ }
73
+
74
+ throwError( error:Error ){
75
+ if(!this.rollbar) throw new Error('Rollbar not initialized')
76
+ this.rollbar.error( error )
77
+ }
78
+
79
+ sendMessage( stream:string, data:Struct){
80
+ return this.writer.xadd( stream, '*', ...this.createRedisMessage( data ))
81
+ }
82
+
83
+ async getRequest( targetservice:string, key:string, data:Struct ){
84
+ // create a message id to subscribe to
85
+ const messageid = uuidv4()
86
+
87
+ //subscribe to message response
88
+ const channel = this.getRequestSubscriptionName(messageid)
89
+ await this.subscribe( channel )
90
+
91
+ // send the message to the correct service
92
+ this.sendMessage( this.getRequestStream(targetservice), {
93
+ request : key,
94
+ messageid : messageid,
95
+ data : data
96
+ })
97
+
98
+ let resolver
99
+ // create a promise to be able to pass on to resolve later
100
+ const promise = new Promise((r) => {
101
+ resolver = r
102
+ })
103
+
104
+ if( !resolver ) return null
105
+
106
+ //setup a timeout
107
+ const timeout = this.setupTimeout( resolver, channel )
108
+
109
+ //setup a response
110
+ this.requestMessageResponse( resolver, timeout )
111
+
112
+ // return a promise that will resolve when the message returns or times out
113
+ return promise
114
+
115
+ }
116
+
117
+ requestMessageResponse( resolve:(value?: any) => void, timeout:NodeJS.Timeout ){
118
+ this.reader.once( 'message', (channel,message) => {
119
+
120
+ if( message.length ) resolve( message );
121
+ else resolve( null )
122
+
123
+ this.unsubscribe( channel )
124
+ clearTimeout( timeout )
125
+
126
+ })
127
+ }
128
+
129
+ unsubscribe( channel:string ){
130
+ this.reader.unsubscribe( channel )
131
+ this.subscriptions = this.subscriptions.filter( s => s != channel )
132
+ }
133
+
134
+ setupTimeout( resolve:(value?: any) => void, channel:string, n:number = 2000 ){
135
+ return setTimeout( () => {
136
+ if( !this.subscriptions.includes( channel )) return
137
+
138
+ console.log( `sub timedout: ${channel}`)
139
+ resolve( null )
140
+ this.unsubscribe( channel )
141
+ }, n )
142
+ }
143
+
144
+ subscribe( channel:string ){
145
+ this.subscriptions.push( channel )
146
+ return this.reader.subscribe( channel )
147
+ }
148
+
149
+ getRequestSubscriptionName( messageid:string ){
150
+ return `messageresponse${messageid}`
151
+ }
152
+
153
+ async sendListEvent( event:string, listitems:any[], data:Struct = {} ){
154
+ const list = await this.addToList.call( this, listitems )
155
+ return this.sendMessage(
156
+ this.getListChannel( event ) , {
157
+ ...data,
158
+ listname : list
159
+ } )
160
+ }
161
+
162
+ async addToList( listitems:any[], listname ?: string ){
163
+ const list = listname??uuidv4()
164
+ await this.writer.lpush( list , ...listitems.map( this.sanitizeValue) )
165
+ return list
166
+ }
167
+
168
+ getListChannel( event:string ){
169
+ return `${this.listprefix}${event}`
170
+ }
171
+
172
+ sanitizeValue( value:any ){
173
+ if( isMap( value )) return stringifyMap( value )
174
+ if( isObject( value )) return JSON.stringify( value )
175
+ if( isArray( value )) return JSON.stringify( value )
176
+ return value
177
+ }
178
+
179
+ createRedisMessage( struct:Struct ){
180
+ return Object.keys( struct ).reduce( (arr:string[],key:keyof Struct) => [...arr,key,this.sanitizeValue( struct[key] )] , [] )
181
+ }
182
+
183
+ async getStreamMessages( stream:string ){
184
+ const streaminfo = await this.getStreamInfo.call( this, stream )
185
+ if( !streaminfo || !isArray( streaminfo ) || streaminfo.length < 10 ) return []
186
+ const streamresponses = streaminfo[9] as StreamResponse[]
187
+ return this.decypherResponse( ...streamresponses ).flatMap( r => r.messages )
188
+ }
189
+
190
+ getStreamInfo( stream:string, count:number = 0 ){
191
+ return this.reader.xinfo('STREAM', stream ,'FULL', 'COUNT', count ) as Promise< null | any[]>
192
+ }
193
+
194
+ async filterStream( stream:string, key : string, value:any ){
195
+ const messages = await this.getStreamMessages.call( this, stream )
196
+ return messages.filter( message => message.parameters?.[key] != null && message.parameters[key] == value )
197
+ }
198
+
199
+ decypherResponse( ...responses:StreamResponse[] ):DecypheredResponse[]{
200
+ return responses.reduce( (resp:DecypheredResponse[], response:StreamResponse) =>
201
+ [
202
+ ...resp,
203
+ {
204
+ stream : response[0],
205
+ messages : response[1].map( (message) => ({
206
+ id : message[0],
207
+ parameters : this.decypherParameters( message[1] )
208
+ }) as DecypheredMessage )
209
+ } as DecypheredResponse
210
+ ]
211
+ , [] as DecypheredResponse[] )
212
+ }
213
+
214
+
215
+ decypherParameters( parameters:string[] ):DecypheredParameters{
216
+ return parameters.reduce( ( params, v:string, n:number ) => ( n == 0 || ( n % 2 == 0)) && parameters.length >= n+1 ? ({
217
+ ...params,
218
+ [v] : parameters[n+1]
219
+ }) : params , {} as DecypheredParameters)
220
+ }
221
+
222
+ }
223
+
224
+
@@ -0,0 +1,63 @@
1
+ import { Redis } from "ioredis"
2
+ import Broker from "./Broker"
3
+
4
+ export default abstract class BrokerClient{
5
+ broker : Broker
6
+ client : Redis
7
+ group : string | null = null
8
+ stream : string | null = null
9
+ list : string | null = null
10
+ callback:Function
11
+
12
+ constructor( broker:Broker, client:Redis, callback:Function, list:string | null, stream:string | null = null, group: string | null = null ){
13
+ this.broker = broker
14
+ this.client = client
15
+ this.list = list
16
+ this.stream = stream
17
+ this.group = group
18
+ this.callback = callback
19
+
20
+ this.listenToStream.call( this )
21
+ }
22
+
23
+ throwError( error:any ){
24
+ return this.broker.throwError( error )
25
+ }
26
+
27
+ decypherResponse( ...responses:StreamResponse[] ):DecypheredResponse[]{
28
+ return this.broker.decypherResponse( ...responses )
29
+ }
30
+
31
+ async getGroupResponse():Promise< StreamResponse[] | null >{
32
+ if( !this.group || !this.stream ) return null
33
+ return await this.client.xreadgroup( 'GROUP', this.group, this.broker.consumername, 'COUNT', 1, 'BLOCK', 0, 'STREAMS', this.stream, '>' ) as StreamResponse[]
34
+ }
35
+
36
+ async getResponse( lastid:string ):Promise< StreamResponse[] | null >{
37
+ if( !this.stream ) return null
38
+ return await this.client.xread("BLOCK", 0, "STREAMS", this.stream, lastid )
39
+ }
40
+
41
+
42
+ async listenToStream( lastid:string = '$'):Promise<Function | null>{
43
+ const responses = await this.getResponse.call( this, lastid )
44
+ if( !responses ) return null
45
+
46
+ const streamResponses = this.decypherResponse( ...responses )
47
+ const messages = streamResponses.flatMap( r => r.messages )
48
+ await Promise.all( messages.map( async (message) => {
49
+ try{
50
+ await this.streamCallback( message )
51
+
52
+ }catch( e:any ){
53
+ this.broker.throwError( e )
54
+ }finally{
55
+
56
+ }
57
+ }) )
58
+
59
+ return this.listenToStream.call( this, messages[messages.length-1]?.id??'$' )
60
+ }
61
+
62
+ abstract streamCallback( message:DecypheredMessage ):Promise<any | null>
63
+ }