comq 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.github/dependabot.yml +2 -2
  2. package/.github/workflows/codeql.yml +1 -1
  3. package/.github/workflows/release.yaml +14 -15
  4. package/docs/reply-stream-topology-dark.jpg +0 -0
  5. package/docs/reply-stream-topology-light.jpg +0 -0
  6. package/examples/readme.md +19 -3
  7. package/examples/streams/consumer.js +36 -0
  8. package/examples/streams/producer.js +44 -0
  9. package/features/backpressure.feature +2 -2
  10. package/features/readme.md +2 -2
  11. package/features/rpc.streams.feature +81 -0
  12. package/features/rpc.streams.shards.feature +37 -0
  13. package/features/steps/.test/context.test.js +1 -1
  14. package/features/steps/.test/events.test.js +1 -1
  15. package/features/steps/.types/context.d.ts +9 -1
  16. package/features/steps/broker.js +2 -0
  17. package/features/steps/common.js +12 -0
  18. package/features/steps/context.js +9 -2
  19. package/features/steps/rpc.js +235 -0
  20. package/lerna.json +1 -2
  21. package/package.json +3 -2
  22. package/readme.md +136 -46
  23. package/source/.io/ReplyPipe.js +158 -0
  24. package/source/.io/ReplyStream.js +139 -0
  25. package/source/.io/const.js +10 -0
  26. package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
  27. package/source/.io/index.js +5 -1
  28. package/source/attributes/failsafe.js +34 -0
  29. package/source/attributes/failsafe.test.js +133 -0
  30. package/source/attributes/index.js +13 -0
  31. package/source/attributes/lazy.js +79 -0
  32. package/source/attributes/lazy.test.js +299 -0
  33. package/source/attributes/memo.js +13 -0
  34. package/source/attributes/memo.test.js +36 -0
  35. package/source/attributes/recall.js +42 -0
  36. package/source/attributes/recall.test.js +92 -0
  37. package/source/attributes/track.js +32 -0
  38. package/source/attributes/track.test.js +133 -0
  39. package/source/channel.js +31 -18
  40. package/source/connect.js +2 -2
  41. package/source/connection.js +4 -11
  42. package/source/emitter.js +13 -0
  43. package/source/encode.js +1 -1
  44. package/source/events.js +2 -2
  45. package/source/io.js +187 -50
  46. package/source/shards/channel.js +13 -10
  47. package/source/shards/connection.js +2 -2
  48. package/test/channel.test.js +3 -1
  49. package/test/encode.test.js +2 -2
  50. package/test/encodings.js +1 -1
  51. package/test/io.diagnostics.test.js +2 -2
  52. package/test/io.request.test.js +4 -23
  53. package/test/shards/channel.test.js +1 -1
  54. package/test/shards/connection.test.js +1 -1
  55. package/types/channel.d.ts +13 -11
  56. package/types/connection.d.ts +3 -3
  57. package/types/diagnostic.d.ts +4 -3
  58. package/types/encoding.d.ts +2 -2
  59. package/types/index.d.ts +4 -4
  60. package/types/io.d.ts +33 -40
@@ -0,0 +1,299 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { timeout } = require('@toa.io/generic')
5
+
6
+ const { lazy } = require('./lazy')
7
+
8
+ it('should be', async () => {
9
+ expect(lazy).toBeDefined()
10
+ })
11
+
12
+ // noinspection JSValidateTypes
13
+ /** @type {jest.MockedFn<(...args: any[]) => Promise<void>>} */
14
+ const action = jest.fn(async () => undefined)
15
+
16
+ // noinspection JSValidateTypes
17
+ /** @type {jest.MockedFn<(...args: any[]) => Promise<void>>} */
18
+ const initialize = jest.fn(async () => undefined)
19
+
20
+ const value = generate()
21
+
22
+ class LazyInitialized {
23
+ log = []
24
+
25
+ action = lazy(this, this.#initialize, this.#action)
26
+
27
+ async #initialize () {
28
+ await timeout(1)
29
+ await initialize()
30
+ this.log.push('initializer')
31
+ }
32
+
33
+ async #action (...args) {
34
+ await action(...args)
35
+ this.log.push('action')
36
+
37
+ return value
38
+ }
39
+ }
40
+
41
+ let instance
42
+
43
+ beforeEach(() => {
44
+ jest.clearAllMocks()
45
+
46
+ instance = new LazyInitialized()
47
+ })
48
+
49
+ it('should call initializer before action', async () => {
50
+ await instance.action()
51
+
52
+ expect(instance.log).toStrictEqual(['initializer', 'action'])
53
+ })
54
+
55
+ it('should pass arguments to action', async () => {
56
+ const args = [generate(), generate()]
57
+
58
+ await instance.action(...args)
59
+
60
+ expect(action).toHaveBeenCalledWith(...args)
61
+ })
62
+
63
+ it('should return value', async () => {
64
+ const output = await instance.action()
65
+
66
+ expect(output).toStrictEqual(value)
67
+ })
68
+
69
+ it('should call initializer once', async () => {
70
+ await instance.action()
71
+ await instance.action()
72
+
73
+ expect(initialize).toHaveBeenCalledTimes(1)
74
+ expect(instance.log).toStrictEqual(['initializer', 'action', 'action'])
75
+ })
76
+
77
+ it('should call initializer once on concurrent calls', async () => {
78
+ await Promise.all([instance.action(), instance.action()])
79
+
80
+ expect(initialize).toHaveBeenCalledTimes(1)
81
+ expect(instance.log).toStrictEqual(['initializer', 'action', 'action'])
82
+ })
83
+
84
+ it('should call initializer once per instance', async () => {
85
+ const instance2 = new LazyInitialized()
86
+
87
+ await instance.action()
88
+ await instance2.action()
89
+
90
+ expect(initialize).toHaveBeenCalledTimes(2)
91
+ })
92
+
93
+ // noinspection JSValidateTypes
94
+ /** @type {jest.MockedFn<(...args: any[]) => Promise<void>>} */
95
+ const initialize2 = jest.fn(async () => undefined)
96
+
97
+ class InitializerSet {
98
+ action = lazy(this, [this.#initialize1, this.#initialize2], this.#action)
99
+
100
+ async #action () {
101
+
102
+ }
103
+
104
+ async #initialize1 () {
105
+ await initialize()
106
+ }
107
+
108
+ async #initialize2 () {
109
+ await initialize2()
110
+ }
111
+ }
112
+
113
+ it('should run list of initializers', async () => {
114
+ const instance = new InitializerSet()
115
+
116
+ await instance.action()
117
+
118
+ expect(initialize).toHaveBeenCalled()
119
+ expect(initialize2).toHaveBeenCalled()
120
+ })
121
+
122
+ class InitializerIntersection {
123
+ do = lazy(this, [this.#initialize1, this.#initialize2], this.#do)
124
+
125
+ undo = lazy(this, this.#initialize2, this.#undo)
126
+
127
+ async #do () {
128
+
129
+ }
130
+
131
+ async #undo () {
132
+
133
+ }
134
+
135
+ async #initialize1 () {
136
+ await initialize()
137
+ }
138
+
139
+ async #initialize2 () {
140
+ await initialize2()
141
+ }
142
+ }
143
+
144
+ it('should call intersecting initializers once', async () => {
145
+ jest.clearAllMocks()
146
+
147
+ const instance = new InitializerIntersection()
148
+
149
+ await instance.do()
150
+ await instance.undo()
151
+
152
+ expect(initialize).toHaveBeenCalledTimes(1)
153
+ expect(initialize2).toHaveBeenCalledTimes(1)
154
+ })
155
+
156
+ it('should call intersecting concurrent initializers once', async () => {
157
+ jest.clearAllMocks()
158
+
159
+ const instance = new InitializerIntersection()
160
+
161
+ await Promise.all([instance.do(), instance.undo()])
162
+
163
+ expect(initialize).toHaveBeenCalledTimes(1)
164
+ expect(initialize2).toHaveBeenCalledTimes(1)
165
+ })
166
+
167
+ class InitializersWithArguments {
168
+ do = lazy(this, [this.#initialize1, this.#initialize2], this.#do)
169
+
170
+ async #do (a, b, c) {}
171
+
172
+ async #initialize1 () {
173
+ await initialize(arguments)
174
+ }
175
+
176
+ async #initialize2 (a, b) {
177
+ await initialize2(arguments)
178
+ }
179
+ }
180
+
181
+ it('should pass arguments to initializers if expected', async () => {
182
+ jest.clearAllMocks()
183
+
184
+ const instance = new InitializersWithArguments()
185
+
186
+ await instance.do(1, 2, 3)
187
+
188
+ expect(initialize).toHaveBeenCalledTimes(1)
189
+
190
+ const args = initialize.mock.calls[0][0]
191
+
192
+ expect(args.length).toStrictEqual(0)
193
+
194
+ expect(initialize2).toHaveBeenCalledTimes(1)
195
+
196
+ const args2 = initialize2.mock.calls[0][0]
197
+
198
+ expect(args2.length).toStrictEqual(2)
199
+ })
200
+
201
+ it('should call conditions with different argument values', async () => {
202
+ jest.clearAllMocks()
203
+
204
+ const instance = new InitializersWithArguments()
205
+
206
+ await instance.do(1, 2, 3)
207
+ await instance.do(1, 2)
208
+ await instance.do(2, 2)
209
+
210
+ expect(initialize).toHaveBeenCalledTimes(1)
211
+ expect(initialize2).toHaveBeenCalledTimes(2)
212
+ })
213
+
214
+ class OrderedInitializers {
215
+ log = []
216
+
217
+ do = lazy(this, [this.#initialize1, this.#initialize2], async () => undefined)
218
+
219
+ async #initialize1 () {
220
+ await timeout(1)
221
+ this.log.push(1)
222
+ }
223
+
224
+ async #initialize2 (_0, _1) {
225
+ this.log.push(2)
226
+ }
227
+ }
228
+
229
+ it('should call initializers sequentially', async () => {
230
+ const instance = new OrderedInitializers()
231
+
232
+ await instance.do()
233
+
234
+ expect(instance.log).toStrictEqual([1, 2])
235
+ })
236
+
237
+ it('should override argument values', async () => {
238
+ const method = /** @type {Function} */ jest.fn()
239
+
240
+ class Test {
241
+ do = lazy(this, this.#update, method)
242
+
243
+ #update (foo, bar) {
244
+ return [foo + ' updated', bar + ' updated']
245
+ }
246
+ }
247
+
248
+ const test = new Test()
249
+ const foo = generate()
250
+ const bar = generate()
251
+
252
+ await test.do(foo, bar)
253
+
254
+ expect(method).toHaveBeenCalledWith(foo + ' updated', bar + ' updated')
255
+ })
256
+
257
+ it('should partially override argument values', async () => {
258
+ const method = /** @type {Function} */ jest.fn()
259
+
260
+ class Test {
261
+ do = lazy(this, this.#update, method)
262
+
263
+ #update (foo, _) {
264
+ return [foo + ' updated']
265
+ }
266
+ }
267
+
268
+ const test = new Test()
269
+ const foo = generate()
270
+ const bar = generate()
271
+
272
+ await test.do(foo, bar)
273
+
274
+ expect(method).toHaveBeenCalledWith(foo + ' updated', bar)
275
+ })
276
+
277
+ describe('reset', () => {
278
+ it('should be', async () => {
279
+ expect(lazy.reset).toBeDefined()
280
+ })
281
+
282
+ it('should reset initialization', async () => {
283
+ const instance = new InitializerIntersection()
284
+
285
+ await instance.do()
286
+ await instance.undo()
287
+
288
+ expect(initialize).toHaveBeenCalledTimes(1)
289
+ expect(initialize2).toHaveBeenCalledTimes(1)
290
+
291
+ lazy.reset(instance)
292
+
293
+ await instance.do()
294
+ await instance.undo()
295
+
296
+ expect(initialize).toHaveBeenCalledTimes(2)
297
+ expect(initialize2).toHaveBeenCalledTimes(2)
298
+ })
299
+ })
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ function memo (fn) {
4
+ return (...args) => {
5
+ if (fn[MEMO] === undefined) fn[MEMO] = fn(...args)
6
+
7
+ return fn[MEMO]
8
+ }
9
+ }
10
+
11
+ const MEMO = Symbol('memo')
12
+
13
+ exports.memo = memo
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { memo } = require('./memo')
4
+
5
+ let fn
6
+ let memoized
7
+
8
+ beforeEach(() => {
9
+ jest.clearAllMocks()
10
+
11
+ fn = jest.fn(() => Math.random())
12
+ memoized = memo(fn)
13
+ })
14
+
15
+ it('should return value', async () => {
16
+ const value = memoized()
17
+
18
+ expect(value).toBe(fn.mock.results[0].value)
19
+ })
20
+
21
+ it('should pass arguments', async () => {
22
+ const args = [1, 2, 3]
23
+
24
+ memoized(...args)
25
+
26
+ expect(fn).toHaveBeenCalledWith(...args)
27
+ })
28
+
29
+ it('should memoize value', async () => {
30
+ const one = memoized()
31
+ const two = memoized()
32
+
33
+ expect(fn).toHaveBeenCalledTimes(1)
34
+ expect(one).toBe(fn.mock.results[0].value)
35
+ expect(two).toBe(fn.mock.results[0].value)
36
+ })
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ function recall (context, method = undefined) {
4
+ if (method === undefined) return replay(context)
5
+ else return recorder(context, method)
6
+ }
7
+
8
+ const recorder = (context, method) => async function (...args) {
9
+ if (context[METHODS] === undefined) context[METHODS] = []
10
+
11
+ if (method[CALLS] === undefined) {
12
+ context[METHODS].push(method)
13
+ method[CALLS] = []
14
+ }
15
+
16
+ const result = await method.apply(context, args)
17
+
18
+ method[CALLS].push(args)
19
+
20
+ return result
21
+ }
22
+
23
+ async function replay (context) {
24
+ if (context[METHODS] === undefined) return
25
+
26
+ const promises = []
27
+
28
+ for (const method of context[METHODS]) {
29
+ for (const args of method[CALLS]) {
30
+ const promise = method.apply(context, args)
31
+
32
+ promises.push(promise)
33
+ }
34
+ }
35
+
36
+ await Promise.all(promises)
37
+ }
38
+
39
+ const METHODS = Symbol('context methods')
40
+ const CALLS = Symbol('method calls')
41
+
42
+ exports.recall = recall
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { recall } = require('./recall')
5
+
6
+ it('should be', async () => {
7
+ expect(recall).toBeDefined()
8
+ })
9
+
10
+ const context = { foo: generate() }
11
+
12
+ /** @type {jest.MockedFn<(...args: any[]) => any>} */
13
+ let method
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks()
17
+
18
+ method = /** @type {jest.MockedFn<(...args: any[]) => any>} */
19
+ jest.fn(function () { return this.foo })
20
+ })
21
+
22
+ it('should return function', async () => {
23
+ const func = recall(context, method)
24
+
25
+ expect(func).toBeInstanceOf(Function)
26
+ })
27
+
28
+ it('should return result', async () => {
29
+ const func = recall(context, method)
30
+ const output = await func()
31
+
32
+ expect(output).toStrictEqual(await method.mock.results[0].value)
33
+ })
34
+
35
+ it('should call method within the context', async () => {
36
+ const func = recall(context, method)
37
+ const output = await func()
38
+
39
+ expect(output).toStrictEqual(context.foo)
40
+ })
41
+
42
+ it('should pass arguments', async () => {
43
+ const args = [generate(), generate()]
44
+ const func = recall(context, method)
45
+
46
+ await func(...args)
47
+
48
+ expect(method).toHaveBeenCalledWith(...args)
49
+ })
50
+
51
+ it('should re-call', async () => {
52
+ const args1 = [generate(), generate()]
53
+ const args2 = [generate(), generate(), generate()]
54
+ const func = recall(context, method)
55
+
56
+ await func(...args1)
57
+ await func(...args2)
58
+
59
+ method.mockClear()
60
+
61
+ expect(method).not.toHaveBeenCalled()
62
+
63
+ await recall(context)
64
+
65
+ expect(method).toHaveBeenCalledWith(...args1)
66
+ expect(method).toHaveBeenCalledWith(...args2)
67
+ })
68
+
69
+ it('should not trow on empty re-call', async () => {
70
+ await expect(recall(context)).resolves.not.toThrow()
71
+ })
72
+
73
+ it('should not re-call those thrown exceptions', async () => {
74
+ expect.assertions(2)
75
+
76
+ method.mockImplementationOnce(async () => 1)
77
+ method.mockImplementationOnce(async () => { throw new Error() })
78
+
79
+ const func = recall(context, method)
80
+
81
+ await func()
82
+
83
+ try {
84
+ await func()
85
+ } catch (e) {
86
+ expect(e).toBeDefined()
87
+ }
88
+
89
+ await recall(context)
90
+
91
+ expect(method).toHaveBeenCalledTimes(3)
92
+ })
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ function track (context, method = undefined) {
4
+ if (method === undefined) return promises(context)
5
+
6
+ context[KEY] ??= new Set()
7
+
8
+ const tracking = context[KEY]
9
+
10
+ return async function (...args) {
11
+ const promise = method.apply(this, args)
12
+
13
+ tracking.add(promise)
14
+ promise.catch(noop).finally(() => tracking.delete(promise))
15
+
16
+ return promise
17
+ }
18
+ }
19
+
20
+ function promises (context) {
21
+ if (context[KEY] === undefined) return
22
+
23
+ const promises = context[KEY]
24
+
25
+ return Promise.all(promises)
26
+ }
27
+
28
+ const KEY = Symbol('context tracking key')
29
+
30
+ const noop = () => undefined
31
+
32
+ exports.track = track
@@ -0,0 +1,133 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { promex } = require('@toa.io/generic')
5
+
6
+ const { track } = require('./track')
7
+
8
+ it('should be', async () => {
9
+ expect(track).toBeDefined()
10
+ })
11
+
12
+ let done
13
+ let undone
14
+
15
+ const method1 = /** @type {jest.MockedFn<(a: string, b: string) => Promise>} */
16
+ jest.fn(async function () {
17
+ // should execute in context
18
+ expect(this.ok).toStrictEqual(1)
19
+
20
+ return done
21
+ })
22
+
23
+ const method2 = /** @type {jest.MockedFn<(a: string, b: string) => Promise>} */
24
+ jest.fn(async function () { return undone })
25
+
26
+ class Test {
27
+ ok = 1
28
+ do = track(this, method1)
29
+ undo = track(this, method2)
30
+ }
31
+
32
+ /** @type {Test} */
33
+ let test
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks()
37
+
38
+ done = promex()
39
+ undone = promex()
40
+ test = new Test()
41
+ })
42
+
43
+ it('should return function', async () => {
44
+ expect(test.do).toBeInstanceOf(Function)
45
+ })
46
+
47
+ it('should execute method', async () => {
48
+ const result = generate()
49
+
50
+ done.resolve(result)
51
+
52
+ const args = [generate(), generate()]
53
+
54
+ const output = await test.do(...args)
55
+
56
+ expect(method1).toHaveBeenCalledWith(...args)
57
+ expect(output).toStrictEqual(result)
58
+ })
59
+
60
+ it('should track method execution', async () => {
61
+ expect.assertions(2)
62
+
63
+ setImmediate(async () => {
64
+ setImmediate(() => {
65
+ done.resolve()
66
+
67
+ expect(finished).toStrictEqual(false)
68
+
69
+ setImmediate(() => {
70
+ expect(finished).toStrictEqual(true)
71
+ })
72
+ })
73
+
74
+ let finished = false
75
+
76
+ await track(test)
77
+
78
+ finished = true
79
+ })
80
+
81
+ await test.do()
82
+ })
83
+
84
+ it('should track multiple methods', async () => {
85
+ expect.assertions(3 + 1) // + method1
86
+
87
+ setImmediate(async () => {
88
+ setImmediate(() => {
89
+ done.resolve()
90
+
91
+ expect(finished).toStrictEqual(false)
92
+
93
+ setImmediate(() => {
94
+ undone.resolve()
95
+
96
+ expect(finished).toStrictEqual(false)
97
+ setImmediate(() => expect(finished).toStrictEqual(true))
98
+ })
99
+ })
100
+
101
+ let finished = false
102
+
103
+ await track(test)
104
+
105
+ finished = true
106
+ })
107
+
108
+ await Promise.all([test.do(), test.undo()])
109
+ })
110
+
111
+ it('should resolve if methods haven\'t been called', async () => {
112
+ await track(this)
113
+ })
114
+
115
+ it('should handle exceptions', async () => {
116
+ expect.assertions(2)
117
+
118
+ const exception = new Error(generate())
119
+
120
+ class Bad {
121
+ do = track(this, async () => { throw exception })
122
+ }
123
+
124
+ const b = new Bad()
125
+
126
+ try {
127
+ await b.do()
128
+ } catch (e) {
129
+ expect(e).toStrictEqual(exception)
130
+ }
131
+
132
+ await expect(track(b)).resolves.not.toThrow()
133
+ })