woonplan-packages-redishelper 2.0.2 → 2.0.5
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/dist/tsc/classes/Broker.d.ts +42 -0
- package/dist/tsc/classes/Broker.js +185 -0
- package/dist/tsc/classes/Broker.js.map +1 -0
- package/dist/tsc/classes/BrokerClient.d.ts +17 -0
- package/dist/tsc/classes/BrokerClient.js +52 -0
- package/dist/tsc/classes/BrokerClient.js.map +1 -0
- package/dist/tsc/classes/ListListener.d.ts +8 -0
- package/dist/tsc/classes/ListListener.js +18 -0
- package/dist/tsc/classes/ListListener.js.map +1 -0
- package/dist/tsc/classes/ListRunner.d.ts +11 -0
- package/dist/tsc/classes/ListRunner.js +34 -0
- package/dist/tsc/classes/ListRunner.js.map +1 -0
- package/dist/tsc/classes/Listener.d.ts +7 -0
- package/dist/tsc/classes/Listener.js +14 -0
- package/dist/tsc/classes/Listener.js.map +1 -0
- package/dist/tsc/main.d.ts +2 -0
- package/dist/tsc/main.js +7 -0
- package/dist/tsc/main.js.map +1 -0
- package/dist/tsc/services/utils.d.ts +11 -0
- package/dist/tsc/services/utils.js +47 -0
- package/dist/tsc/services/utils.js.map +1 -0
- package/{src → dist/tsc}/types.d.ts +1 -1
- package/package.json +2 -2
- package/tsconfig.json +1 -1
- package/jest.config.js +0 -18
- package/src/__tests__/Broker.test.ts +0 -223
- package/src/__tests__/Listener.test.ts +0 -89
- package/src/classes/Broker.ts +0 -224
- package/src/classes/BrokerClient.ts +0 -63
- package/src/classes/ListListener.ts +0 -19
- package/src/classes/ListRunner.ts +0 -43
- package/src/classes/Listener.ts +0 -13
- package/src/main.ts +0 -3
- package/src/services/utils.ts +0 -51
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import Rollbar from "rollbar";
|
|
4
|
+
import Listener from "./Listener";
|
|
5
|
+
export default class Broker {
|
|
6
|
+
redisConfig: RedisConfig;
|
|
7
|
+
rollbar?: Rollbar;
|
|
8
|
+
consumername: string;
|
|
9
|
+
listeners: Map<string, Listener>;
|
|
10
|
+
writer: Redis;
|
|
11
|
+
reader: Redis;
|
|
12
|
+
listprefix: string;
|
|
13
|
+
service: string;
|
|
14
|
+
subscriptions: string[];
|
|
15
|
+
requestendpoint?: Function;
|
|
16
|
+
constructor(redisConfig: RedisConfig, rollbarConfig: RollbarConfig, service: string, consumer: string);
|
|
17
|
+
get requeststream(): string;
|
|
18
|
+
getRequestStream(service: string): string;
|
|
19
|
+
setRequestEndpoint(callback: Function): this;
|
|
20
|
+
getRequestCallback(callback: Function): (id: string, parameters: DecypheredParameters) => Promise<number | null>;
|
|
21
|
+
publish(channel: string, result: string): Promise<number>;
|
|
22
|
+
addListener(stream: string, callback: Function, group?: string): this;
|
|
23
|
+
addListListener(event: string, callback: Function): this;
|
|
24
|
+
throwError(error: Error): void;
|
|
25
|
+
sendMessage(stream: string, data: Struct): Promise<string | null>;
|
|
26
|
+
getRequest(targetservice: string, key: string, data: Struct): Promise<unknown>;
|
|
27
|
+
requestMessageResponse(resolve: (value?: any) => void, timeout: NodeJS.Timeout): void;
|
|
28
|
+
unsubscribe(channel: string): void;
|
|
29
|
+
setupTimeout(resolve: (value?: any) => void, channel: string, n?: number): NodeJS.Timeout;
|
|
30
|
+
subscribe(channel: string): Promise<unknown>;
|
|
31
|
+
getRequestSubscriptionName(messageid: string): string;
|
|
32
|
+
sendListEvent(event: string, listitems: any[], data?: Struct): Promise<string | null>;
|
|
33
|
+
addToList(listitems: any[], listname?: string): Promise<string>;
|
|
34
|
+
getListChannel(event: string): string;
|
|
35
|
+
sanitizeValue(value: any): any;
|
|
36
|
+
createRedisMessage(struct: Struct): string[];
|
|
37
|
+
getStreamMessages(stream: string): Promise<DecypheredMessage[]>;
|
|
38
|
+
getStreamInfo(stream: string, count?: number): Promise<any[] | null>;
|
|
39
|
+
filterStream(stream: string, key: string, value: any): Promise<DecypheredMessage[]>;
|
|
40
|
+
decypherResponse(...responses: StreamResponse[]): DecypheredResponse[];
|
|
41
|
+
decypherParameters(parameters: string[]): DecypheredParameters;
|
|
42
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const class_validator_1 = require("class-validator");
|
|
5
|
+
const ioredis_1 = tslib_1.__importDefault(require("ioredis"));
|
|
6
|
+
const rollbar_1 = tslib_1.__importDefault(require("rollbar"));
|
|
7
|
+
const types_1 = require("util/types");
|
|
8
|
+
const utils_1 = require("../services/utils");
|
|
9
|
+
const Listener_1 = tslib_1.__importDefault(require("./Listener"));
|
|
10
|
+
const ListListener_1 = tslib_1.__importDefault(require("./ListListener"));
|
|
11
|
+
const uuid_1 = require("uuid");
|
|
12
|
+
class Broker {
|
|
13
|
+
constructor(redisConfig, rollbarConfig, service, consumer) {
|
|
14
|
+
this.consumername = '';
|
|
15
|
+
this.listeners = new Map();
|
|
16
|
+
this.listprefix = 'listUpdated';
|
|
17
|
+
this.subscriptions = [];
|
|
18
|
+
this.redisConfig = redisConfig;
|
|
19
|
+
this.rollbar = new rollbar_1.default({
|
|
20
|
+
accessToken: rollbarConfig.accessToken,
|
|
21
|
+
environment: rollbarConfig.environment,
|
|
22
|
+
});
|
|
23
|
+
this.writer = new ioredis_1.default(redisConfig.REDISURL);
|
|
24
|
+
this.reader = new ioredis_1.default(redisConfig.REDISURL);
|
|
25
|
+
this.consumername = consumer;
|
|
26
|
+
this.service = service;
|
|
27
|
+
}
|
|
28
|
+
get requeststream() {
|
|
29
|
+
return this.getRequestStream(this.service);
|
|
30
|
+
}
|
|
31
|
+
getRequestStream(service) {
|
|
32
|
+
return `keyRequestedFrom${(0, utils_1.capitalizeFirstLetter)(service)}Service`;
|
|
33
|
+
}
|
|
34
|
+
setRequestEndpoint(callback) {
|
|
35
|
+
return this.addListener(this.requeststream, this.getRequestCallback(callback), this.service);
|
|
36
|
+
}
|
|
37
|
+
getRequestCallback(callback) {
|
|
38
|
+
return async (id, parameters) => {
|
|
39
|
+
const result = await callback(id, parameters);
|
|
40
|
+
if (!parameters.messageid)
|
|
41
|
+
return null;
|
|
42
|
+
const channel = this.getRequestSubscriptionName(parameters.messageid);
|
|
43
|
+
return this.publish(channel, result);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
publish(channel, result) {
|
|
47
|
+
return this.writer.publish(channel, result);
|
|
48
|
+
}
|
|
49
|
+
addListener(stream, callback, group) {
|
|
50
|
+
const client = new ioredis_1.default(this.redisConfig.REDISURL);
|
|
51
|
+
this.listeners.set(stream, new Listener_1.default(this, client, stream, callback, group));
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
addListListener(event, callback) {
|
|
55
|
+
const client = new ioredis_1.default(this.redisConfig.REDISURL);
|
|
56
|
+
const channel = this.getListChannel(event);
|
|
57
|
+
this.listeners.set(channel, new ListListener_1.default(this, client, channel, callback));
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
throwError(error) {
|
|
61
|
+
if (!this.rollbar)
|
|
62
|
+
throw new Error('Rollbar not initialized');
|
|
63
|
+
this.rollbar.error(error);
|
|
64
|
+
}
|
|
65
|
+
sendMessage(stream, data) {
|
|
66
|
+
return this.writer.xadd(stream, '*', ...this.createRedisMessage(data));
|
|
67
|
+
}
|
|
68
|
+
async getRequest(targetservice, key, data) {
|
|
69
|
+
// create a message id to subscribe to
|
|
70
|
+
const messageid = (0, uuid_1.v4)();
|
|
71
|
+
//subscribe to message response
|
|
72
|
+
const channel = this.getRequestSubscriptionName(messageid);
|
|
73
|
+
await this.subscribe(channel);
|
|
74
|
+
// send the message to the correct service
|
|
75
|
+
this.sendMessage(this.getRequestStream(targetservice), {
|
|
76
|
+
request: key,
|
|
77
|
+
messageid: messageid,
|
|
78
|
+
data: data
|
|
79
|
+
});
|
|
80
|
+
let resolver;
|
|
81
|
+
// create a promise to be able to pass on to resolve later
|
|
82
|
+
const promise = new Promise((r) => {
|
|
83
|
+
resolver = r;
|
|
84
|
+
});
|
|
85
|
+
if (!resolver)
|
|
86
|
+
return null;
|
|
87
|
+
//setup a timeout
|
|
88
|
+
const timeout = this.setupTimeout(resolver, channel);
|
|
89
|
+
//setup a response
|
|
90
|
+
this.requestMessageResponse(resolver, timeout);
|
|
91
|
+
// return a promise that will resolve when the message returns or times out
|
|
92
|
+
return promise;
|
|
93
|
+
}
|
|
94
|
+
requestMessageResponse(resolve, timeout) {
|
|
95
|
+
this.reader.once('message', (channel, message) => {
|
|
96
|
+
if (message.length)
|
|
97
|
+
resolve(message);
|
|
98
|
+
else
|
|
99
|
+
resolve(null);
|
|
100
|
+
this.unsubscribe(channel);
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
unsubscribe(channel) {
|
|
105
|
+
this.reader.unsubscribe(channel);
|
|
106
|
+
this.subscriptions = this.subscriptions.filter(s => s != channel);
|
|
107
|
+
}
|
|
108
|
+
setupTimeout(resolve, channel, n = 2000) {
|
|
109
|
+
return setTimeout(() => {
|
|
110
|
+
if (!this.subscriptions.includes(channel))
|
|
111
|
+
return;
|
|
112
|
+
console.log(`sub timedout: ${channel}`);
|
|
113
|
+
resolve(null);
|
|
114
|
+
this.unsubscribe(channel);
|
|
115
|
+
}, n);
|
|
116
|
+
}
|
|
117
|
+
subscribe(channel) {
|
|
118
|
+
this.subscriptions.push(channel);
|
|
119
|
+
return this.reader.subscribe(channel);
|
|
120
|
+
}
|
|
121
|
+
getRequestSubscriptionName(messageid) {
|
|
122
|
+
return `messageresponse${messageid}`;
|
|
123
|
+
}
|
|
124
|
+
async sendListEvent(event, listitems, data = {}) {
|
|
125
|
+
const list = await this.addToList.call(this, listitems);
|
|
126
|
+
return this.sendMessage(this.getListChannel(event), {
|
|
127
|
+
...data,
|
|
128
|
+
listname: list
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async addToList(listitems, listname) {
|
|
132
|
+
const list = listname ?? (0, uuid_1.v4)();
|
|
133
|
+
await this.writer.lpush(list, ...listitems.map(this.sanitizeValue));
|
|
134
|
+
return list;
|
|
135
|
+
}
|
|
136
|
+
getListChannel(event) {
|
|
137
|
+
return `${this.listprefix}${event}`;
|
|
138
|
+
}
|
|
139
|
+
sanitizeValue(value) {
|
|
140
|
+
if ((0, types_1.isMap)(value))
|
|
141
|
+
return (0, utils_1.stringifyMap)(value);
|
|
142
|
+
if ((0, class_validator_1.isObject)(value))
|
|
143
|
+
return JSON.stringify(value);
|
|
144
|
+
if ((0, class_validator_1.isArray)(value))
|
|
145
|
+
return JSON.stringify(value);
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
createRedisMessage(struct) {
|
|
149
|
+
return Object.keys(struct).reduce((arr, key) => [...arr, key, this.sanitizeValue(struct[key])], []);
|
|
150
|
+
}
|
|
151
|
+
async getStreamMessages(stream) {
|
|
152
|
+
const streaminfo = await this.getStreamInfo.call(this, stream);
|
|
153
|
+
if (!streaminfo || !(0, class_validator_1.isArray)(streaminfo) || streaminfo.length < 10)
|
|
154
|
+
return [];
|
|
155
|
+
const streamresponses = streaminfo[9];
|
|
156
|
+
return this.decypherResponse(...streamresponses).flatMap(r => r.messages);
|
|
157
|
+
}
|
|
158
|
+
getStreamInfo(stream, count = 0) {
|
|
159
|
+
return this.reader.xinfo('STREAM', stream, 'FULL', 'COUNT', count);
|
|
160
|
+
}
|
|
161
|
+
async filterStream(stream, key, value) {
|
|
162
|
+
const messages = await this.getStreamMessages.call(this, stream);
|
|
163
|
+
return messages.filter(message => message.parameters?.[key] != null && message.parameters[key] == value);
|
|
164
|
+
}
|
|
165
|
+
decypherResponse(...responses) {
|
|
166
|
+
return responses.reduce((resp, response) => [
|
|
167
|
+
...resp,
|
|
168
|
+
{
|
|
169
|
+
stream: response[0],
|
|
170
|
+
messages: response[1].map((message) => ({
|
|
171
|
+
id: message[0],
|
|
172
|
+
parameters: this.decypherParameters(message[1])
|
|
173
|
+
}))
|
|
174
|
+
}
|
|
175
|
+
], []);
|
|
176
|
+
}
|
|
177
|
+
decypherParameters(parameters) {
|
|
178
|
+
return parameters.reduce((params, v, n) => (n == 0 || (n % 2 == 0)) && parameters.length >= n + 1 ? ({
|
|
179
|
+
...params,
|
|
180
|
+
[v]: parameters[n + 1]
|
|
181
|
+
}) : params, {});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
exports.default = Broker;
|
|
185
|
+
//# sourceMappingURL=Broker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Broker.js","sourceRoot":"","sources":["../../../src/classes/Broker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
import Broker from "./Broker";
|
|
3
|
+
export default abstract class BrokerClient {
|
|
4
|
+
broker: Broker;
|
|
5
|
+
client: Redis;
|
|
6
|
+
group: string | null;
|
|
7
|
+
stream: string | null;
|
|
8
|
+
list: string | null;
|
|
9
|
+
callback: Function;
|
|
10
|
+
constructor(broker: Broker, client: Redis, callback: Function, list: string | null, stream?: string | null, group?: string | null);
|
|
11
|
+
throwError(error: any): void;
|
|
12
|
+
decypherResponse(...responses: StreamResponse[]): DecypheredResponse[];
|
|
13
|
+
getGroupResponse(): Promise<StreamResponse[] | null>;
|
|
14
|
+
getResponse(lastid: string): Promise<StreamResponse[] | null>;
|
|
15
|
+
listenToStream(lastid?: string): Promise<Function | null>;
|
|
16
|
+
abstract streamCallback(message: DecypheredMessage): Promise<any | null>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class BrokerClient {
|
|
4
|
+
constructor(broker, client, callback, list, stream = null, group = null) {
|
|
5
|
+
this.group = null;
|
|
6
|
+
this.stream = null;
|
|
7
|
+
this.list = null;
|
|
8
|
+
this.broker = broker;
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.list = list;
|
|
11
|
+
this.stream = stream;
|
|
12
|
+
this.group = group;
|
|
13
|
+
this.callback = callback;
|
|
14
|
+
this.listenToStream.call(this);
|
|
15
|
+
}
|
|
16
|
+
throwError(error) {
|
|
17
|
+
return this.broker.throwError(error);
|
|
18
|
+
}
|
|
19
|
+
decypherResponse(...responses) {
|
|
20
|
+
return this.broker.decypherResponse(...responses);
|
|
21
|
+
}
|
|
22
|
+
async getGroupResponse() {
|
|
23
|
+
if (!this.group || !this.stream)
|
|
24
|
+
return null;
|
|
25
|
+
return await this.client.xreadgroup('GROUP', this.group, this.broker.consumername, 'COUNT', 1, 'BLOCK', 0, 'STREAMS', this.stream, '>');
|
|
26
|
+
}
|
|
27
|
+
async getResponse(lastid) {
|
|
28
|
+
if (!this.stream)
|
|
29
|
+
return null;
|
|
30
|
+
return await this.client.xread("BLOCK", 0, "STREAMS", this.stream, lastid);
|
|
31
|
+
}
|
|
32
|
+
async listenToStream(lastid = '$') {
|
|
33
|
+
const responses = await this.getResponse.call(this, lastid);
|
|
34
|
+
if (!responses)
|
|
35
|
+
return null;
|
|
36
|
+
const streamResponses = this.decypherResponse(...responses);
|
|
37
|
+
const messages = streamResponses.flatMap(r => r.messages);
|
|
38
|
+
await Promise.all(messages.map(async (message) => {
|
|
39
|
+
try {
|
|
40
|
+
await this.streamCallback(message);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
this.broker.throwError(e);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
return this.listenToStream.call(this, messages[messages.length - 1]?.id ?? '$');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.default = BrokerClient;
|
|
52
|
+
//# sourceMappingURL=BrokerClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrokerClient.js","sourceRoot":"","sources":["../../../src/classes/BrokerClient.ts"],"names":[],"mappings":";;AAGA,MAA8B,YAAY;IAQxC,YAAa,MAAa,EAAE,MAAY,EAAE,QAAiB,EAAE,IAAkB,EAAE,SAAuB,IAAI,EAAE,QAAuB,IAAI;QALzI,UAAK,GAAmB,IAAI,CAAA;QAC5B,WAAM,GAAmB,IAAI,CAAA;QAC7B,SAAI,GAAmB,IAAI,CAAA;QAIzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QAExB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAE,IAAI,CAAE,CAAA;IAClC,CAAC;IAED,UAAU,CAAE,KAAS;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,CAAE,KAAK,CAAE,CAAA;IACxC,CAAC;IAED,gBAAgB,CAAE,GAAG,SAA0B;QAC7C,OAAO,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAE,GAAG,SAAS,CAAE,CAAA;IACrD,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM;YAAG,OAAO,IAAI,CAAA;QAC7C,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAE,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAuB,CAAA;IAChK,CAAC;IAED,KAAK,CAAC,WAAW,CAAE,MAAa;QAC9B,IAAI,CAAC,IAAI,CAAC,MAAM;YAAG,OAAO,IAAI,CAAA;QAC9B,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAE,CAAA;IAC7E,CAAC;IAGD,KAAK,CAAC,cAAc,CAAE,SAAgB,GAAG;QACvC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAE,IAAI,EAAE,MAAM,CAAE,CAAA;QAC7D,IAAI,CAAC,SAAS;YAAG,OAAO,IAAI,CAAA;QAE5B,MAAM,eAAe,GAAG,IAAI,CAAC,gBAAgB,CAAE,GAAG,SAAS,CAAE,CAAA;QAC7D,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAE,CAAA;QAC3D,MAAM,OAAO,CAAC,GAAG,CAAE,QAAQ,CAAC,GAAG,CAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACjD,IAAG;gBACD,MAAM,IAAI,CAAC,cAAc,CAAE,OAAO,CAAE,CAAA;aAErC;YAAA,OAAO,CAAK,EAAE;gBACb,IAAI,CAAC,MAAM,CAAC,UAAU,CAAE,CAAC,CAAE,CAAA;aAC5B;oBAAO;aAEP;QACH,CAAC,CAAC,CAAE,CAAA;QAEJ,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAE,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAC,CAAC,CAAC,EAAE,EAAE,IAAE,GAAG,CAAE,CAAA;IAC/E,CAAC;CAGF;AA3DD,+BA2DC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
import Broker from "./Broker";
|
|
3
|
+
import BrokerClient from "./BrokerClient";
|
|
4
|
+
import ListRunner from "./ListRunner";
|
|
5
|
+
export default class Listener extends BrokerClient {
|
|
6
|
+
constructor(broker: Broker, client: Redis, list: string, callback: Function);
|
|
7
|
+
streamCallback(message: DecypheredMessage): Promise<ListRunner | null>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const BrokerClient_1 = tslib_1.__importDefault(require("./BrokerClient"));
|
|
5
|
+
const ListRunner_1 = tslib_1.__importDefault(require("./ListRunner"));
|
|
6
|
+
class Listener extends BrokerClient_1.default {
|
|
7
|
+
constructor(broker, client, list, callback) {
|
|
8
|
+
super(broker, client, callback, list);
|
|
9
|
+
}
|
|
10
|
+
async streamCallback(message) {
|
|
11
|
+
const listrunnercallback = await this.callback(message.id, message.parameters);
|
|
12
|
+
if (message.parameters.listname)
|
|
13
|
+
return new ListRunner_1.default(this.client, message.parameters.listname, listrunnercallback);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.default = Listener;
|
|
18
|
+
//# sourceMappingURL=ListListener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListListener.js","sourceRoot":"","sources":["../../../src/classes/ListListener.ts"],"names":[],"mappings":";;;AAEA,0EAAyC;AACzC,sEAAqC;AAErC,MAAqB,QAAS,SAAQ,sBAAY;IAChD,YAAa,MAAa,EAAE,MAAY,EAAE,IAAW,EAAE,QAAiB;QACtE,KAAK,CAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAE,CAAA;IACzC,CAAC;IAED,KAAK,CAAC,cAAc,CAAE,OAAyB;QAC7C,MAAM,kBAAkB,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,UAAU,CAAE,CAAA;QAEhF,IAAI,OAAO,CAAC,UAAU,CAAC,QAAQ;YAC7B,OAAO,IAAI,oBAAU,CAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAA;QAEtF,OAAO,IAAI,CAAA;IACb,CAAC;CACF;AAbD,2BAaC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
export default class ListRunner {
|
|
3
|
+
listname: string;
|
|
4
|
+
itemsDone: number;
|
|
5
|
+
itemsPerCall: number;
|
|
6
|
+
callback: Function;
|
|
7
|
+
client: Redis;
|
|
8
|
+
constructor(client: Redis, listname: string, callback: Function, itemsPerCall?: number);
|
|
9
|
+
run(): Promise<Function | void>;
|
|
10
|
+
nextitems(listname: string): Promise<string[] | null>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class ListRunner {
|
|
4
|
+
constructor(client, listname, callback, itemsPerCall = 1) {
|
|
5
|
+
this.itemsDone = 0;
|
|
6
|
+
this.client = client;
|
|
7
|
+
this.callback = callback;
|
|
8
|
+
this.listname = listname;
|
|
9
|
+
this.itemsPerCall = itemsPerCall;
|
|
10
|
+
this.run.call(this);
|
|
11
|
+
}
|
|
12
|
+
async run() {
|
|
13
|
+
const items = await this.nextitems.call(this, this.listname);
|
|
14
|
+
if (!items) {
|
|
15
|
+
console.log(`finished with ${this.itemsDone} jobs`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await this.callback(items);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.log(e);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
this.itemsDone += this.itemsPerCall;
|
|
26
|
+
return this.run.call(this);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async nextitems(listname) {
|
|
30
|
+
return this.client.lpop(listname, this.itemsPerCall);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.default = ListRunner;
|
|
34
|
+
//# sourceMappingURL=ListRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListRunner.js","sourceRoot":"","sources":["../../../src/classes/ListRunner.ts"],"names":[],"mappings":";;AAEA,MAAqB,UAAU;IAQ7B,YAAa,MAAY,EAAE,QAAe,EAAE,QAAiB,EAAE,eAAwB,CAAC;QANxF,cAAS,GAAU,CAAC,CAAA;QAOlB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;QAEhC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAE,IAAI,CAAE,CAAA;IACvB,CAAC;IAGD,KAAK,CAAC,GAAG;QACP,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAE,CAAA;QAC9D,IAAI,CAAC,KAAK,EAAE;YACV,OAAO,CAAC,GAAG,CAAE,iBAAiB,IAAI,CAAC,SAAS,OAAO,CAAC,CAAA;YACpD,OAAM;SACP;QAED,IAAG;YACD,MAAM,IAAI,CAAC,QAAQ,CAAE,KAAK,CAAE,CAAA;SAC7B;QAAA,OAAO,CAAK,EAAE;YACb,OAAO,CAAC,GAAG,CAAE,CAAC,CAAE,CAAA;SAEjB;gBAAO;YACN,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,YAAY,CAAA;YACnC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAE,IAAI,CAAE,CAAA;SAC7B;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAE,QAAe;QAC9B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAE,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAE,CAAA;IACxD,CAAC;CAEF;AAxCD,6BAwCC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
import Broker from "./Broker";
|
|
3
|
+
import BrokerClient from "./BrokerClient";
|
|
4
|
+
export default class Listener extends BrokerClient {
|
|
5
|
+
constructor(broker: Broker, client: Redis, stream: string, callback: Function, group?: string);
|
|
6
|
+
streamCallback(message: DecypheredMessage): Promise<any>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const BrokerClient_1 = tslib_1.__importDefault(require("./BrokerClient"));
|
|
5
|
+
class Listener extends BrokerClient_1.default {
|
|
6
|
+
constructor(broker, client, stream, callback, group) {
|
|
7
|
+
super(broker, client, callback, null, stream, group);
|
|
8
|
+
}
|
|
9
|
+
streamCallback(message) {
|
|
10
|
+
return this.callback(message.id, message.parameters);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.default = Listener;
|
|
14
|
+
//# sourceMappingURL=Listener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Listener.js","sourceRoot":"","sources":["../../../src/classes/Listener.ts"],"names":[],"mappings":";;;AAEA,0EAAyC;AAEzC,MAAqB,QAAS,SAAQ,sBAAY;IAChD,YAAa,MAAa,EAAE,MAAY,EAAE,MAAa,EAAE,QAAiB,EAAE,KAAe;QACzF,KAAK,CAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAE,CAAA;IACxD,CAAC;IAED,cAAc,CAAC,OAA0B;QACvC,OAAO,IAAI,CAAC,QAAQ,CAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,UAAU,CAAE,CAAA;IACxD,CAAC;CACF;AARD,2BAQC"}
|
package/dist/tsc/main.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Broker = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const Broker_1 = tslib_1.__importDefault(require("./classes/Broker"));
|
|
6
|
+
exports.Broker = Broker_1.default;
|
|
7
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":";;;;AAAA,sEAAqC;AAE5B,iBAFF,gBAAM,CAEE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const isJSON: (str: any) => boolean;
|
|
2
|
+
interface ObjectToOmitFrom {
|
|
3
|
+
[index: string]: any;
|
|
4
|
+
}
|
|
5
|
+
declare const omit: (objectToOmitFrom: ObjectToOmitFrom, keys: (keyof ObjectToOmitFrom)[]) => {};
|
|
6
|
+
declare const mapJsonReplacer: (_key: any, value: Map<any, any> | any) => any;
|
|
7
|
+
declare const mapJsonReviver: (_key: any, value: JsonStringifiedMap | any) => any;
|
|
8
|
+
declare const stringifyMap: (map: Map<any, any>) => string;
|
|
9
|
+
declare const parseMap: (json: string) => Map<any, any>;
|
|
10
|
+
declare const capitalizeFirstLetter: (string: string) => string;
|
|
11
|
+
export { isJSON, omit, mapJsonReviver, stringifyMap, parseMap, mapJsonReplacer, capitalizeFirstLetter };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.capitalizeFirstLetter = exports.mapJsonReplacer = exports.parseMap = exports.stringifyMap = exports.mapJsonReviver = exports.omit = exports.isJSON = void 0;
|
|
4
|
+
const isJSON = (str) => {
|
|
5
|
+
if (typeof str == 'number')
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
JSON.parse(str);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
};
|
|
15
|
+
exports.isJSON = isJSON;
|
|
16
|
+
const omit = (objectToOmitFrom, keys) => Object.keys(objectToOmitFrom)
|
|
17
|
+
.filter(key => keys.indexOf(key) == -1)
|
|
18
|
+
.reduce((struct, key) => Object.assign(struct, { [key]: objectToOmitFrom[key] }), {});
|
|
19
|
+
exports.omit = omit;
|
|
20
|
+
const mapJsonReplacer = (_key, value) => {
|
|
21
|
+
if (value instanceof Map) {
|
|
22
|
+
return {
|
|
23
|
+
dataType: 'Map',
|
|
24
|
+
value: Array.from(value.entries()), // or with spread: value: [...value]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
exports.mapJsonReplacer = mapJsonReplacer;
|
|
32
|
+
const mapJsonReviver = (_key, value) => {
|
|
33
|
+
if (typeof value === 'object' && value !== null) {
|
|
34
|
+
if (value.dataType === 'Map') {
|
|
35
|
+
return new Map(value.value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
exports.mapJsonReviver = mapJsonReviver;
|
|
41
|
+
const stringifyMap = (map) => JSON.stringify(map, exports.mapJsonReplacer);
|
|
42
|
+
exports.stringifyMap = stringifyMap;
|
|
43
|
+
const parseMap = (json) => JSON.parse(json, exports.mapJsonReviver);
|
|
44
|
+
exports.parseMap = parseMap;
|
|
45
|
+
const capitalizeFirstLetter = (string) => string.charAt(0).toUpperCase() + string.slice(1);
|
|
46
|
+
exports.capitalizeFirstLetter = capitalizeFirstLetter;
|
|
47
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/services/utils.ts"],"names":[],"mappings":";;;AAAA,MAAM,MAAM,GAAG,CAAE,GAAO,EAAG,EAAE;IAC3B,IAAI,OAAO,GAAG,IAAI,QAAQ;QAAG,OAAO,KAAK,CAAA;IACzC,IAAG;QACD,IAAI,CAAC,KAAK,CAAE,GAAG,CAAE,CAAA;KAClB;IAAA,OAAO,KAAK,EAAE;QACb,OAAO,KAAK,CAAC;KACd;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAA;AAyCQ,wBAAM;AAlCf,MAAM,IAAI,GAAG,CAAE,gBAAiC,EAAE,IAA+B,EAAE,EAAE,CACrF,MAAM,CAAC,IAAI,CAAE,gBAAgB,CAAE;KAC9B,MAAM,CAAE,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAE,GAAG,CAAE,IAAI,CAAC,CAAC,CAAE;KAC1C,MAAM,CAAE,CAAC,MAAa,EAAC,GAAG,EAAE,EAAE,CAC7B,MAAM,CAAC,MAAM,CAAE,MAAM,EAAE,EAAC,CAAC,GAAG,CAAC,EAAC,gBAAgB,CAAC,GAAG,CAAC,EAAC,CAAE,EACtD,EAAE,CACH,CAAA;AA4BgB,oBAAI;AAxBrB,MAAM,eAAe,GAAG,CAAE,IAAQ,EAAE,KAAwB,EAAG,EAAE;IAC/D,IAAG,KAAK,YAAY,GAAG,EAAE;QACvB,OAAO;YACL,QAAQ,EAAE,KAAK;YACf,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,oCAAoC;SACnD,CAAA;KACxB;SAAM;QACL,OAAO,KAAK,CAAC;KACd;AACH,CAAC,CAAA;AAe8D,0CAAe;AAb9E,MAAM,cAAc,GAAG,CAAE,IAAQ,EAAE,KAA8B,EAAG,EAAE;IACpE,IAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE;QAC9C,IAAI,KAAK,CAAC,QAAQ,KAAK,KAAK,EAAE;YAC5B,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;SAC7B;KACF;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAA;AAMsB,wCAAc;AAJrC,MAAM,YAAY,GAAG,CAAE,GAAgB,EAAS,EAAE,CAAC,IAAI,CAAC,SAAS,CAAE,GAAG,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;AAIzD,oCAAY;AAHnD,MAAM,QAAQ,GAAG,CAAE,IAAW,EAAgB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAE,IAAI,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;AAGrC,4BAAQ;AAF7D,MAAM,qBAAqB,GAAG,CAAC,MAAa,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAEjB,sDAAqB"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "woonplan-packages-redishelper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "Jasper Denk <jasper@e-trias.nl>",
|
|
6
6
|
"repository": "https://github.com/e-line-websolutions/woonplan-micro-energyio",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"keywords": [],
|
|
9
9
|
"main": "./dist/tsc/main.js",
|
|
10
|
-
"types": "./dist/tsc/
|
|
10
|
+
"types": "./dist/tsc/types.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "ts-node --esm src/main.ts",
|
|
13
13
|
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx",
|
package/tsconfig.json
CHANGED
package/jest.config.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -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
|
-
})
|
package/src/classes/Broker.ts
DELETED
|
@@ -1,224 +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 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
|
-
|
|
@@ -1,63 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Redis } from "ioredis"
|
|
2
|
-
import Broker from "./Broker"
|
|
3
|
-
import BrokerClient from "./BrokerClient"
|
|
4
|
-
import ListRunner from "./ListRunner"
|
|
5
|
-
|
|
6
|
-
export default class Listener extends BrokerClient{
|
|
7
|
-
constructor( broker:Broker, client:Redis, list:string, callback:Function ){
|
|
8
|
-
super( broker, client, callback, list )
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async streamCallback( message:DecypheredMessage ){
|
|
12
|
-
const listrunnercallback = await this.callback( message.id, message.parameters )
|
|
13
|
-
|
|
14
|
-
if( message.parameters.listname )
|
|
15
|
-
return new ListRunner( this.client, message.parameters.listname, listrunnercallback)
|
|
16
|
-
|
|
17
|
-
return null
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -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
|
-
}
|
package/src/classes/Listener.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Redis } from "ioredis"
|
|
2
|
-
import Broker from "./Broker"
|
|
3
|
-
import BrokerClient from "./BrokerClient"
|
|
4
|
-
|
|
5
|
-
export default class Listener extends BrokerClient{
|
|
6
|
-
constructor( broker:Broker, client:Redis, stream:string, callback:Function, group ?: string ){
|
|
7
|
-
super( broker, client, callback, null, stream, group )
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
streamCallback(message: DecypheredMessage): Promise<any> {
|
|
11
|
-
return this.callback( message.id, message.parameters )
|
|
12
|
-
}
|
|
13
|
-
}
|
package/src/main.ts
DELETED
package/src/services/utils.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
const isJSON = ( str:any ) => {
|
|
3
|
-
if( typeof str == 'number' ) return false
|
|
4
|
-
try{
|
|
5
|
-
JSON.parse( str )
|
|
6
|
-
}catch( error ){
|
|
7
|
-
return false;
|
|
8
|
-
}
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface ObjectToOmitFrom{
|
|
13
|
-
[index: string]: any;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const omit = ( objectToOmitFrom:ObjectToOmitFrom, keys:(keyof ObjectToOmitFrom)[]) =>
|
|
18
|
-
Object.keys( objectToOmitFrom )
|
|
19
|
-
.filter( key => keys.indexOf( key ) == -1 )
|
|
20
|
-
.reduce( (struct:object,key) =>
|
|
21
|
-
Object.assign( struct, {[key]:objectToOmitFrom[key]} ) ,
|
|
22
|
-
{}
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const mapJsonReplacer = ( _key:any, value:Map<any,any> | any ) => {
|
|
28
|
-
if(value instanceof Map) {
|
|
29
|
-
return {
|
|
30
|
-
dataType: 'Map',
|
|
31
|
-
value: Array.from(value.entries()), // or with spread: value: [...value]
|
|
32
|
-
} as JsonStringifiedMap
|
|
33
|
-
} else {
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const mapJsonReviver = ( _key:any, value:JsonStringifiedMap | any ) => {
|
|
39
|
-
if(typeof value === 'object' && value !== null) {
|
|
40
|
-
if (value.dataType === 'Map') {
|
|
41
|
-
return new Map(value.value);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const stringifyMap = ( map:Map<any,any> ):string=> JSON.stringify( map, exports.mapJsonReplacer)
|
|
48
|
-
const parseMap = ( json:string ):Map<any,any> => JSON.parse( json, exports.mapJsonReviver)
|
|
49
|
-
const capitalizeFirstLetter = (string:string) => string.charAt(0).toUpperCase() + string.slice(1)
|
|
50
|
-
|
|
51
|
-
export { isJSON, omit, mapJsonReviver, stringifyMap, parseMap, mapJsonReplacer, capitalizeFirstLetter }
|