woonplan-packages-redishelper 2.0.0 → 2.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.
@@ -1,223 +0,0 @@
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
- })
@@ -1,89 +0,0 @@
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
- })
@@ -1,225 +0,0 @@
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 { RollbarConfig, RedisConfig, Struct, DecypheredParameters, StreamResponse, DecypheredResponse, DecypheredMessage } from "../types"
7
- import Listener from "./Listener"
8
- import ListListener from "./ListListener"
9
- import { v4 as uuidv4 } from 'uuid'
10
-
11
- export default class Broker{
12
- redisConfig : RedisConfig
13
- rollbar ?: Rollbar
14
- consumername : string = ''
15
- listeners: Map<string,Listener> = new Map()
16
- writer : Redis
17
- reader : Redis
18
- listprefix : string = 'listUpdated'
19
- service: string
20
- subscriptions : string[] = []
21
-
22
- requestendpoint ?: Function
23
-
24
- constructor( redisConfig:RedisConfig, rollbarConfig:RollbarConfig, service:string, consumer : string ){
25
- this.redisConfig = redisConfig
26
- this.rollbar = new Rollbar({
27
- accessToken: rollbarConfig.accessToken,
28
- environment: rollbarConfig.environment,
29
- })
30
- this.writer = new IORedis( redisConfig.REDISURL as string )
31
- this.reader = new IORedis( redisConfig.REDISURL as string )
32
- this.consumername = consumer
33
- this.service = service
34
- }
35
-
36
- get requeststream(){
37
- return this.getRequestStream( this.service )
38
- }
39
-
40
- getRequestStream( service:string ){
41
- return `keyRequestedFrom${capitalizeFirstLetter(service)}Service`
42
- }
43
-
44
- setRequestEndpoint( callback:Function ){
45
- return this.addListener( this.requeststream, this.getRequestCallback( callback ), this.service )
46
- }
47
-
48
- getRequestCallback( callback:Function ){
49
- return async ( id:string, parameters:DecypheredParameters ) => {
50
- const result = await callback( id, parameters )
51
- if( !parameters.messageid ) return null
52
- const channel = this.getRequestSubscriptionName(parameters.messageid)
53
- return this.publish( channel, result )
54
- }
55
- }
56
-
57
- publish( channel:string, result:string ){
58
- return this.writer.publish( channel, result )
59
- }
60
-
61
- addListener( stream:string, callback:Function, group ?: string){
62
- const client = new IORedis( this.redisConfig.REDISURL as string )
63
- this.listeners.set( stream, new Listener( this, client, stream, callback, group ))
64
- return this
65
- }
66
-
67
- addListListener( event:string, callback:Function ){
68
- const client = new IORedis( this.redisConfig.REDISURL as string )
69
- const channel = this.getListChannel( event )
70
- this.listeners.set( channel, new ListListener( this, client, channel, callback ))
71
-
72
- return this
73
- }
74
-
75
- throwError( error:Error ){
76
- if(!this.rollbar) throw new Error('Rollbar not initialized')
77
- this.rollbar.error( error )
78
- }
79
-
80
- sendMessage( stream:string, data:Struct){
81
- return this.writer.xadd( stream, '*', ...this.createRedisMessage( data ))
82
- }
83
-
84
- async getRequest( targetservice:string, key:string, data:Struct ){
85
- // create a message id to subscribe to
86
- const messageid = uuidv4()
87
-
88
- //subscribe to message response
89
- const channel = this.getRequestSubscriptionName(messageid)
90
- await this.subscribe( channel )
91
-
92
- // send the message to the correct service
93
- this.sendMessage( this.getRequestStream(targetservice), {
94
- request : key,
95
- messageid : messageid,
96
- data : data
97
- })
98
-
99
- let resolver
100
- // create a promise to be able to pass on to resolve later
101
- const promise = new Promise((r) => {
102
- resolver = r
103
- })
104
-
105
- if( !resolver ) return null
106
-
107
- //setup a timeout
108
- const timeout = this.setupTimeout( resolver, channel )
109
-
110
- //setup a response
111
- this.requestMessageResponse( resolver, timeout )
112
-
113
- // return a promise that will resolve when the message returns or times out
114
- return promise
115
-
116
- }
117
-
118
- requestMessageResponse( resolve:(value?: any) => void, timeout:NodeJS.Timeout ){
119
- this.reader.once( 'message', (channel,message) => {
120
-
121
- if( message.length ) resolve( message );
122
- else resolve( null )
123
-
124
- this.unsubscribe( channel )
125
- clearTimeout( timeout )
126
-
127
- })
128
- }
129
-
130
- unsubscribe( channel:string ){
131
- this.reader.unsubscribe( channel )
132
- this.subscriptions = this.subscriptions.filter( s => s != channel )
133
- }
134
-
135
- setupTimeout( resolve:(value?: any) => void, channel:string, n:number = 2000 ){
136
- return setTimeout( () => {
137
- if( !this.subscriptions.includes( channel )) return
138
-
139
- console.log( `sub timedout: ${channel}`)
140
- resolve( null )
141
- this.unsubscribe( channel )
142
- }, n )
143
- }
144
-
145
- subscribe( channel:string ){
146
- this.subscriptions.push( channel )
147
- return this.reader.subscribe( channel )
148
- }
149
-
150
- getRequestSubscriptionName( messageid:string ){
151
- return `messageresponse${messageid}`
152
- }
153
-
154
- async sendListEvent( event:string, listitems:any[], data:Struct = {} ){
155
- const list = await this.addToList.call( this, listitems )
156
- return this.sendMessage(
157
- this.getListChannel( event ) , {
158
- ...data,
159
- listname : list
160
- } )
161
- }
162
-
163
- async addToList( listitems:any[], listname ?: string ){
164
- const list = listname??uuidv4()
165
- await this.writer.lpush( list , ...listitems.map( this.sanitizeValue) )
166
- return list
167
- }
168
-
169
- getListChannel( event:string ){
170
- return `${this.listprefix}${event}`
171
- }
172
-
173
- sanitizeValue( value:any ){
174
- if( isMap( value )) return stringifyMap( value )
175
- if( isObject( value )) return JSON.stringify( value )
176
- if( isArray( value )) return JSON.stringify( value )
177
- return value
178
- }
179
-
180
- createRedisMessage( struct:Struct ){
181
- return Object.keys( struct ).reduce( (arr:string[],key:keyof Struct) => [...arr,key,this.sanitizeValue( struct[key] )] , [] )
182
- }
183
-
184
- async getStreamMessages( stream:string ){
185
- const streaminfo = await this.getStreamInfo.call( this, stream )
186
- if( !streaminfo || !isArray( streaminfo ) || streaminfo.length < 10 ) return []
187
- const streamresponses = streaminfo[9] as StreamResponse[]
188
- return this.decypherResponse( ...streamresponses ).flatMap( r => r.messages )
189
- }
190
-
191
- getStreamInfo( stream:string, count:number = 0 ){
192
- return this.reader.xinfo('STREAM', stream ,'FULL', 'COUNT', count ) as Promise< null | any[]>
193
- }
194
-
195
- async filterStream( stream:string, key : string, value:any ){
196
- const messages = await this.getStreamMessages.call( this, stream )
197
- return messages.filter( message => message.parameters?.[key] != null && message.parameters[key] == value )
198
- }
199
-
200
- decypherResponse( ...responses:StreamResponse[] ):DecypheredResponse[]{
201
- return responses.reduce( (resp:DecypheredResponse[], response:StreamResponse) =>
202
- [
203
- ...resp,
204
- {
205
- stream : response[0],
206
- messages : response[1].map( (message) => ({
207
- id : message[0],
208
- parameters : this.decypherParameters( message[1] )
209
- }) as DecypheredMessage )
210
- } as DecypheredResponse
211
- ]
212
- , [] as DecypheredResponse[] )
213
- }
214
-
215
-
216
- decypherParameters( parameters:string[] ):DecypheredParameters{
217
- return parameters.reduce( ( params, v:string, n:number ) => ( n == 0 || ( n % 2 == 0)) && parameters.length >= n+1 ? ({
218
- ...params,
219
- [v] : parameters[n+1]
220
- }) : params , {} as DecypheredParameters)
221
- }
222
-
223
- }
224
-
225
-
@@ -1,64 +0,0 @@
1
- import { Redis } from "ioredis"
2
- import { DecypheredMessage, DecypheredResponse, StreamResponse } from "../types"
3
- import Broker from "./Broker"
4
-
5
- export default abstract class BrokerClient{
6
- broker : Broker
7
- client : Redis
8
- group : string | null = null
9
- stream : string | null = null
10
- list : string | null = null
11
- callback:Function
12
-
13
- constructor( broker:Broker, client:Redis, callback:Function, list:string | null, stream:string | null = null, group: string | null = null ){
14
- this.broker = broker
15
- this.client = client
16
- this.list = list
17
- this.stream = stream
18
- this.group = group
19
- this.callback = callback
20
-
21
- this.listenToStream.call( this )
22
- }
23
-
24
- throwError( error:any ){
25
- return this.broker.throwError( error )
26
- }
27
-
28
- decypherResponse( ...responses:StreamResponse[] ):DecypheredResponse[]{
29
- return this.broker.decypherResponse( ...responses )
30
- }
31
-
32
- async getGroupResponse():Promise< StreamResponse[] | null >{
33
- if( !this.group || !this.stream ) return null
34
- return await this.client.xreadgroup( 'GROUP', this.group, this.broker.consumername, 'COUNT', 1, 'BLOCK', 0, 'STREAMS', this.stream, '>' ) as StreamResponse[]
35
- }
36
-
37
- async getResponse( lastid:string ):Promise< StreamResponse[] | null >{
38
- if( !this.stream ) return null
39
- return await this.client.xread("BLOCK", 0, "STREAMS", this.stream, lastid )
40
- }
41
-
42
-
43
- async listenToStream( lastid:string = '$'):Promise<Function | null>{
44
- const responses = await this.getResponse.call( this, lastid )
45
- if( !responses ) return null
46
-
47
- const streamResponses = this.decypherResponse( ...responses )
48
- const messages = streamResponses.flatMap( r => r.messages )
49
- await Promise.all( messages.map( async (message) => {
50
- try{
51
- await this.streamCallback( message )
52
-
53
- }catch( e:any ){
54
- this.broker.throwError( e )
55
- }finally{
56
-
57
- }
58
- }) )
59
-
60
- return this.listenToStream.call( this, messages[messages.length-1]?.id??'$' )
61
- }
62
-
63
- abstract streamCallback( message:DecypheredMessage ):Promise<any | null>
64
- }
@@ -1,20 +0,0 @@
1
- import { Redis } from "ioredis"
2
- import { DecypheredMessage } from "../types"
3
- import Broker from "./Broker"
4
- import BrokerClient from "./BrokerClient"
5
- import ListRunner from "./ListRunner"
6
-
7
- export default class Listener extends BrokerClient{
8
- constructor( broker:Broker, client:Redis, list:string, callback:Function ){
9
- super( broker, client, callback, list )
10
- }
11
-
12
- async streamCallback( message:DecypheredMessage ){
13
- const listrunnercallback = await this.callback( message.id, message.parameters )
14
-
15
- if( message.parameters.listname )
16
- return new ListRunner( this.client, message.parameters.listname, listrunnercallback)
17
-
18
- return null
19
- }
20
- }
@@ -1,43 +0,0 @@
1
- import { Redis } from "ioredis"
2
-
3
- export default class ListRunner{
4
- listname:string
5
- itemsDone:number = 0
6
- itemsPerCall:number
7
- callback:Function
8
- client : Redis
9
-
10
-
11
- constructor( client:Redis, listname:string, callback:Function, itemsPerCall : number = 1 ){
12
- this.client = client
13
- this.callback = callback
14
- this.listname = listname
15
- this.itemsPerCall = itemsPerCall
16
-
17
- this.run.call( this )
18
- }
19
-
20
-
21
- async run():Promise<Function|void>{
22
- const items = await this.nextitems.call( this, this.listname )
23
- if( !items ){
24
- console.log( `finished with ${this.itemsDone} jobs`)
25
- return
26
- }
27
-
28
- try{
29
- await this.callback( items )
30
- }catch( e:any ){
31
- console.log( e )
32
-
33
- }finally{
34
- this.itemsDone += this.itemsPerCall
35
- return this.run.call( this )
36
- }
37
- }
38
-
39
- async nextitems( listname:string ):Promise<string[] | null>{
40
- return this.client.lpop( listname, this.itemsPerCall )
41
- }
42
-
43
- }
@@ -1,14 +0,0 @@
1
- import { Redis } from "ioredis"
2
- import { DecypheredMessage } from "../types"
3
- import Broker from "./Broker"
4
- import BrokerClient from "./BrokerClient"
5
-
6
- export default class Listener extends BrokerClient{
7
- constructor( broker:Broker, client:Redis, stream:string, callback:Function, group ?: string ){
8
- super( broker, client, callback, null, stream, group )
9
- }
10
-
11
- streamCallback(message: DecypheredMessage): Promise<any> {
12
- return this.callback( message.id, message.parameters )
13
- }
14
- }
@@ -1,5 +0,0 @@
1
- import Broker from "./Broker";
2
-
3
- export default class Requester extends Broker{
4
-
5
- }