@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* One term as leader. It lasts until `resign` or until the process dies, when
|
|
4
|
+
* the kernel releases the claim and another campaigner can win.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Leadership implements AsyncDisposable {
|
|
7
|
+
#private;
|
|
8
|
+
/** Grows with every term, so a newer leader can always outrank an older one. */
|
|
9
|
+
readonly epoch: bigint;
|
|
10
|
+
constructor(epoch: bigint, claim: DatabaseSync);
|
|
11
|
+
resign(): Promise<void>;
|
|
12
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=leadership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leadership.d.ts","sourceRoot":"","sources":["../../src/leader-election/leadership.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;GAGG;AACH,qBAAa,UAAW,YAAW,eAAe;;IAChD,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAGX,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY;IAKxC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAM7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;CAGvC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One term as leader. It lasts until `resign` or until the process dies, when
|
|
3
|
+
* the kernel releases the claim and another campaigner can win.
|
|
4
|
+
*/
|
|
5
|
+
export class Leadership {
|
|
6
|
+
/** Grows with every term, so a newer leader can always outrank an older one. */
|
|
7
|
+
epoch;
|
|
8
|
+
#claim;
|
|
9
|
+
constructor(epoch, claim) {
|
|
10
|
+
this.epoch = epoch;
|
|
11
|
+
this.#claim = claim;
|
|
12
|
+
}
|
|
13
|
+
async resign() {
|
|
14
|
+
if (!this.#claim.isOpen)
|
|
15
|
+
return;
|
|
16
|
+
this.#claim.exec('ROLLBACK');
|
|
17
|
+
this.#claim.close();
|
|
18
|
+
}
|
|
19
|
+
[Symbol.asyncDispose]() {
|
|
20
|
+
return this.resign();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=leadership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leadership.js","sourceRoot":"","sources":["../../src/leader-election/leadership.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,OAAO,UAAU;IACrB,gFAAgF;IACvE,KAAK,CAAS;IACd,MAAM,CAAe;IAE9B,YAAY,KAAa,EAAE,KAAmB;QAC5C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,OAAO;QAChC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAED,CAAC,MAAM,CAAC,YAAY,CAAC;QACnB,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;IACvB,CAAC;CACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TokenSource } from '../../fencing/token-source.ts';
|
|
2
|
+
import { type Lease } from '../../mutex/lease.ts';
|
|
3
|
+
import type { AcquireOptions, LockStore } from '../../mutex/lock-store.ts';
|
|
4
|
+
export interface FileLockStoreOptions {
|
|
5
|
+
/** Milliseconds a waiter sleeps between attempts. */
|
|
6
|
+
pollInterval?: number;
|
|
7
|
+
/** Defaults to durable per-key counter files beside the locks. */
|
|
8
|
+
tokens?: TokenSource;
|
|
9
|
+
}
|
|
10
|
+
export interface PollOptions {
|
|
11
|
+
/** Stops between attempts when it aborts; the poll then rejects with `signal.reason`. */
|
|
12
|
+
signal?: AbortSignal | undefined;
|
|
13
|
+
/** Whether the wait between attempts keeps the process alive. */
|
|
14
|
+
keepAlive?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Shares one lock directory between processes: each key maps to `<directory>/<key>.lock`. */
|
|
17
|
+
export declare abstract class FileLockStore implements LockStore {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(directory: string, { pollInterval, tokens, }?: FileLockStoreOptions);
|
|
20
|
+
acquire(key: string, { signal }?: AcquireOptions): Promise<Lease>;
|
|
21
|
+
tryAcquire(key: string): Promise<Lease | undefined>;
|
|
22
|
+
/** Holds the lock at `path`, waiting for other holders, until `signal` aborts. */
|
|
23
|
+
protected abstract lock(path: string, signal: AbortSignal | undefined): Promise<AsyncDisposable>;
|
|
24
|
+
/** Holds the lock at `path` only if no other holder has it now. */
|
|
25
|
+
protected abstract tryLock(path: string): Promise<AsyncDisposable | undefined>;
|
|
26
|
+
protected poll<T>(attempt: () => Promise<T | undefined>, { signal, keepAlive }?: PollOptions): Promise<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Serializes evicting a dead holder. Without it, two waiters that saw the
|
|
29
|
+
* same dead holder could each remove a lock the other had just taken.
|
|
30
|
+
* A process dying inside `task` leaves `<path>.reclaim` behind, which then
|
|
31
|
+
* has to be removed by hand.
|
|
32
|
+
*/
|
|
33
|
+
protected withReclaimLock(path: string, task: () => Promise<void>): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=file-lock-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/file-system/file-lock-store.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,KAAK,KAAK,EAAY,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAK3E,MAAM,WAAW,oBAAoB;IACnC,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACjC,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,8FAA8F;AAC9F,8BAAsB,aAAc,YAAW,SAAS;;gBAMpD,SAAS,EAAE,MAAM,EACjB,EACE,YAAiB,EACjB,MAAuC,GACxC,GAAE,oBAAyB;IAOxB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,GAAE,cAAmB,GAAG,OAAO,CAAC,KAAK,CAAC;IAMrE,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;IAKzD,kFAAkF;IAClF,SAAS,CAAC,QAAQ,CAAC,IAAI,CACrB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,eAAe,CAAC;IAE3B,mEAAmE;IACnE,SAAS,CAAC,QAAQ,CAAC,OAAO,CACxB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;cAEvB,IAAI,CAAC,CAAC,EACpB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,EACrC,EAAE,MAAM,EAAE,SAAgB,EAAE,GAAE,WAAgB,GAC7C,OAAO,CAAC,CAAC,CAAC;IAab;;;;;OAKG;cACa,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC;CAcxE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdir, unlink } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { FileTokenSource } from "../../fencing/file-token-source.js";
|
|
5
|
+
import { leaseFor } from "../../mutex/lease.js";
|
|
6
|
+
import { createExclusive } from "../../shared/fs/create-exclusive.js";
|
|
7
|
+
import { safeFileName } from "../../shared/fs/safe-file-name.js";
|
|
8
|
+
import { Owner } from "./owner.js";
|
|
9
|
+
/** Shares one lock directory between processes: each key maps to `<directory>/<key>.lock`. */
|
|
10
|
+
export class FileLockStore {
|
|
11
|
+
#directory;
|
|
12
|
+
#pollInterval;
|
|
13
|
+
#tokens;
|
|
14
|
+
constructor(directory, { pollInterval = 10, tokens = new FileTokenSource(directory), } = {}) {
|
|
15
|
+
this.#directory = directory;
|
|
16
|
+
this.#pollInterval = pollInterval;
|
|
17
|
+
this.#tokens = tokens;
|
|
18
|
+
}
|
|
19
|
+
async acquire(key, { signal } = {}) {
|
|
20
|
+
signal?.throwIfAborted();
|
|
21
|
+
const held = await this.lock(await this.#pathFor(key), signal);
|
|
22
|
+
return leaseFor(key, held, this.#tokens);
|
|
23
|
+
}
|
|
24
|
+
async tryAcquire(key) {
|
|
25
|
+
const held = await this.tryLock(await this.#pathFor(key));
|
|
26
|
+
return held && leaseFor(key, held, this.#tokens);
|
|
27
|
+
}
|
|
28
|
+
async poll(attempt, { signal, keepAlive = true } = {}) {
|
|
29
|
+
for (;;) {
|
|
30
|
+
const result = await attempt();
|
|
31
|
+
if (result !== undefined)
|
|
32
|
+
return result;
|
|
33
|
+
try {
|
|
34
|
+
await delay(this.#pollInterval, undefined, { signal, ref: keepAlive });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Serializes evicting a dead holder. Without it, two waiters that saw the
|
|
44
|
+
* same dead holder could each remove a lock the other had just taken.
|
|
45
|
+
* A process dying inside `task` leaves `<path>.reclaim` behind, which then
|
|
46
|
+
* has to be removed by hand.
|
|
47
|
+
*/
|
|
48
|
+
async withReclaimLock(path, task) {
|
|
49
|
+
const reclaim = `${path}.reclaim`;
|
|
50
|
+
if (!(await createExclusive(reclaim, Owner.current().serialize())))
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
await task();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await unlink(reclaim);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async #pathFor(key) {
|
|
60
|
+
await mkdir(this.#directory, { recursive: true });
|
|
61
|
+
return join(this.#directory, `${safeFileName(key)}.lock`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=file-lock-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock-store.js","sourceRoot":"","sources":["../../../src/lock-stores/file-system/file-lock-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FileLockStore } from './file-lock-store.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Whoever creates the lock file holds the key; releasing deletes it. Waiters
|
|
4
|
+
* retry in no particular order, so this store is not FIFO.
|
|
5
|
+
*/
|
|
6
|
+
export declare class LockFileStore extends FileLockStore {
|
|
7
|
+
#private;
|
|
8
|
+
protected lock(path: string, signal: AbortSignal | undefined): Promise<AsyncDisposable>;
|
|
9
|
+
protected tryLock(path: string): Promise<AsyncDisposable | undefined>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=lock-file-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-file-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/file-system/lock-file-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAGrD;;;GAGG;AACH,qBAAa,aAAc,SAAQ,aAAa;;IAC9C,SAAS,CAAC,IAAI,CACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,eAAe,CAAC;cAKX,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;CAsB5E"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
2
|
+
import { createExclusive } from "../../shared/fs/create-exclusive.js";
|
|
3
|
+
import { isErrno } from "../../shared/fs/errno.js";
|
|
4
|
+
import { FileLockStore } from "./file-lock-store.js";
|
|
5
|
+
import { Owner } from "./owner.js";
|
|
6
|
+
/**
|
|
7
|
+
* Whoever creates the lock file holds the key; releasing deletes it. Waiters
|
|
8
|
+
* retry in no particular order, so this store is not FIFO.
|
|
9
|
+
*/
|
|
10
|
+
export class LockFileStore extends FileLockStore {
|
|
11
|
+
lock(path, signal) {
|
|
12
|
+
const me = Owner.current();
|
|
13
|
+
return this.poll(() => this.#attempt(path, me), { signal });
|
|
14
|
+
}
|
|
15
|
+
async tryLock(path) {
|
|
16
|
+
const me = Owner.current();
|
|
17
|
+
// An attempt that finds a dead holder removes it, so one more attempt can then succeed.
|
|
18
|
+
return (await this.#attempt(path, me)) ?? this.#attempt(path, me);
|
|
19
|
+
}
|
|
20
|
+
async #attempt(path, me) {
|
|
21
|
+
if (await createExclusive(path, me.serialize())) {
|
|
22
|
+
return { [Symbol.asyncDispose]: () => unlink(path) };
|
|
23
|
+
}
|
|
24
|
+
const holder = await readHolder(path);
|
|
25
|
+
if (holder && !holder.isAlive()) {
|
|
26
|
+
await this.withReclaimLock(path, async () => {
|
|
27
|
+
if ((await readHolder(path))?.id === holder.id)
|
|
28
|
+
await unlink(path);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function readHolder(path) {
|
|
35
|
+
try {
|
|
36
|
+
return Owner.parse(await readFile(path, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isErrno(error, 'ENOENT'))
|
|
40
|
+
return undefined;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=lock-file-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-file-store.js","sourceRoot":"","sources":["../../../src/lock-stores/file-system/lock-file-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC;;;GAGG;AACH,MAAM,OAAO,aAAc,SAAQ,aAAa;IACpC,IAAI,CACZ,IAAY,EACZ,MAA+B;QAE/B,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9D,CAAC;IAES,KAAK,CAAC,OAAO,CAAC,IAAY;QAClC,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;QAC3B,wFAAwF;QACxF,OAAO,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,QAAQ,CACZ,IAAY,EACZ,EAAS;QAET,IAAI,MAAM,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC;YAChD,OAAO,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACvD,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;YAChC,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;gBAC1C,IAAI,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,KAAK,MAAM,CAAC,EAAE;oBAAE,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;YACrE,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF;AAED,KAAK,UAAU,UAAU,CAAC,IAAY;IACpC,IAAI,CAAC;QACH,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAC;QAC/C,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifies one lock request and the process that made it, so waiters can
|
|
3
|
+
* evict a holder whose process died. A reused pid only delays recovery; it
|
|
4
|
+
* never breaks exclusion. Holders on another host are never evicted.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Owner {
|
|
7
|
+
readonly pid: number;
|
|
8
|
+
readonly host: string;
|
|
9
|
+
readonly id: string;
|
|
10
|
+
constructor(pid: number, host: string, id: string);
|
|
11
|
+
static current(): Owner;
|
|
12
|
+
static parse(serialized: string): Owner;
|
|
13
|
+
serialize(): string;
|
|
14
|
+
isAlive(): boolean;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=owner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/file-system/owner.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,qBAAa,KAAK;IAChB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAER,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;IAMjD,MAAM,CAAC,OAAO,IAAI,KAAK;IAIvB,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK;IAavC,SAAS,IAAI,MAAM;IAInB,OAAO,IAAI,OAAO;CASnB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { isErrno } from "../../shared/fs/errno.js";
|
|
4
|
+
import { isRecord } from "../../shared/is-record.js";
|
|
5
|
+
/**
|
|
6
|
+
* Identifies one lock request and the process that made it, so waiters can
|
|
7
|
+
* evict a holder whose process died. A reused pid only delays recovery; it
|
|
8
|
+
* never breaks exclusion. Holders on another host are never evicted.
|
|
9
|
+
*/
|
|
10
|
+
export class Owner {
|
|
11
|
+
pid;
|
|
12
|
+
host;
|
|
13
|
+
id;
|
|
14
|
+
constructor(pid, host, id) {
|
|
15
|
+
this.pid = pid;
|
|
16
|
+
this.host = host;
|
|
17
|
+
this.id = id;
|
|
18
|
+
}
|
|
19
|
+
static current() {
|
|
20
|
+
return new Owner(process.pid, hostname(), randomUUID());
|
|
21
|
+
}
|
|
22
|
+
static parse(serialized) {
|
|
23
|
+
const parsed = JSON.parse(serialized);
|
|
24
|
+
if (!isRecord(parsed) ||
|
|
25
|
+
typeof parsed.pid !== 'number' ||
|
|
26
|
+
typeof parsed.host !== 'string' ||
|
|
27
|
+
typeof parsed.id !== 'string') {
|
|
28
|
+
throw new SyntaxError(`Not a lock owner: ${serialized}`);
|
|
29
|
+
}
|
|
30
|
+
return new Owner(parsed.pid, parsed.host, parsed.id);
|
|
31
|
+
}
|
|
32
|
+
serialize() {
|
|
33
|
+
return JSON.stringify({ pid: this.pid, host: this.host, id: this.id });
|
|
34
|
+
}
|
|
35
|
+
isAlive() {
|
|
36
|
+
if (this.host !== hostname())
|
|
37
|
+
return true;
|
|
38
|
+
try {
|
|
39
|
+
process.kill(this.pid, 0);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return !isErrno(error, 'ESRCH');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner.js","sourceRoot":"","sources":["../../../src/lock-stores/file-system/owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAErD;;;;GAIG;AACH,MAAM,OAAO,KAAK;IACP,GAAG,CAAS;IACZ,IAAI,CAAS;IACb,EAAE,CAAS;IAEpB,YAAY,GAAW,EAAE,IAAY,EAAE,EAAU;QAC/C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,CAAC;IAED,MAAM,CAAC,OAAO;QACZ,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,UAAkB;QAC7B,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/C,IACE,CAAC,QAAQ,CAAC,MAAM,CAAC;YACjB,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;YAC9B,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;YAC/B,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,EAC7B,CAAC;YACD,MAAM,IAAI,WAAW,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FileLockStore } from './file-lock-store.ts';
|
|
2
|
+
/**
|
|
3
|
+
* FIFO queue in one file: each waiter appends its ticket line and holds the
|
|
4
|
+
* lock once its ticket is first.
|
|
5
|
+
*
|
|
6
|
+
* Appends are atomic (O_APPEND), and the file is only ever replaced by the
|
|
7
|
+
* live head releasing or by the reclaim-lock holder evicting a dead head, so
|
|
8
|
+
* two rewrites never race. A rewrite racing an append can drop that ticket;
|
|
9
|
+
* its waiter notices and appends it again.
|
|
10
|
+
*/
|
|
11
|
+
export declare class TicketQueueFileStore extends FileLockStore {
|
|
12
|
+
#private;
|
|
13
|
+
protected lock(path: string, signal: AbortSignal | undefined): Promise<AsyncDisposable>;
|
|
14
|
+
protected tryLock(path: string): Promise<AsyncDisposable | undefined>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=ticket-queue-file-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-queue-file-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/file-system/ticket-queue-file-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAGrD;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;;cACrC,IAAI,CAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,eAAe,CAAC;cAWX,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;CA6D5E"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { appendFile, readFile } from 'node:fs/promises';
|
|
2
|
+
import { atomicWrite } from "../../shared/fs/atomic-write.js";
|
|
3
|
+
import { isErrno } from "../../shared/fs/errno.js";
|
|
4
|
+
import { FileLockStore } from "./file-lock-store.js";
|
|
5
|
+
import { Owner } from "./owner.js";
|
|
6
|
+
/**
|
|
7
|
+
* FIFO queue in one file: each waiter appends its ticket line and holds the
|
|
8
|
+
* lock once its ticket is first.
|
|
9
|
+
*
|
|
10
|
+
* Appends are atomic (O_APPEND), and the file is only ever replaced by the
|
|
11
|
+
* live head releasing or by the reclaim-lock holder evicting a dead head, so
|
|
12
|
+
* two rewrites never race. A rewrite racing an append can drop that ticket;
|
|
13
|
+
* its waiter notices and appends it again.
|
|
14
|
+
*/
|
|
15
|
+
export class TicketQueueFileStore extends FileLockStore {
|
|
16
|
+
async lock(path, signal) {
|
|
17
|
+
const me = Owner.current();
|
|
18
|
+
await enqueue(path, me);
|
|
19
|
+
try {
|
|
20
|
+
return await this.poll(() => this.#attempt(path, me), { signal });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
this.#leaveWhenFirst(path, me);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async tryLock(path) {
|
|
28
|
+
const [head] = await readTickets(path);
|
|
29
|
+
await this.#evictIfDead(path, head);
|
|
30
|
+
if ((await readTickets(path)).length > 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
const me = Owner.current();
|
|
33
|
+
await enqueue(path, me);
|
|
34
|
+
const [first] = await readTickets(path);
|
|
35
|
+
if (first?.id === me.id)
|
|
36
|
+
return holding(path, me);
|
|
37
|
+
// Another waiter appended at the same moment and is first.
|
|
38
|
+
this.#leaveWhenFirst(path, me);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
async #evictIfDead(path, head) {
|
|
42
|
+
if (head && !head.isAlive()) {
|
|
43
|
+
await this.withReclaimLock(path, () => evictDeadHead(path));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async #attempt(path, me) {
|
|
47
|
+
const tickets = await readTickets(path);
|
|
48
|
+
if (!tickets.some((ticket) => ticket.id === me.id)) {
|
|
49
|
+
await enqueue(path, me);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const [head] = tickets;
|
|
53
|
+
if (head?.id === me.id)
|
|
54
|
+
return holding(path, me);
|
|
55
|
+
await this.#evictIfDead(path, head);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A waiter that gives up cannot remove its ticket: only the head may
|
|
60
|
+
* rewrite the queue. So its ticket stays in line, and this removes it when
|
|
61
|
+
* it reaches the head. The wait does not keep the process alive; if the
|
|
62
|
+
* process stops first, waiters evict the ticket as a dead head.
|
|
63
|
+
*/
|
|
64
|
+
#leaveWhenFirst(path, me) {
|
|
65
|
+
this.poll(async () => {
|
|
66
|
+
const tickets = await readTickets(path);
|
|
67
|
+
if (!tickets.some((ticket) => ticket.id === me.id))
|
|
68
|
+
return true;
|
|
69
|
+
const [head] = tickets;
|
|
70
|
+
if (head?.id === me.id) {
|
|
71
|
+
await removeTicket(path, me);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
await this.#evictIfDead(path, head);
|
|
75
|
+
return undefined;
|
|
76
|
+
}, { keepAlive: false }).catch(() => {
|
|
77
|
+
// The directory is gone, so the ticket is gone too.
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function holding(path, me) {
|
|
82
|
+
return { [Symbol.asyncDispose]: () => removeTicket(path, me) };
|
|
83
|
+
}
|
|
84
|
+
async function removeTicket(path, me) {
|
|
85
|
+
const remaining = await readTickets(path);
|
|
86
|
+
await replace(path, remaining.filter((ticket) => ticket.id !== me.id));
|
|
87
|
+
}
|
|
88
|
+
async function evictDeadHead(path) {
|
|
89
|
+
const [head, ...rest] = await readTickets(path);
|
|
90
|
+
if (head && !head.isAlive())
|
|
91
|
+
await replace(path, rest);
|
|
92
|
+
}
|
|
93
|
+
async function enqueue(path, owner) {
|
|
94
|
+
await appendFile(path, `${owner.serialize()}\n`);
|
|
95
|
+
}
|
|
96
|
+
async function readTickets(path) {
|
|
97
|
+
try {
|
|
98
|
+
const content = await readFile(path, 'utf8');
|
|
99
|
+
// The last segment is empty or a ticket still being appended.
|
|
100
|
+
return content.split('\n').slice(0, -1).map(Owner.parse);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (isErrno(error, 'ENOENT'))
|
|
104
|
+
return [];
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function replace(path, tickets) {
|
|
109
|
+
await atomicWrite(path, tickets.map((ticket) => `${ticket.serialize()}\n`).join(''));
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=ticket-queue-file-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-queue-file-store.js","sourceRoot":"","sources":["../../../src/lock-stores/file-system/ticket-queue-file-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { Connection, ConnectionHandlers } from '../remote/connection.ts';
|
|
3
|
+
import { type LockRequest, type LockResponse } from '../remote/protocol.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The parent's end of the IPC channel to one child. The kernel closes the
|
|
6
|
+
* channel when the child dies, so `disconnect` reports even a SIGKILL.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ChildProcessConnection implements Connection<LockResponse, LockRequest> {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(child: ChildProcess);
|
|
11
|
+
send(response: LockResponse): Promise<void>;
|
|
12
|
+
listen({ message, close }: ConnectionHandlers<LockRequest>): void;
|
|
13
|
+
/** The running child already keeps the parent alive. */
|
|
14
|
+
ref(): void;
|
|
15
|
+
unref(): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=child-process-connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-process-connection.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/ipc/child-process-connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,UAAU,CACvD,YAAY,EACZ,WAAW,CACZ;;gBAIa,KAAK,EAAE,YAAY;IAI/B,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY3C,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,kBAAkB,CAAC,WAAW,CAAC;IAiB1D,wDAAwD;IACxD,GAAG;IAEH,KAAK;IAEL,KAAK;CAGN"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { unwrap, wrap } from "../remote/envelope.js";
|
|
2
|
+
import { isLockRequest, } from "../remote/protocol.js";
|
|
3
|
+
/**
|
|
4
|
+
* The parent's end of the IPC channel to one child. The kernel closes the
|
|
5
|
+
* channel when the child dies, so `disconnect` reports even a SIGKILL.
|
|
6
|
+
*/
|
|
7
|
+
export class ChildProcessConnection {
|
|
8
|
+
#child;
|
|
9
|
+
#detach = () => { };
|
|
10
|
+
constructor(child) {
|
|
11
|
+
this.#child = child;
|
|
12
|
+
}
|
|
13
|
+
send(response) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (!this.#child.connected) {
|
|
16
|
+
reject(new Error('The child process has disconnected.'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.#child.send(wrap(response), (error) => error ? reject(error) : resolve());
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
listen({ message, close }) {
|
|
23
|
+
const onMessage = (envelope) => {
|
|
24
|
+
const request = unwrap(envelope);
|
|
25
|
+
if (isLockRequest(request))
|
|
26
|
+
message(request);
|
|
27
|
+
};
|
|
28
|
+
const onDisconnect = () => {
|
|
29
|
+
this.close();
|
|
30
|
+
close();
|
|
31
|
+
};
|
|
32
|
+
this.#child.on('message', onMessage);
|
|
33
|
+
this.#child.once('disconnect', onDisconnect);
|
|
34
|
+
this.#detach = () => {
|
|
35
|
+
this.#child.off('message', onMessage);
|
|
36
|
+
this.#child.off('disconnect', onDisconnect);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The running child already keeps the parent alive. */
|
|
40
|
+
ref() { }
|
|
41
|
+
unref() { }
|
|
42
|
+
close() {
|
|
43
|
+
this.#detach();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=child-process-connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-process-connection.js","sourceRoot":"","sources":["../../../src/lock-stores/ipc/child-process-connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAGL,aAAa,GACd,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,MAAM,OAAO,sBAAsB;IAIxB,MAAM,CAAe;IAC9B,OAAO,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;IAEnB,YAAY,KAAmB;QAC7B,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,IAAI,CAAC,QAAsB;QACzB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC,CAAC;gBACzD,OAAO;YACT,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CACzC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAClC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAmC;QACxD,MAAM,SAAS,GAAG,CAAC,QAAiB,EAAE,EAAE;YACtC,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;YACjC,IAAI,aAAa,CAAC,OAAO,CAAC;gBAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/C,CAAC,CAAC;QACF,MAAM,YAAY,GAAG,GAAG,EAAE;YACxB,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,KAAK,EAAE,CAAC;QACV,CAAC,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,GAAG,GAAG,EAAE;YAClB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACtC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC9C,CAAC,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,GAAG,KAAI,CAAC;IAER,KAAK,KAAI,CAAC;IAEV,KAAK;QACH,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;CACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { TokenSource } from '../../fencing/token-source.ts';
|
|
3
|
+
import type { Lease } from '../../mutex/lease.ts';
|
|
4
|
+
import type { AcquireOptions, LockStore } from '../../mutex/lock-store.ts';
|
|
5
|
+
export interface IpcLockCoordinatorOptions {
|
|
6
|
+
/** Defaults to an in-memory counter, which lives as long as this process. */
|
|
7
|
+
tokens?: TokenSource;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The parent side of IPC locking: grants keys to this process and to every
|
|
11
|
+
* child it adopts, which use `IpcStore`. Its reach is one process tree.
|
|
12
|
+
*/
|
|
13
|
+
export declare class IpcLockCoordinator implements LockStore {
|
|
14
|
+
#private;
|
|
15
|
+
constructor({ tokens, }?: IpcLockCoordinatorOptions);
|
|
16
|
+
acquire(key: string, options?: AcquireOptions): Promise<Lease>;
|
|
17
|
+
tryAcquire(key: string): Promise<Lease | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Serves `child`, which must have an IPC channel. When the child exits,
|
|
20
|
+
* everything it held or waited for is released.
|
|
21
|
+
*/
|
|
22
|
+
adopt(child: ChildProcess): void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ipc-lock-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipc-lock-coordinator.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/ipc/ipc-lock-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI3E,MAAM,WAAW,yBAAyB;IACxC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,SAAS;;gBAGtC,EACV,MAAiC,GAClC,GAAE,yBAA8B;IAIjC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC;IAI9D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;IAInD;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,YAAY;CAG1B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CounterTokenSource } from "../../fencing/counter-token-source.js";
|
|
2
|
+
import { LockCoordinator } from "../remote/lock-coordinator.js";
|
|
3
|
+
import { ChildProcessConnection } from "./child-process-connection.js";
|
|
4
|
+
/**
|
|
5
|
+
* The parent side of IPC locking: grants keys to this process and to every
|
|
6
|
+
* child it adopts, which use `IpcStore`. Its reach is one process tree.
|
|
7
|
+
*/
|
|
8
|
+
export class IpcLockCoordinator {
|
|
9
|
+
#coordinator;
|
|
10
|
+
constructor({ tokens = new CounterTokenSource(), } = {}) {
|
|
11
|
+
this.#coordinator = new LockCoordinator({ tokens });
|
|
12
|
+
}
|
|
13
|
+
acquire(key, options) {
|
|
14
|
+
return this.#coordinator.acquire(key, options);
|
|
15
|
+
}
|
|
16
|
+
tryAcquire(key) {
|
|
17
|
+
return this.#coordinator.tryAcquire(key);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Serves `child`, which must have an IPC channel. When the child exits,
|
|
21
|
+
* everything it held or waited for is released.
|
|
22
|
+
*/
|
|
23
|
+
adopt(child) {
|
|
24
|
+
this.#coordinator.serve(new ChildProcessConnection(child));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=ipc-lock-coordinator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipc-lock-coordinator.js","sourceRoot":"","sources":["../../../src/lock-stores/ipc/ipc-lock-coordinator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAI3E,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAOvE;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IACpB,YAAY,CAAkB;IAEvC,YAAY,EACV,MAAM,GAAG,IAAI,kBAAkB,EAAE,MACJ,EAAE;QAC/B,IAAI,CAAC,YAAY,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,OAAwB;QAC3C,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IAED,UAAU,CAAC,GAAW;QACpB,OAAO,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAmB;QACvB,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7D,CAAC;CACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Lease } from '../../mutex/lease.ts';
|
|
2
|
+
import type { AcquireOptions, LockStore } from '../../mutex/lock-store.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The child-process side of `IpcLockCoordinator`: asks the parent for keys over
|
|
5
|
+
* the IPC channel. If the parent dies, waiting acquires reject with
|
|
6
|
+
* `CoordinatorUnavailableError`; held keys stay exclusive, because no
|
|
7
|
+
* coordinator is left to grant them to anyone else.
|
|
8
|
+
*/
|
|
9
|
+
export declare class IpcStore implements LockStore {
|
|
10
|
+
#private;
|
|
11
|
+
constructor();
|
|
12
|
+
acquire(key: string, options?: AcquireOptions): Promise<Lease>;
|
|
13
|
+
tryAcquire(key: string): Promise<Lease | undefined>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=ipc-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipc-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/ipc/ipc-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAgB3E;;;;;GAKG;AACH,qBAAa,QAAS,YAAW,SAAS;;;IAYxC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC;IAI9D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;CAGpD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RemoteLockClient } from "../remote/remote-lock-client.js";
|
|
2
|
+
import { ProcessChannelConnection } from "./process-channel-connection.js";
|
|
3
|
+
/** The channel to the parent cannot be re-established, so it is handed out once. */
|
|
4
|
+
class ProcessChannelConnector {
|
|
5
|
+
#handedOut = false;
|
|
6
|
+
async connect() {
|
|
7
|
+
if (this.#handedOut || !process.connected)
|
|
8
|
+
return undefined;
|
|
9
|
+
this.#handedOut = true;
|
|
10
|
+
return new ProcessChannelConnection();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The child-process side of `IpcLockCoordinator`: asks the parent for keys over
|
|
15
|
+
* the IPC channel. If the parent dies, waiting acquires reject with
|
|
16
|
+
* `CoordinatorUnavailableError`; held keys stay exclusive, because no
|
|
17
|
+
* coordinator is left to grant them to anyone else.
|
|
18
|
+
*/
|
|
19
|
+
export class IpcStore {
|
|
20
|
+
#client;
|
|
21
|
+
constructor() {
|
|
22
|
+
if (!process.send) {
|
|
23
|
+
throw new Error('IpcStore needs an IPC channel to its parent; start this process with fork() or an "ipc" stdio entry.');
|
|
24
|
+
}
|
|
25
|
+
this.#client = new RemoteLockClient(new ProcessChannelConnector());
|
|
26
|
+
}
|
|
27
|
+
acquire(key, options) {
|
|
28
|
+
return this.#client.acquire(key, options);
|
|
29
|
+
}
|
|
30
|
+
tryAcquire(key) {
|
|
31
|
+
return this.#client.tryAcquire(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=ipc-store.js.map
|