@zukhruf/mutex 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/dist/fencing/counter-token-source.d.ts +11 -0
- package/dist/fencing/counter-token-source.d.ts.map +1 -0
- package/dist/fencing/counter-token-source.js +13 -0
- package/dist/fencing/counter-token-source.js.map +1 -0
- package/dist/fencing/epoch-token-source.d.ts +13 -0
- package/dist/fencing/epoch-token-source.d.ts.map +1 -0
- package/dist/fencing/epoch-token-source.js +27 -0
- package/dist/fencing/epoch-token-source.js.map +1 -0
- package/dist/fencing/fencing-token.d.ts +12 -0
- package/dist/fencing/fencing-token.d.ts.map +1 -0
- package/dist/fencing/fencing-token.js +18 -0
- package/dist/fencing/fencing-token.js.map +1 -0
- package/dist/fencing/file-token-source.d.ts +13 -0
- package/dist/fencing/file-token-source.d.ts.map +1 -0
- package/dist/fencing/file-token-source.js +35 -0
- package/dist/fencing/file-token-source.js.map +1 -0
- package/dist/fencing/monotonic-clock-token-source.d.ts +12 -0
- package/dist/fencing/monotonic-clock-token-source.d.ts.map +1 -0
- package/dist/fencing/monotonic-clock-token-source.js +15 -0
- package/dist/fencing/monotonic-clock-token-source.js.map +1 -0
- package/dist/fencing/token-source.d.ts +9 -0
- package/dist/fencing/token-source.d.ts.map +1 -0
- package/dist/fencing/token-source.js +2 -0
- package/dist/fencing/token-source.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/leader-election/index.d.ts +3 -0
- package/dist/leader-election/index.d.ts.map +1 -0
- package/dist/leader-election/index.js +3 -0
- package/dist/leader-election/index.js.map +1 -0
- package/dist/leader-election/leader-election.d.ts +23 -0
- package/dist/leader-election/leader-election.d.ts.map +1 -0
- package/dist/leader-election/leader-election.js +77 -0
- package/dist/leader-election/leader-election.js.map +1 -0
- package/dist/leader-election/leadership.d.ts +14 -0
- package/dist/leader-election/leadership.d.ts.map +1 -0
- package/dist/leader-election/leadership.js +23 -0
- package/dist/leader-election/leadership.js.map +1 -0
- package/dist/lock-stores/file-system/file-lock-store.d.ts +35 -0
- package/dist/lock-stores/file-system/file-lock-store.d.ts.map +1 -0
- package/dist/lock-stores/file-system/file-lock-store.js +64 -0
- package/dist/lock-stores/file-system/file-lock-store.js.map +1 -0
- package/dist/lock-stores/file-system/lock-file-store.d.ts +11 -0
- package/dist/lock-stores/file-system/lock-file-store.d.ts.map +1 -0
- package/dist/lock-stores/file-system/lock-file-store.js +44 -0
- package/dist/lock-stores/file-system/lock-file-store.js.map +1 -0
- package/dist/lock-stores/file-system/owner.d.ts +16 -0
- package/dist/lock-stores/file-system/owner.d.ts.map +1 -0
- package/dist/lock-stores/file-system/owner.js +47 -0
- package/dist/lock-stores/file-system/owner.js.map +1 -0
- package/dist/lock-stores/file-system/ticket-queue-file-store.d.ts +16 -0
- package/dist/lock-stores/file-system/ticket-queue-file-store.d.ts.map +1 -0
- package/dist/lock-stores/file-system/ticket-queue-file-store.js +111 -0
- package/dist/lock-stores/file-system/ticket-queue-file-store.js.map +1 -0
- package/dist/lock-stores/ipc/child-process-connection.d.ts +18 -0
- package/dist/lock-stores/ipc/child-process-connection.d.ts.map +1 -0
- package/dist/lock-stores/ipc/child-process-connection.js +46 -0
- package/dist/lock-stores/ipc/child-process-connection.js.map +1 -0
- package/dist/lock-stores/ipc/ipc-lock-coordinator.d.ts +24 -0
- package/dist/lock-stores/ipc/ipc-lock-coordinator.d.ts.map +1 -0
- package/dist/lock-stores/ipc/ipc-lock-coordinator.js +27 -0
- package/dist/lock-stores/ipc/ipc-lock-coordinator.js.map +1 -0
- package/dist/lock-stores/ipc/ipc-store.d.ts +15 -0
- package/dist/lock-stores/ipc/ipc-store.d.ts.map +1 -0
- package/dist/lock-stores/ipc/ipc-store.js +34 -0
- package/dist/lock-stores/ipc/ipc-store.js.map +1 -0
- package/dist/lock-stores/ipc/process-channel-connection.d.ts +18 -0
- package/dist/lock-stores/ipc/process-channel-connection.d.ts.map +1 -0
- package/dist/lock-stores/ipc/process-channel-connection.js +52 -0
- package/dist/lock-stores/ipc/process-channel-connection.js.map +1 -0
- package/dist/lock-stores/memory/memory-store.d.ts +14 -0
- package/dist/lock-stores/memory/memory-store.d.ts.map +1 -0
- package/dist/lock-stores/memory/memory-store.js +41 -0
- package/dist/lock-stores/memory/memory-store.js.map +1 -0
- package/dist/lock-stores/remote/connection.d.ts +17 -0
- package/dist/lock-stores/remote/connection.d.ts.map +1 -0
- package/dist/lock-stores/remote/connection.js +2 -0
- package/dist/lock-stores/remote/connection.js.map +1 -0
- package/dist/lock-stores/remote/connector.d.ts +12 -0
- package/dist/lock-stores/remote/connector.d.ts.map +1 -0
- package/dist/lock-stores/remote/connector.js +2 -0
- package/dist/lock-stores/remote/connector.js.map +1 -0
- package/dist/lock-stores/remote/coordinator-unavailable-error.d.ts +6 -0
- package/dist/lock-stores/remote/coordinator-unavailable-error.d.ts.map +1 -0
- package/dist/lock-stores/remote/coordinator-unavailable-error.js +10 -0
- package/dist/lock-stores/remote/coordinator-unavailable-error.js.map +1 -0
- package/dist/lock-stores/remote/envelope.d.ts +7 -0
- package/dist/lock-stores/remote/envelope.d.ts.map +1 -0
- package/dist/lock-stores/remote/envelope.js +11 -0
- package/dist/lock-stores/remote/envelope.js.map +1 -0
- package/dist/lock-stores/remote/lock-coordinator.d.ts +34 -0
- package/dist/lock-stores/remote/lock-coordinator.d.ts.map +1 -0
- package/dist/lock-stores/remote/lock-coordinator.js +150 -0
- package/dist/lock-stores/remote/lock-coordinator.js.map +1 -0
- package/dist/lock-stores/remote/protocol.d.ts +36 -0
- package/dist/lock-stores/remote/protocol.d.ts.map +1 -0
- package/dist/lock-stores/remote/protocol.js +33 -0
- package/dist/lock-stores/remote/protocol.js.map +1 -0
- package/dist/lock-stores/remote/remote-lock-client.d.ts +17 -0
- package/dist/lock-stores/remote/remote-lock-client.d.ts.map +1 -0
- package/dist/lock-stores/remote/remote-lock-client.js +172 -0
- package/dist/lock-stores/remote/remote-lock-client.js.map +1 -0
- package/dist/lock-stores/socket/electing-connector.d.ts +21 -0
- package/dist/lock-stores/socket/electing-connector.d.ts.map +1 -0
- package/dist/lock-stores/socket/electing-connector.js +45 -0
- package/dist/lock-stores/socket/electing-connector.js.map +1 -0
- package/dist/lock-stores/socket/lock-server.d.ts +23 -0
- package/dist/lock-stores/socket/lock-server.d.ts.map +1 -0
- package/dist/lock-stores/socket/lock-server.js +67 -0
- package/dist/lock-stores/socket/lock-server.js.map +1 -0
- package/dist/lock-stores/socket/socket-connection.d.ts +18 -0
- package/dist/lock-stores/socket/socket-connection.d.ts.map +1 -0
- package/dist/lock-stores/socket/socket-connection.js +54 -0
- package/dist/lock-stores/socket/socket-connection.js.map +1 -0
- package/dist/lock-stores/socket/socket-store.d.ts +35 -0
- package/dist/lock-stores/socket/socket-store.d.ts.map +1 -0
- package/dist/lock-stores/socket/socket-store.js +84 -0
- package/dist/lock-stores/socket/socket-store.js.map +1 -0
- package/dist/lock-stores/sqlite/sqlite-store.d.ts +21 -0
- package/dist/lock-stores/sqlite/sqlite-store.d.ts.map +1 -0
- package/dist/lock-stores/sqlite/sqlite-store.js +105 -0
- package/dist/lock-stores/sqlite/sqlite-store.js.map +1 -0
- package/dist/lock-stores/thread/parent-port-connection.d.ts +20 -0
- package/dist/lock-stores/thread/parent-port-connection.d.ts.map +1 -0
- package/dist/lock-stores/thread/parent-port-connection.js +44 -0
- package/dist/lock-stores/thread/parent-port-connection.js.map +1 -0
- package/dist/lock-stores/thread/thread-lock-coordinator.d.ts +22 -0
- package/dist/lock-stores/thread/thread-lock-coordinator.d.ts.map +1 -0
- package/dist/lock-stores/thread/thread-lock-coordinator.js +25 -0
- package/dist/lock-stores/thread/thread-lock-coordinator.js.map +1 -0
- package/dist/lock-stores/thread/thread-store.d.ts +13 -0
- package/dist/lock-stores/thread/thread-store.d.ts.map +1 -0
- package/dist/lock-stores/thread/thread-store.js +37 -0
- package/dist/lock-stores/thread/thread-store.js.map +1 -0
- package/dist/lock-stores/thread/worker-connection.d.ts +19 -0
- package/dist/lock-stores/thread/worker-connection.d.ts.map +1 -0
- package/dist/lock-stores/thread/worker-connection.js +45 -0
- package/dist/lock-stores/thread/worker-connection.js.map +1 -0
- package/dist/mutex/acquire-mode.d.ts +30 -0
- package/dist/mutex/acquire-mode.d.ts.map +1 -0
- package/dist/mutex/acquire-mode.js +2 -0
- package/dist/mutex/acquire-mode.js.map +1 -0
- package/dist/mutex/acquire-modes/modes.d.ts +9 -0
- package/dist/mutex/acquire-modes/modes.d.ts.map +1 -0
- package/dist/mutex/acquire-modes/modes.js +9 -0
- package/dist/mutex/acquire-modes/modes.js.map +1 -0
- package/dist/mutex/acquire-modes/skip-if-busy-mode.d.ts +15 -0
- package/dist/mutex/acquire-modes/skip-if-busy-mode.d.ts.map +1 -0
- package/dist/mutex/acquire-modes/skip-if-busy-mode.js +27 -0
- package/dist/mutex/acquire-modes/skip-if-busy-mode.js.map +1 -0
- package/dist/mutex/acquire-modes/wait-mode.d.ts +9 -0
- package/dist/mutex/acquire-modes/wait-mode.d.ts.map +1 -0
- package/dist/mutex/acquire-modes/wait-mode.js +8 -0
- package/dist/mutex/acquire-modes/wait-mode.js.map +1 -0
- package/dist/mutex/key.d.ts +22 -0
- package/dist/mutex/key.d.ts.map +1 -0
- package/dist/mutex/key.js +21 -0
- package/dist/mutex/key.js.map +1 -0
- package/dist/mutex/lease.d.ts +9 -0
- package/dist/mutex/lease.d.ts.map +1 -0
- package/dist/mutex/lease.js +12 -0
- package/dist/mutex/lease.js.map +1 -0
- package/dist/mutex/lock-lost-error.d.ts +6 -0
- package/dist/mutex/lock-lost-error.d.ts.map +1 -0
- package/dist/mutex/lock-lost-error.js +10 -0
- package/dist/mutex/lock-lost-error.js.map +1 -0
- package/dist/mutex/lock-store.d.ts +13 -0
- package/dist/mutex/lock-store.d.ts.map +1 -0
- package/dist/mutex/lock-store.js +2 -0
- package/dist/mutex/lock-store.js.map +1 -0
- package/dist/mutex/mutex.d.ts +25 -0
- package/dist/mutex/mutex.d.ts.map +1 -0
- package/dist/mutex/mutex.js +41 -0
- package/dist/mutex/mutex.js.map +1 -0
- package/dist/shared/fs/atomic-write.d.ts +3 -0
- package/dist/shared/fs/atomic-write.d.ts.map +1 -0
- package/dist/shared/fs/atomic-write.js +34 -0
- package/dist/shared/fs/atomic-write.js.map +1 -0
- package/dist/shared/fs/create-exclusive.d.ts +6 -0
- package/dist/shared/fs/create-exclusive.d.ts.map +1 -0
- package/dist/shared/fs/create-exclusive.js +24 -0
- package/dist/shared/fs/create-exclusive.js.map +1 -0
- package/dist/shared/fs/errno.d.ts +2 -0
- package/dist/shared/fs/errno.d.ts.map +1 -0
- package/dist/shared/fs/errno.js +4 -0
- package/dist/shared/fs/errno.js.map +1 -0
- package/dist/shared/fs/safe-file-name.d.ts +8 -0
- package/dist/shared/fs/safe-file-name.d.ts.map +1 -0
- package/dist/shared/fs/safe-file-name.js +10 -0
- package/dist/shared/fs/safe-file-name.js.map +1 -0
- package/dist/shared/is-record.d.ts +3 -0
- package/dist/shared/is-record.d.ts.map +1 -0
- package/dist/shared/is-record.js +3 -0
- package/dist/shared/is-record.js.map +1 -0
- package/dist/shared/sqlite/is-busy.d.ts +3 -0
- package/dist/shared/sqlite/is-busy.d.ts.map +1 -0
- package/dist/shared/sqlite/is-busy.js +10 -0
- package/dist/shared/sqlite/is-busy.js.map +1 -0
- package/dist/shared/until-aborted.d.ts +3 -0
- package/dist/shared/until-aborted.d.ts.map +1 -0
- package/dist/shared/until-aborted.js +19 -0
- package/dist/shared/until-aborted.js.map +1 -0
- package/package.json +50 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ezzabuzaid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zukhruf/mutex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A mutex for Node.js with interchangeable lock stores. You select who shares the locks: one object, the threads of one process, a parent process and its children, or all processes on one host. The code that uses the mutex stays the same. Each lease has a fencing token, so a resource can refuse the late writes of a holder that lost its key.
|
|
4
|
+
|
|
5
|
+
The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
Two requests ask for the last item at the same time. Each request reads the stock, waits for the database, and then writes the stock. Both requests read `1`, so both sell the item.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
if (stock > 0) {
|
|
13
|
+
// both requests see 1
|
|
14
|
+
await saveReservation(); // the other request runs here
|
|
15
|
+
stock -= 1; // both requests write
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A mutex lets one holder at a time run this code for a key:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { MemoryStore, Mutex } from '@zukhruf/mutex';
|
|
23
|
+
|
|
24
|
+
const mutex = new Mutex(new MemoryStore());
|
|
25
|
+
|
|
26
|
+
const reserved = await mutex.acquire('product:42', async () => {
|
|
27
|
+
if (stock === 0) return false;
|
|
28
|
+
await saveReservation();
|
|
29
|
+
stock -= 1;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The full program is in the recipe [Stop two requests from selling the last item](./docs/recipes/reserve-the-last-item.md).
|
|
35
|
+
|
|
36
|
+
## Select a lock store
|
|
37
|
+
|
|
38
|
+
First find who writes to the resource. Then select the lock store with that [reach](./docs/concepts/reach.md).
|
|
39
|
+
|
|
40
|
+
| Lock store | Reach | Order | A holder stops | Needs |
|
|
41
|
+
| ---------------------------------------------------------------- | ----------------------- | --------------------------------------- | ------------------------------- | --------------------------- |
|
|
42
|
+
| [MemoryStore](./docs/stores/memory-store.md) | One object | First come, first served | The locks stop with the process | Nothing |
|
|
43
|
+
| [ThreadStore](./docs/stores/thread-store.md) | One process (threads) | First come, first served | Released when the worker exits | `adopt(worker)` |
|
|
44
|
+
| [IpcStore](./docs/stores/ipc-store.md) | Parent and its children | First come, first served | Released in approximately 2 ms | `fork()` and `adopt(child)` |
|
|
45
|
+
| [TicketQueueFileStore](./docs/stores/ticket-queue-file-store.md) | One host | First come, first served | Released after a process check | A shared directory |
|
|
46
|
+
| [LockFileStore](./docs/stores/lock-file-store.md) | One host | No order | Released after a process check | A shared directory |
|
|
47
|
+
| [SqliteStore](./docs/stores/sqlite-store.md) | One host | First come, first served in one process | Released by the kernel | A shared directory |
|
|
48
|
+
| [SocketStore](./docs/stores/socket-store.md) | One host | First come, first served | Released in approximately 2 ms | A shared directory |
|
|
49
|
+
|
|
50
|
+
If you are not sure:
|
|
51
|
+
|
|
52
|
+
- **One process:** use `MemoryStore`.
|
|
53
|
+
- **Several processes on one host:** use `SqliteStore`.
|
|
54
|
+
|
|
55
|
+
## Acquire modes
|
|
56
|
+
|
|
57
|
+
A key is exclusive for every caller. The acquire mode decides only what one caller does while the key is busy: wait (the default), or skip.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { Modes } from '@zukhruf/mutex';
|
|
61
|
+
|
|
62
|
+
const report = mutex.key('report:daily', { mode: Modes.skipIfBusy() });
|
|
63
|
+
|
|
64
|
+
const tick = await report.run(buildReport); // a cron tick skips if a report runs
|
|
65
|
+
if (!tick.acquired) return;
|
|
66
|
+
|
|
67
|
+
const fresh = await report.run(buildReport, { mode: Modes.wait() }); // an admin waits, then runs
|
|
68
|
+
await mutex.acquire('product:42', reserve, {
|
|
69
|
+
mode: Modes.skipIfBusy({ waitAtMost: 500 }),
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A mode that can skip returns `{ acquired: true, value } | { acquired: false }`, and TypeScript makes you check `acquired`. See [Acquire modes](./docs/concepts/acquire-modes.md).
|
|
74
|
+
|
|
75
|
+
## Fencing tokens
|
|
76
|
+
|
|
77
|
+
A holder can lose its key and not know it, for example when its process freezes. Each lease has a fencing token that increases with each grant. Send the token with each write, and let the resource refuse lower tokens:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
await mutex.acquire('product:42', async (lease) => {
|
|
81
|
+
await database.run(
|
|
82
|
+
`UPDATE stock SET quantity = quantity - 1, fence = ?
|
|
83
|
+
WHERE product = ? AND fence <= ? AND quantity > 0`,
|
|
84
|
+
[lease.token.value, 'product:42', lease.token.value],
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [Fencing tokens](./docs/concepts/fencing-tokens.md) and the recipe [Protect a database from stale holders](./docs/recipes/fence-a-database.md).
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
**Concepts**
|
|
94
|
+
|
|
95
|
+
- [Reach](./docs/concepts/reach.md): who can share a lock, and how to select it.
|
|
96
|
+
- [Acquire modes](./docs/concepts/acquire-modes.md): wait or skip while a key is busy.
|
|
97
|
+
- [Fencing tokens](./docs/concepts/fencing-tokens.md): how a resource refuses a stale holder.
|
|
98
|
+
- [Leader election](./docs/concepts/leader-election.md): how `SocketStore` selects its coordinator.
|
|
99
|
+
- [Failure modes](./docs/concepts/failure-modes.md): what each lock store does when something stops.
|
|
100
|
+
|
|
101
|
+
**Lock stores**: one page for each lock store, with What, Why, When, When not, How it works, Failure modes, Options, and Evidence. See the table above.
|
|
102
|
+
|
|
103
|
+
**Recipes**: one use case each, with a full program that you can run.
|
|
104
|
+
|
|
105
|
+
1. [Stop two requests from selling the last item](./docs/recipes/reserve-the-last-item.md)
|
|
106
|
+
2. [Several app instances on one host](./docs/recipes/several-instances-on-one-host.md)
|
|
107
|
+
3. [A worker pool that you start](./docs/recipes/worker-pool.md)
|
|
108
|
+
4. [Worker threads that share a lock](./docs/recipes/worker-threads.md)
|
|
109
|
+
5. [Protect a database from stale holders](./docs/recipes/fence-a-database.md)
|
|
110
|
+
6. [Survive a crashed holder](./docs/recipes/survive-a-crashed-holder.md)
|
|
111
|
+
7. [Run a job in only one process](./docs/recipes/singleton-job-with-leader-election.md)
|
|
112
|
+
8. [Write your own lock store](./docs/recipes/write-your-own-lock-store.md)
|
|
113
|
+
9. [Skip a job that is already running](./docs/recipes/skip-a-job-that-is-already-running.md)
|
|
114
|
+
|
|
115
|
+
**Decisions**: the [architecture decision records](./docs/adr) tell why the design is as it is.
|
|
116
|
+
|
|
117
|
+
## Use it
|
|
118
|
+
|
|
119
|
+
This project is an experiment. You need Node.js 26.9 or later.
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npm install @zukhruf/mutex
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { Mutex, SqliteStore } from '@zukhruf/mutex';
|
|
127
|
+
import { LeaderElection } from '@zukhruf/mutex/leader-election';
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
This package is part of the [zukhruf](../../README.md) workspace. Run the commands from the workspace root.
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
npm install
|
|
136
|
+
npx nx run mutex:test # builds, then runs all tests in src/
|
|
137
|
+
npx nx run mutex:typecheck # formats, lints, then type checks
|
|
138
|
+
npx nx run mutex:build # compiles src/ to dist/
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The tests run from `src/`, not from `dist/`: they start workers and child processes from `.ts` files.
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
src/
|
|
145
|
+
mutex/ Mutex, Key, acquire modes, Lease, LockStore, LockLostError
|
|
146
|
+
fencing/ fencing tokens and token sources
|
|
147
|
+
lock-stores/ one folder for each lock store
|
|
148
|
+
remote/ the coordinator and client that ThreadStore, IpcStore and SocketStore share
|
|
149
|
+
leader-election/ leader election (separate entry point, not part of the mutex)
|
|
150
|
+
shared/ small file system and SQLite helpers
|
|
151
|
+
testing/ test helpers and the matrix of lock stores (not published)
|
|
152
|
+
docs/
|
|
153
|
+
concepts/ stores/ recipes/ adr/
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The tests run each lock store through the same scenarios: single process, many threads, many processes, a killed holder, and a failover. Mutation tests broke the mechanisms on purpose, and a test found each break. A test that breaks something stops the run at once (`--test-force-exit`), so a broken release fails in seconds and does not hang.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FencingToken } from './fencing-token.ts';
|
|
2
|
+
import type { TokenSource } from './token-source.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Counts in memory, so tokens restart when the process does. Only fence
|
|
5
|
+
* resources that do not outlive the process with it.
|
|
6
|
+
*/
|
|
7
|
+
export declare class CounterTokenSource implements TokenSource {
|
|
8
|
+
#private;
|
|
9
|
+
next(_key: string): Promise<FencingToken>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=counter-token-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counter-token-source.d.ts","sourceRoot":"","sources":["../../src/fencing/counter-token-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,WAAW;;IAG9C,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAIhD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FencingToken } from "./fencing-token.js";
|
|
2
|
+
/**
|
|
3
|
+
* Counts in memory, so tokens restart when the process does. Only fence
|
|
4
|
+
* resources that do not outlive the process with it.
|
|
5
|
+
*/
|
|
6
|
+
export class CounterTokenSource {
|
|
7
|
+
#last = 0n;
|
|
8
|
+
async next(_key) {
|
|
9
|
+
this.#last++;
|
|
10
|
+
return new FencingToken(this.#last);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=counter-token-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counter-token-source.js","sourceRoot":"","sources":["../../src/fencing/counter-token-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IAC7B,KAAK,GAAG,EAAE,CAAC;IAEX,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FencingToken } from './fencing-token.ts';
|
|
2
|
+
import type { TokenSource } from './token-source.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Packs `epoch << 32 | sequence`, so every token minted in a newer epoch beats
|
|
5
|
+
* every token from an older one: a new leader outranks all of its
|
|
6
|
+
* predecessor's grants. The result fits a signed 64-bit integer.
|
|
7
|
+
*/
|
|
8
|
+
export declare class EpochTokenSource implements TokenSource {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(epoch: bigint);
|
|
11
|
+
next(_key: string): Promise<FencingToken>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=epoch-token-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"epoch-token-source.d.ts","sourceRoot":"","sources":["../../src/fencing/epoch-token-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAMrD;;;;GAIG;AACH,qBAAa,gBAAiB,YAAW,WAAW;;gBAItC,KAAK,EAAE,MAAM;IAOnB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAOhD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { FencingToken } from "./fencing-token.js";
|
|
2
|
+
const SEQUENCE_BITS = 32n;
|
|
3
|
+
const EPOCH_LIMIT = 1n << 31n;
|
|
4
|
+
const SEQUENCE_LIMIT = 1n << SEQUENCE_BITS;
|
|
5
|
+
/**
|
|
6
|
+
* Packs `epoch << 32 | sequence`, so every token minted in a newer epoch beats
|
|
7
|
+
* every token from an older one: a new leader outranks all of its
|
|
8
|
+
* predecessor's grants. The result fits a signed 64-bit integer.
|
|
9
|
+
*/
|
|
10
|
+
export class EpochTokenSource {
|
|
11
|
+
#epoch;
|
|
12
|
+
#sequence = 0n;
|
|
13
|
+
constructor(epoch) {
|
|
14
|
+
if (epoch < 0n || epoch >= EPOCH_LIMIT) {
|
|
15
|
+
throw new RangeError(`Epoch ${epoch} is outside [0, ${EPOCH_LIMIT}).`);
|
|
16
|
+
}
|
|
17
|
+
this.#epoch = epoch;
|
|
18
|
+
}
|
|
19
|
+
async next(_key) {
|
|
20
|
+
this.#sequence++;
|
|
21
|
+
if (this.#sequence >= SEQUENCE_LIMIT) {
|
|
22
|
+
throw new RangeError(`Epoch ${this.#epoch} has no tokens left.`);
|
|
23
|
+
}
|
|
24
|
+
return new FencingToken((this.#epoch << SEQUENCE_BITS) | this.#sequence);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=epoch-token-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"epoch-token-source.js","sourceRoot":"","sources":["../../src/fencing/epoch-token-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,WAAW,GAAG,EAAE,IAAI,GAAG,CAAC;AAC9B,MAAM,cAAc,GAAG,EAAE,IAAI,aAAa,CAAC;AAE3C;;;;GAIG;AACH,MAAM,OAAO,gBAAgB;IAClB,MAAM,CAAS;IACxB,SAAS,GAAG,EAAE,CAAC;IAEf,YAAY,KAAa;QACvB,IAAI,KAAK,GAAG,EAAE,IAAI,KAAK,IAAI,WAAW,EAAE,CAAC;YACvC,MAAM,IAAI,UAAU,CAAC,SAAS,KAAK,mBAAmB,WAAW,IAAI,CAAC,CAAC;QACzE,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,IAAI,CAAC,SAAS,EAAE,CAAC;QACjB,IAAI,IAAI,CAAC,SAAS,IAAI,cAAc,EAAE,CAAC;YACrC,MAAM,IAAI,UAAU,CAAC,SAAS,IAAI,CAAC,MAAM,sBAAsB,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,IAAI,YAAY,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,aAAa,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;IAC3E,CAAC;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grows with every grant of a lock. A resource that rejects writes carrying a
|
|
3
|
+
* token older than the newest it has seen ignores holders that were superseded
|
|
4
|
+
* without knowing it (frozen, partitioned, or outlived by a failover).
|
|
5
|
+
*/
|
|
6
|
+
export declare class FencingToken {
|
|
7
|
+
readonly value: bigint;
|
|
8
|
+
constructor(value: bigint);
|
|
9
|
+
isNewerThan(other: FencingToken): boolean;
|
|
10
|
+
toString(): string;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=fencing-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fencing-token.d.ts","sourceRoot":"","sources":["../../src/fencing/fencing-token.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,YAAY;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,KAAK,EAAE,MAAM;IAIzB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAIzC,QAAQ,IAAI,MAAM;CAGnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grows with every grant of a lock. A resource that rejects writes carrying a
|
|
3
|
+
* token older than the newest it has seen ignores holders that were superseded
|
|
4
|
+
* without knowing it (frozen, partitioned, or outlived by a failover).
|
|
5
|
+
*/
|
|
6
|
+
export class FencingToken {
|
|
7
|
+
value;
|
|
8
|
+
constructor(value) {
|
|
9
|
+
this.value = value;
|
|
10
|
+
}
|
|
11
|
+
isNewerThan(other) {
|
|
12
|
+
return this.value > other.value;
|
|
13
|
+
}
|
|
14
|
+
toString() {
|
|
15
|
+
return this.value.toString();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=fencing-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fencing-token.js","sourceRoot":"","sources":["../../src/fencing/fencing-token.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,YAAY;IACd,KAAK,CAAS;IAEvB,YAAY,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,WAAW,CAAC,KAAmB;QAC7B,OAAO,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAClC,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FencingToken } from './fencing-token.ts';
|
|
2
|
+
import type { TokenSource } from './token-source.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Keeps one counter file per key in `directory`, so tokens keep growing across
|
|
5
|
+
* process restarts and can fence durable resources. Relies on the caller holding
|
|
6
|
+
* the key's lock: read, increment and replace are not atomic on their own.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FileTokenSource implements TokenSource {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(directory: string);
|
|
11
|
+
next(key: string): Promise<FencingToken>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=file-token-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-token-source.d.ts","sourceRoot":"","sources":["../../src/fencing/file-token-source.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,WAAW;;gBAGrC,SAAS,EAAE,MAAM;IAIvB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAO/C"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { atomicWrite } from "../shared/fs/atomic-write.js";
|
|
4
|
+
import { isErrno } from "../shared/fs/errno.js";
|
|
5
|
+
import { safeFileName } from "../shared/fs/safe-file-name.js";
|
|
6
|
+
import { FencingToken } from "./fencing-token.js";
|
|
7
|
+
/**
|
|
8
|
+
* Keeps one counter file per key in `directory`, so tokens keep growing across
|
|
9
|
+
* process restarts and can fence durable resources. Relies on the caller holding
|
|
10
|
+
* the key's lock: read, increment and replace are not atomic on their own.
|
|
11
|
+
*/
|
|
12
|
+
export class FileTokenSource {
|
|
13
|
+
#directory;
|
|
14
|
+
constructor(directory) {
|
|
15
|
+
this.#directory = directory;
|
|
16
|
+
}
|
|
17
|
+
async next(key) {
|
|
18
|
+
await mkdir(this.#directory, { recursive: true });
|
|
19
|
+
const path = join(this.#directory, `${safeFileName(key)}.fence`);
|
|
20
|
+
const token = (await readCounter(path)) + 1n;
|
|
21
|
+
await atomicWrite(path, token.toString());
|
|
22
|
+
return new FencingToken(token);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readCounter(path) {
|
|
26
|
+
try {
|
|
27
|
+
return BigInt(await readFile(path, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (isErrno(error, 'ENOENT'))
|
|
31
|
+
return 0n;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=file-token-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-token-source.js","sourceRoot":"","sources":["../../src/fencing/file-token-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACjB,UAAU,CAAS;IAE5B,YAAY,SAAiB;QAC3B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAW;QACpB,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjE,MAAM,KAAK,GAAG,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;QAC7C,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC1C,OAAO,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;CACF;AAED,KAAK,UAAU,WAAW,CAAC,IAAY;IACrC,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACxC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FencingToken } from './fencing-token.ts';
|
|
2
|
+
import type { TokenSource } from './token-source.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Reads the process-wide monotonic clock, which every worker thread shares, so
|
|
5
|
+
* grants serialized by a lock read strictly later times without shared memory.
|
|
6
|
+
* Tokens restart when the process does.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MonotonicClockTokenSource implements TokenSource {
|
|
9
|
+
#private;
|
|
10
|
+
next(_key: string): Promise<FencingToken>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=monotonic-clock-token-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monotonic-clock-token-source.d.ts","sourceRoot":"","sources":["../../src/fencing/monotonic-clock-token-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,qBAAa,yBAA0B,YAAW,WAAW;;IAGrD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAKhD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FencingToken } from "./fencing-token.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads the process-wide monotonic clock, which every worker thread shares, so
|
|
4
|
+
* grants serialized by a lock read strictly later times without shared memory.
|
|
5
|
+
* Tokens restart when the process does.
|
|
6
|
+
*/
|
|
7
|
+
export class MonotonicClockTokenSource {
|
|
8
|
+
#last = 0n;
|
|
9
|
+
async next(_key) {
|
|
10
|
+
const now = process.hrtime.bigint();
|
|
11
|
+
this.#last = now > this.#last ? now : this.#last + 1n;
|
|
12
|
+
return new FencingToken(this.#last);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=monotonic-clock-token-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monotonic-clock-token-source.js","sourceRoot":"","sources":["../../src/fencing/monotonic-clock-token-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;;;GAIG;AACH,MAAM,OAAO,yBAAyB;IACpC,KAAK,GAAG,EAAE,CAAC;IAEX,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QACtD,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FencingToken } from './fencing-token.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Mints fencing tokens. Callers hold the key's lock while calling `next`, so
|
|
4
|
+
* minting for one key is never concurrent and each token is newer than the last.
|
|
5
|
+
*/
|
|
6
|
+
export interface TokenSource {
|
|
7
|
+
next(key: string): Promise<FencingToken>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=token-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-source.d.ts","sourceRoot":"","sources":["../../src/fencing/token-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-source.js","sourceRoot":"","sources":["../../src/fencing/token-source.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { Mutex } from './mutex/mutex.ts';
|
|
2
|
+
export { Key } from './mutex/key.ts';
|
|
3
|
+
export { leaseFor, type Lease } from './mutex/lease.ts';
|
|
4
|
+
export type { AcquireOptions, LockStore } from './mutex/lock-store.ts';
|
|
5
|
+
export { LockLostError } from './mutex/lock-lost-error.ts';
|
|
6
|
+
export type { Acquired, AcquireMode, ModeResult, NotAcquired, Outcome, OutcomeResults, } from './mutex/acquire-mode.ts';
|
|
7
|
+
export { Modes } from './mutex/acquire-modes/modes.ts';
|
|
8
|
+
export { WaitMode } from './mutex/acquire-modes/wait-mode.ts';
|
|
9
|
+
export { SkipIfBusyMode, type SkipIfBusyOptions, } from './mutex/acquire-modes/skip-if-busy-mode.ts';
|
|
10
|
+
export { FencingToken } from './fencing/fencing-token.ts';
|
|
11
|
+
export type { TokenSource } from './fencing/token-source.ts';
|
|
12
|
+
export { CounterTokenSource } from './fencing/counter-token-source.ts';
|
|
13
|
+
export { FileTokenSource } from './fencing/file-token-source.ts';
|
|
14
|
+
export { MonotonicClockTokenSource } from './fencing/monotonic-clock-token-source.ts';
|
|
15
|
+
export { EpochTokenSource } from './fencing/epoch-token-source.ts';
|
|
16
|
+
export { MemoryStore, type MemoryStoreOptions, } from './lock-stores/memory/memory-store.ts';
|
|
17
|
+
export { ThreadLockCoordinator, type ThreadLockCoordinatorOptions, } from './lock-stores/thread/thread-lock-coordinator.ts';
|
|
18
|
+
export { ThreadStore } from './lock-stores/thread/thread-store.ts';
|
|
19
|
+
export { IpcLockCoordinator, type IpcLockCoordinatorOptions, } from './lock-stores/ipc/ipc-lock-coordinator.ts';
|
|
20
|
+
export { IpcStore } from './lock-stores/ipc/ipc-store.ts';
|
|
21
|
+
export { CoordinatorUnavailableError } from './lock-stores/remote/coordinator-unavailable-error.ts';
|
|
22
|
+
export { FileLockStore, type FileLockStoreOptions, } from './lock-stores/file-system/file-lock-store.ts';
|
|
23
|
+
export { TicketQueueFileStore } from './lock-stores/file-system/ticket-queue-file-store.ts';
|
|
24
|
+
export { LockFileStore } from './lock-stores/file-system/lock-file-store.ts';
|
|
25
|
+
export { SqliteStore } from './lock-stores/sqlite/sqlite-store.ts';
|
|
26
|
+
export { SocketStore, type SocketRole, type SocketStoreOptions, } from './lock-stores/socket/socket-store.ts';
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,OAAO,EACP,cAAc,GACf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAC9D,OAAO,EACL,cAAc,EACd,KAAK,iBAAiB,GACvB,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAEnE,OAAO,EACL,WAAW,EACX,KAAK,kBAAkB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,GAClC,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACnE,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,uDAAuD,CAAC;AACpG,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,GAC1B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sDAAsD,CAAC;AAC5F,OAAO,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACnE,OAAO,EACL,WAAW,EACX,KAAK,UAAU,EACf,KAAK,kBAAkB,GACxB,MAAM,sCAAsC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { Mutex } from "./mutex/mutex.js";
|
|
2
|
+
export { Key } from "./mutex/key.js";
|
|
3
|
+
export { leaseFor } from "./mutex/lease.js";
|
|
4
|
+
export { LockLostError } from "./mutex/lock-lost-error.js";
|
|
5
|
+
export { Modes } from "./mutex/acquire-modes/modes.js";
|
|
6
|
+
export { WaitMode } from "./mutex/acquire-modes/wait-mode.js";
|
|
7
|
+
export { SkipIfBusyMode, } from "./mutex/acquire-modes/skip-if-busy-mode.js";
|
|
8
|
+
export { FencingToken } from "./fencing/fencing-token.js";
|
|
9
|
+
export { CounterTokenSource } from "./fencing/counter-token-source.js";
|
|
10
|
+
export { FileTokenSource } from "./fencing/file-token-source.js";
|
|
11
|
+
export { MonotonicClockTokenSource } from "./fencing/monotonic-clock-token-source.js";
|
|
12
|
+
export { EpochTokenSource } from "./fencing/epoch-token-source.js";
|
|
13
|
+
export { MemoryStore, } from "./lock-stores/memory/memory-store.js";
|
|
14
|
+
export { ThreadLockCoordinator, } from "./lock-stores/thread/thread-lock-coordinator.js";
|
|
15
|
+
export { ThreadStore } from "./lock-stores/thread/thread-store.js";
|
|
16
|
+
export { IpcLockCoordinator, } from "./lock-stores/ipc/ipc-lock-coordinator.js";
|
|
17
|
+
export { IpcStore } from "./lock-stores/ipc/ipc-store.js";
|
|
18
|
+
export { CoordinatorUnavailableError } from "./lock-stores/remote/coordinator-unavailable-error.js";
|
|
19
|
+
export { FileLockStore, } from "./lock-stores/file-system/file-lock-store.js";
|
|
20
|
+
export { TicketQueueFileStore } from "./lock-stores/file-system/ticket-queue-file-store.js";
|
|
21
|
+
export { LockFileStore } from "./lock-stores/file-system/lock-file-store.js";
|
|
22
|
+
export { SqliteStore } from "./lock-stores/sqlite/sqlite-store.js";
|
|
23
|
+
export { SocketStore, } from "./lock-stores/socket/socket-store.js";
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAc,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAS3D,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAC9D,OAAO,EACL,cAAc,GAEf,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAEnE,OAAO,EACL,WAAW,GAEZ,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,qBAAqB,GAEtB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACnE,OAAO,EACL,kBAAkB,GAEnB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,uDAAuD,CAAC;AACpG,OAAO,EACL,aAAa,GAEd,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sDAAsD,CAAC;AAC5F,OAAO,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACnE,OAAO,EACL,WAAW,GAGZ,MAAM,sCAAsC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/leader-election/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/leader-election/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Leadership } from './leadership.ts';
|
|
2
|
+
export interface LeaderElectionOptions {
|
|
3
|
+
/** Milliseconds between attempts while another process leads. */
|
|
4
|
+
pollInterval?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CampaignOptions {
|
|
7
|
+
/** Milliseconds to keep trying before conceding to the current leader. */
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Elects one leader among the processes sharing `directory`. The claim is an
|
|
12
|
+
* exclusive SQLite transaction on `leader.lock`, held for the whole term, so the
|
|
13
|
+
* kernel ends the term if the leader dies. Each term gets a higher epoch than
|
|
14
|
+
* every term before it, recorded in `leader.epoch`. Never delete `leader.lock`
|
|
15
|
+
* while campaigners run: a new file would let a second leader win.
|
|
16
|
+
*/
|
|
17
|
+
export declare class LeaderElection {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(directory: string, { pollInterval }?: LeaderElectionOptions);
|
|
20
|
+
/** Resolves with the new term, or `undefined` if another process still leads after `timeout`. */
|
|
21
|
+
campaign({ timeout }?: CampaignOptions): Promise<Leadership | undefined>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=leader-election.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leader-election.d.ts","sourceRoot":"","sources":["../../src/leader-election/leader-election.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,qBAAa,cAAc;;gBAKvB,SAAS,EAAE,MAAM,EACjB,EAAE,YAAiB,EAAE,GAAE,qBAA0B;IAMnD,iGAAiG;IAC3F,QAAQ,CAAC,EAAE,OAAW,EAAE,GAAE,eAAoB,GAAG,OAAO,CAC5D,UAAU,GAAG,SAAS,CACvB;CAwCF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { atomicWrite } from "../shared/fs/atomic-write.js";
|
|
6
|
+
import { isErrno } from "../shared/fs/errno.js";
|
|
7
|
+
import { isBusy } from "../shared/sqlite/is-busy.js";
|
|
8
|
+
import { Leadership } from "./leadership.js";
|
|
9
|
+
/**
|
|
10
|
+
* Elects one leader among the processes sharing `directory`. The claim is an
|
|
11
|
+
* exclusive SQLite transaction on `leader.lock`, held for the whole term, so the
|
|
12
|
+
* kernel ends the term if the leader dies. Each term gets a higher epoch than
|
|
13
|
+
* every term before it, recorded in `leader.epoch`. Never delete `leader.lock`
|
|
14
|
+
* while campaigners run: a new file would let a second leader win.
|
|
15
|
+
*/
|
|
16
|
+
export class LeaderElection {
|
|
17
|
+
#directory;
|
|
18
|
+
#pollInterval;
|
|
19
|
+
constructor(directory, { pollInterval = 10 } = {}) {
|
|
20
|
+
this.#directory = directory;
|
|
21
|
+
this.#pollInterval = pollInterval;
|
|
22
|
+
}
|
|
23
|
+
/** Resolves with the new term, or `undefined` if another process still leads after `timeout`. */
|
|
24
|
+
async campaign({ timeout = 0 } = {}) {
|
|
25
|
+
await mkdir(this.#directory, { recursive: true });
|
|
26
|
+
// A busy timeout above zero would block this process's event loop while it
|
|
27
|
+
// waits, so the claim is retried here instead.
|
|
28
|
+
const claim = new DatabaseSync(join(this.#directory, 'leader.lock'), {
|
|
29
|
+
timeout: 0,
|
|
30
|
+
});
|
|
31
|
+
const deadline = performance.now() + timeout;
|
|
32
|
+
try {
|
|
33
|
+
for (;;) {
|
|
34
|
+
if (this.#tryClaim(claim))
|
|
35
|
+
return new Leadership(await this.#nextEpoch(), claim);
|
|
36
|
+
if (performance.now() >= deadline)
|
|
37
|
+
break;
|
|
38
|
+
await delay(this.#pollInterval);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
claim.close();
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
claim.close();
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
#tryClaim(claim) {
|
|
49
|
+
try {
|
|
50
|
+
claim.exec('BEGIN EXCLUSIVE');
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (isBusy(error))
|
|
55
|
+
return false;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Safe without further locking: only the claim holder gets here. */
|
|
60
|
+
async #nextEpoch() {
|
|
61
|
+
const path = join(this.#directory, 'leader.epoch');
|
|
62
|
+
const epoch = (await readEpoch(path)) + 1n;
|
|
63
|
+
await atomicWrite(path, epoch.toString());
|
|
64
|
+
return epoch;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function readEpoch(path) {
|
|
68
|
+
try {
|
|
69
|
+
return BigInt(await readFile(path, 'utf8'));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (isErrno(error, 'ENOENT'))
|
|
73
|
+
return 0n;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=leader-election.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leader-election.js","sourceRoot":"","sources":["../../src/leader-election/leader-election.ts"],"names":[],"mappings":"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"}
|