comq 0.8.0 → 0.10.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/.github/dependabot.yml +2 -2
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release.yaml +16 -15
- package/docs/reply-stream-topology-dark.jpg +0 -0
- package/docs/reply-stream-topology-light.jpg +0 -0
- package/examples/readme.md +19 -3
- package/examples/streams/consumer.js +36 -0
- package/examples/streams/producer.js +44 -0
- package/features/backpressure.feature +2 -2
- package/features/events.feature +5 -0
- package/features/readme.md +2 -2
- package/features/recovery.feature +0 -3
- package/features/rpc.feature +15 -10
- package/features/rpc.streams.feature +81 -0
- package/features/rpc.streams.shards.feature +37 -0
- package/features/steps/.test/context.test.js +1 -1
- package/features/steps/.test/events.test.js +2 -2
- package/features/steps/.test/rpc.test.js +1 -1
- package/features/steps/.types/context.d.ts +9 -1
- package/features/steps/common.js +12 -0
- package/features/steps/context.js +9 -2
- package/features/steps/events.js +29 -0
- package/features/steps/rpc.js +273 -0
- package/lerna.json +1 -2
- package/package.json +2 -2
- package/readme.md +159 -31
- package/source/.io/ReplyPipe.js +158 -0
- package/source/.io/ReplyStream.js +142 -0
- package/source/.io/const.js +10 -0
- package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
- package/source/.io/index.js +5 -1
- package/source/attributes/failsafe.js +34 -0
- package/source/attributes/failsafe.test.js +133 -0
- package/source/attributes/index.js +13 -0
- package/source/attributes/lazy.js +79 -0
- package/source/attributes/lazy.test.js +299 -0
- package/source/attributes/memo.js +13 -0
- package/source/attributes/memo.test.js +36 -0
- package/source/attributes/recall.js +42 -0
- package/source/attributes/recall.test.js +92 -0
- package/source/attributes/track.js +32 -0
- package/source/attributes/track.test.js +133 -0
- package/source/channel.js +33 -18
- package/source/connect.js +2 -2
- package/source/connection.js +4 -11
- package/source/emitter.js +13 -0
- package/source/encode.js +1 -1
- package/source/events.js +3 -3
- package/source/io.js +184 -48
- package/source/pipeline.js +43 -0
- package/source/shards/channel.js +28 -12
- package/source/shards/connection.js +2 -2
- package/test/channel.test.js +9 -1
- package/test/encode.test.js +2 -2
- package/test/encodings.js +1 -1
- package/test/io.diagnostics.test.js +3 -3
- package/test/io.emit.test.js +18 -3
- package/test/io.request.test.js +45 -23
- package/test/pipeline.test.js +81 -0
- package/test/shards/channel.test.js +39 -3
- package/test/shards/connection.test.js +1 -1
- package/types/channel.d.ts +13 -11
- package/types/connection.d.ts +3 -3
- package/types/diagnostic.d.ts +4 -3
- package/types/encoding.d.ts +2 -2
- package/types/index.d.ts +4 -4
- package/types/io.d.ts +40 -32
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events')
|
|
4
|
+
const { control, HEARTBEAT_INTERVAL } = require('./const')
|
|
5
|
+
|
|
6
|
+
/** @typedef {(message: any, properties?: comq.amqp.options.Publish) => Promise<void>} Reply */
|
|
7
|
+
|
|
8
|
+
class ReplyPipe extends EventEmitter {
|
|
9
|
+
#index = -1
|
|
10
|
+
#interrupted = false
|
|
11
|
+
#heartbeatInterval = global['COMQ_TESTING_HEARTBEAT_INTERVAL'] || HEARTBEAT_INTERVAL
|
|
12
|
+
|
|
13
|
+
/** @type {ReturnType<setInterval> | null} */
|
|
14
|
+
#interval = null
|
|
15
|
+
|
|
16
|
+
/** @type {string} */
|
|
17
|
+
#replyTo
|
|
18
|
+
|
|
19
|
+
/** @type {Record<string, comq.amqp.options.Publish>} */
|
|
20
|
+
#properties
|
|
21
|
+
|
|
22
|
+
/** @type {import('node:stream').Readable} */
|
|
23
|
+
#stream
|
|
24
|
+
|
|
25
|
+
/** @type {comq.Channel} */
|
|
26
|
+
#channel
|
|
27
|
+
|
|
28
|
+
/** @type {comq.ReplyEmitter} */
|
|
29
|
+
#feedback
|
|
30
|
+
|
|
31
|
+
/** @type {Reply} */
|
|
32
|
+
#reply
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {comq.amqp.Message} request
|
|
36
|
+
* @param {stream.Readable} stream
|
|
37
|
+
* @param {comq.Channel} channel
|
|
38
|
+
* @param {comq.ReplyEmitter} feedback
|
|
39
|
+
* @param {Reply} reply
|
|
40
|
+
*/
|
|
41
|
+
constructor (request, stream, channel, feedback, reply) {
|
|
42
|
+
super()
|
|
43
|
+
|
|
44
|
+
const { correlationId, replyTo } = request.properties
|
|
45
|
+
|
|
46
|
+
this.#stream = stream
|
|
47
|
+
this.#channel = channel
|
|
48
|
+
this.#feedback = feedback
|
|
49
|
+
this.#reply = reply
|
|
50
|
+
this.#replyTo = replyTo
|
|
51
|
+
|
|
52
|
+
this.#properties = {
|
|
53
|
+
chunk: { correlationId, ...CHUNK },
|
|
54
|
+
control: { correlationId, replyTo: feedback.queue, ...CONTROL }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
channel.diagnose('return', this.#onReturn)
|
|
58
|
+
feedback.on(correlationId, this.#control)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async pipe () {
|
|
62
|
+
await this.#transmit(control.ok, this.#properties.control)
|
|
63
|
+
|
|
64
|
+
this.#stream.on('data', this.#onData)
|
|
65
|
+
this.#stream.on('close', this.#close)
|
|
66
|
+
this.#heartbeat()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
destroy () {
|
|
70
|
+
this.#close()
|
|
71
|
+
this.#stream.destroy()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async #transmit (data, properties) {
|
|
75
|
+
this.#index++
|
|
76
|
+
|
|
77
|
+
const ok = await this.#reply(data,
|
|
78
|
+
{ ...properties, headers: { index: this.#index } })
|
|
79
|
+
|
|
80
|
+
if (!ok) this.#interrupt()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#heartbeat () {
|
|
84
|
+
if (this.#interval !== null) clearInterval(this.#interval)
|
|
85
|
+
|
|
86
|
+
this.#interval = setInterval(
|
|
87
|
+
() => this.#transmit(control.heartbeat, this.#properties.control),
|
|
88
|
+
this.#heartbeatInterval
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#interrupt () {
|
|
93
|
+
this.#interrupted = true
|
|
94
|
+
this.destroy()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#clear () {
|
|
98
|
+
clearInterval(this.#interval)
|
|
99
|
+
|
|
100
|
+
this.#stream.removeAllListeners()
|
|
101
|
+
this.#channel.forget('return', this.#onReturn)
|
|
102
|
+
this.#feedback.off(this.#properties.control.correlationId, this.#control)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#close = () => {
|
|
106
|
+
this.emit('close')
|
|
107
|
+
this.#clear()
|
|
108
|
+
|
|
109
|
+
if (!this.#interrupted) {
|
|
110
|
+
void this.#transmit(control.end, this.#properties.control)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#onData = async (chunk) => {
|
|
115
|
+
await this.#transmit(chunk, this.#properties.chunk)
|
|
116
|
+
|
|
117
|
+
this.#heartbeat()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#onReturn = (message) => {
|
|
121
|
+
if (message.fields.routingKey === this.#replyTo)
|
|
122
|
+
this.#interrupt()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#control = (message) => {
|
|
126
|
+
switch (message) {
|
|
127
|
+
case control.end:
|
|
128
|
+
this.#interrupt()
|
|
129
|
+
break
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`Unknown control message: ${message}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {comq.amqp.Message} request
|
|
137
|
+
* @param {stream.Readable} stream
|
|
138
|
+
* @param {comq.Channel} channel
|
|
139
|
+
* @param {comq.ReplyEmitter} control
|
|
140
|
+
* @param {Reply} reply
|
|
141
|
+
* @return {Promise<comq.Destroyable>}
|
|
142
|
+
*/
|
|
143
|
+
static async create (request, stream, channel, control, reply) {
|
|
144
|
+
const pipe = new ReplyPipe(request, stream, channel, control, reply)
|
|
145
|
+
|
|
146
|
+
await pipe.pipe()
|
|
147
|
+
|
|
148
|
+
return /** @type {comq.Destroyable} */ pipe
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @type {comq.amqp.options.Publish} */
|
|
153
|
+
const CHUNK = { mandatory: true }
|
|
154
|
+
|
|
155
|
+
/** @type {comq.amqp.options.Publish} */
|
|
156
|
+
const CONTROL = { type: 'control', mandatory: true }
|
|
157
|
+
|
|
158
|
+
exports.ReplyPipe = ReplyPipe
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { IDLE_INTERVAL, control } = require('./const')
|
|
4
|
+
|
|
5
|
+
const { Readable } = require('node:stream')
|
|
6
|
+
|
|
7
|
+
class ReplyStream extends Readable {
|
|
8
|
+
#confirmation
|
|
9
|
+
|
|
10
|
+
/** @type {ReturnType<setTimeout> | null} */
|
|
11
|
+
#timeout = null
|
|
12
|
+
|
|
13
|
+
/** @type {comq.ReplyEmitter} */
|
|
14
|
+
#emitter
|
|
15
|
+
|
|
16
|
+
/** @type {string} */
|
|
17
|
+
#correlationId
|
|
18
|
+
|
|
19
|
+
#control
|
|
20
|
+
|
|
21
|
+
#reply
|
|
22
|
+
|
|
23
|
+
#idleInterval
|
|
24
|
+
|
|
25
|
+
#index = 0
|
|
26
|
+
|
|
27
|
+
#buffered = 0
|
|
28
|
+
|
|
29
|
+
/** @type {Record<number, unknown>} */
|
|
30
|
+
#queue = {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {comq.Request} request
|
|
34
|
+
* @param {any} reply
|
|
35
|
+
*/
|
|
36
|
+
constructor (request, reply) {
|
|
37
|
+
super({ objectMode: true })
|
|
38
|
+
|
|
39
|
+
this.#confirmation = request.reply
|
|
40
|
+
this.#emitter = request.emitter
|
|
41
|
+
this.#correlationId = request.properties.correlationId
|
|
42
|
+
this.#idleInterval = global['COMQ_TESTING_IDLE_INTERVAL'] || IDLE_INTERVAL
|
|
43
|
+
this.#reply = reply
|
|
44
|
+
|
|
45
|
+
this.#emitter.on(this.#correlationId, this._arrange.bind(this))
|
|
46
|
+
this.#confirmation.catch(this._clear.bind(this))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_destroy (error, callback) {
|
|
50
|
+
this._clear()
|
|
51
|
+
this.push(null)
|
|
52
|
+
|
|
53
|
+
void this.#reply(this.#control, control.end)
|
|
54
|
+
super._destroy(error, callback)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_read (_) {}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {unknown} payload
|
|
61
|
+
* @param {comq.amqp.Properties} properties
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
_arrange (payload, properties) {
|
|
65
|
+
if (properties.headers.index !== this.#index) {
|
|
66
|
+
this._buffer(payload, properties)
|
|
67
|
+
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this._add(payload, properties)
|
|
72
|
+
|
|
73
|
+
if (this.#buffered > 0) {
|
|
74
|
+
let message
|
|
75
|
+
|
|
76
|
+
while ((message = this.#queue[this.#index])) {
|
|
77
|
+
delete this.#queue[this.#index]
|
|
78
|
+
this._add(message.payload, message.properties)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.#buffered = Object.keys(this.#queue).length
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {unknown} payload
|
|
87
|
+
* @param {comq.amqp.Properties} properties
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
_add (payload, properties) {
|
|
91
|
+
this._heartbeat()
|
|
92
|
+
this.#index++
|
|
93
|
+
|
|
94
|
+
if (properties.type === 'control')
|
|
95
|
+
this._control(payload, properties)
|
|
96
|
+
else if (!this.push(payload))
|
|
97
|
+
this._clear()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_buffer (payload, properties) {
|
|
101
|
+
if (this.#buffered > MAX_BUFFER_SIZE) this.destroy()
|
|
102
|
+
|
|
103
|
+
this.#buffered++
|
|
104
|
+
this.#queue[properties.headers.index] = { payload, properties }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} message
|
|
109
|
+
* @param {comq.amqp.Properties} properties
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
_control (message, properties) {
|
|
113
|
+
switch (message) {
|
|
114
|
+
case control.ok:
|
|
115
|
+
this.#control = { properties }
|
|
116
|
+
this.#confirmation.resolve()
|
|
117
|
+
break
|
|
118
|
+
case control.heartbeat:
|
|
119
|
+
break
|
|
120
|
+
case control.end:
|
|
121
|
+
this.push(null)
|
|
122
|
+
break
|
|
123
|
+
default:
|
|
124
|
+
throw new Error(`Unknown reply stream control message: '${message}'`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_heartbeat () {
|
|
129
|
+
if (this.#timeout !== null) clearTimeout(this.#timeout)
|
|
130
|
+
|
|
131
|
+
this.#timeout = setTimeout(() => this.destroy(), this.#idleInterval)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_clear () {
|
|
135
|
+
clearTimeout(this.#timeout)
|
|
136
|
+
this.#emitter.removeAllListeners(this.#correlationId)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const MAX_BUFFER_SIZE = 1000
|
|
141
|
+
|
|
142
|
+
exports.ReplyStream = ReplyStream
|
|
@@ -2,24 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomBytes } = require('node:crypto')
|
|
4
4
|
const { EventEmitter } = require('node:events')
|
|
5
|
-
|
|
6
5
|
const { concat } = require('./concat')
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @param {string} label
|
|
10
9
|
* @returns {comq.ReplyEmitter}
|
|
11
10
|
*/
|
|
12
|
-
|
|
11
|
+
function createReplyEmitter (label) {
|
|
13
12
|
const id = randomBytes(8).toString('hex')
|
|
14
13
|
const queue = concat(label, id)
|
|
15
14
|
const emitter = new EventEmitter()
|
|
16
|
-
const once = (name, callback) => emitter.once(name, callback)
|
|
17
|
-
const emit = (name, value) => emitter.emit(name, value)
|
|
18
|
-
const clear = () => emitter.removeAllListeners()
|
|
19
15
|
|
|
20
16
|
emitter.setMaxListeners(0)
|
|
17
|
+
emitter.queue = queue
|
|
21
18
|
|
|
22
|
-
return
|
|
19
|
+
return /** @type {comq.ReplyEmitter} */ emitter
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
exports.createReplyEmitter = createReplyEmitter
|
package/source/.io/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { concat } = require('./concat')
|
|
4
|
-
const { createReplyEmitter } = require('./
|
|
4
|
+
const { createReplyEmitter } = require('./createReplyEmitter')
|
|
5
|
+
const { ReplyStream } = require('./ReplyStream')
|
|
6
|
+
const { ReplyPipe } = require('./ReplyPipe')
|
|
5
7
|
|
|
6
8
|
exports.concat = concat
|
|
7
9
|
exports.createReplyEmitter = createReplyEmitter
|
|
10
|
+
exports.ReplyStream = ReplyStream
|
|
11
|
+
exports.ReplyPipe = ReplyPipe
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {object} context
|
|
5
|
+
* @param {(exception: Error) => Promise<unknown>} recover
|
|
6
|
+
* @param {(...args: unknown[]) => Promise<unknown>} method
|
|
7
|
+
*/
|
|
8
|
+
function failsafe (context, recover, method = undefined) {
|
|
9
|
+
// two arguments passed
|
|
10
|
+
if (method === undefined) {
|
|
11
|
+
method = recover
|
|
12
|
+
recover = undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return async function call (...args) {
|
|
16
|
+
if (call[DISABLED] === true) return await method.apply(context, args)
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return await method.apply(context, args)
|
|
20
|
+
} catch (exception) {
|
|
21
|
+
if (recover !== undefined && await recover.call(context, exception) === false) throw exception
|
|
22
|
+
|
|
23
|
+
return call.apply(this, args)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
failsafe.disable = (...methods) => {
|
|
29
|
+
for (const method of methods) method[DISABLED] = true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DISABLED = Symbol('disabled')
|
|
33
|
+
|
|
34
|
+
exports.failsafe = failsafe
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { failsafe } = require('./failsafe')
|
|
5
|
+
|
|
6
|
+
it('should be', async () => {
|
|
7
|
+
expect(failsafe).toBeDefined()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const fn = /** @type {jest.MockedFunction} */
|
|
15
|
+
jest.fn(async () => generate())
|
|
16
|
+
const recover = /** @type {jest.MockedFunction<(...args: any[]) => Promise<any>>} */
|
|
17
|
+
jest.fn(async () => true)
|
|
18
|
+
|
|
19
|
+
class FailsafeTest {
|
|
20
|
+
doWithRecovery = failsafe(this, this.#recover, async (...args) => {
|
|
21
|
+
return fn(...args)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
doWithoutRecovery = failsafe(this, (...args) => {
|
|
25
|
+
return fn(...args)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
async #recover (exception) {
|
|
29
|
+
return recover(exception)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @type {FailsafeTest} */
|
|
34
|
+
let instance
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.clearAllMocks()
|
|
38
|
+
|
|
39
|
+
instance = new FailsafeTest()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe.each([
|
|
43
|
+
['with', 'doWithRecovery'],
|
|
44
|
+
['without', 'doWithoutRecovery']
|
|
45
|
+
])('%s recovery', (_, method) => {
|
|
46
|
+
it('should run fn', async () => {
|
|
47
|
+
await instance[method]()
|
|
48
|
+
|
|
49
|
+
expect(fn).toHaveBeenCalled()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should return value', async () => {
|
|
53
|
+
const value = await instance[method]()
|
|
54
|
+
|
|
55
|
+
expect(value).toStrictEqual(await fn.mock.results[0].value)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should pass arguments', async () => {
|
|
59
|
+
const args = [generate(), generate()]
|
|
60
|
+
|
|
61
|
+
await instance[method](...args)
|
|
62
|
+
|
|
63
|
+
expect(fn).toHaveBeenCalledWith(...args)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should call again', async () => {
|
|
67
|
+
fn.mockImplementationOnce(async () => { throw new Error() })
|
|
68
|
+
|
|
69
|
+
await instance[method]()
|
|
70
|
+
|
|
71
|
+
expect(fn).toHaveBeenCalledTimes(2)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('recovery function', () => {
|
|
76
|
+
it('should recover on exception', async () => {
|
|
77
|
+
fn.mockImplementationOnce(async () => { throw new Error() })
|
|
78
|
+
|
|
79
|
+
const value = await instance.doWithRecovery()
|
|
80
|
+
|
|
81
|
+
expect(value).toStrictEqual(await fn.mock.results[1].value)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should throw on recovery failure', async () => {
|
|
85
|
+
const exception = generate()
|
|
86
|
+
|
|
87
|
+
fn.mockImplementationOnce(async () => { throw exception })
|
|
88
|
+
recover.mockImplementationOnce(async () => false)
|
|
89
|
+
|
|
90
|
+
await expect(instance.doWithRecovery()).rejects.toStrictEqual(exception)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should pass exception to recover', async () => {
|
|
94
|
+
const exception = generate()
|
|
95
|
+
|
|
96
|
+
fn.mockImplementationOnce(async () => { throw exception })
|
|
97
|
+
|
|
98
|
+
await instance.doWithRecovery()
|
|
99
|
+
|
|
100
|
+
expect(recover).toHaveBeenCalledWith(exception)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should call recover within context', async () => {
|
|
104
|
+
class Test {
|
|
105
|
+
foo = 1
|
|
106
|
+
|
|
107
|
+
do = failsafe(this, this.recover, fn)
|
|
108
|
+
|
|
109
|
+
recover () {
|
|
110
|
+
expect(this.foo).toStrictEqual(1)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const instance = new Test()
|
|
115
|
+
|
|
116
|
+
fn.mockImplementationOnce(() => { throw new Error() })
|
|
117
|
+
|
|
118
|
+
instance.do()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('disable', () => {
|
|
123
|
+
it('should throw exception', async () => {
|
|
124
|
+
const exception = generate()
|
|
125
|
+
|
|
126
|
+
failsafe.disable(instance.doWithRecovery, instance.doWithoutRecovery)
|
|
127
|
+
|
|
128
|
+
fn.mockImplementation(async () => { throw exception })
|
|
129
|
+
|
|
130
|
+
await expect(instance.doWithRecovery()).rejects.toStrictEqual(exception)
|
|
131
|
+
await expect(instance.doWithoutRecovery()).rejects.toStrictEqual(exception)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { memo } = require('./memo')
|
|
4
|
+
const { failsafe } = require('./failsafe')
|
|
5
|
+
const { lazy } = require('./lazy')
|
|
6
|
+
const { track } = require('./track')
|
|
7
|
+
const { recall } = require('./recall')
|
|
8
|
+
|
|
9
|
+
exports.memo = memo
|
|
10
|
+
exports.failsafe = failsafe
|
|
11
|
+
exports.lazy = lazy
|
|
12
|
+
exports.track = track
|
|
13
|
+
exports.recall = recall
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function lazy (context, initializers, method) {
|
|
4
|
+
if (context[LOCK] === undefined) context[LOCK] = Symbol('methods locking key')
|
|
5
|
+
|
|
6
|
+
if (!Array.isArray(initializers)) initializers = [initializers]
|
|
7
|
+
|
|
8
|
+
return async function (...args) {
|
|
9
|
+
const override = await call(context, initializers, args)
|
|
10
|
+
|
|
11
|
+
if (override !== undefined) args.splice(0, override.length, ...override)
|
|
12
|
+
|
|
13
|
+
return method.apply(this, args)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} context
|
|
19
|
+
* @param {Function[]} initializers
|
|
20
|
+
* @param {any[]} args
|
|
21
|
+
* @returns {Promise<any[]>}
|
|
22
|
+
*/
|
|
23
|
+
async function call (context, initializers, args) {
|
|
24
|
+
let override
|
|
25
|
+
|
|
26
|
+
for (const init of initializers) {
|
|
27
|
+
const result = await resolve(context, init, args)
|
|
28
|
+
|
|
29
|
+
if (result !== undefined) override = result
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return override
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} context
|
|
37
|
+
* @param {Function} init
|
|
38
|
+
* @param {any[]} args
|
|
39
|
+
* @returns {Promise}
|
|
40
|
+
*/
|
|
41
|
+
function resolve (context, init, args) {
|
|
42
|
+
const key = context[LOCK]
|
|
43
|
+
const expected = args.slice(0, init.length)
|
|
44
|
+
|
|
45
|
+
return lock(context, init, expected, key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {object} context
|
|
50
|
+
* @param {(...args: unknown[]) => Promise<unknown>} init
|
|
51
|
+
* @param {unknown[]} args
|
|
52
|
+
* @param {Symbol} key
|
|
53
|
+
* @returns {Promise<unknown>}
|
|
54
|
+
*/
|
|
55
|
+
function lock (context, init, args, key) {
|
|
56
|
+
if (init[key] === undefined) init[key] = []
|
|
57
|
+
|
|
58
|
+
const locks = init[key]
|
|
59
|
+
const found = locks.find((lock) => lock.args.reduce((match, argument, i) => match && argument === args[i], true))
|
|
60
|
+
|
|
61
|
+
if (found !== undefined) return found.promise
|
|
62
|
+
|
|
63
|
+
const promise = init.apply(context, args)
|
|
64
|
+
const lock = { args, promise }
|
|
65
|
+
|
|
66
|
+
locks.push(lock)
|
|
67
|
+
|
|
68
|
+
return promise
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function reset (context) {
|
|
72
|
+
context[LOCK] = context[LOCK] = Symbol('methods locking key')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const LOCK = Symbol('context locking key')
|
|
76
|
+
|
|
77
|
+
lazy.reset = reset
|
|
78
|
+
|
|
79
|
+
exports.lazy = lazy
|