@lakutata/mutex 0.0.0-stage → 3.0.0-beta.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.
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,265 @@
1
+ import { Component } from '@lakutata/core';
2
+ import { MutexLock } from './MutexLock.js';
3
+ import { MemoryMutexOptions } from './options/MemoryMutexOptions.js';
4
+ import { FileMutexOptions } from './options/FileMutexOptions.js';
5
+ import { RedisMutexOptions } from './options/RedisMutexOptions.js';
6
+ import { PostgresMutexOptions } from './options/PostgresMutexOptions.js';
7
+ import { MysqlMutexOptions } from './options/MysqlMutexOptions.js';
8
+ /**
9
+ * The `driver` option of a {@link Mutex}: the store holding the locks, told apart by `type`. The store decides who
10
+ * shares the locks: `'memory'` the components of one process, `'file'` the processes of one host, `'redis'`,
11
+ * `'postgres'` and `'mysql'` every instance connected to the same server.
12
+ */
13
+ export type MutexDriverOptions = MemoryMutexOptions | FileMutexOptions | RedisMutexOptions | PostgresMutexOptions | MysqlMutexOptions;
14
+ /**
15
+ * The options of {@link Mutex.acquire} and {@link Mutex.synchronized} ({@link Mutex.tryAcquire} only reads `ttl`).
16
+ */
17
+ export type MutexAcquireOptions = {
18
+ /**
19
+ * How long the lock stays held without a renewal, in milliseconds. The component renews it every third of this time
20
+ * while it is held, so it only expires when its holder is gone or cannot reach the store. Ignored by the PostgreSQL
21
+ * and MySQL advisory locks, which end with their connection instead.
22
+ * @default the component's `ttl` option (30000)
23
+ */
24
+ ttl?: number;
25
+ /**
26
+ * How long to wait for the lock, in milliseconds: `0` tries once, `Infinity` waits until it is acquired. When the
27
+ * time is up, the call rejects with {@link MutexTimeoutException}. An attempt sent to the store is given at least a
28
+ * second to answer, even past the time: a short timeout (`0` included) is not cut short by the round trip of a store
29
+ * (a file, a network), while a store which does not answer still ends the wait.
30
+ * @default the component's `timeout` option (10000)
31
+ */
32
+ timeout?: number;
33
+ /**
34
+ * Stops the wait when aborted: the call rejects with the signal's reason. An attempt already sent to the store is not
35
+ * cancelled, and a lock it acquires is returned.
36
+ * @default none (the wait only ends with the lock or the timeout)
37
+ */
38
+ signal?: AbortSignal;
39
+ };
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
+ export declare class Mutex extends Component {
85
+ #private;
86
+ /**
87
+ * The store holding the locks, told apart by `type`: `'memory'` (the components of one process), `'file'` (the
88
+ * processes of one host), `'redis'`, `'postgres'` or `'mysql'` (every instance connected to the server).
89
+ * @default {type: 'memory'}
90
+ */
91
+ protected readonly driver: MutexDriverOptions;
92
+ /**
93
+ * The prefix added to the lock names in the store, which keeps apart the applications sharing a store. Two
94
+ * components with the same driver and prefix share their locks.
95
+ * @default 'lakutata:mutex:'
96
+ */
97
+ protected readonly prefix: string;
98
+ /**
99
+ * How long a lock stays held without a renewal, in milliseconds (a positive integer); the default of
100
+ * {@link MutexAcquireOptions.ttl}.
101
+ * @default 30000
102
+ */
103
+ protected readonly ttl: number;
104
+ /**
105
+ * How long {@link Mutex.acquire} waits for a lock, in milliseconds (`0` tries once, `Infinity` waits without limit);
106
+ * the default of {@link MutexAcquireOptions.timeout}.
107
+ * @default 10000
108
+ */
109
+ protected readonly timeout: number;
110
+ /**
111
+ * The delay before the second attempt to acquire a lock, in milliseconds. It doubles after each attempt up to one
112
+ * second, each delay randomized between its half and its whole to spread the waiters.
113
+ * @default 50
114
+ */
115
+ protected readonly retryDelay: number;
116
+ /**
117
+ * Create the driver chosen by the `driver` option; it connects at its first use.
118
+ * @throws {Error} When the `table` option of a PostgreSQL or MySQL driver is not a valid table name.
119
+ * @protected
120
+ */
121
+ protected init(): Promise<void>;
122
+ /**
123
+ * Release the locks this component holds (their failures ignored), then close the driver's connections.
124
+ * @protected
125
+ */
126
+ protected destroy(): Promise<void>;
127
+ /**
128
+ * Mark a lock as lost: stop its renewal, forget it, and abort its signal with a {@link MutexLostException}. The lock
129
+ * is not released in the store: it stays there until it expires (or its connection closes).
130
+ * @param lock The lock lost.
131
+ * @param message Why it is lost, added to the exception's message.
132
+ * @protected
133
+ */
134
+ protected lost(lock: MutexLock, message: string): void;
135
+ /**
136
+ * Call the driver, turning its errors into a {@link MutexDriverException} (a {@link MutexDriverNotFoundException} is
137
+ * rethrown as it is).
138
+ * @param call The driver call.
139
+ * @returns What the driver call returns.
140
+ * @throws {MutexDriverException} When the driver call fails.
141
+ * @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
142
+ * @protected
143
+ */
144
+ protected call<T>(call: () => Promise<T>): Promise<T>;
145
+ /**
146
+ * Try to acquire a lock once, without waiting. The lock is renewed automatically while it is held (expiring
147
+ * drivers), until it is released or lost.
148
+ * @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
149
+ * @param options `ttl`: how long the lock stays held without a renewal, in milliseconds (the component's `ttl` by
150
+ * default).
151
+ * @returns The lock, or `undefined` when another holder has it (this component included: the locks are not
152
+ * reentrant).
153
+ * @throws {TypeError} When the name is not a non-empty string.
154
+ * @throws {MutexDriverException} When the store fails (unreachable, a failed query).
155
+ * @throws {MutexDriverNotFoundException} When the driver's client package (`redis`, `pg`, `mysql2`) is not installed.
156
+ * @example
157
+ * ```typescript
158
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
159
+ *
160
+ * declare const mutex: Mutex
161
+ *
162
+ * //A cleanup run by whichever instance gets there first, skipped by the others
163
+ * const lock: MutexLock | undefined = await mutex.tryAcquire('cleanup')
164
+ * if (lock) {
165
+ * try {
166
+ * console.log('cleaning up')
167
+ * } finally {
168
+ * await lock.release()
169
+ * }
170
+ * }
171
+ * ```
172
+ */
173
+ tryAcquire(name: string, options?: Pick<MutexAcquireOptions, 'ttl'>): Promise<MutexLock | undefined>;
174
+ /**
175
+ * Plan the next renewal of a lock of an expiring driver: a third of its time to live (renewed twice before it
176
+ * expires). A renewal failing (the store unreachable) is tried again until the lock expires, then the lock is lost.
177
+ * @param lock The lock held by the component; a lock released or lost is not renewed.
178
+ * @protected
179
+ */
180
+ protected renew(lock: MutexLock): void;
181
+ /**
182
+ * An attempt of {@link Mutex.acquire}, bounded by its deadline but given at least a second to answer (the round trip
183
+ * of a store is not cut short, a timeout of 0 tries once): an attempt hanging (the store unreachable) fails then, the
184
+ * lock it acquires later released.
185
+ * @param name The lock name.
186
+ * @param options The options of the acquisition (its `ttl`).
187
+ * @param deadline The time the wait ends, in milliseconds since the epoch.
188
+ * @param timeout The timeout of the wait, in milliseconds, for the message of its exception.
189
+ * @returns The lock acquired, or `undefined` when another holder has it.
190
+ * @throws {MutexTimeoutException} When the attempt does not answer before the deadline and its second of grace.
191
+ * @protected
192
+ */
193
+ protected attempt(name: string, options: MutexAcquireOptions, deadline: number, timeout: number): Promise<MutexLock | undefined>;
194
+ /**
195
+ * Acquire a lock, waiting up to the timeout for its holder to release it (or for it to expire). The store is polled:
196
+ * the first retry comes after `retryDelay` milliseconds, the delay doubling up to one second. The lock is renewed
197
+ * automatically while it is held, until it is released or lost; release it with {@link MutexLock.release}, or
198
+ * declare it with `await using`.
199
+ * @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
200
+ * @param options The lock's `ttl`, the `timeout` of the wait and an abort `signal` (the component's options by
201
+ * default).
202
+ * @returns The lock acquired.
203
+ * @throws {MutexTimeoutException} When the lock is not acquired before the timeout (a `timeout` of `0` after one
204
+ * attempt).
205
+ * @throws {TypeError} When the name is not a non-empty string.
206
+ * @throws {MutexDriverException} When the store fails; the wait stops at the first failure.
207
+ * @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
208
+ * @throws The signal's reason (an `AbortError` `DOMException` by default) when the signal is aborted before the lock
209
+ * is acquired.
210
+ * @example
211
+ * ```typescript
212
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
213
+ *
214
+ * declare const mutex: Mutex
215
+ *
216
+ * //Waits up to 3 seconds, then rejects with MutexTimeoutException
217
+ * const lock: MutexLock = await mutex.acquire('order:42', {timeout: 3000})
218
+ * try {
219
+ * console.log(`order 42 is ours, fencing token ${lock.fence}`)
220
+ * } finally {
221
+ * await lock.release()
222
+ * }
223
+ *
224
+ * //Released at the end of the block, even when it throws
225
+ * {
226
+ * await using scoped: MutexLock = await mutex.acquire('order:43', {signal: AbortSignal.timeout(5000)})
227
+ * console.log(scoped.name)
228
+ * }
229
+ * ```
230
+ */
231
+ acquire(name: string, options?: MutexAcquireOptions): Promise<MutexLock>;
232
+ /**
233
+ * Run a function holding a lock: acquire it as {@link Mutex.acquire} does, run the function, then release the lock
234
+ * whether the function returned or threw (a failure of the release is ignored). When the lock is lost while the
235
+ * function runs, the function is not interrupted: its `lock.signal` is aborted, and it should check it.
236
+ * @param name The lock name, a non-empty string; the component's `prefix` is added to it in the store.
237
+ * @param fn The function to run, given the lock.
238
+ * @param options The lock's `ttl`, the `timeout` of the wait and an abort `signal` (the component's options by
239
+ * default).
240
+ * @returns What the function returns (awaited).
241
+ * @throws {MutexTimeoutException} When the lock is not acquired before the timeout; the function does not run.
242
+ * @throws {MutexDriverException} When the store fails while acquiring the lock.
243
+ * @throws {MutexDriverNotFoundException} When the driver's client package is not installed.
244
+ * @throws The function's error, after the lock is released.
245
+ * @example
246
+ * ```typescript
247
+ * import type {Mutex, MutexLock} from 'lakutata/com/mutex'
248
+ *
249
+ * declare const mutex: Mutex
250
+ * declare function chargeOrder(id: number, signal: AbortSignal): Promise<string>
251
+ *
252
+ * //One payment of order 42 at a time, in every instance sharing the store
253
+ * const receipt: string = await mutex.synchronized('order:42', (lock: MutexLock): Promise<string> => {
254
+ * //Aborted with a MutexLostException when the lock is lost
255
+ * return chargeOrder(42, lock.signal)
256
+ * }, {timeout: 5000})
257
+ * console.log(receipt)
258
+ * ```
259
+ */
260
+ synchronized<T>(name: string, fn: (lock: MutexLock) => T | Promise<T>, options?: MutexAcquireOptions): Promise<T>;
261
+ /**
262
+ * The locks this component holds now (not released, not lost), in their acquisition order; a new array on each read.
263
+ */
264
+ get heldLocks(): MutexLock[];
265
+ }
@@ -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,90 @@
1
+ /**
2
+ * The options of {@link Synchronized}: the lock component used, and how the lock is waited for and held.
3
+ */
4
+ export type SynchronizedOptions = {
5
+ /**
6
+ * The name under which the {@link Mutex} component is registered, resolved from the controller's container.
7
+ * @default 'mutex'
8
+ */
9
+ mutex?: string;
10
+ /**
11
+ * How long the request waits for the lock, in milliseconds, before failing with a {@link MutexTimeoutException}.
12
+ * Ignored when `wait` is `false`.
13
+ * @default the component's `timeout` option (10000)
14
+ */
15
+ timeout?: number;
16
+ /**
17
+ * Whether to wait for the lock; `false` fails the request at once with a {@link MutexTimeoutException} (HTTP 409)
18
+ * when another request holds the lock.
19
+ * @default true
20
+ */
21
+ wait?: boolean;
22
+ /**
23
+ * How long the lock stays held without a renewal, in milliseconds (renewed automatically while the action runs).
24
+ * @default the component's `ttl` option (30000)
25
+ */
26
+ ttl?: number;
27
+ };
28
+ /**
29
+ * Build a lock name from a template, replacing each `{field}` with that field of the input (`{a.b}` reads a nested
30
+ * field). {@link Synchronized} uses it for its template names; a template without `{}` is returned as it is.
31
+ * @param template The template, such as `'order:{id}'` or `'user:{user.id}:export'`.
32
+ * @param input The object whose fields fill the template (an action's validated input).
33
+ * @returns The lock name.
34
+ * @throws {TypeError} When a field of the template is missing, `null`, or an object (only strings, numbers and other
35
+ * primitives are accepted).
36
+ * @example
37
+ * ```typescript
38
+ * import {lockNameOf} from 'lakutata/com/mutex'
39
+ *
40
+ * console.log(lockNameOf('order:{id}', {id: 42})) //order:42
41
+ * console.log(lockNameOf('user:{user.id}:export', {user: {id: 'u7'}})) //user:u7:export
42
+ * ```
43
+ */
44
+ export declare function lockNameOf(template: string, input: unknown): string;
45
+ /**
46
+ * Run a controller action holding a lock, acquired once its input is validated and its access control passed, released
47
+ * after it (whether it returns or throws). The requests needing the same lock run one at a time in every instance
48
+ * sharing the lock store, whatever their transport (HTTP, CLI, service); the others run in parallel.
49
+ *
50
+ * The lock comes from the {@link Mutex} component registered as `mutex` (or `options.mutex`); a missing component
51
+ * fails the request. The wait for the lock stops when the request is aborted (its client gone), and a request not
52
+ * getting the lock in time fails with a {@link MutexTimeoutException} (HTTP 409). The action is not interrupted when
53
+ * its lock is lost: check the controller's `this.signal` for a long work.
54
+ * @param name The lock name: a template filled from the fields of the action's input (`'order:{id}'`, see
55
+ * {@link lockNameOf}; a missing field fails the request with a `TypeError`), or a function of the input and the
56
+ * request's context returning it.
57
+ * @param options The lock component, the wait and the lock's `ttl`.
58
+ * @returns The method decorator.
59
+ * @throws {TypeError} When the class is declared, if the name is neither a non-empty string nor a function, or if the
60
+ * decorated member is not a method.
61
+ * @throws {MutexTimeoutException} From the action, when the lock is not acquired in time (HTTP 409).
62
+ * @throws {MutexDriverException} From the action, when the lock store fails.
63
+ * @example
64
+ * ```typescript
65
+ * import {Controller} from 'lakutata/com/entrypoint'
66
+ * import {ServiceAction} from 'lakutata/decorator/ctrl'
67
+ * import {Synchronized} from 'lakutata/com/mutex'
68
+ *
69
+ * class OrderController extends Controller {
70
+ * //The payments of an order one at a time, the other orders in parallel
71
+ * @ServiceAction({cmd: 'order.pay'})
72
+ * @Synchronized('order:{id}')
73
+ * public async pay(input: {id: number}): Promise<void> {
74
+ * }
75
+ *
76
+ * //Fails at once (HTTP 409) while another request rebuilds the index
77
+ * @ServiceAction({cmd: 'search.reindex'})
78
+ * @Synchronized('search:reindex', {wait: false})
79
+ * public async reindex(): Promise<void> {
80
+ * }
81
+ *
82
+ * //A lock named by a function, from the component registered as "locks"
83
+ * @ServiceAction({cmd: 'user.export'})
84
+ * @Synchronized((input: {userId: string}): string => `export:${input.userId}`, {timeout: 5000, mutex: 'locks'})
85
+ * public async export(input: {userId: string}): Promise<void> {
86
+ * }
87
+ * }
88
+ * ```
89
+ */
90
+ export declare function Synchronized(name: string | ((input: any, context: any) => string), options?: SynchronizedOptions): (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
@@ -0,0 +1,115 @@
1
+ import { MutexDriver, type DriverLock } from './MutexDriver.js';
2
+ /**
3
+ * The content of a lock file: its holder (its token, its process and its host), its expiration
4
+ */
5
+ type LockFile = {
6
+ name: string;
7
+ token: string;
8
+ pid: number;
9
+ host: string;
10
+ expires: number;
11
+ };
12
+ /**
13
+ * The file driver (`{type: 'file'}`): each lock is a file of a directory, created exclusively, shared by the processes of
14
+ * one host using the same directory and prefix (several workers or applications of a machine). It needs no package.
15
+ * The processes must share the directory: the default one (`lakutata-mutex` in the operating system's temporary
16
+ * directory) can differ between users. A directory shared by several hosts (a network file system) works only as far
17
+ * as its exclusive creation and renames are atomic, and only the expiration then frees the lock of a crashed host.
18
+ *
19
+ * The locks expire after their `ttl` unless renewed; a lock whose process is gone on this host is taken over at once,
20
+ * without waiting for its expiration. The driver gives no fencing token (`fence` is `undefined`).
21
+ * @example
22
+ * ```typescript
23
+ * import {FileMutexDriver, type DriverLock} from 'lakutata/com/mutex'
24
+ *
25
+ * //The workers of this machine run the migration one at a time
26
+ * const driver: FileMutexDriver = new FileMutexDriver('/var/lock/my-app', 'my-app:')
27
+ * const lock: DriverLock | undefined = await driver.acquire('migrate', 60000)
28
+ * if (lock) {
29
+ * try {
30
+ * console.log('migrating')
31
+ * } finally {
32
+ * await driver.release('migrate', lock)
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ export declare class FileMutexDriver extends MutexDriver {
38
+ #private;
39
+ protected readonly directory: string;
40
+ protected readonly prefix: string;
41
+ /**
42
+ * `true`: the locks expire after their `ttl` unless renewed.
43
+ */
44
+ readonly expiring: boolean;
45
+ /**
46
+ * Create a driver on a directory, created at the first acquisition when it does not exist.
47
+ * @param directory The directory of the lock files.
48
+ * @param prefix The prefix added to the lock names; `''` by default.
49
+ */
50
+ constructor(directory: string, prefix?: string);
51
+ /**
52
+ * The path of a lock's file: the SHA-256 digest of the prefixed name, with the extension `.lock`.
53
+ * @param name The lock name.
54
+ * @returns The file path.
55
+ */
56
+ protected file(name: string): string;
57
+ /**
58
+ * Read a lock file.
59
+ * @param file The file path.
60
+ * @returns Its content, or `undefined` when it does not exist or cannot be parsed (being written).
61
+ */
62
+ protected read(file: string): Promise<LockFile | undefined>;
63
+ /**
64
+ * Whether a lock file is no longer held: expired, or written by another process of this host which no longer runs.
65
+ * @param lock The content of the lock file.
66
+ * @returns Whether it may be taken over.
67
+ */
68
+ protected stale(lock: LockFile): boolean;
69
+ /**
70
+ * Whether a lock file which cannot be read was abandoned (its process ended while writing it) rather than being
71
+ * written: unchanged for more than 5 seconds.
72
+ * @param file The file path.
73
+ * @returns Whether it may be taken over.
74
+ */
75
+ protected abandoned(file: string): Promise<boolean>;
76
+ /**
77
+ * Remove a lock file when it is still the one read, by renaming it first so that only one process takes it. A file
78
+ * of another holder taken by mistake is put back when its place is still free.
79
+ * @param file The file path.
80
+ * @param token The token read in the file, or `undefined` for a file which could not be read.
81
+ * @returns Whether the file was removed.
82
+ */
83
+ protected take(file: string, token: string | undefined): Promise<boolean>;
84
+ /**
85
+ * Try to acquire a lock once: create its file exclusively, or take over a file expired, abandoned, or whose process
86
+ * of this host is gone.
87
+ * @param name The lock name.
88
+ * @param ttl How long the lock stays held without a renewal, in milliseconds.
89
+ * @returns The lock (without fencing token), or `undefined` when another holder has it.
90
+ * @throws {Error} When the directory cannot be created or the file written (permissions, disk full).
91
+ */
92
+ acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
93
+ /**
94
+ * Release a lock by removing its file, when the file still holds this holder's token.
95
+ * @param name The lock name.
96
+ * @param lock The lock acquired.
97
+ * @returns Whether it was released.
98
+ */
99
+ release(name: string, lock: DriverLock): Promise<boolean>;
100
+ /**
101
+ * Renew a lock whose file still holds this holder's token, for `ttl` milliseconds from now: the file is taken away
102
+ * (renamed), checked, rewritten and given back without replacing a lock another process acquired meanwhile (the
103
+ * renewal fails then). A lock expired is not renewed (lost), even when no other process took it over yet.
104
+ * @param name The lock name.
105
+ * @param lock The lock acquired.
106
+ * @param ttl How long the lock stays held without another renewal, in milliseconds.
107
+ * @returns Whether it was renewed.
108
+ */
109
+ extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
110
+ /**
111
+ * Do nothing: the driver keeps no file open; the lock files stay until they are released or taken over.
112
+ */
113
+ close(): Promise<void>;
114
+ }
115
+ export {};
@@ -0,0 +1,60 @@
1
+ import { MutexDriver, type DriverLock } from './MutexDriver.js';
2
+ /**
3
+ * The memory driver (`{type: 'memory'}`, the default of a {@link Mutex}): the locks live in the process memory, shared
4
+ * by every component and driver of the process using the same prefix, and invisible to the other processes. Use it for
5
+ * development, tests and an application running as a single process. It needs no package.
6
+ *
7
+ * The locks expire after their `ttl` unless renewed. The fencing token is a counter of the process, starting again at 1
8
+ * when the process restarts. The locks are lost with the process.
9
+ * @example
10
+ * ```typescript
11
+ * import {MemoryMutexDriver, type DriverLock} from 'lakutata/com/mutex'
12
+ *
13
+ * const driver: MemoryMutexDriver = new MemoryMutexDriver('test:')
14
+ * const first: DriverLock | undefined = await driver.acquire('job', 1000)
15
+ * console.log(await driver.acquire('job', 1000)) //undefined: held by the first
16
+ * if (first) console.log(await driver.release('job', first)) //true
17
+ * await driver.close()
18
+ * ```
19
+ */
20
+ export declare class MemoryMutexDriver extends MutexDriver {
21
+ #private;
22
+ protected readonly prefix: string;
23
+ /**
24
+ * `true`: the locks expire after their `ttl` unless renewed.
25
+ */
26
+ readonly expiring: boolean;
27
+ /**
28
+ * Create a driver on the process memory.
29
+ * @param prefix The prefix added to the lock names; the drivers of the process with the same prefix share their
30
+ * locks. `''` by default.
31
+ */
32
+ constructor(prefix?: string);
33
+ /**
34
+ * Try to acquire a lock once: granted when nobody holds it or its holder let it expire.
35
+ * @param name The lock name.
36
+ * @param ttl How long the lock stays held without a renewal, in milliseconds.
37
+ * @returns The lock with a fencing token, or `undefined` when another holder has it.
38
+ */
39
+ acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
40
+ /**
41
+ * Release a lock still held by this holder.
42
+ * @param name The lock name.
43
+ * @param lock The lock acquired.
44
+ * @returns Whether it was released (`false` when another holder has taken it since).
45
+ */
46
+ release(name: string, lock: DriverLock): Promise<boolean>;
47
+ /**
48
+ * Renew a lock still held by this holder, for `ttl` milliseconds from now. A lock expired is not renewed (lost),
49
+ * even when no other holder took it yet.
50
+ * @param name The lock name.
51
+ * @param lock The lock acquired.
52
+ * @param ttl How long the lock stays held without another renewal, in milliseconds.
53
+ * @returns Whether it was renewed.
54
+ */
55
+ extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
56
+ /**
57
+ * Do nothing: the locks stay in the process memory until they are released or expire.
58
+ */
59
+ close(): Promise<void>;
60
+ }