/
index.js
219 lines (191 loc) · 4.63 KB
/
index.js
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
'use strict'
const createNamespace = require('cls-hooked').createNamespace
const ns = createNamespace('_node_request_local_storage')
const AsyncLock = require('async-lock')
/**
* @module node-rls
*/
exports = module.exports = {}
/**
* Thrown when context has not yet been initialized
*
* @static
* @param {string} message - Error message
* @constructor
* @type {Error}
*/
function NotInitializedError (message) {
Error.captureStackTrace(this, this.constructor)
this.name = this.constructor.name
this.message = message
}
function getMeta () {
return ns.get('_meta')
}
async function withLock (callback) {
const meta = getMeta()
if (meta === undefined) {
throw new exports.NotInitializedError('RLS context not initialized')
}
return meta.lock.acquire('key', callback)
}
async function incrDecrNumber (key, count, valueCb) {
if (count === undefined) {
count = 1
} else {
// This else block is for test coverage
}
const meta = getMeta()
return withLock(() => {
const kv = meta.kv
let value = kv[key]
// Assume we want to initialize the counter
if (value === undefined) {
value = 0
}
if (typeof value !== 'number') {
const type = typeof value
throw new Error(`Counter is not a number, was '${type}'`)
}
value = valueCb(value, count)
kv[key] = value
return value
})
}
exports.NotInitializedError = NotInitializedError
/**
* Create a new context and run callback.
*
* @async
* @param {function} callback - Async callback
* @param {Object} kv - Object to use as kv store (this sacrifices atomicity)
* @returns {Object} Result from callback
* @example
* // Any called function will have access to conext
* // even async ones
*
* function func () {
* return RLS.get('requestid')
* }
*
* RLS.run(async () => {
* const requestid = 'somerandomvalue'
* await RLS.set('requestid', requestid)
*
* assert.strictEqual(await func(), requestid)
* })
*/
exports.run = async function (callback, kv) {
return ns.runAndReturn(async () => {
// Wrap all set/get operations using this meta object
// This is so we can do proper locking
ns.set('_meta', {
'kv': kv || {}, // Key/value storage
'lock': new AsyncLock()
})
return callback()
})
}
/**
* Get object from storage.
*
* @async
* @param {String} key - Storage key
* @returns {Object} Stored object
* @throws {NotInitializedError}
*/
exports.get = async function (key) {
return withLock(async () => {
return getMeta().kv[key]
})
}
/**
* Get KV store as a javascript object
* Mutating this object will NOT mutate the KV store
*
* @returns {Object} Shallow copy of KV store
* @throws {NotInitializedError}
*/
exports.copy = function () {
const meta = getMeta()
if (meta === undefined) {
throw new exports.NotInitializedError('RLS context not initialized')
}
return Object.assign({}, meta.kv)
}
/**
* Set storage object
*
* @async
* @param {String} key - Storage key
* @param {Object} value - Storage key
* @returns {undefined} Returns nothing
* @throws {NotInitializedError}
*/
exports.set = async function (key, value) {
return withLock(async () => {
const meta = getMeta()
meta.kv[key] = value
})
}
/**
* Delete storage object
*
* @async
* @param {String} key - Storage key
* @returns {undefined} Returns nothing
* @throws {NotInitializedError}
*/
exports.delete = async function (key) {
return withLock(async () => {
const meta = getMeta()
delete meta.kv[key]
})
}
/**
* Update storage from a map
*
* @async
* @param {Object} obj - KV mapping object
* @returns {undefined} No return value
* @throws {NotInitializedError}
* @example
* // Update storage with all values from object
* const obj = {foo: 'bar', someKey: 'someValue'}
* await update(obj)
* console.log(await get('foo')) // Prints bar
*/
exports.update = async function (obj) {
const meta = getMeta()
return withLock(async () => {
Object.assign(meta.kv, obj)
})
}
/**
* Atomically increment a counter
*
* @async
* @param {Object} key - Storage key
* @param {Object} count - Increment by count
* @throws {NotInitializedError}
* @returns {Number} Counter after increment
*/
exports.incr = async function (key, count) {
return incrDecrNumber(key, count, (value, count) => {
return value + count
})
}
/**
* Atomically decrement a counter
*
* @async
* @param {Object} key - Storage key
* @param {Object} count - Decrement by count
* @throws {NotInitializedError}
* @returns {Number} Counter after decrement
*/
exports.decr = async function (key, count) {
return incrDecrNumber(key, count, (value, count) => {
return value - count
})
}