@lakutata/mutex 0.0.0-stage → 3.0.0-beta.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 (88) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +212 -2
  3. package/dist/cjs/exports/Mutex.d.ts +18 -0
  4. package/dist/cjs/exports/Mutex.js +37 -0
  5. package/dist/cjs/mutex/Mutex.d.ts +265 -0
  6. package/dist/cjs/mutex/Mutex.js +403 -0
  7. package/dist/cjs/mutex/MutexLock.d.ts +91 -0
  8. package/dist/cjs/mutex/MutexLock.js +113 -0
  9. package/dist/cjs/mutex/Synchronized.d.ts +90 -0
  10. package/dist/cjs/mutex/Synchronized.js +93 -0
  11. package/dist/cjs/mutex/drivers/FileMutexDriver.d.ts +115 -0
  12. package/dist/cjs/mutex/drivers/FileMutexDriver.js +230 -0
  13. package/dist/cjs/mutex/drivers/MemoryMutexDriver.d.ts +60 -0
  14. package/dist/cjs/mutex/drivers/MemoryMutexDriver.js +95 -0
  15. package/dist/cjs/mutex/drivers/MutexDriver.d.ts +82 -0
  16. package/dist/cjs/mutex/drivers/MutexDriver.js +38 -0
  17. package/dist/cjs/mutex/drivers/MysqlMutexDriver.d.ts +135 -0
  18. package/dist/cjs/mutex/drivers/MysqlMutexDriver.js +244 -0
  19. package/dist/cjs/mutex/drivers/PostgresMutexDriver.d.ts +136 -0
  20. package/dist/cjs/mutex/drivers/PostgresMutexDriver.js +230 -0
  21. package/dist/cjs/mutex/drivers/RedisMutexDriver.d.ts +129 -0
  22. package/dist/cjs/mutex/drivers/RedisMutexDriver.js +180 -0
  23. package/dist/cjs/mutex/exceptions/MutexDriverException.d.ts +12 -0
  24. package/dist/cjs/mutex/exceptions/MutexDriverException.js +19 -0
  25. package/dist/cjs/mutex/exceptions/MutexDriverNotFoundException.d.ts +12 -0
  26. package/dist/cjs/mutex/exceptions/MutexDriverNotFoundException.js +19 -0
  27. package/dist/cjs/mutex/exceptions/MutexLostException.d.ts +13 -0
  28. package/dist/cjs/mutex/exceptions/MutexLostException.js +20 -0
  29. package/dist/cjs/mutex/exceptions/MutexTimeoutException.d.ts +16 -0
  30. package/dist/cjs/mutex/exceptions/MutexTimeoutException.js +23 -0
  31. package/dist/cjs/mutex/lib/LoadDriver.d.ts +6 -0
  32. package/dist/cjs/mutex/lib/LoadDriver.js +64 -0
  33. package/dist/cjs/mutex/lib/Names.d.ts +14 -0
  34. package/dist/cjs/mutex/lib/Names.js +26 -0
  35. package/dist/cjs/mutex/options/FileMutexOptions.d.ts +33 -0
  36. package/dist/cjs/mutex/options/FileMutexOptions.js +45 -0
  37. package/dist/cjs/mutex/options/MemoryMutexOptions.d.ts +27 -0
  38. package/dist/cjs/mutex/options/MemoryMutexOptions.js +41 -0
  39. package/dist/cjs/mutex/options/MysqlMutexOptions.d.ts +70 -0
  40. package/dist/cjs/mutex/options/MysqlMutexOptions.js +77 -0
  41. package/dist/cjs/mutex/options/PostgresMutexOptions.d.ts +83 -0
  42. package/dist/cjs/mutex/options/PostgresMutexOptions.js +89 -0
  43. package/dist/cjs/mutex/options/RedisMutexOptions.d.ts +71 -0
  44. package/dist/cjs/mutex/options/RedisMutexOptions.js +77 -0
  45. package/dist/cjs/package.json +1 -0
  46. package/dist/esm/exports/Mutex.js +18 -0
  47. package/dist/esm/mutex/Mutex.js +397 -0
  48. package/dist/esm/mutex/MutexLock.js +109 -0
  49. package/dist/esm/mutex/Synchronized.js +89 -0
  50. package/dist/esm/mutex/drivers/FileMutexDriver.js +223 -0
  51. package/dist/esm/mutex/drivers/MemoryMutexDriver.js +91 -0
  52. package/dist/esm/mutex/drivers/MutexDriver.js +34 -0
  53. package/dist/esm/mutex/drivers/MysqlMutexDriver.js +240 -0
  54. package/dist/esm/mutex/drivers/PostgresMutexDriver.js +226 -0
  55. package/dist/esm/mutex/drivers/RedisMutexDriver.js +176 -0
  56. package/dist/esm/mutex/exceptions/MutexDriverException.js +15 -0
  57. package/dist/esm/mutex/exceptions/MutexDriverNotFoundException.js +15 -0
  58. package/dist/esm/mutex/exceptions/MutexLostException.js +16 -0
  59. package/dist/esm/mutex/exceptions/MutexTimeoutException.js +19 -0
  60. package/dist/esm/mutex/lib/LoadDriver.js +28 -0
  61. package/dist/esm/mutex/lib/Names.js +21 -0
  62. package/dist/esm/mutex/options/FileMutexOptions.js +41 -0
  63. package/dist/esm/mutex/options/MemoryMutexOptions.js +37 -0
  64. package/dist/esm/mutex/options/MysqlMutexOptions.js +73 -0
  65. package/dist/esm/mutex/options/PostgresMutexOptions.js +85 -0
  66. package/dist/esm/mutex/options/RedisMutexOptions.js +73 -0
  67. package/dist/types/exports/Mutex.d.ts +18 -0
  68. package/dist/types/mutex/Mutex.d.ts +265 -0
  69. package/dist/types/mutex/MutexLock.d.ts +91 -0
  70. package/dist/types/mutex/Synchronized.d.ts +90 -0
  71. package/dist/types/mutex/drivers/FileMutexDriver.d.ts +115 -0
  72. package/dist/types/mutex/drivers/MemoryMutexDriver.d.ts +60 -0
  73. package/dist/types/mutex/drivers/MutexDriver.d.ts +82 -0
  74. package/dist/types/mutex/drivers/MysqlMutexDriver.d.ts +135 -0
  75. package/dist/types/mutex/drivers/PostgresMutexDriver.d.ts +136 -0
  76. package/dist/types/mutex/drivers/RedisMutexDriver.d.ts +129 -0
  77. package/dist/types/mutex/exceptions/MutexDriverException.d.ts +12 -0
  78. package/dist/types/mutex/exceptions/MutexDriverNotFoundException.d.ts +12 -0
  79. package/dist/types/mutex/exceptions/MutexLostException.d.ts +13 -0
  80. package/dist/types/mutex/exceptions/MutexTimeoutException.d.ts +16 -0
  81. package/dist/types/mutex/lib/LoadDriver.d.ts +6 -0
  82. package/dist/types/mutex/lib/Names.d.ts +14 -0
  83. package/dist/types/mutex/options/FileMutexOptions.d.ts +33 -0
  84. package/dist/types/mutex/options/MemoryMutexOptions.d.ts +27 -0
  85. package/dist/types/mutex/options/MysqlMutexOptions.d.ts +70 -0
  86. package/dist/types/mutex/options/PostgresMutexOptions.d.ts +83 -0
  87. package/dist/types/mutex/options/RedisMutexOptions.d.ts +71 -0
  88. package/package.json +57 -4
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Mutex = void 0;
16
+ const node_os_1 = require("node:os");
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const promises_1 = require("node:timers/promises");
19
+ const core_1 = require("@lakutata/core");
20
+ const di_1 = require("@lakutata/core/decorator/di");
21
+ const MutexLock_js_1 = require("./MutexLock.js");
22
+ const MemoryMutexDriver_js_1 = require("./drivers/MemoryMutexDriver.js");
23
+ const FileMutexDriver_js_1 = require("./drivers/FileMutexDriver.js");
24
+ const RedisMutexDriver_js_1 = require("./drivers/RedisMutexDriver.js");
25
+ const PostgresMutexDriver_js_1 = require("./drivers/PostgresMutexDriver.js");
26
+ const MysqlMutexDriver_js_1 = require("./drivers/MysqlMutexDriver.js");
27
+ const MemoryMutexOptions_js_1 = require("./options/MemoryMutexOptions.js");
28
+ const FileMutexOptions_js_1 = require("./options/FileMutexOptions.js");
29
+ const RedisMutexOptions_js_1 = require("./options/RedisMutexOptions.js");
30
+ const PostgresMutexOptions_js_1 = require("./options/PostgresMutexOptions.js");
31
+ const MysqlMutexOptions_js_1 = require("./options/MysqlMutexOptions.js");
32
+ const MutexTimeoutException_js_1 = require("./exceptions/MutexTimeoutException.js");
33
+ const MutexLostException_js_1 = require("./exceptions/MutexLostException.js");
34
+ const MutexDriverException_js_1 = require("./exceptions/MutexDriverException.js");
35
+ /**
36
+ * The time an attempt sent to the store is given to answer, past the deadline of the wait too: a timeout of 0 tries
37
+ * once (a round trip of the store is not cut short), and a store which does not answer still ends the wait
38
+ */
39
+ const ATTEMPT_GRACE = 1000;
40
+ /**
41
+ * The lock component: named locks giving a work to one holder at a time, between the instances and the processes of an
42
+ * application as its driver shares them (see {@link MutexDriverOptions}). Register it in the `components` of an
43
+ * application (`{class: Mutex, driver: {...}}`, named `mutex` for `@Synchronized` and the `exclusive` schedules) and
44
+ * get it with `getObject()` or `@Inject`; it is a singleton. Without a `driver`, the locks are in memory.
45
+ *
46
+ * A lock is acquired with a random token: only its holder ({@link MutexLock}) releases or renews it. The expiring
47
+ * drivers (memory, file, Redis, the `table` mode of PostgreSQL and MySQL) hold it for its `ttl` and the component renews
48
+ * it every third of that time, so a crashed holder keeps it at most one `ttl`. The advisory locks of PostgreSQL and
49
+ * MySQL do not expire: the database releases them when the component's connection closes.
50
+ *
51
+ * A lock is lost when a renewal finds it expired or taken, when a renewal fails (the store unreachable), or when the
52
+ * connection holding an advisory lock closes: its {@link MutexLock.signal} is then aborted with a
53
+ * {@link MutexLostException}, and the work it protects should stop. The locks are not reentrant: acquiring a lock this
54
+ * component already holds waits for its release like any other holder.
55
+ *
56
+ * The driver connects at its first use, not when the component is created: a store which cannot be reached, or whose
57
+ * client package is missing, fails the first acquisition. The component releases the locks it holds and closes the
58
+ * driver when it is destroyed (the application stopping).
59
+ * @example
60
+ * ```typescript
61
+ * import {Application} from 'lakutata'
62
+ * import {Mutex, type MutexLock} from 'lakutata/com/mutex'
63
+ *
64
+ * Application
65
+ * .run(() => ({
66
+ * id: 'mutex.app',
67
+ * name: 'Mutex',
68
+ * components: {
69
+ * //The locks shared by every instance connected to this Redis server
70
+ * mutex: {class: Mutex, driver: {type: 'redis', url: 'redis://127.0.0.1:6379'}, ttl: 30000, timeout: 10000}
71
+ * }
72
+ * }))
73
+ * .onLaunched(async (app: Application): Promise<void> => {
74
+ * const mutex: Mutex = await app.getObject('mutex')
75
+ * //Acquired before the function, released after it, even when it throws
76
+ * const total: number = await mutex.synchronized('report:daily', async (lock: MutexLock): Promise<number> => {
77
+ * lock.signal.throwIfAborted()
78
+ * return 42
79
+ * })
80
+ * console.log(total)
81
+ * })
82
+ * ```
83
+ */
84
+ let Mutex = class Mutex extends core_1.Component {
85
+ #driver;
86
+ #held = new Map();
87
+ /**
88
+ * Create the driver chosen by the `driver` option; it connects at its first use.
89
+ * @throws {Error} When the `table` option of a PostgreSQL or MySQL driver is not a valid table name.
90
+ * @protected
91
+ */
92
+ async init() {
93
+ const options = this.driver;
94
+ switch (options.type) {
95
+ case 'file':
96
+ this.#driver = new FileMutexDriver_js_1.FileMutexDriver(options.directory ?? node_path_1.default.join((0, node_os_1.tmpdir)(), 'lakutata-mutex'), this.prefix);
97
+ break;
98
+ case 'redis':
99
+ this.#driver = new RedisMutexDriver_js_1.RedisMutexDriver(options, this.prefix);
100
+ break;
101
+ case 'postgres':
102
+ this.#driver = new PostgresMutexDriver_js_1.PostgresMutexDriver(options, this.prefix, options.mode, options.table);
103
+ break;
104
+ case 'mysql':
105
+ this.#driver = new MysqlMutexDriver_js_1.MysqlMutexDriver(options, this.prefix, options.mode, options.table);
106
+ break;
107
+ default:
108
+ this.#driver = new MemoryMutexDriver_js_1.MemoryMutexDriver(this.prefix);
109
+ }
110
+ //The locks of a connection closed
111
+ this.#driver.onLost = (names) => {
112
+ for (const lock of [...this.#held.keys()])
113
+ if (names.includes(lock.name))
114
+ this.lost(lock, 'The connection holding the lock is closed');
115
+ };
116
+ }
117
+ /**
118
+ * Release the locks this component holds (their failures ignored), then close the driver's connections.
119
+ * @protected
120
+ */
121
+ async destroy() {
122
+ await Promise.allSettled([...this.#held.keys()].map((lock) => lock.release()));
123
+ await this.#driver?.close();
124
+ }
125
+ /**
126
+ * Mark a lock as lost: stop its renewal, forget it, and abort its signal with a {@link MutexLostException}. The lock
127
+ * is not released in the store: it stays there until it expires (or its connection closes).
128
+ * @param lock The lock lost.
129
+ * @param message Why it is lost, added to the exception's message.
130
+ * @protected
131
+ */
132
+ lost(lock, message) {
133
+ const held = this.#held.get(lock);
134
+ clearTimeout(held?.timer);
135
+ this.#held.delete(lock);
136
+ lock.lose(new MutexLostException_js_1.MutexLostException(`The lock ${lock.name} is lost: ${message}`));
137
+ }
138
+ /**
139
+ * Call the driver, turning its errors into a {@link MutexDriverException} (a {@link MutexDriverNotFoundException} is
140
+ * rethrown as it is).
141
+ * @param call The driver call.
142
+ * @returns What the driver call returns.
143
+ * @throws {MutexDriverException} When the driver call fails.
144
+ * @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
145
+ * @protected
146
+ */
147
+ async call(call) {
148
+ try {
149
+ return await call();
150
+ }
151
+ catch (error) {
152
+ if (error instanceof MutexDriverException_js_1.MutexDriverException || error?.errno === 'E_MUTEX_DRIVER_NOT_FOUND')
153
+ throw error;
154
+ throw new MutexDriverException_js_1.MutexDriverException(`The driver ${this.driver.type} of the locks failed: ${error?.message ?? error}`);
155
+ }
156
+ }
157
+ /**
158
+ * Try to acquire a lock once, without waiting. The lock is renewed automatically while it is held (expiring
159
+ * drivers), until it is released or lost.
160
+ * @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
161
+ * @param options `ttl`: how long the lock stays held without a renewal, in milliseconds (the component's `ttl` by
162
+ * default).
163
+ * @returns The lock, or `undefined` when another holder has it (this component included: the locks are not
164
+ * reentrant).
165
+ * @throws {TypeError} When the name is not a non-empty string.
166
+ * @throws {MutexDriverException} When the store fails (unreachable, a failed query).
167
+ * @throws {MutexDriverNotFoundException} When the driver's client package (`redis`, `pg`, `mysql2`) is not installed.
168
+ * @example
169
+ * ```typescript
170
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
171
+ *
172
+ * declare const mutex: Mutex
173
+ *
174
+ * //A cleanup run by whichever instance gets there first, skipped by the others
175
+ * const lock: MutexLock | undefined = await mutex.tryAcquire('cleanup')
176
+ * if (lock) {
177
+ * try {
178
+ * console.log('cleaning up')
179
+ * } finally {
180
+ * await lock.release()
181
+ * }
182
+ * }
183
+ * ```
184
+ */
185
+ async tryAcquire(name, options = {}) {
186
+ if (typeof name !== 'string' || !name)
187
+ throw new TypeError('The name of a lock is a string which is not empty');
188
+ const ttl = options.ttl ?? this.ttl;
189
+ const startedAt = Date.now();
190
+ const acquired = await this.call(() => this.#driver.acquire(name, ttl));
191
+ if (!acquired)
192
+ return undefined;
193
+ const lock = new MutexLock_js_1.MutexLock(name, acquired.token, acquired.fence, {
194
+ release: async (target) => {
195
+ const held = this.#held.get(target);
196
+ clearTimeout(held?.timer);
197
+ this.#held.delete(target);
198
+ return await this.call(() => this.#driver.release(name, acquired));
199
+ },
200
+ extend: async (target, next) => {
201
+ const held = this.#held.get(target);
202
+ if (!held)
203
+ return false;
204
+ if (next !== undefined)
205
+ held.ttl = next;
206
+ const renewedAt = Date.now();
207
+ const extended = await this.call(() => this.#driver.extend(name, acquired, held.ttl));
208
+ if (!extended) {
209
+ this.lost(target, 'it expired before its renewal');
210
+ return false;
211
+ }
212
+ held.expiresAt = renewedAt + held.ttl;
213
+ //The next renewal follows its new time to live
214
+ this.renew(target);
215
+ return true;
216
+ }
217
+ });
218
+ this.#held.set(lock, { ttl: ttl, lock: acquired, expiresAt: startedAt + ttl });
219
+ this.renew(lock);
220
+ return lock;
221
+ }
222
+ /**
223
+ * Plan the next renewal of a lock of an expiring driver: a third of its time to live (renewed twice before it
224
+ * expires). A renewal failing (the store unreachable) is tried again until the lock expires, then the lock is lost.
225
+ * @param lock The lock held by the component; a lock released or lost is not renewed.
226
+ * @protected
227
+ */
228
+ renew(lock) {
229
+ const held = this.#held.get(lock);
230
+ if (!held || !this.#driver.expiring)
231
+ return;
232
+ clearTimeout(held.timer);
233
+ held.timer = setTimeout(() => {
234
+ lock.extend().catch((error) => {
235
+ if (!this.#held.has(lock))
236
+ return;
237
+ if (Date.now() >= held.expiresAt)
238
+ return this.lost(lock, error.message);
239
+ //Tried again before it expires
240
+ this.renew(lock);
241
+ });
242
+ }, Math.max(10, Math.floor(held.ttl / 3)));
243
+ held.timer.unref();
244
+ }
245
+ /**
246
+ * An attempt of {@link Mutex.acquire}, bounded by its deadline but given at least a second to answer (the round trip
247
+ * of a store is not cut short, a timeout of 0 tries once): an attempt hanging (the store unreachable) fails then, the
248
+ * lock it acquires later released.
249
+ * @param name The lock name.
250
+ * @param options The options of the acquisition (its `ttl`).
251
+ * @param deadline The time the wait ends, in milliseconds since the epoch.
252
+ * @param timeout The timeout of the wait, in milliseconds, for the message of its exception.
253
+ * @returns The lock acquired, or `undefined` when another holder has it.
254
+ * @throws {MutexTimeoutException} When the attempt does not answer before the deadline and its second of grace.
255
+ * @protected
256
+ */
257
+ async attempt(name, options, deadline, timeout) {
258
+ const attempt = this.tryAcquire(name, options);
259
+ let timer;
260
+ //Given the time left, and at least ATTEMPT_GRACE: the deadline ends the wait, not an attempt answering
261
+ const expired = new Promise((resolve) => {
262
+ timer = setTimeout(() => resolve('expired'), Math.max(deadline - Date.now(), ATTEMPT_GRACE));
263
+ });
264
+ try {
265
+ const result = await Promise.race([attempt, expired]);
266
+ if (result !== 'expired')
267
+ return result;
268
+ }
269
+ finally {
270
+ clearTimeout(timer);
271
+ }
272
+ //The store did not answer: a lock it acquires later is released
273
+ attempt.then((late) => late?.release(), () => undefined);
274
+ throw new MutexTimeoutException_js_1.MutexTimeoutException(`The lock ${name} is not acquired in ${timeout} ms`);
275
+ }
276
+ /**
277
+ * Acquire a lock, waiting up to the timeout for its holder to release it (or for it to expire). The store is polled:
278
+ * the first retry comes after `retryDelay` milliseconds, the delay doubling up to one second. The lock is renewed
279
+ * automatically while it is held, until it is released or lost; release it with {@link MutexLock.release}, or
280
+ * declare it with `await using`.
281
+ * @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
282
+ * @param options The lock's `ttl`, the `timeout` of the wait and an abort `signal` (the component's options by
283
+ * default).
284
+ * @returns The lock acquired.
285
+ * @throws {MutexTimeoutException} When the lock is not acquired before the timeout (a `timeout` of `0` after one
286
+ * attempt).
287
+ * @throws {TypeError} When the name is not a non-empty string.
288
+ * @throws {MutexDriverException} When the store fails; the wait stops at the first failure.
289
+ * @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
290
+ * @throws The signal's reason (an `AbortError` `DOMException` by default) when the signal is aborted before the lock
291
+ * is acquired.
292
+ * @example
293
+ * ```typescript
294
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
295
+ *
296
+ * declare const mutex: Mutex
297
+ *
298
+ * //Waits up to 3 seconds, then rejects with MutexTimeoutException
299
+ * const lock: MutexLock = await mutex.acquire('order:42', {timeout: 3000})
300
+ * try {
301
+ * console.log(`order 42 is ours, fencing token ${lock.fence}`)
302
+ * } finally {
303
+ * await lock.release()
304
+ * }
305
+ *
306
+ * //Released at the end of the block, even when it throws
307
+ * {
308
+ * await using scoped: MutexLock = await mutex.acquire('order:43', {signal: AbortSignal.timeout(5000)})
309
+ * console.log(scoped.name)
310
+ * }
311
+ * ```
312
+ */
313
+ async acquire(name, options = {}) {
314
+ const timeout = options.timeout ?? this.timeout;
315
+ const deadline = Date.now() + timeout;
316
+ let delay = this.retryDelay;
317
+ for (;;) {
318
+ options.signal?.throwIfAborted();
319
+ const lock = await this.attempt(name, options, deadline, timeout);
320
+ if (lock)
321
+ return lock;
322
+ const remaining = deadline - Date.now();
323
+ if (remaining <= 0)
324
+ throw new MutexTimeoutException_js_1.MutexTimeoutException(`The lock ${name} is not acquired in ${timeout} ms`);
325
+ //Backed off up to a second, spread between the waiters
326
+ try {
327
+ await (0, promises_1.setTimeout)(Math.min(remaining, delay * (0.5 + Math.random() / 2)), undefined, { signal: options.signal });
328
+ }
329
+ catch (error) {
330
+ //The reason of its abort
331
+ throw options.signal?.aborted ? options.signal.reason : error;
332
+ }
333
+ delay = Math.min(delay * 2, 1000);
334
+ }
335
+ }
336
+ /**
337
+ * Run a function holding a lock: acquire it as {@link Mutex.acquire} does, run the function, then release the lock
338
+ * whether the function returned or threw (a failure of the release is ignored). When the lock is lost while the
339
+ * function runs, the function is not interrupted: its `lock.signal` is aborted, and it should check it.
340
+ * @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
341
+ * @param fn The function to run, given the lock.
342
+ * @param options The lock's `ttl`, the `timeout` of the wait and an abort `signal` (the component's options by
343
+ * default).
344
+ * @returns What the function returns (awaited).
345
+ * @throws {MutexTimeoutException} When the lock is not acquired before the timeout; the function does not run.
346
+ * @throws {MutexDriverException} When the store fails while acquiring the lock.
347
+ * @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
348
+ * @throws The function's error, after the lock is released.
349
+ * @example
350
+ * ```typescript
351
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
352
+ *
353
+ * declare const mutex: Mutex
354
+ * declare function chargeOrder(id: number, signal: AbortSignal): Promise<string>
355
+ *
356
+ * //One payment of order 42 at a time, in every instance sharing the store
357
+ * const receipt: string = await mutex.synchronized('order:42', (lock: MutexLock): Promise<string> => {
358
+ * //Aborted with a MutexLostException when the lock is lost
359
+ * return chargeOrder(42, lock.signal)
360
+ * }, {timeout: 5000})
361
+ * console.log(receipt)
362
+ * ```
363
+ */
364
+ async synchronized(name, fn, options = {}) {
365
+ const lock = await this.acquire(name, options);
366
+ try {
367
+ return await fn(lock);
368
+ }
369
+ finally {
370
+ await lock.release().catch(() => undefined);
371
+ }
372
+ }
373
+ /**
374
+ * The locks this component holds now (not released, not lost), in their acquisition order; a new array on each read.
375
+ */
376
+ get heldLocks() {
377
+ return [...this.#held.keys()];
378
+ }
379
+ };
380
+ exports.Mutex = Mutex;
381
+ __decorate([
382
+ (0, di_1.Configurable)(core_1.DTO.Alternatives(MemoryMutexOptions_js_1.MemoryMutexOptions.required(), FileMutexOptions_js_1.FileMutexOptions.required(), RedisMutexOptions_js_1.RedisMutexOptions.required(), PostgresMutexOptions_js_1.PostgresMutexOptions.required(), MysqlMutexOptions_js_1.MysqlMutexOptions.required()).optional().default({ type: 'memory' })),
383
+ __metadata("design:type", Object)
384
+ ], Mutex.prototype, "driver", void 0);
385
+ __decorate([
386
+ (0, di_1.Configurable)(core_1.DTO.String().allow('').optional().default('lakutata:mutex:')),
387
+ __metadata("design:type", String)
388
+ ], Mutex.prototype, "prefix", void 0);
389
+ __decorate([
390
+ (0, di_1.Configurable)(core_1.DTO.Number().integer().positive().optional().default(30000)),
391
+ __metadata("design:type", Number)
392
+ ], Mutex.prototype, "ttl", void 0);
393
+ __decorate([
394
+ (0, di_1.Configurable)(core_1.DTO.Number().min(0).optional().default(10000)),
395
+ __metadata("design:type", Number)
396
+ ], Mutex.prototype, "timeout", void 0);
397
+ __decorate([
398
+ (0, di_1.Configurable)(core_1.DTO.Number().integer().positive().optional().default(50)),
399
+ __metadata("design:type", Number)
400
+ ], Mutex.prototype, "retryDelay", void 0);
401
+ exports.Mutex = Mutex = __decorate([
402
+ (0, di_1.Singleton)()
403
+ ], Mutex);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A lock held, returned by {@link Mutex.acquire} and {@link Mutex.tryAcquire} (and given to the function of
3
+ * {@link Mutex.synchronized}); the component creates it. It stays held until it is released (once: the next releases do
4
+ * nothing) or lost; the component renews it in the store meanwhile. Declared with `await using`, it is released at the
5
+ * end of its scope.
6
+ *
7
+ * Its {@link MutexLock.signal} is aborted when it is lost: a renewal found it expired or taken by another holder, a
8
+ * renewal failed, or the connection holding it closed. A lost lock is not released in the store: another holder may get
9
+ * it once it expires. The work it protects should stop then, and the writes it makes may carry its
10
+ * {@link MutexLock.fence} so that the store refuses the writes of an older holder.
11
+ * @example
12
+ * ```typescript
13
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
14
+ *
15
+ * declare const mutex: Mutex
16
+ * declare function writeReport(fence: number | undefined, signal: AbortSignal): Promise<void>
17
+ *
18
+ * await using lock: MutexLock = await mutex.acquire('report:daily')
19
+ * lock.signal.addEventListener('abort', (): void => console.warn(`lost: ${lock.signal.reason}`))
20
+ * await writeReport(lock.fence, lock.signal)
21
+ * console.log(lock.held, Date.now() - lock.acquiredAt.getTime())
22
+ * ```
23
+ */
24
+ export declare class MutexLock implements AsyncDisposable {
25
+ #private;
26
+ readonly name: string;
27
+ readonly token: string;
28
+ readonly fence: number | undefined;
29
+ protected readonly actions: {
30
+ release: (lock: MutexLock) => Promise<boolean>;
31
+ extend: (lock: MutexLock, ttl?: number) => Promise<boolean>;
32
+ };
33
+ /**
34
+ * When the lock was acquired (the time this object was created, once the store granted it).
35
+ */
36
+ readonly acquiredAt: Date;
37
+ /**
38
+ * Create a held lock (the component does it, when the store grants a lock).
39
+ * @param name The lock name, without the component's prefix.
40
+ * @param token The random token identifying this holder in the store.
41
+ * @param fence The fencing token given by the store, or `undefined` when the driver gives none.
42
+ * @param actions How the component releases and renews the lock.
43
+ */
44
+ constructor(name: string, token: string, fence: number | undefined, actions: {
45
+ release: (lock: MutexLock) => Promise<boolean>;
46
+ extend: (lock: MutexLock, ttl?: number) => Promise<boolean>;
47
+ });
48
+ /**
49
+ * Whether the lock is still held: `false` once it is released or lost. It reflects what the component knows; an
50
+ * expiring lock whose renewal is late may already be expired in the store.
51
+ */
52
+ get held(): boolean;
53
+ /**
54
+ * Aborted when the lock is lost, with a {@link MutexLostException} as its reason: a renewal found it expired or taken,
55
+ * a renewal failed, or the connection holding it closed. The work the lock protects should stop then. It is not
56
+ * aborted by a release.
57
+ */
58
+ get signal(): AbortSignal;
59
+ /**
60
+ * Release the lock in the store and stop its renewal. Only the first call releases it; the next ones, and the calls
61
+ * on a lost lock, return `false` without reaching the store. The lock no longer counts as held even when the store
62
+ * fails, in which case it stays in the store until it expires.
63
+ * @returns Whether the store still had the lock for this holder (`false` when it had expired, been taken, or its
64
+ * connection had closed).
65
+ * @throws {MutexDriverException} When the store fails.
66
+ */
67
+ release(): Promise<boolean>;
68
+ /**
69
+ * Renew the lock now, for `ttl` milliseconds from now; the component already renews it automatically every third of
70
+ * its `ttl`. A `ttl` given here is used by the next automatic renewals too, their interval following it. For the
71
+ * advisory locks (which do not expire), it checks that the lock is still held by a query on its connection.
72
+ * @param ttl How long the lock stays held without a renewal, in milliseconds; the lock's last `ttl` (the one of its
73
+ * acquisition) by default.
74
+ * @returns Whether the lock is still held; when the store no longer has it, the lock is marked lost (its signal
75
+ * aborted) and `false` is returned.
76
+ * @throws {MutexDriverException} When the store fails (the lock is not marked lost then).
77
+ */
78
+ extend(ttl?: number): Promise<boolean>;
79
+ /**
80
+ * Mark the lock as lost and abort its signal with the reason; the component calls it when a renewal fails or the
81
+ * connection holding the lock closes. It does nothing on a lock no longer held, and it does not release the lock in
82
+ * the store nor stop its renewal: the applications do not call it.
83
+ * @param reason The signal's abort reason, a {@link MutexLostException} from the component.
84
+ */
85
+ lose(reason: Error): void;
86
+ /**
87
+ * Release the lock at the end of an `await using` scope, as {@link MutexLock.release} does.
88
+ * @throws {MutexDriverException} When the store fails.
89
+ */
90
+ [Symbol.asyncDispose](): Promise<void>;
91
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MutexLock = void 0;
4
+ /**
5
+ * A lock held, returned by {@link Mutex.acquire} and {@link Mutex.tryAcquire} (and given to the function of
6
+ * {@link Mutex.synchronized}); the component creates it. It stays held until it is released (once: the next releases do
7
+ * nothing) or lost; the component renews it in the store meanwhile. Declared with `await using`, it is released at the
8
+ * end of its scope.
9
+ *
10
+ * Its {@link MutexLock.signal} is aborted when it is lost: a renewal found it expired or taken by another holder, a
11
+ * renewal failed, or the connection holding it closed. A lost lock is not released in the store: another holder may get
12
+ * it once it expires. The work it protects should stop then, and the writes it makes may carry its
13
+ * {@link MutexLock.fence} so that the store refuses the writes of an older holder.
14
+ * @example
15
+ * ```typescript
16
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
17
+ *
18
+ * declare const mutex: Mutex
19
+ * declare function writeReport(fence: number | undefined, signal: AbortSignal): Promise<void>
20
+ *
21
+ * await using lock: MutexLock = await mutex.acquire('report:daily')
22
+ * lock.signal.addEventListener('abort', (): void => console.warn(`lost: ${lock.signal.reason}`))
23
+ * await writeReport(lock.fence, lock.signal)
24
+ * console.log(lock.held, Date.now() - lock.acquiredAt.getTime())
25
+ * ```
26
+ */
27
+ class MutexLock {
28
+ #abort;
29
+ #held;
30
+ /**
31
+ * Create a held lock (the component does it, when the store grants a lock).
32
+ * @param name The lock name, without the component's prefix.
33
+ * @param token The random token identifying this holder in the store.
34
+ * @param fence The fencing token given by the store, or `undefined` when the driver gives none.
35
+ * @param actions How the component releases and renews the lock.
36
+ */
37
+ constructor(name, token, fence, actions) {
38
+ this.name = name;
39
+ this.token = token;
40
+ this.fence = fence;
41
+ this.actions = actions;
42
+ this.#abort = new AbortController();
43
+ this.#held = true;
44
+ /**
45
+ * When the lock was acquired (the time this object was created, once the store granted it).
46
+ */
47
+ this.acquiredAt = new Date();
48
+ }
49
+ /**
50
+ * Whether the lock is still held: `false` once it is released or lost. It reflects what the component knows; an
51
+ * expiring lock whose renewal is late may already be expired in the store.
52
+ */
53
+ get held() {
54
+ return this.#held;
55
+ }
56
+ /**
57
+ * Aborted when the lock is lost, with a {@link MutexLostException} as its reason: a renewal found it expired or taken,
58
+ * a renewal failed, or the connection holding it closed. The work the lock protects should stop then. It is not
59
+ * aborted by a release.
60
+ */
61
+ get signal() {
62
+ return this.#abort.signal;
63
+ }
64
+ /**
65
+ * Release the lock in the store and stop its renewal. Only the first call releases it; the next ones, and the calls
66
+ * on a lost lock, return `false` without reaching the store. The lock no longer counts as held even when the store
67
+ * fails, in which case it stays in the store until it expires.
68
+ * @returns Whether the store still had the lock for this holder (`false` when it had expired, been taken, or its
69
+ * connection had closed).
70
+ * @throws {MutexDriverException} When the store fails.
71
+ */
72
+ async release() {
73
+ if (!this.#held)
74
+ return false;
75
+ this.#held = false;
76
+ return await this.actions.release(this);
77
+ }
78
+ /**
79
+ * Renew the lock now, for `ttl` milliseconds from now; the component already renews it automatically every third of
80
+ * its `ttl`. A `ttl` given here is used by the next automatic renewals too, their interval following it. For the
81
+ * advisory locks (which do not expire), it checks that the lock is still held by a query on its connection.
82
+ * @param ttl How long the lock stays held without a renewal, in milliseconds; the lock's last `ttl` (the one of its
83
+ * acquisition) by default.
84
+ * @returns Whether the lock is still held; when the store no longer has it, the lock is marked lost (its signal
85
+ * aborted) and `false` is returned.
86
+ * @throws {MutexDriverException} When the store fails (the lock is not marked lost then).
87
+ */
88
+ async extend(ttl) {
89
+ if (!this.#held)
90
+ return false;
91
+ return await this.actions.extend(this, ttl);
92
+ }
93
+ /**
94
+ * Mark the lock as lost and abort its signal with the reason; the component calls it when a renewal fails or the
95
+ * connection holding the lock closes. It does nothing on a lock no longer held, and it does not release the lock in
96
+ * the store nor stop its renewal: the applications do not call it.
97
+ * @param reason The signal's abort reason, a {@link MutexLostException} from the component.
98
+ */
99
+ lose(reason) {
100
+ if (!this.#held)
101
+ return;
102
+ this.#held = false;
103
+ this.#abort.abort(reason);
104
+ }
105
+ /**
106
+ * Release the lock at the end of an `await using` scope, as {@link MutexLock.release} does.
107
+ * @throws {MutexDriverException} When the store fails.
108
+ */
109
+ async [Symbol.asyncDispose]() {
110
+ await this.release();
111
+ }
112
+ }
113
+ exports.MutexLock = MutexLock;