better-lock 1.0.2 → 2.0.2-beta
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/.eslintrc.js +24 -0
- package/.idea/better-lock.iml +1 -7
- package/.idea/codeStyles/Project.xml +336 -5
- package/.idea/compiler.xml +7 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -2
- package/.idea/jsLibraryMappings.xml +0 -1
- package/.idea/markdown.xml +9 -0
- package/.idea/modules.xml +1 -1
- package/.idea/prettier.xml +8 -0
- package/.prettierrc.yaml +1 -3
- package/.travis.yml +2 -2
- package/README.md +113 -73
- package/dist/better_lock.d.ts +50 -0
- package/dist/better_lock.js +317 -0
- package/dist/better_lock.js.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +131 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +75 -0
- package/dist/internals.js +75 -0
- package/dist/internals.js.map +1 -0
- package/dist/options.d.ts +54 -0
- package/dist/options.js +3 -0
- package/dist/options.js.map +1 -0
- package/dist/tools.d.ts +19 -0
- package/dist/tools.js +109 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/jest.config.js +4 -0
- package/package.json +32 -9
- package/spec/better_lock.spec.ts +614 -0
- package/src/better_lock.ts +430 -0
- package/src/errors.ts +124 -0
- package/src/index.ts +54 -0
- package/src/internals.ts +118 -0
- package/src/options.ts +63 -0
- package/src/tools.ts +111 -0
- package/src/types.ts +29 -0
- package/tsconfig.json +13 -0
- package/.idea/markdown-navigator/profiles_settings.xml +0 -3
- package/.idea/markdown-navigator.xml +0 -78
- package/.idea/misc.xml +0 -6
- package/.idea/watcherTasks.xml +0 -8
- package/index.js +0 -19
- package/spec/better_lock.spec.js +0 -602
- package/src/better_lock.js +0 -405
- package/src/errors.js +0 -81
- package/src/options.js +0 -104
- package/src/tools.js +0 -125
- package/src/types.js +0 -110
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BetterLockExecutionTimeoutError,
|
|
3
|
+
BetterLockInternalError,
|
|
4
|
+
BetterLockInvalidArgumentError,
|
|
5
|
+
BetterLockJobAbortedError,
|
|
6
|
+
BetterLockQueueOverflowError,
|
|
7
|
+
BetterLockWaitTimeoutError,
|
|
8
|
+
} from './errors';
|
|
9
|
+
import { KeyQueue, LockJob } from './internals';
|
|
10
|
+
import { ILockJobOptions, IOptions } from './options';
|
|
11
|
+
import * as tools from './tools';
|
|
12
|
+
import { ICallback, ICallbackExecutor, ILockKey, IPromiseExecutor } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a better lock object.
|
|
16
|
+
*/
|
|
17
|
+
export class BetterLock {
|
|
18
|
+
/**
|
|
19
|
+
* Default options to be used when creating BetterLock instances.
|
|
20
|
+
*/
|
|
21
|
+
static DEFAULT_OPTIONS: IOptions = {
|
|
22
|
+
extend_stack_traces: true,
|
|
23
|
+
promise_tester: p => p && typeof p.then === 'function',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
private options: IOptions;
|
|
27
|
+
private log: (msg: string) => void;
|
|
28
|
+
private queues: { [key: string]: KeyQueue };
|
|
29
|
+
|
|
30
|
+
constructor(options?: IOptions) {
|
|
31
|
+
this.options = tools.assign({}, [BetterLock.DEFAULT_OPTIONS, options]);
|
|
32
|
+
this.log = tools.makeLog(this.options.name, this.options.log);
|
|
33
|
+
this.queues = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Return or create a key queue for given key
|
|
38
|
+
*/
|
|
39
|
+
private getQueue(key: ILockKey): KeyQueue {
|
|
40
|
+
if (key === undefined || key === null) {
|
|
41
|
+
key = KeyQueue.DEFAULT_QUEUE_KEY;
|
|
42
|
+
}
|
|
43
|
+
let queue = this.queues[key];
|
|
44
|
+
if (!queue) {
|
|
45
|
+
queue = this.queues[key] = new KeyQueue(String(key));
|
|
46
|
+
}
|
|
47
|
+
return queue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns true if the caller can *immediately* acquire the lock. There is nothing holding the key and nothing in the queue.
|
|
52
|
+
*/
|
|
53
|
+
public canAcquire(key?: string): boolean {
|
|
54
|
+
const queue = this.getQueue(key);
|
|
55
|
+
return !queue.active && !queue.jobs.length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private normalizeAndValidateKey(key: ILockKey) {
|
|
59
|
+
if (key === undefined) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (key === null) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (tools.isString(key)) {
|
|
66
|
+
return key;
|
|
67
|
+
}
|
|
68
|
+
if (tools.isNumber(key)) {
|
|
69
|
+
return String(key);
|
|
70
|
+
}
|
|
71
|
+
// Invalid key value
|
|
72
|
+
throw new BetterLockInvalidArgumentError(
|
|
73
|
+
this.options.name,
|
|
74
|
+
'key',
|
|
75
|
+
'an instance of ILockKey: string, number, undefined or null',
|
|
76
|
+
key
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public acquire<TResult>(
|
|
81
|
+
executor: IPromiseExecutor<TResult>,
|
|
82
|
+
jobOptions?: ILockJobOptions
|
|
83
|
+
): Promise<TResult>;
|
|
84
|
+
public acquire<TResult>(
|
|
85
|
+
executor: ICallbackExecutor<TResult>,
|
|
86
|
+
jobOptions?: ILockJobOptions
|
|
87
|
+
): Promise<TResult>;
|
|
88
|
+
public acquire<TResult>(
|
|
89
|
+
executor: ICallbackExecutor<TResult>,
|
|
90
|
+
callback: ICallback<TResult>,
|
|
91
|
+
jobOptions?: ILockJobOptions
|
|
92
|
+
): void;
|
|
93
|
+
public acquire<TResult>(
|
|
94
|
+
key: ILockKey | Array<ILockKey>,
|
|
95
|
+
executor: IPromiseExecutor<TResult>,
|
|
96
|
+
jobOptions?: ILockJobOptions
|
|
97
|
+
): Promise<TResult>;
|
|
98
|
+
public acquire<TResult>(
|
|
99
|
+
key: ILockKey | Array<ILockKey>,
|
|
100
|
+
executor: ICallbackExecutor<TResult>,
|
|
101
|
+
jobOptions?: ILockJobOptions
|
|
102
|
+
): Promise<TResult>;
|
|
103
|
+
public acquire<TResult>(
|
|
104
|
+
key: ILockKey | Array<ILockKey>,
|
|
105
|
+
executor: ICallbackExecutor<TResult>,
|
|
106
|
+
callback: ICallback<TResult>,
|
|
107
|
+
jobOptions?: ILockJobOptions
|
|
108
|
+
): void;
|
|
109
|
+
/**
|
|
110
|
+
* Acquire the lock for given key or list of keys. If waiting on a list, keys will be acquired in order,
|
|
111
|
+
* as they become free. Any deadlock prevention must be handled by the caller.
|
|
112
|
+
*
|
|
113
|
+
* Once the lock is acquired, we will call the executor function, with a "done" method.
|
|
114
|
+
* You must call "done" to release the lock. Alternatively, return a Promise; lock will be released
|
|
115
|
+
* once promise resolves or rejects.
|
|
116
|
+
*
|
|
117
|
+
* Return values from your executor will be passed to callback (or the resulting promise).
|
|
118
|
+
*
|
|
119
|
+
* @param key Named key or array of keys for this particular call. Calls with different keys will be run in parallel. Not required.
|
|
120
|
+
* @param executor Function that will run inside the lock. Required.
|
|
121
|
+
* @param callback Function to be called after the executor finishes or if we never enter the lock (timeout, queue depletion). Leave out to use promises.
|
|
122
|
+
* @param jobOptions Options to be applied on this job only
|
|
123
|
+
*/
|
|
124
|
+
public acquire(key, executor?, callback?, jobOptions?) {
|
|
125
|
+
if (tools.isFunction(key) || tools.isObject(key)) {
|
|
126
|
+
// Presume we weren't given a key (first form)
|
|
127
|
+
jobOptions = callback;
|
|
128
|
+
callback = executor;
|
|
129
|
+
executor = key;
|
|
130
|
+
key = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Repackage "key" into an array of keys
|
|
134
|
+
const keys = [];
|
|
135
|
+
if (Array.isArray(key)) {
|
|
136
|
+
const arrayOfKeys = key;
|
|
137
|
+
// Make sure we don't add duplicates
|
|
138
|
+
const seenKeys = new Set();
|
|
139
|
+
for (let i = 0; i < arrayOfKeys.length; i++) {
|
|
140
|
+
const normalizedKey = this.normalizeAndValidateKey(arrayOfKeys[i]);
|
|
141
|
+
if (!seenKeys.has(normalizedKey)) {
|
|
142
|
+
keys.push(normalizedKey);
|
|
143
|
+
seenKeys.add(normalizedKey);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
keys.push(this.normalizeAndValidateKey(key));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Create callback wrapper for promise interface
|
|
151
|
+
if (!tools.isFunction(callback)) {
|
|
152
|
+
if (jobOptions === undefined) {
|
|
153
|
+
jobOptions = callback;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
callback = tools.callbackWithPromise();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validate other options
|
|
160
|
+
if (!tools.isFunction(executor)) {
|
|
161
|
+
throw new BetterLockInvalidArgumentError(
|
|
162
|
+
this.options.name,
|
|
163
|
+
'executor',
|
|
164
|
+
'a function',
|
|
165
|
+
executor
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!tools.isFunction(callback)) {
|
|
169
|
+
throw new BetterLockInvalidArgumentError(
|
|
170
|
+
this.options.name,
|
|
171
|
+
'callback',
|
|
172
|
+
'a function',
|
|
173
|
+
callback
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Prepare job options
|
|
178
|
+
const effectiveJobOptions = jobOptions
|
|
179
|
+
? tools.assign({}, [this.options, jobOptions])
|
|
180
|
+
: // No need to create new object since these will be read-only and are subset of global options
|
|
181
|
+
this.options;
|
|
182
|
+
|
|
183
|
+
const job = new LockJob(keys, executor, callback, effectiveJobOptions);
|
|
184
|
+
|
|
185
|
+
// Set incoming stack
|
|
186
|
+
if (effectiveJobOptions.extend_stack_traces) {
|
|
187
|
+
const tempErr = new Error();
|
|
188
|
+
Error.captureStackTrace(tempErr, this.acquire);
|
|
189
|
+
job.incoming_stack = tempErr.stack;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add job to its key queues
|
|
193
|
+
const queuesToUpdate = [];
|
|
194
|
+
for (let i = 0; i < keys.length; i++) {
|
|
195
|
+
const queue = this.getQueue(keys[i]);
|
|
196
|
+
queue.jobs.push(job);
|
|
197
|
+
queuesToUpdate.push(queue);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Start wait timer, if enabled
|
|
201
|
+
if (tools.isNumber(job.options.wait_timeout)) {
|
|
202
|
+
job.wait_timeout_id = setTimeout(() => this.onWaitTimeout(job), job.options.wait_timeout);
|
|
203
|
+
|
|
204
|
+
// NOTE: timeout of 0 will NOT trigger error if lock can be acquired immediately.
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.log(`Enqueued ${job}`);
|
|
208
|
+
|
|
209
|
+
// Perform update of queues this job has modified. We don't do this in next tick or something,
|
|
210
|
+
// because we want canAcquire to immediately return false
|
|
211
|
+
if (queuesToUpdate.length) {
|
|
212
|
+
this.update(queuesToUpdate);
|
|
213
|
+
} else {
|
|
214
|
+
// Special case. We are given empty list of keys. We will immediately execute jobs like this.
|
|
215
|
+
this.executeJob(job);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (callback as any).promise;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Update state of given list of queues.
|
|
223
|
+
*/
|
|
224
|
+
private update(queues: KeyQueue[]) {
|
|
225
|
+
for (let i = 0; i < queues.length; i++) {
|
|
226
|
+
const queue = queues[i];
|
|
227
|
+
|
|
228
|
+
// If key is available, have next job grab it
|
|
229
|
+
if (!queue.active && queue.jobs.length) {
|
|
230
|
+
const job = queue.jobs.splice(0, 1)[0];
|
|
231
|
+
queue.active = job;
|
|
232
|
+
job.waiting_count--;
|
|
233
|
+
|
|
234
|
+
if (job.waiting_count <= 0) {
|
|
235
|
+
// This job can be executed now.
|
|
236
|
+
this.executeJob(job);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle wait queue overflow
|
|
241
|
+
if (tools.isNumber(this.options.queue_size) && this.options.queue_size < queue.jobs.length) {
|
|
242
|
+
// Overflow. Reject the most recent job
|
|
243
|
+
const mostRecentJob = queue.jobs[queue.jobs.length - 1];
|
|
244
|
+
this.log(
|
|
245
|
+
`${queue.toString()} has overflown, so most recent job (${mostRecentJob}) was kicked out`
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
this.endJob(mostRecentJob, [
|
|
249
|
+
new BetterLockQueueOverflowError(
|
|
250
|
+
this.options.name,
|
|
251
|
+
queue.key,
|
|
252
|
+
queue.jobs.length,
|
|
253
|
+
mostRecentJob
|
|
254
|
+
),
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private executeJob(job: LockJob<any>) {
|
|
261
|
+
this.log(`Executing ${job}`);
|
|
262
|
+
|
|
263
|
+
// Clear wait timeout
|
|
264
|
+
clearTimeout(job.wait_timeout_id);
|
|
265
|
+
job.wait_timeout_id = null;
|
|
266
|
+
|
|
267
|
+
// We want to do the rest of this in a separate context, because we don't want user executor code
|
|
268
|
+
// to ever interfere with the code calling acquire().
|
|
269
|
+
|
|
270
|
+
setImmediate(() => {
|
|
271
|
+
// Make sure job hasn't been ended in the meantime
|
|
272
|
+
if (job.executed_at || job.ended_at) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Validate this job is holding all queues it needs
|
|
277
|
+
for (let i = 0; i < job.keys.length; i++) {
|
|
278
|
+
const queue = this.getQueue(job.keys[i]);
|
|
279
|
+
if (queue.active !== job) {
|
|
280
|
+
// This should never happen
|
|
281
|
+
return this.endJob(job, [
|
|
282
|
+
new BetterLockInternalError(
|
|
283
|
+
this.options.name,
|
|
284
|
+
`Corrupted wait queue state for ${queue.key}`
|
|
285
|
+
),
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Mark job as having started executing
|
|
291
|
+
job.executed_at = new Date();
|
|
292
|
+
|
|
293
|
+
if (tools.isNumber(job.options.execution_timeout)) {
|
|
294
|
+
job.execution_timeout_id = setTimeout(
|
|
295
|
+
() => this.onExecutionTimeout(job),
|
|
296
|
+
job.options.execution_timeout
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const lockDone = (...args) => {
|
|
301
|
+
this.log(`Done called for ${job}`);
|
|
302
|
+
|
|
303
|
+
this.endJob(job, args);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (job.executor.length > 0) {
|
|
307
|
+
// Callback interface
|
|
308
|
+
job.executor(lockDone);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Promise interface
|
|
313
|
+
let executorResult;
|
|
314
|
+
try {
|
|
315
|
+
executorResult = (job.executor as any)();
|
|
316
|
+
} catch (err) {
|
|
317
|
+
// Error thrown directly from executor
|
|
318
|
+
lockDone(err);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Promise sniffing
|
|
322
|
+
if (this.options.promise_tester(executorResult)) {
|
|
323
|
+
executorResult.then(
|
|
324
|
+
res => lockDone(null, res),
|
|
325
|
+
err => lockDone(err)
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
// "promise" is just some random value. We don't have to wait
|
|
329
|
+
lockDone(null, executorResult);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private onWaitTimeout(job: LockJob<any>) {
|
|
335
|
+
this.log(
|
|
336
|
+
`${job} has timed out after waiting in queue for ${
|
|
337
|
+
new Date().valueOf() - job.enqueued_at.valueOf()
|
|
338
|
+
}ms`
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
this.endJob(job, [new BetterLockWaitTimeoutError(this.options.name, job)]);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private onExecutionTimeout(job: LockJob<any>) {
|
|
345
|
+
this.log(
|
|
346
|
+
`${job} has timed out after executing for ${
|
|
347
|
+
new Date().valueOf() - job.executed_at.valueOf()
|
|
348
|
+
}ms`
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
this.endJob(job, [new BetterLockExecutionTimeoutError(this.options.name, job)]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private endJob(job: LockJob<any>, callbackArgs: any[]) {
|
|
355
|
+
if (job.ended_at) {
|
|
356
|
+
this.log(
|
|
357
|
+
`WARNING: ${job} is trying to end, but it has already ended at ${job.ended_at.toISOString()}. Called with arguments: ${callbackArgs}`
|
|
358
|
+
);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
clearTimeout(job.wait_timeout_id);
|
|
363
|
+
job.wait_timeout_id = null;
|
|
364
|
+
|
|
365
|
+
clearTimeout(job.execution_timeout_id);
|
|
366
|
+
job.execution_timeout_id = null;
|
|
367
|
+
|
|
368
|
+
job.ended_at = new Date();
|
|
369
|
+
|
|
370
|
+
const queuesToUpdate = [];
|
|
371
|
+
for (let i = 0; i < job.keys.length; i++) {
|
|
372
|
+
const queue = this.getQueue(job.keys[i]);
|
|
373
|
+
queuesToUpdate.push(queue);
|
|
374
|
+
|
|
375
|
+
if (queue.active === job) {
|
|
376
|
+
// This job was holding on the queue's key
|
|
377
|
+
queue.active = null;
|
|
378
|
+
} else {
|
|
379
|
+
// This job was still in wait list for this queue
|
|
380
|
+
const index = queue.jobs.indexOf(job);
|
|
381
|
+
if (index >= 0) {
|
|
382
|
+
// Remove from queue
|
|
383
|
+
queue.jobs.splice(index, 1);
|
|
384
|
+
} else {
|
|
385
|
+
// Something is wrong. Where did this job come from?
|
|
386
|
+
this.log(
|
|
387
|
+
`WARNING: ${job} is ending, but it is not found anywhere in the queue for ${queue.key}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Call the callback
|
|
394
|
+
try {
|
|
395
|
+
job.callback.apply(null, callbackArgs);
|
|
396
|
+
} finally {
|
|
397
|
+
// Whatever happens, schedule a queue update
|
|
398
|
+
setImmediate(() => this.update(queuesToUpdate));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Abort all jobs for a given key (or from the default job queue, if no key is given).
|
|
404
|
+
* Job executors will not be called. Callbacks will be called with JobAbortedError.
|
|
405
|
+
* Currently executing job will not be interrupted.
|
|
406
|
+
* @param [key]
|
|
407
|
+
*/
|
|
408
|
+
public abort(key?: ILockKey) {
|
|
409
|
+
const queue = this.getQueue(key);
|
|
410
|
+
queue.jobs.slice().forEach(job => {
|
|
411
|
+
this.endJob(job, [new BetterLockJobAbortedError(this.options.name, job)]);
|
|
412
|
+
});
|
|
413
|
+
if (queue.active && !queue.active.executed_at) {
|
|
414
|
+
// If we have a job holding this queue that hasn't been executed yet, abort it as well
|
|
415
|
+
this.endJob(queue.active, [new BetterLockJobAbortedError(this.options.name, queue.active)]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Abort all pending jobs from all queues.
|
|
421
|
+
* Currently executing jobs will not be interrupted.
|
|
422
|
+
*/
|
|
423
|
+
public abortAll() {
|
|
424
|
+
this.abort(null);
|
|
425
|
+
for (const key in this.queues) {
|
|
426
|
+
// noinspection JSUnfilteredForInLoop
|
|
427
|
+
this.abort(key);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { KeyQueue, LockJob } from './internals';
|
|
2
|
+
import { IErrorName } from './types';
|
|
3
|
+
|
|
4
|
+
export class BetterLockError extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* Name of the lock that caused the error
|
|
7
|
+
*/
|
|
8
|
+
lock_name: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Error name, maps to error's class name. Eg. BetterLockError.
|
|
12
|
+
*/
|
|
13
|
+
name: IErrorName;
|
|
14
|
+
|
|
15
|
+
constructor(lockName, message, incomingStack = null) {
|
|
16
|
+
// Fix typescript custom Error prototype chain
|
|
17
|
+
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
|
18
|
+
// https://github.com/reduardo7/ts-base-error/blob/master/src/index.ts
|
|
19
|
+
const trueProto = new.target.prototype;
|
|
20
|
+
|
|
21
|
+
if (lockName) {
|
|
22
|
+
super(`[${lockName}] ${message}`);
|
|
23
|
+
} else {
|
|
24
|
+
super(message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Object.setPrototypeOf(this, trueProto);
|
|
28
|
+
|
|
29
|
+
this.lock_name = lockName;
|
|
30
|
+
this.name = this.constructor.name as IErrorName;
|
|
31
|
+
|
|
32
|
+
if (incomingStack) {
|
|
33
|
+
const withoutFirstLine = incomingStack.split('\n').slice(1).join('\n');
|
|
34
|
+
this.stack =
|
|
35
|
+
this.stack +
|
|
36
|
+
'\n --------------------------------------------------------------------------------\n' +
|
|
37
|
+
withoutFirstLine;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class BetterLockInternalError extends BetterLockError {
|
|
43
|
+
constructor(lockName, message) {
|
|
44
|
+
const lastChar = message[message.length - 1];
|
|
45
|
+
if (lastChar !== '.' && lastChar !== '!' && lastChar !== '?') {
|
|
46
|
+
message += '.';
|
|
47
|
+
}
|
|
48
|
+
super(lockName, message + ' This is probably a bug inside "better-lock" library');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class BetterLockInvalidArgumentError extends BetterLockError {
|
|
53
|
+
constructor(lockName, public argument, expected, actual) {
|
|
54
|
+
super(lockName, `Argument "${argument}" must be ${expected} (got: "${actual}")`);
|
|
55
|
+
this.argument = argument;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class BetterLockWaitTimeoutError extends BetterLockError {
|
|
60
|
+
public job_id: number;
|
|
61
|
+
public keys: string[];
|
|
62
|
+
public enqueued_at: Date;
|
|
63
|
+
|
|
64
|
+
constructor(lockName, job: LockJob<any>) {
|
|
65
|
+
const message = `${job} has timed out after ${
|
|
66
|
+
new Date().valueOf() - job.enqueued_at.valueOf()
|
|
67
|
+
}ms in wait queue`;
|
|
68
|
+
|
|
69
|
+
super(lockName, message, job.incoming_stack);
|
|
70
|
+
|
|
71
|
+
this.job_id = job.id;
|
|
72
|
+
this.keys = job.keys;
|
|
73
|
+
this.enqueued_at = job.enqueued_at;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class BetterLockExecutionTimeoutError extends BetterLockError {
|
|
78
|
+
public job_id: number;
|
|
79
|
+
public keys: string[];
|
|
80
|
+
public executed_at: Date;
|
|
81
|
+
|
|
82
|
+
constructor(lockName, job: LockJob<any>) {
|
|
83
|
+
const message = `${job} has timed out after ${
|
|
84
|
+
new Date().valueOf() - job.executed_at.valueOf()
|
|
85
|
+
}ms of execution`;
|
|
86
|
+
|
|
87
|
+
super(lockName, message, job.incoming_stack);
|
|
88
|
+
|
|
89
|
+
this.job_id = job.id;
|
|
90
|
+
this.keys = job.keys;
|
|
91
|
+
this.executed_at = job.executed_at;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class BetterLockQueueOverflowError extends BetterLockError {
|
|
96
|
+
public job_id: number;
|
|
97
|
+
public keys: string[];
|
|
98
|
+
public job_count: number;
|
|
99
|
+
public kicked_out_job_id: number;
|
|
100
|
+
|
|
101
|
+
constructor(lockName, key, count, job: LockJob<any>) {
|
|
102
|
+
const keyDesignation = key === KeyQueue.DEFAULT_QUEUE_KEY ? '' : ` for key "${key}"`;
|
|
103
|
+
const message = `Too many jobs (${count}) are waiting${keyDesignation}. The most recent job (${job}) was kicked out`;
|
|
104
|
+
|
|
105
|
+
super(lockName, message, job.incoming_stack);
|
|
106
|
+
|
|
107
|
+
this.job_id = job.id;
|
|
108
|
+
this.keys = job.keys;
|
|
109
|
+
this.job_count = count;
|
|
110
|
+
this.kicked_out_job_id = job.id;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class BetterLockJobAbortedError extends BetterLockError {
|
|
115
|
+
public job_id: number;
|
|
116
|
+
public keys: string[];
|
|
117
|
+
|
|
118
|
+
constructor(lockName, job) {
|
|
119
|
+
super(lockName, `${job} has been aborted by the user`, job.incoming_stack);
|
|
120
|
+
|
|
121
|
+
this.job_id = job.id;
|
|
122
|
+
this.keys = job.keys;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BetterLock as BetterLockUnadorned } from './better_lock';
|
|
2
|
+
import {
|
|
3
|
+
BetterLockError,
|
|
4
|
+
BetterLockExecutionTimeoutError,
|
|
5
|
+
BetterLockInternalError,
|
|
6
|
+
BetterLockInvalidArgumentError,
|
|
7
|
+
BetterLockJobAbortedError,
|
|
8
|
+
BetterLockQueueOverflowError,
|
|
9
|
+
BetterLockWaitTimeoutError,
|
|
10
|
+
} from './errors';
|
|
11
|
+
import { ILockJobOptions as BetterLockJobOptions, IOptions as BetterLockOptions } from './options';
|
|
12
|
+
import {
|
|
13
|
+
IErrorName as BetterLockErrorName,
|
|
14
|
+
IExecutor as BetterLockExecutor,
|
|
15
|
+
ILockKey as BetterLockKey,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
BetterLockOptions,
|
|
20
|
+
BetterLockJobOptions,
|
|
21
|
+
BetterLockExecutor,
|
|
22
|
+
BetterLockKey,
|
|
23
|
+
BetterLockErrorName,
|
|
24
|
+
// Errors
|
|
25
|
+
BetterLockError,
|
|
26
|
+
BetterLockExecutionTimeoutError,
|
|
27
|
+
BetterLockInternalError,
|
|
28
|
+
BetterLockInvalidArgumentError,
|
|
29
|
+
BetterLockJobAbortedError,
|
|
30
|
+
BetterLockQueueOverflowError,
|
|
31
|
+
BetterLockWaitTimeoutError,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function attachExports<TTarget, TExports extends { [key: string]: any }>(
|
|
35
|
+
target: TTarget,
|
|
36
|
+
exports: TExports
|
|
37
|
+
): TTarget & TExports {
|
|
38
|
+
Object.assign(target, exports);
|
|
39
|
+
return target as any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const BetterLock = attachExports(BetterLockUnadorned, {
|
|
43
|
+
BetterLock: BetterLockUnadorned,
|
|
44
|
+
|
|
45
|
+
Error: BetterLockError,
|
|
46
|
+
InternalError: BetterLockInternalError,
|
|
47
|
+
InvalidArgumentError: BetterLockInvalidArgumentError,
|
|
48
|
+
WaitTimeoutError: BetterLockWaitTimeoutError,
|
|
49
|
+
ExecutionTimeoutError: BetterLockExecutionTimeoutError,
|
|
50
|
+
QueueOverflowError: BetterLockQueueOverflowError,
|
|
51
|
+
JobAbortedError: BetterLockJobAbortedError,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export default BetterLock;
|