-
Notifications
You must be signed in to change notification settings - Fork 2
/
recycler.ts
115 lines (107 loc) · 2.84 KB
/
recycler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import _ from 'lodash'
import { Logger } from 'src/utils/logger'
import { sleep } from 'src/utils/utils'
export type RecyclerOpts<T> = {
factory: () => Promise<T>
destroyer: (current: T) => Promise<void>
autoRecycle?: (current: T, recycleCb: () => void) => void
retryInterval?: number
logger?: Logger
}
export type StopOptions = {
destroy?: boolean
}
export type Recycler<T> = {
current: T
recycle: () => Promise<void>
stop: (opts?: StopOptions) => Promise<void>
}
/**
* Creates a recycler object. Useful for recreating hard-to-recover objects while
* making the process easy for the caller.
*
* Example usage:
*
* const { current, recycle, stop } = await createRecycler({
* factory: async () => {
* const conn = await createDifficultConnection()
* return conn
* },
* destroyer: async (currentConn) => {
* // The destroyer must destroy the recycled object so that
* // the old connection won't end up calling callbacks
* currentConn.close()
* },
* autoRecycle: (justCreatedConn, cb) => {
* // Add as many triggers as you wish, cb ensures that it
* // only recycles once
* justCreatedConn.on('error', cb)
* justCreatedConn.on('close', cb)
* }
* })
*/
export async function createRecycler<T>({
factory,
destroyer,
autoRecycle,
retryInterval = 5000,
logger = console,
}: RecyclerOpts<T>): Promise<Recycler<T>> {
let stopRecycle = false
let current = await factory()
initAutoRecycleTrigger(current)
async function recycle() {
logger.log('Recycling ...')
await destroyer(current)
while (!stopRecycle) {
try {
current = await factory()
initAutoRecycleTrigger(current)
break
} catch (err) {
logger.warn('Recycler failed to re-create', err)
} finally {
await sleep(retryInterval)
}
}
}
async function stop(opts: StopOptions = {}) {
stopRecycle = true
if (opts.destroy) {
await destroyer(current)
}
}
function initAutoRecycleTrigger(obj: T) {
if (autoRecycle) {
autoRecycle(
obj,
_.once(async () => {
if (stopRecycle) {
return
}
await sleep(retryInterval)
void recycle()
})
)
}
}
return {
// The user of a recycler object can remain using the same object reference because
// it links to this proxy. Recycler switches the underlying target transparently under
// the hood.
current: new Proxy(
{},
{
// Dynamically forward all the traps to the associated methods on the mutable target
get: (_target, prop, receiver) => {
return Reflect.get(current as any, prop, receiver)
},
set: (_tarset, prop, receiver) => {
return Reflect.get(current as any, prop, receiver)
},
}
) as T,
recycle,
stop,
}
}