@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,82 @@
1
+ /**
2
+ * A lock granted by a {@link MutexDriver}: what its holder gives back to release or renew it.
3
+ */
4
+ export type DriverLock = {
5
+ /**
6
+ * The random token identifying the holder (32 hexadecimal characters); the store only releases or renews the lock
7
+ * for it.
8
+ */
9
+ token: string;
10
+ /**
11
+ * The fencing token: a number increasing with each acquisition in the store, given by the memory and Redis drivers
12
+ * and the `table` mode of PostgreSQL and MySQL; `undefined` for the file driver and the advisory locks.
13
+ */
14
+ fence?: number;
15
+ };
16
+ /**
17
+ * The base class of the lock drivers: it acquires, releases and renews the locks in a store shared by their holders.
18
+ * A {@link Mutex} creates its driver from its `driver` option and adds the waiting, the automatic renewal and the lost
19
+ * locks; a custom subclass cannot be given to it. The drivers are exported to use a store directly (scripts, tests):
20
+ * they neither wait nor renew, and their errors are the clients' errors, not a {@link MutexDriverException}.
21
+ * @example
22
+ * ```typescript
23
+ * import {MemoryMutexDriver, type DriverLock, type MutexDriver} from 'lakutata/com/mutex'
24
+ *
25
+ * async function once(driver: MutexDriver, name: string, work: () => Promise<void>): Promise<boolean> {
26
+ * const lock: DriverLock | undefined = await driver.acquire(name, 10000)
27
+ * if (!lock) return false
28
+ * try {
29
+ * await work()
30
+ * } finally {
31
+ * await driver.release(name, lock)
32
+ * }
33
+ * return true
34
+ * }
35
+ *
36
+ * const driver: MutexDriver = new MemoryMutexDriver('script:')
37
+ * console.log(await once(driver, 'migrate', async (): Promise<void> => undefined))
38
+ * await driver.close()
39
+ * ```
40
+ */
41
+ export declare abstract class MutexDriver {
42
+ /**
43
+ * Whether the locks expire after their `ttl` unless renewed: `true` for the memory, file and Redis drivers and the
44
+ * `table` mode; `false` for the advisory locks of PostgreSQL and MySQL, which end with their connection instead.
45
+ */
46
+ abstract readonly expiring: boolean;
47
+ /**
48
+ * Called with the names of the locks the driver lost at once, when the connection holding advisory locks closes or
49
+ * fails. A {@link Mutex} sets it; it does nothing by default.
50
+ */
51
+ onLost: (names: string[]) => void;
52
+ /**
53
+ * Try to acquire a lock once, without waiting.
54
+ * @param name The lock name; the driver's prefix is added to it in the store.
55
+ * @param ttl How long the lock stays held without a renewal, in milliseconds (ignored by the advisory locks).
56
+ * @returns The lock granted, or `undefined` when another holder has it.
57
+ * @throws {MutexDriverNotFoundException} When the client package of the driver is not installed.
58
+ */
59
+ abstract acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
60
+ /**
61
+ * Release a lock, when the store still has it for this holder.
62
+ * @param name The lock name, as acquired.
63
+ * @param lock The lock returned by {@link MutexDriver.acquire}.
64
+ * @returns Whether it was released: `false` when the store no longer has it for this holder (expired, taken, or its
65
+ * connection closed).
66
+ */
67
+ abstract release(name: string, lock: DriverLock): Promise<boolean>;
68
+ /**
69
+ * Renew a lock for `ttl` milliseconds from now, when the store still has it for this holder; for the advisory locks,
70
+ * check that it is still held.
71
+ * @param name The lock name, as acquired.
72
+ * @param lock The lock returned by {@link MutexDriver.acquire}.
73
+ * @param ttl How long the lock stays held without another renewal, in milliseconds.
74
+ * @returns Whether the lock is still held by this holder.
75
+ */
76
+ abstract extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
77
+ /**
78
+ * Close the driver's connections; the advisory locks they hold are released by the database. The driver is not meant
79
+ * to be used after it.
80
+ */
81
+ abstract close(): Promise<void>;
82
+ }
@@ -0,0 +1,135 @@
1
+ import { MutexDriver, type DriverLock } from './MutexDriver.js';
2
+ /**
3
+ * The MySQL (or MariaDB) connection of a {@link MysqlMutexDriver}.
4
+ */
5
+ export type MysqlMutexConnection = {
6
+ /**
7
+ * The host name or IP address of the server.
8
+ */
9
+ host: string;
10
+ /**
11
+ * The port of the server.
12
+ * @default 3306
13
+ */
14
+ port?: number;
15
+ /**
16
+ * The database holding the table of the `table` mode (the named locks are global to the server).
17
+ */
18
+ database: string;
19
+ /**
20
+ * The user name.
21
+ */
22
+ username: string;
23
+ /**
24
+ * The password.
25
+ */
26
+ password: string;
27
+ };
28
+ /**
29
+ * The MySQL driver (`{type: 'mysql'}`, MySQL or MariaDB): the locks are shared by every instance connected to the same
30
+ * server with the same prefix. It needs the optional peer dependency `mysql2` (3); without it, the first acquisition
31
+ * fails with {@link MutexDriverNotFoundException}. It has two modes:
32
+ * - `'advisory'` (the default): named locks (`GET_LOCK`) taken on one connection of the driver, opened at the first use.
33
+ * They do not expire and need no renewal: MySQL releases them when the connection closes, so a crashed process frees
34
+ * them at once. When the connection closes or fails, every lock it held is lost (a {@link Mutex} aborts their
35
+ * signals) and the next acquisition opens a new connection. A connection cut without the client noticing it is not
36
+ * detected by the renewals. No fencing token. The named locks are not replicated by the clusters (Galera, Group
37
+ * Replication): every holder must connect to the same server.
38
+ * - `'table'`: one row per lock in a table, created at the first use with its counter table `<table>_fence`. The rows
39
+ * expire by the database clock after their `ttl` unless renewed, so it works through the poolers and the clusters, and
40
+ * each acquisition gets a fencing token from the counter. A pool of 4 connections is used.
41
+ * @example
42
+ * ```typescript
43
+ * import {MysqlMutexDriver, type DriverLock} from 'lakutata/com/mutex'
44
+ *
45
+ * const driver: MysqlMutexDriver = new MysqlMutexDriver({
46
+ * host: '127.0.0.1',
47
+ * database: 'app',
48
+ * username: 'app',
49
+ * password: 'secret'
50
+ * }, 'my-app:')
51
+ * const lock: DriverLock | undefined = await driver.acquire('invoice:7', 30000)
52
+ * if (lock) await driver.release('invoice:7', lock)
53
+ * //Closing the connection releases its named locks
54
+ * await driver.close()
55
+ * ```
56
+ */
57
+ export declare class MysqlMutexDriver extends MutexDriver {
58
+ #private;
59
+ protected readonly connection: MysqlMutexConnection;
60
+ protected readonly prefix: string;
61
+ protected readonly mode: 'advisory' | 'table';
62
+ protected readonly table: string;
63
+ /**
64
+ * `true` in the `table` mode (the rows expire), `false` in the `advisory` mode (the locks end with the connection).
65
+ */
66
+ readonly expiring: boolean;
67
+ /**
68
+ * Create a driver on a MySQL server; it connects at its first use.
69
+ * @param connection The connection to the server.
70
+ * @param prefix The prefix added to the lock names.
71
+ * @param mode `'advisory'` (the default) or `'table'`.
72
+ * @param table The table of the `table` mode, in the connection's database; `'lakutata_mutex'` by default.
73
+ * @throws {Error} When the table is not a valid name (letters, digits and `_`, not starting with a digit, without a
74
+ * database qualifier).
75
+ */
76
+ constructor(connection: MysqlMutexConnection, prefix: string, mode?: 'advisory' | 'table', table?: string);
77
+ /**
78
+ * The configuration given to `mysql2` for the connections.
79
+ * @returns The `mysql2` connection configuration.
80
+ */
81
+ protected config(): Record<string, unknown>;
82
+ /**
83
+ * The name of a lock for `GET_LOCK`: `lakutata:` and the start of the SHA-256 digest of the prefixed name, 64
84
+ * characters in all (the limit of MySQL).
85
+ * @param name The lock name.
86
+ * @returns The MySQL lock name.
87
+ */
88
+ protected lockName(name: string): string;
89
+ /**
90
+ * The connection holding the named locks, opened at the first call; when it ends or fails, its locks are reported
91
+ * lost through {@link MutexDriver.onLost}.
92
+ * @returns The `mysql2` connection.
93
+ * @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
94
+ */
95
+ protected session(): Promise<any>;
96
+ /**
97
+ * The connection pool of the `table` mode, created at the first call along with the tables; created again at the
98
+ * next call when that fails.
99
+ * @returns The `mysql2` pool.
100
+ * @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
101
+ */
102
+ protected pool(): Promise<any>;
103
+ /**
104
+ * Try to acquire a lock once: a named lock of the driver's connection, or a row inserted (or taken over when
105
+ * expired) in the table.
106
+ * @param name The lock name.
107
+ * @param ttl How long the lock stays held without a renewal, in milliseconds (the `table` mode only).
108
+ * @returns The lock (with a fencing token in the `table` mode), or `undefined` when another holder has it, this
109
+ * driver included.
110
+ * @throws {MutexDriverNotFoundException} When the package `mysql2` is not installed.
111
+ */
112
+ acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
113
+ /**
114
+ * Release a lock still held by this holder: `RELEASE_LOCK`, or delete the row.
115
+ * @param name The lock name.
116
+ * @param lock The lock acquired.
117
+ * @returns Whether it was released (`false` when the row expired and was taken, or the connection was lost).
118
+ */
119
+ release(name: string, lock: DriverLock): Promise<boolean>;
120
+ /**
121
+ * Renew a lock: in the `table` mode, push back the expiration of its row when it still holds this holder's token; in
122
+ * the `advisory` mode, check that the driver still holds it on its connection, by a query (a connection cut without
123
+ * notice fails it).
124
+ * @param name The lock name.
125
+ * @param lock The lock acquired.
126
+ * @param ttl How long the lock stays held without another renewal, in milliseconds (the `table` mode only).
127
+ * @returns Whether the lock is still held by this holder.
128
+ */
129
+ extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
130
+ /**
131
+ * Close the connection (MySQL releases its named locks) and the pool; the rows of the `table` mode stay until they
132
+ * expire.
133
+ */
134
+ close(): Promise<void>;
135
+ }
@@ -0,0 +1,136 @@
1
+ import { MutexDriver, type DriverLock } from './MutexDriver.js';
2
+ /**
3
+ * The PostgreSQL connection of a {@link PostgresMutexDriver}.
4
+ */
5
+ export type PostgresMutexConnection = {
6
+ /**
7
+ * The host name or IP address of the server.
8
+ */
9
+ host: string;
10
+ /**
11
+ * The port of the server.
12
+ * @default 5432
13
+ */
14
+ port?: number;
15
+ /**
16
+ * The database holding the locks (and the table of the `table` mode).
17
+ */
18
+ database: string;
19
+ /**
20
+ * The user name.
21
+ */
22
+ username: string;
23
+ /**
24
+ * The password.
25
+ */
26
+ password: string;
27
+ /**
28
+ * Whether to connect with TLS (`ssl: true` given to `pg`, which verifies the server certificate).
29
+ * @default false
30
+ */
31
+ ssl?: boolean;
32
+ };
33
+ /**
34
+ * The PostgreSQL driver (`{type: 'postgres'}`): the locks are shared by every instance connected to the same database
35
+ * with the same prefix. It needs the optional peer dependency `pg` (8); without it, the first acquisition fails with
36
+ * {@link MutexDriverNotFoundException}. It has two modes:
37
+ * - `'advisory'` (the default): session advisory locks (`pg_try_advisory_lock`) taken on one connection of the driver,
38
+ * opened at the first use. They do not expire and need no renewal: PostgreSQL releases them when the connection
39
+ * closes, so a crashed process frees them at once. When the connection closes or fails, every lock it held is lost
40
+ * (a {@link Mutex} aborts their signals) and the next acquisition opens a new connection. A connection cut without
41
+ * the client noticing it is not detected by the renewals. No fencing token. The lock names are hashed to 64-bit keys,
42
+ * shared with the other advisory locks of the database. It does not work through a pooler in transaction mode
43
+ * (PgBouncer), which does not keep the session.
44
+ * - `'table'`: one row per lock in a table, created at the first use with its sequence `<table>_fence`. The rows expire
45
+ * by the database clock after their `ttl` unless renewed, so it works through the poolers and on the replicated
46
+ * setups writing to one primary, and each acquisition gets a fencing token from the sequence. A pool of 4 connections
47
+ * is used.
48
+ * @example
49
+ * ```typescript
50
+ * import {PostgresMutexDriver, type DriverLock} from 'lakutata/com/mutex'
51
+ *
52
+ * const driver: PostgresMutexDriver = new PostgresMutexDriver({
53
+ * host: '127.0.0.1',
54
+ * database: 'app',
55
+ * username: 'app',
56
+ * password: 'secret'
57
+ * }, 'my-app:', 'table', 'app_locks')
58
+ * const lock: DriverLock | undefined = await driver.acquire('invoice:7', 30000)
59
+ * if (lock) await driver.release('invoice:7', lock)
60
+ * await driver.close()
61
+ * ```
62
+ */
63
+ export declare class PostgresMutexDriver extends MutexDriver {
64
+ #private;
65
+ protected readonly connection: PostgresMutexConnection;
66
+ protected readonly prefix: string;
67
+ protected readonly mode: 'advisory' | 'table';
68
+ protected readonly table: string;
69
+ /**
70
+ * `true` in the `table` mode (the rows expire), `false` in the `advisory` mode (the locks end with the connection).
71
+ */
72
+ readonly expiring: boolean;
73
+ /**
74
+ * Create a driver on a PostgreSQL database; it connects at its first use.
75
+ * @param connection The connection to the database.
76
+ * @param prefix The prefix added to the lock names.
77
+ * @param mode `'advisory'` (the default) or `'table'`.
78
+ * @param table The table of the `table` mode, optionally qualified by its schema (`locks.mutex`); `'lakutata_mutex'`
79
+ * by default.
80
+ * @throws {Error} When the table is not a valid PostgreSQL name (letters, digits and `_`, not starting with a digit).
81
+ */
82
+ constructor(connection: PostgresMutexConnection, prefix: string, mode?: 'advisory' | 'table', table?: string);
83
+ /**
84
+ * The configuration given to `pg` for the connection.
85
+ * @returns The `pg` client configuration.
86
+ */
87
+ protected config(): Record<string, unknown>;
88
+ /**
89
+ * The connection holding the advisory locks, opened at the first call; when it ends or fails, its locks are reported
90
+ * lost through {@link MutexDriver.onLost}.
91
+ * @returns The connected `pg` client.
92
+ * @throws {MutexDriverNotFoundException} When the package `pg` is not installed.
93
+ * @protected
94
+ */
95
+ protected client(): Promise<any>;
96
+ /**
97
+ * The connection pool of the `table` mode, created at the first call along with the table and its sequence; created
98
+ * again at the next call when that fails.
99
+ * @returns The `pg` pool.
100
+ * @throws {MutexDriverNotFoundException} When the package `pg` is not installed.
101
+ * @protected
102
+ */
103
+ protected pool(): Promise<any>;
104
+ /**
105
+ * Try to acquire a lock once: an advisory lock of the driver's connection, or a row inserted (or taken over when
106
+ * expired) in the table.
107
+ * @param name The lock name.
108
+ * @param ttl How long the lock stays held without a renewal, in milliseconds (the `table` mode only).
109
+ * @returns The lock (with a fencing token in the `table` mode), or `undefined` when another holder has it, this
110
+ * driver included.
111
+ * @throws {MutexDriverNotFoundException} When the package `pg` is not installed.
112
+ */
113
+ acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
114
+ /**
115
+ * Release a lock still held by this holder: unlock the advisory lock, or delete the row.
116
+ * @param name The lock name.
117
+ * @param lock The lock acquired.
118
+ * @returns Whether it was released (`false` when the row expired and was taken, or the connection was lost).
119
+ */
120
+ release(name: string, lock: DriverLock): Promise<boolean>;
121
+ /**
122
+ * Renew a lock: in the `table` mode, push back the expiration of its row when it still holds this holder's token; in
123
+ * the `advisory` mode, check that the driver still holds it on its connection, by a query (a connection cut
124
+ * without notice fails it).
125
+ * @param name The lock name.
126
+ * @param lock The lock acquired.
127
+ * @param ttl How long the lock stays held without another renewal, in milliseconds (the `table` mode only).
128
+ * @returns Whether the lock is still held by this holder.
129
+ */
130
+ extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
131
+ /**
132
+ * Close the connection (PostgreSQL releases its advisory locks) and the pool; the rows of the `table` mode stay until
133
+ * they expire.
134
+ */
135
+ close(): Promise<void>;
136
+ }
@@ -0,0 +1,129 @@
1
+ import { MutexDriver, type DriverLock } from './MutexDriver.js';
2
+ /**
3
+ * The Redis connection of a {@link RedisMutexDriver}: a `url`, or a `host` and a `port`.
4
+ */
5
+ export type RedisMutexConnection = {
6
+ /**
7
+ * The server URL (`redis://user:password@host:6379/0`, `rediss://` for TLS). When set, `host`, `port` and `tls` are
8
+ * ignored; `username`, `password` and `database` apply when the URL does not give them.
9
+ * @default none
10
+ */
11
+ url?: string;
12
+ /**
13
+ * The host name or IP address of the server.
14
+ * @default '127.0.0.1'
15
+ */
16
+ host?: string;
17
+ /**
18
+ * The port of the server.
19
+ * @default 6379
20
+ */
21
+ port?: number;
22
+ /**
23
+ * The index of the Redis database.
24
+ * @default 0
25
+ */
26
+ database?: number;
27
+ /**
28
+ * The user name (Redis 6 ACL).
29
+ * @default none (the default user)
30
+ */
31
+ username?: string;
32
+ /**
33
+ * The password.
34
+ * @default none
35
+ */
36
+ password?: string;
37
+ /**
38
+ * Whether to connect with TLS (ignored with a `url`: use `rediss://`).
39
+ * @default false
40
+ */
41
+ tls?: boolean;
42
+ };
43
+ /**
44
+ * The Redis driver (`{type: 'redis'}`): the locks are Redis keys holding their holder's token, shared by every instance
45
+ * connected to the same server (or cluster) with the same prefix. It needs the optional peer dependency `redis` (4.6 or
46
+ * 5); without it, the first acquisition fails with {@link MutexDriverNotFoundException}.
47
+ *
48
+ * The keys expire after their `ttl` unless renewed (`PEXPIRE`), and are released and renewed only for their holder's
49
+ * token. Each acquisition increments a fencing token kept in a second key (`<key>:fence`, never expiring); the lock's
50
+ * name is the key's hash tag, so both keys stay in the same slot of a cluster. Configure the server with
51
+ * `maxmemory-policy noeviction`: an evicted key is a lock lost. The locks of one primary are lost if it fails over before
52
+ * replicating them.
53
+ *
54
+ * The client connects at the first use. While the server cannot be reached, the calls wait for it to come back
55
+ * instead of failing (the client retries without limit): an acquisition does not end at its timeout, and the renewals
56
+ * do not complete, so the held locks are not marked lost while their keys may expire in Redis.
57
+ * @example
58
+ * ```typescript
59
+ * import {RedisMutexDriver, type DriverLock} from 'lakutata/com/mutex'
60
+ *
61
+ * const driver: RedisMutexDriver = new RedisMutexDriver({url: 'redis://127.0.0.1:6379'}, 'my-app:')
62
+ * const lock: DriverLock | undefined = await driver.acquire('invoice:7', 30000)
63
+ * if (lock) {
64
+ * //Increases with each acquisition: written along with the data to refuse an older holder's writes
65
+ * console.log(lock.fence)
66
+ * await driver.release('invoice:7', lock)
67
+ * }
68
+ * await driver.close()
69
+ * ```
70
+ */
71
+ export declare class RedisMutexDriver extends MutexDriver {
72
+ #private;
73
+ protected readonly connection: RedisMutexConnection;
74
+ protected readonly prefix: string;
75
+ /**
76
+ * `true`: the locks expire after their `ttl` unless renewed.
77
+ */
78
+ readonly expiring: boolean;
79
+ /**
80
+ * Create a driver on a Redis server; it connects at its first use.
81
+ * @param connection The connection to the server.
82
+ * @param prefix The prefix of the keys.
83
+ */
84
+ constructor(connection: RedisMutexConnection, prefix: string);
85
+ /**
86
+ * The Redis client, connected at the first call; the concurrent first calls share one connection. An unreachable
87
+ * server fails the calls: the first connection is given up after 5 seconds, and while the client reconnects the
88
+ * calls fail at once (its commands are not queued).
89
+ * @returns The connected client.
90
+ * @throws {MutexDriverNotFoundException} When the package `redis` is not installed.
91
+ * @throws {Error} When the server is not reachable.
92
+ * @protected
93
+ */
94
+ protected client(): Promise<any>;
95
+ /**
96
+ * The key of a lock: `<prefix>{<name>}`, the name being the hash tag that keeps the lock and its fencing token in the
97
+ * same slot of a cluster.
98
+ * @param name The lock name.
99
+ * @returns The Redis key.
100
+ */
101
+ protected key(name: string): string;
102
+ /**
103
+ * Try to acquire a lock once: set its key when it does not exist, with a fencing token incremented.
104
+ * @param name The lock name.
105
+ * @param ttl How long the lock stays held without a renewal, in milliseconds (rounded up).
106
+ * @returns The lock with its fencing token, or `undefined` when another holder has it.
107
+ * @throws {MutexDriverNotFoundException} When the package `redis` is not installed.
108
+ */
109
+ acquire(name: string, ttl: number): Promise<DriverLock | undefined>;
110
+ /**
111
+ * Release a lock by deleting its key, when it still holds this holder's token.
112
+ * @param name The lock name.
113
+ * @param lock The lock acquired.
114
+ * @returns Whether it was released (`false` when the key expired or was taken).
115
+ */
116
+ release(name: string, lock: DriverLock): Promise<boolean>;
117
+ /**
118
+ * Renew a lock whose key still holds this holder's token, for `ttl` milliseconds from now.
119
+ * @param name The lock name.
120
+ * @param lock The lock acquired.
121
+ * @param ttl How long the lock stays held without another renewal, in milliseconds (rounded up).
122
+ * @returns Whether it was renewed (`false` when the key expired or was taken).
123
+ */
124
+ extend(name: string, lock: DriverLock, ttl: number): Promise<boolean>;
125
+ /**
126
+ * Close the connection (`QUIT`, or a disconnection when it fails); the keys stay until they expire.
127
+ */
128
+ close(): Promise<void>;
129
+ }
@@ -0,0 +1,12 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by the {@link Mutex} methods when the lock store fails (unreachable, a failed query, a file error), its
4
+ * message carrying the driver's error. An automatic renewal failing this way makes the lock lost instead. Its `errno` is
5
+ * `'E_MUTEX_DRIVER'`.
6
+ */
7
+ export declare class MutexDriverException extends Exception {
8
+ /**
9
+ * `'E_MUTEX_DRIVER'`.
10
+ */
11
+ errno: string | number;
12
+ }
@@ -0,0 +1,12 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by the first acquisition of a {@link Mutex} (or a driver) whose client package is not installed: `redis` for
4
+ * the Redis driver, `pg` for PostgreSQL, `mysql2` for MySQL. Its message names the package to install. Its `errno` is
5
+ * `'E_MUTEX_DRIVER_NOT_FOUND'`.
6
+ */
7
+ export declare class MutexDriverNotFoundException extends Exception {
8
+ /**
9
+ * `'E_MUTEX_DRIVER_NOT_FOUND'`.
10
+ */
11
+ errno: string | number;
12
+ }
@@ -0,0 +1,13 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * The abort reason of {@link MutexLock.signal} when a lock is lost: a renewal found it expired or taken by another
4
+ * holder, a renewal failed, or the connection holding it closed. It is not thrown by the component: the work holding the
5
+ * lock meets it through the signal (`signal.throwIfAborted()`, an `AbortSignal`-aware API). Its `errno` is
6
+ * `'E_MUTEX_LOST'`.
7
+ */
8
+ export declare class MutexLostException extends Exception {
9
+ /**
10
+ * `'E_MUTEX_LOST'`.
11
+ */
12
+ errno: string | number;
13
+ }
@@ -0,0 +1,16 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by {@link Mutex.acquire} and {@link Mutex.synchronized} when the lock is not acquired before the timeout
4
+ * (another holder kept it), and by a {@link Synchronized} action whose lock is held by another request (HTTP 409
5
+ * Conflict). Its `errno` is `'E_MUTEX_TIMEOUT'`.
6
+ */
7
+ export declare class MutexTimeoutException extends Exception {
8
+ /**
9
+ * `'E_MUTEX_TIMEOUT'`.
10
+ */
11
+ errno: string | number;
12
+ /**
13
+ * The HTTP status the transports answer with: 409 (Conflict).
14
+ */
15
+ statusCode: number;
16
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Load the client package of a driver (an optional dependency)
3
+ * @param member a member of its API, to find it in the ESM namespace or the CommonJS exports
4
+ * @param packageName
5
+ */
6
+ export declare function loadDriver<T = any>(member: string, packageName: string): Promise<T>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The token of a lock acquired (its holder checked by it)
3
+ */
4
+ export declare function lockToken(): string;
5
+ /**
6
+ * The digest of the name of a lock (hexadecimal)
7
+ * @param name
8
+ */
9
+ export declare function nameDigest(name: string): string;
10
+ /**
11
+ * The key of 64 bits of the name of a lock (the advisory locks of PostgreSQL), a signed bigint as a string
12
+ * @param name
13
+ */
14
+ export declare function nameKey64(name: string): string;
@@ -0,0 +1,33 @@
1
+ import { DTO } from '@lakutata/core';
2
+ /**
3
+ * The options of the file driver ({@link FileMutexDriver}): one lock file per lock in a directory, shared by the
4
+ * processes of one host using the same directory (several workers or applications of a machine). It needs no package.
5
+ * The locks expire after their `ttl` unless renewed, a lock whose process is gone is taken over at once, and there is no
6
+ * fencing token.
7
+ * @example
8
+ * ```typescript
9
+ * import {Application} from 'lakutata'
10
+ * import {Mutex} from 'lakutata/com/mutex'
11
+ *
12
+ * Application.run(() => ({
13
+ * id: 'file-mutex.app',
14
+ * name: 'FileMutex',
15
+ * components: {
16
+ * //Every worker of this machine uses the same directory
17
+ * mutex: {class: Mutex, driver: {type: 'file', directory: '/var/lock/my-app'}}
18
+ * }
19
+ * }))
20
+ * ```
21
+ */
22
+ export declare class FileMutexOptions extends DTO {
23
+ /**
24
+ * The driver type: `'file'`.
25
+ */
26
+ type: 'file';
27
+ /**
28
+ * The directory of the lock files, created when missing. The processes sharing the locks must use the same one; the
29
+ * default depends on the operating system's temporary directory, which can differ between users.
30
+ * @default path.join(os.tmpdir(), 'lakutata-mutex')
31
+ */
32
+ directory?: string;
33
+ }
@@ -0,0 +1,27 @@
1
+ import { DTO } from '@lakutata/core';
2
+ /**
3
+ * The options of the memory driver ({@link MemoryMutexDriver}), the default of a {@link Mutex}: the locks live in the
4
+ * process memory, shared by the components of the process and invisible to the other processes. For development, tests
5
+ * and an application running as a single process; it needs no package. The locks expire after their `ttl` unless
6
+ * renewed, and carry fencing tokens counted by the process.
7
+ * @example
8
+ * ```typescript
9
+ * import {Application} from 'lakutata'
10
+ * import {Mutex} from 'lakutata/com/mutex'
11
+ *
12
+ * Application.run(() => ({
13
+ * id: 'memory-mutex.app',
14
+ * name: 'MemoryMutex',
15
+ * components: {
16
+ * //The same as {class: Mutex}
17
+ * mutex: {class: Mutex, driver: {type: 'memory'}}
18
+ * }
19
+ * }))
20
+ * ```
21
+ */
22
+ export declare class MemoryMutexOptions extends DTO {
23
+ /**
24
+ * The driver type: `'memory'`.
25
+ */
26
+ type: 'memory';
27
+ }