@weave-js/redis-transport 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/lib/index.js +114 -77
- package/package.json +5 -5
- package/test/adapter.test.js +139 -20
- package/test/unit.test.js +203 -0
- package/.vscode/launch.json +0 -35
- package/LICENSE +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.14.0](https://github.com/weave-microservices/weave/compare/@weave-js/redis-transport@0.12.1...@weave-js/redis-transport@0.14.0) (2023-05-04)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
* Release Version 0.14.0 (#279) ([12a003c](https://github.com/weave-microservices/weave/commit/12a003c05b2960b20810fbbad496258b5d4b0606)), closes [#279](https://github.com/weave-microservices/weave/issues/279) [#276](https://github.com/weave-microservices/weave/issues/276) [#278](https://github.com/weave-microservices/weave/issues/278)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### BREAKING CHANGES
|
|
13
|
+
|
|
14
|
+
* Removed http error codes from errors and added correct codes
|
|
15
|
+
|
|
16
|
+
* chore: published 0.13.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# [0.13.0](https://github.com/weave-microservices/weave/compare/@weave-js/redis-transport@0.12.1...@weave-js/redis-transport@0.13.0) (2022-08-16)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @weave-js/redis-transport
|
package/lib/index.js
CHANGED
|
@@ -3,102 +3,139 @@
|
|
|
3
3
|
* -----
|
|
4
4
|
* Copyright 2021 Fachwerk
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
const { TransportAdapters } = require('@weave-js/core');
|
|
6
|
+
const redis = require('redis')
|
|
7
|
+
const { defaultsDeep } = require('@weave-js/utils')
|
|
8
|
+
const { TransportAdapters } = require('@weave-js/core')
|
|
10
9
|
|
|
11
10
|
const defaultOptions = {
|
|
12
11
|
port: 6379,
|
|
13
12
|
host: '127.0.0.1'
|
|
14
|
-
}
|
|
13
|
+
}
|
|
15
14
|
|
|
16
15
|
const RedisTransportAdapter = (adapterOptions) => {
|
|
17
|
-
let clientSub
|
|
18
|
-
let clientPub
|
|
16
|
+
let clientSub
|
|
17
|
+
let clientPub
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
adapterOptions = defaultsDeep(adapterOptions, defaultOptions);
|
|
19
|
+
adapterOptions = defaultsDeep(adapterOptions, defaultOptions)
|
|
22
20
|
|
|
23
21
|
return Object.assign(TransportAdapters.BaseAdapter(adapterOptions), {
|
|
24
22
|
name: 'REDIS',
|
|
23
|
+
|
|
25
24
|
connect () {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.isConnected = true;
|
|
40
|
-
resolve();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
clientPub.on('error', error => {
|
|
44
|
-
this.log.error('Redis PUB error:', error.message);
|
|
45
|
-
this.isConnected = false;
|
|
46
|
-
reject(error);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
clientPub.on('close', () => {
|
|
50
|
-
if (this.isConnected) {
|
|
51
|
-
this.isConnected = false;
|
|
52
|
-
this.interruptionCount++;
|
|
53
|
-
this.log.warn('Redis PUB disconnected.');
|
|
54
|
-
this.disconnected();
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
clientSub.on('error', error => {
|
|
60
|
-
this.log.error('Redis PUB error:', error.message);
|
|
61
|
-
reject(error);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
clientSub.on('message', (topic, message) => {
|
|
65
|
-
const type = topic.split('.')[1];
|
|
66
|
-
this.incomingMessage(type, message);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
clientSub.on('close', () => {
|
|
70
|
-
this.log.warn('Redis SUB disconnected.');
|
|
71
|
-
});
|
|
25
|
+
if (clientSub || clientPub) {
|
|
26
|
+
return Promise.reject(new Error('Adapter already connected.'))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Ohne explizite Strategie gibt v3 je nach Fehlerbild auf,
|
|
30
|
+
// dann bleibt 'reconnecting' aus. Solange eine Zahl kommt, wird retried.
|
|
31
|
+
const retryStrategy = ({ attempt }) => Math.min(attempt * 100, 3000)
|
|
32
|
+
|
|
33
|
+
clientPub = redis.createClient({ ...adapterOptions, retry_strategy: retryStrategy })
|
|
34
|
+
clientSub = redis.createClient({
|
|
35
|
+
...adapterOptions,
|
|
36
|
+
retry_strategy: retryStrategy,
|
|
37
|
+
return_buffers: true // binärsichere Payload + korrekte Byte-Statistik
|
|
72
38
|
})
|
|
39
|
+
|
|
40
|
+
const isUp = () =>
|
|
41
|
+
Boolean(clientSub && clientSub.ready && clientPub && clientPub.ready)
|
|
42
|
+
|
|
43
|
+
const syncState = () => {
|
|
44
|
+
const up = isUp()
|
|
45
|
+
if (up === this.isConnected) return
|
|
46
|
+
|
|
47
|
+
this.isConnected = up
|
|
48
|
+
|
|
49
|
+
if (up) {
|
|
50
|
+
const attempts = this.repeatAttemptCounter
|
|
51
|
+
this.repeatAttemptCounter = 0
|
|
52
|
+
this.log.info(`Redis reconnected${attempts ? ` after ${attempts} attempts` : ''}.`)
|
|
53
|
+
this.connected({ wasReconnect: true })
|
|
54
|
+
} else {
|
|
55
|
+
this.interruptCounter++
|
|
56
|
+
this.log.warn(`Redis disconnected (SUB: ${clientSub.ready}, PUB: ${clientPub.ready}).`)
|
|
57
|
+
this.disconnected()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const waitForReady = (client) => new Promise((resolve, reject) => {
|
|
62
|
+
const onReady = () => { client.removeListener('error', onError); resolve() }
|
|
63
|
+
const onError = (error) => { client.removeListener('ready', onReady); reject(error) }
|
|
64
|
+
client.once('ready', onReady)
|
|
65
|
+
client.once('error', onError)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return Promise.all([waitForReady(clientPub), waitForReady(clientSub)])
|
|
73
69
|
.then(() => {
|
|
74
|
-
this.
|
|
75
|
-
|
|
70
|
+
this.isConnected = true
|
|
71
|
+
|
|
72
|
+
for (const [client, label] of [[clientSub, 'SUB'], [clientPub, 'PUB']]) {
|
|
73
|
+
client.on('ready', syncState)
|
|
74
|
+
client.on('end', syncState)
|
|
75
|
+
|
|
76
|
+
client.on('reconnecting', () => {
|
|
77
|
+
this.repeatAttemptCounter++
|
|
78
|
+
this.log.debug(`Redis ${label} reconnecting (attempt ${this.repeatAttemptCounter}).`)
|
|
79
|
+
syncState()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Dauerhaft nötig: EventEmitter ohne error-Listener wirft.
|
|
83
|
+
client.on('error', (error) => {
|
|
84
|
+
const detail = !error
|
|
85
|
+
? '(no error object)'
|
|
86
|
+
: error.message || error.code || error.constructor.name
|
|
87
|
+
this.log.error(`Redis ${label} error: ${detail}`)
|
|
88
|
+
syncState()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
clientSub.on('message', (topic, message) => {
|
|
93
|
+
const type = topic.toString().split('.')[1]
|
|
94
|
+
this.incomingMessage(type, message)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
this.log.info('Redis SUB and PUB clients connected.')
|
|
98
|
+
this.connected()
|
|
99
|
+
})
|
|
100
|
+
.catch((error) => {
|
|
101
|
+
if (clientSub) clientSub.end(true)
|
|
102
|
+
if (clientPub) clientPub.end(true)
|
|
103
|
+
clientSub = clientPub = null
|
|
104
|
+
throw error
|
|
105
|
+
})
|
|
76
106
|
},
|
|
107
|
+
|
|
77
108
|
subscribe (type, nodeId) {
|
|
78
|
-
return new Promise(resolve => {
|
|
79
|
-
const topic = this.getTopic(type, nodeId)
|
|
80
|
-
clientSub.subscribe(topic, () =>
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
});
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const topic = this.getTopic(type, nodeId)
|
|
111
|
+
clientSub.subscribe(topic, (error) => error ? reject(error) : resolve())
|
|
112
|
+
})
|
|
84
113
|
},
|
|
114
|
+
|
|
85
115
|
send (message) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const topic = this.getTopic(message.type, message.targetNodeId);
|
|
90
|
-
clientPub.publish(topic, data);
|
|
116
|
+
if (!this.isConnected) {
|
|
117
|
+
this.log.debug('Message dropped, adapter not connected.', { type: message.type })
|
|
118
|
+
return Promise.resolve()
|
|
91
119
|
}
|
|
92
|
-
|
|
120
|
+
|
|
121
|
+
const data = this.serialize(message)
|
|
122
|
+
if (!data) return Promise.resolve()
|
|
123
|
+
|
|
124
|
+
this.updateStatisticSent(data.length)
|
|
125
|
+
clientPub.publish(this.getTopic(message.type, message.targetNodeId), data)
|
|
126
|
+
|
|
127
|
+
return Promise.resolve()
|
|
93
128
|
},
|
|
129
|
+
|
|
94
130
|
close () {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
131
|
+
const quit = (client) => client
|
|
132
|
+
? new Promise((resolve) => client.quit(() => resolve()))
|
|
133
|
+
: Promise.resolve()
|
|
134
|
+
|
|
135
|
+
return Promise.all([quit(clientPub), quit(clientSub)])
|
|
136
|
+
.then(() => { clientSub = clientPub = null })
|
|
100
137
|
}
|
|
101
|
-
})
|
|
102
|
-
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
103
140
|
|
|
104
|
-
module.exports = RedisTransportAdapter
|
|
141
|
+
module.exports = RedisTransportAdapter
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weave-js/redis-transport",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "weave redis transport adapter module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
},
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@weave-js/core": "
|
|
23
|
+
"@weave-js/core": "0.16.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@weave-js/core": "^0.
|
|
26
|
+
"@weave-js/core": "^0.16.0"
|
|
27
27
|
},
|
|
28
28
|
"jest": {
|
|
29
29
|
"testEnvironment": "node",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@weave-js/utils": "^0.
|
|
36
|
+
"@weave-js/utils": "^0.13.0",
|
|
37
37
|
"redis": "^3.1.2"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "84fd5e4af3af96e8c004bcfba40bb3cb35f23fa7"
|
|
40
40
|
}
|
package/test/adapter.test.js
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
const { Weave } = require('@weave-js/core');
|
|
2
2
|
const REDISTransport = require('../lib/index');
|
|
3
3
|
|
|
4
|
+
const waitUntil = (predicate, timeout = 5000, interval = 25) =>
|
|
5
|
+
new Promise((resolve, reject) => {
|
|
6
|
+
const startedAt = Date.now();
|
|
7
|
+
|
|
8
|
+
const check = () => {
|
|
9
|
+
if (predicate()) {
|
|
10
|
+
return resolve();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (Date.now() - startedAt > timeout) {
|
|
14
|
+
return reject(new Error('Timeout while waiting for condition'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setTimeout(check, interval);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
check();
|
|
21
|
+
});
|
|
22
|
+
|
|
4
23
|
describe('REDIS transport adapter', () => {
|
|
5
|
-
|
|
6
|
-
let broker2;
|
|
24
|
+
const namespace = `redis-test-${process.pid}-${Date.now()}`;
|
|
7
25
|
const startedHook1 = jest.fn();
|
|
8
26
|
const startedHook2 = jest.fn();
|
|
27
|
+
const eventHandler = jest.fn();
|
|
28
|
+
const broadcastHandler1 = jest.fn();
|
|
29
|
+
const broadcastHandler2 = jest.fn();
|
|
9
30
|
|
|
10
|
-
|
|
31
|
+
let broker1;
|
|
32
|
+
let broker2;
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
11
35
|
broker1 = Weave({
|
|
12
36
|
nodeId: 'node1',
|
|
13
37
|
logger: {
|
|
14
38
|
enabled: false
|
|
15
39
|
},
|
|
16
|
-
namespace
|
|
40
|
+
namespace,
|
|
17
41
|
transport: {
|
|
18
42
|
adapter: REDISTransport()
|
|
19
43
|
},
|
|
@@ -25,7 +49,13 @@ describe('REDIS transport adapter', () => {
|
|
|
25
49
|
actions: {
|
|
26
50
|
hello (context) {
|
|
27
51
|
return 'Hello from ' + context.nodeId;
|
|
52
|
+
},
|
|
53
|
+
greet (context) {
|
|
54
|
+
return `Hello ${context.data.name}!`;
|
|
28
55
|
}
|
|
56
|
+
},
|
|
57
|
+
events: {
|
|
58
|
+
'user.broadcasted': broadcastHandler1
|
|
29
59
|
}
|
|
30
60
|
});
|
|
31
61
|
|
|
@@ -34,7 +64,7 @@ describe('REDIS transport adapter', () => {
|
|
|
34
64
|
logger: {
|
|
35
65
|
enabled: false
|
|
36
66
|
},
|
|
37
|
-
namespace
|
|
67
|
+
namespace,
|
|
38
68
|
transport: {
|
|
39
69
|
adapter: REDISTransport()
|
|
40
70
|
},
|
|
@@ -46,34 +76,123 @@ describe('REDIS transport adapter', () => {
|
|
|
46
76
|
actions: {
|
|
47
77
|
hello () {
|
|
48
78
|
return 'Hello from ' + this.broker.nodeId;
|
|
79
|
+
},
|
|
80
|
+
echo (context) {
|
|
81
|
+
return context.data;
|
|
82
|
+
},
|
|
83
|
+
fail () {
|
|
84
|
+
throw new Error('Expected failure');
|
|
49
85
|
}
|
|
86
|
+
},
|
|
87
|
+
events: {
|
|
88
|
+
'user.created': eventHandler,
|
|
89
|
+
'user.broadcasted': broadcastHandler2
|
|
50
90
|
}
|
|
51
91
|
});
|
|
52
92
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
93
|
+
await Promise.all([broker1.start(), broker2.start()]);
|
|
94
|
+
|
|
95
|
+
await Promise.all([
|
|
96
|
+
broker1.waitForServices(['testService2']),
|
|
97
|
+
broker2.waitForServices(['testService1'])
|
|
56
98
|
]);
|
|
57
99
|
});
|
|
58
100
|
|
|
59
|
-
|
|
101
|
+
afterAll(() => {
|
|
60
102
|
return Promise.all([broker1.stop(), broker2.stop()]);
|
|
61
103
|
});
|
|
62
104
|
|
|
63
|
-
it('should connect', () => {
|
|
105
|
+
it('should connect and call the started hooks exactly once', () => {
|
|
64
106
|
expect(startedHook1).toBeCalledTimes(1);
|
|
65
107
|
expect(startedHook2).toBeCalledTimes(1);
|
|
66
108
|
});
|
|
67
109
|
|
|
68
|
-
it('should
|
|
69
|
-
broker1
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
it('should discover the remote node', () => {
|
|
111
|
+
const node = broker1.runtime.registry.nodeCollection.get('node2');
|
|
112
|
+
expect(node).toBeDefined();
|
|
113
|
+
expect(node.isAvailable).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should call a remote action (node1 -> node2)', async () => {
|
|
117
|
+
const result = await broker1.call('testService2.hello');
|
|
118
|
+
expect(result).toBe('Hello from node2');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should call a remote action (node2 -> node1)', async () => {
|
|
122
|
+
const result = await broker2.call('testService1.hello');
|
|
123
|
+
expect(result).toBe('Hello from node1');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should pass parameters to a remote action', async () => {
|
|
127
|
+
const result = await broker2.call('testService1.greet', { name: 'Weave' });
|
|
128
|
+
expect(result).toBe('Hello Weave!');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should transfer complex payloads without losing data', async () => {
|
|
132
|
+
const payload = {
|
|
133
|
+
text: 'Hänsel & Grätel äöüß',
|
|
134
|
+
number: 42.5,
|
|
135
|
+
flag: true,
|
|
136
|
+
nothing: null,
|
|
137
|
+
nested: { list: [1, 2, 3], deep: { key: 'value' } }
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const result = await broker1.call('testService2.echo', payload);
|
|
141
|
+
expect(result).toEqual(payload);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should propagate errors of remote actions to the caller', async () => {
|
|
145
|
+
await expect(broker1.call('testService2.fail'))
|
|
146
|
+
.rejects
|
|
147
|
+
.toThrow('Expected failure');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should reject calls to unknown actions', async () => {
|
|
151
|
+
await expect(broker1.call('testService2.unknownAction')).rejects.toThrow();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should deliver emitted events to the remote node', async () => {
|
|
155
|
+
eventHandler.mockClear();
|
|
156
|
+
|
|
157
|
+
broker1.emit('user.created', { id: 1 });
|
|
158
|
+
|
|
159
|
+
await waitUntil(() => eventHandler.mock.calls.length === 1);
|
|
160
|
+
|
|
161
|
+
const context = eventHandler.mock.calls[0][0];
|
|
162
|
+
expect(context.data).toEqual({ id: 1 });
|
|
163
|
+
expect(context.callerNodeId).toBe('node1');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should deliver broadcast events to all nodes', async () => {
|
|
167
|
+
broadcastHandler1.mockClear();
|
|
168
|
+
broadcastHandler2.mockClear();
|
|
169
|
+
|
|
170
|
+
broker1.broadcast('user.broadcasted', { id: 2 });
|
|
171
|
+
|
|
172
|
+
await waitUntil(() =>
|
|
173
|
+
broadcastHandler1.mock.calls.length === 1 &&
|
|
174
|
+
broadcastHandler2.mock.calls.length === 1
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const remoteContext = broadcastHandler2.mock.calls[0][0];
|
|
178
|
+
expect(remoteContext.data).toEqual({ id: 2 });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle multiple parallel remote calls', async () => {
|
|
182
|
+
const results = await Promise.all(
|
|
183
|
+
Array.from({ length: 25 }, (_, index) =>
|
|
184
|
+
broker1.call('testService2.echo', { index })
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
results.forEach((result, index) => {
|
|
189
|
+
expect(result).toEqual({ index });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should respond to ping messages of remote nodes', async () => {
|
|
194
|
+
const result = await broker1.ping('node2');
|
|
195
|
+
expect(result.nodeId).toBe('node2');
|
|
196
|
+
expect(typeof result.elapsedTime).toBe('number');
|
|
78
197
|
});
|
|
79
198
|
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const RedisTransportAdapter = require('../lib/index');
|
|
2
|
+
|
|
3
|
+
const createMockLog = () => ({
|
|
4
|
+
info: jest.fn(),
|
|
5
|
+
debug: jest.fn(),
|
|
6
|
+
warn: jest.fn(),
|
|
7
|
+
error: jest.fn()
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const createMockRuntime = ({ namespace, nodeId = 'unit-node' } = {}) => {
|
|
11
|
+
const log = createMockLog();
|
|
12
|
+
|
|
13
|
+
const broker = {
|
|
14
|
+
nodeId,
|
|
15
|
+
options: { namespace },
|
|
16
|
+
handleError (error) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const transport = {
|
|
22
|
+
log,
|
|
23
|
+
statistics: {
|
|
24
|
+
sent: { packages: 0 },
|
|
25
|
+
received: { packages: 0 }
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return { broker, transport, log };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const initAdapter = async (adapter, runtimeOptions) => {
|
|
33
|
+
const runtime = createMockRuntime(runtimeOptions);
|
|
34
|
+
await adapter.init(runtime.broker, runtime.transport, () => {});
|
|
35
|
+
return runtime;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const waitForBusEvent = (adapter, eventName, timeout = 5000) =>
|
|
39
|
+
new Promise((resolve, reject) => {
|
|
40
|
+
const timer = setTimeout(
|
|
41
|
+
() => reject(new Error(`Timeout waiting for bus event "${eventName}"`)),
|
|
42
|
+
timeout
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
adapter.bus.once(eventName, (...args) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve(args);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// The connect() promise of the adapter is resolved through the
|
|
52
|
+
// "$adapter.connected" bus event, so we wait for the event instead.
|
|
53
|
+
const connectAdapter = (adapter) => {
|
|
54
|
+
const connected = waitForBusEvent(adapter, '$adapter.connected');
|
|
55
|
+
adapter.connect();
|
|
56
|
+
return connected;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe('REDIS adapter (unit)', () => {
|
|
60
|
+
let adapter;
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
if (adapter) {
|
|
64
|
+
await adapter.close();
|
|
65
|
+
adapter = null;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should expose the adapter name "REDIS"', () => {
|
|
70
|
+
adapter = RedisTransportAdapter();
|
|
71
|
+
expect(adapter.name).toBe('REDIS');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should not be connected initially', () => {
|
|
75
|
+
adapter = RedisTransportAdapter();
|
|
76
|
+
expect(adapter.isConnected).toBe(false);
|
|
77
|
+
expect(adapter.interruptCounter).toBe(0);
|
|
78
|
+
expect(adapter.repeatAttemptCounter).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should build topics with the namespace prefix', async () => {
|
|
82
|
+
adapter = RedisTransportAdapter();
|
|
83
|
+
await initAdapter(adapter, { namespace: 'unit-ns' });
|
|
84
|
+
|
|
85
|
+
expect(adapter.getTopic('INFO', 'node1')).toBe('weave-unit-ns.INFO.node1');
|
|
86
|
+
expect(adapter.getTopic('DISCOVERY')).toBe('weave-unit-ns.DISCOVERY');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should build topics without a namespace', async () => {
|
|
90
|
+
adapter = RedisTransportAdapter();
|
|
91
|
+
await initAdapter(adapter, {});
|
|
92
|
+
|
|
93
|
+
expect(adapter.getTopic('INFO', 'node1')).toBe('weave.INFO.node1');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should resolve and drop the message when sending while disconnected', async () => {
|
|
97
|
+
adapter = RedisTransportAdapter();
|
|
98
|
+
const { log } = await initAdapter(adapter, { namespace: 'unit-send-disconnected' });
|
|
99
|
+
|
|
100
|
+
await adapter.send({ type: 'INFO', targetNodeId: 'node2', payload: {} });
|
|
101
|
+
|
|
102
|
+
expect(log.debug).toBeCalledTimes(1);
|
|
103
|
+
expect(log.debug).toBeCalledWith(
|
|
104
|
+
'Message dropped, adapter is not connected',
|
|
105
|
+
{ type: 'INFO' }
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should resolve close() even if the adapter was never connected', async () => {
|
|
110
|
+
adapter = RedisTransportAdapter();
|
|
111
|
+
await initAdapter(adapter, { namespace: 'unit-close' });
|
|
112
|
+
await adapter.close();
|
|
113
|
+
adapter = null;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should connect to a running redis server and emit "$adapter.connected"', async () => {
|
|
117
|
+
adapter = RedisTransportAdapter();
|
|
118
|
+
await initAdapter(adapter, { namespace: 'unit-connect' });
|
|
119
|
+
|
|
120
|
+
await connectAdapter(adapter);
|
|
121
|
+
|
|
122
|
+
expect(adapter.isConnected).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should merge the adapter options with the default options', async () => {
|
|
126
|
+
// Explicit options matching the defaults must work the same way.
|
|
127
|
+
adapter = RedisTransportAdapter({ port: 6379, host: '127.0.0.1' });
|
|
128
|
+
await initAdapter(adapter, { namespace: 'unit-options' });
|
|
129
|
+
|
|
130
|
+
await connectAdapter(adapter);
|
|
131
|
+
|
|
132
|
+
expect(adapter.isConnected).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should deliver published messages to subscribers of the topic', async () => {
|
|
136
|
+
adapter = RedisTransportAdapter();
|
|
137
|
+
const { broker } = await initAdapter(adapter, { namespace: `unit-roundtrip-${Date.now()}` });
|
|
138
|
+
|
|
139
|
+
await connectAdapter(adapter);
|
|
140
|
+
await adapter.subscribe('EVENT', 'node2');
|
|
141
|
+
|
|
142
|
+
const incoming = waitForBusEvent(adapter, '$adapter.message');
|
|
143
|
+
await adapter.send({
|
|
144
|
+
type: 'EVENT',
|
|
145
|
+
targetNodeId: 'node2',
|
|
146
|
+
payload: { eventName: 'user.created' }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const [messageType, data] = await incoming;
|
|
150
|
+
|
|
151
|
+
expect(messageType).toBe('EVENT');
|
|
152
|
+
expect(data.payload.eventName).toBe('user.created');
|
|
153
|
+
expect(data.payload.sender).toBe(broker.nodeId);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should update the sent and received statistics', async () => {
|
|
157
|
+
adapter = RedisTransportAdapter();
|
|
158
|
+
const { transport } = await initAdapter(adapter, { namespace: `unit-stats-${Date.now()}` });
|
|
159
|
+
|
|
160
|
+
await connectAdapter(adapter);
|
|
161
|
+
await adapter.subscribe('HEARTBEAT');
|
|
162
|
+
|
|
163
|
+
const incoming = waitForBusEvent(adapter, '$adapter.message');
|
|
164
|
+
await adapter.send({ type: 'HEARTBEAT', payload: {} });
|
|
165
|
+
await incoming;
|
|
166
|
+
|
|
167
|
+
expect(transport.statistics.sent.packages).toBeGreaterThan(0);
|
|
168
|
+
expect(transport.statistics.received.packages).toBeGreaterThan(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should not deliver messages of topics without a subscription', async () => {
|
|
172
|
+
adapter = RedisTransportAdapter();
|
|
173
|
+
await initAdapter(adapter, { namespace: `unit-no-sub-${Date.now()}` });
|
|
174
|
+
|
|
175
|
+
await connectAdapter(adapter);
|
|
176
|
+
await adapter.subscribe('INFO');
|
|
177
|
+
|
|
178
|
+
const received = [];
|
|
179
|
+
adapter.bus.on('$adapter.message', (type) => received.push(type));
|
|
180
|
+
|
|
181
|
+
await adapter.send({ type: 'HEARTBEAT', payload: {} });
|
|
182
|
+
|
|
183
|
+
const incoming = waitForBusEvent(adapter, '$adapter.message');
|
|
184
|
+
await adapter.send({ type: 'INFO', payload: {} });
|
|
185
|
+
await incoming;
|
|
186
|
+
|
|
187
|
+
expect(received).toEqual(['INFO']);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should not receive its own messages after close()', async () => {
|
|
191
|
+
adapter = RedisTransportAdapter();
|
|
192
|
+
await initAdapter(adapter, { namespace: `unit-closed-${Date.now()}` });
|
|
193
|
+
|
|
194
|
+
await connectAdapter(adapter);
|
|
195
|
+
await adapter.subscribe('INFO');
|
|
196
|
+
await adapter.close();
|
|
197
|
+
|
|
198
|
+
// After close the clients are gone; sending must still resolve without throwing.
|
|
199
|
+
adapter.isConnected = false;
|
|
200
|
+
await adapter.send({ type: 'INFO', payload: {} });
|
|
201
|
+
adapter = null;
|
|
202
|
+
});
|
|
203
|
+
});
|
package/.vscode/launch.json
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
|
|
3
|
-
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
|
|
4
|
-
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
-
"version": "0.2.0",
|
|
6
|
-
"configurations": [
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
"type": "node",
|
|
10
|
-
"request": "launch",
|
|
11
|
-
"name": "Jest All",
|
|
12
|
-
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
|
13
|
-
"args": ["--runInBand"],
|
|
14
|
-
"console": "integratedTerminal",
|
|
15
|
-
"internalConsoleOptions": "neverOpen",
|
|
16
|
-
"disableOptimisticBPs": true,
|
|
17
|
-
"windows": {
|
|
18
|
-
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"type": "node",
|
|
23
|
-
"request": "launch",
|
|
24
|
-
"name": "Jest Current File",
|
|
25
|
-
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
|
26
|
-
"args": ["${relativeFile}"],
|
|
27
|
-
"console": "integratedTerminal",
|
|
28
|
-
"internalConsoleOptions": "neverOpen",
|
|
29
|
-
"disableOptimisticBPs": true,
|
|
30
|
-
"windows": {
|
|
31
|
-
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
]
|
|
35
|
-
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020 Fachwerk Software
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|