@dbx-tools/core 0.6.64 → 0.6.66
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/README.md +81 -1
- package/index.ts +3 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -1
- package/lib/src/bin.d.ts +31 -0
- package/lib/src/bin.js +130 -0
- package/lib/src/process-lock.d.ts +88 -0
- package/lib/src/process-lock.js +355 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -2
- package/src/bin.ts +186 -0
- package/src/process-lock.ts +414 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @dbx-tools/core
|
|
2
2
|
|
|
3
|
-
Node-only core helpers for process execution and project
|
|
3
|
+
Node-only core helpers for binary installation, process execution, and project
|
|
4
|
+
discovery.
|
|
4
5
|
|
|
5
6
|
Import this package when code needs `node:child_process`, `node:fs`, or
|
|
6
7
|
`node:path`. Browser-safe utilities live in
|
|
@@ -16,6 +17,36 @@ Key features:
|
|
|
16
17
|
the current working directory.
|
|
17
18
|
- Safe filesystem stat and project naming helpers for CLIs and projen synth.
|
|
18
19
|
- YAML/JSON brand-context discovery and loading with shared Zod validation.
|
|
20
|
+
- Idempotent executable downloads with zip/tar extraction and atomic installs.
|
|
21
|
+
- Keyed mutual exclusion across the main thread and its worker threads.
|
|
22
|
+
|
|
23
|
+
## Install A Binary
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { chmod } from "node:fs/promises";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { bin } from "@dbx-tools/core";
|
|
30
|
+
|
|
31
|
+
const executable = await bin.ensure("tool", releaseUrl, {
|
|
32
|
+
autoUnpackage: true,
|
|
33
|
+
selector: async ({ source }) => {
|
|
34
|
+
const path = join(source, "tool");
|
|
35
|
+
await chmod(path, 0o755);
|
|
36
|
+
return path;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`bin.ensure()` installs to `$HOME/.<name>/bin/<name>` and returns its `root`,
|
|
42
|
+
`binDir`, and executable `path`. An existing executable returns immediately, so
|
|
43
|
+
a URL resolver is only called when installation is necessary. Concurrent
|
|
44
|
+
callers use `processLock` with a check-lock-check-load sequence, preventing
|
|
45
|
+
duplicate downloads across the main thread and wired workers. Direct downloads
|
|
46
|
+
are selected by default. Zip, tar, tar.gz, and tgz archives can be unpacked
|
|
47
|
+
automatically; a single-file archive needs no selector, while a selector can
|
|
48
|
+
choose and prepare a binary from a larger archive. The selected file must be
|
|
49
|
+
executable before it is atomically moved into place.
|
|
19
50
|
|
|
20
51
|
## Load Brand Context
|
|
21
52
|
|
|
@@ -80,6 +111,50 @@ const argv = exec.shlex('pnpm exec prettier --write "README.md"');
|
|
|
80
111
|
`shlex()` is a small parser for command strings that need to become argv arrays.
|
|
81
112
|
Prefer explicit argv arrays when possible.
|
|
82
113
|
|
|
114
|
+
## Serialize Work Across Threads
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { processLock } from "@dbx-tools/core";
|
|
118
|
+
|
|
119
|
+
await processLock.withProcessLock(["cache", name], async () => {
|
|
120
|
+
if (!(await exists(name))) await build(name);
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`withProcessLock()` runs the callback while holding the named lock, releasing it
|
|
125
|
+
when the callback settles. Callers sharing a key are serialized; distinct keys
|
|
126
|
+
run concurrently. The key is any value with a stable identity - a string, a
|
|
127
|
+
`["invoice", id]` tuple, a config object - canonicalized by `object.toStableKey`,
|
|
128
|
+
so structure counts: `["invoice", 7]` and `"invoice_7"` are different locks.
|
|
129
|
+
|
|
130
|
+
Use it instead of a module-level promise chain when worker threads are involved.
|
|
131
|
+
A promise chain only serializes the thread it lives on, so a worker pool runs one
|
|
132
|
+
callback per thread; the coordinator here is shared, so the key admits one
|
|
133
|
+
callback for the whole process.
|
|
134
|
+
|
|
135
|
+
Workers opt in when they are constructed:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { Worker } from "node:worker_threads";
|
|
139
|
+
|
|
140
|
+
new Worker(url, processLock.processLockWorkerOptions({ workerData: { tenant } }));
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
That preserves your own `workerData` and `transferList`, and lets the worker lock
|
|
144
|
+
during module initialization. For a worker you did not construct, call
|
|
145
|
+
`attachProcessLock(worker)` and have the worker `await processLockAttached()`
|
|
146
|
+
first - the port arrives by message, so it is not available quite as early.
|
|
147
|
+
|
|
148
|
+
A thread that exits while holding a lock releases it, so a crashed worker cannot
|
|
149
|
+
wedge a key. Locks are held only as long as the callback runs, and an idle lock
|
|
150
|
+
never keeps the process from exiting.
|
|
151
|
+
|
|
152
|
+
The scope is the THREADS of one process - `withProcessLock` shares nothing with a
|
|
153
|
+
second `node` invocation or another replica. When the critical section spans a
|
|
154
|
+
deployment, use
|
|
155
|
+
[`@dbx-tools/postgres`](../postgres)'s `withAdvisoryLock`, which puts the arbiter
|
|
156
|
+
in PostgreSQL where every replica can see it.
|
|
157
|
+
|
|
83
158
|
## Discover Project Roots
|
|
84
159
|
|
|
85
160
|
```ts
|
|
@@ -97,6 +172,11 @@ basename. `project.stat()` returns `undefined` instead of throwing.
|
|
|
97
172
|
## Modules
|
|
98
173
|
|
|
99
174
|
- `exec` - async/sync process spawning, stdio handling, abort wiring, and shlex.
|
|
175
|
+
- `bin` - executable download, optional archive extraction, selection, and
|
|
176
|
+
atomic installation.
|
|
100
177
|
- `project` - root discovery, project naming, git-remote parsing, and safe
|
|
101
178
|
filesystem stat.
|
|
102
179
|
- `brand` - YAML/JSON discovery, parsing, validation, and asset path resolution.
|
|
180
|
+
- `processLock` - keyed mutual exclusion across the main thread and its workers,
|
|
181
|
+
with worker wiring (`processLockWorkerOptions`, `attachProcessLock`,
|
|
182
|
+
`processLockAttached`).
|
package/index.ts
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
// Regenerated from the exporting modules in ./src.
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
|
+
export * as bin from "./src/bin.ts";
|
|
5
6
|
export * as brand from "./src/brand.ts";
|
|
6
7
|
export * as exec from "./src/exec.ts";
|
|
7
8
|
export * as file from "./src/file.ts";
|
|
9
|
+
export * as processLock from "./src/process-lock.ts";
|
|
8
10
|
export * as project from "./src/project.ts";
|
|
11
|
+
export type { BinContext, BinSelectionContext, BinSelector, BinOptions, BinUrl } from "./src/bin.ts";
|
|
9
12
|
export { BrandContextSchema, defaultBrandContext, parseBrandContext, brandContextJsonSchema, brandContextPrompt } from "./src/brand.ts";
|
|
10
13
|
export type { BrandContext, BrandContextInput } from "./src/brand.ts";
|
|
11
14
|
export { COMMAND_NOT_FOUND_EXIT_CODE } from "./src/exec.ts";
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export * as bin from "./src/bin.ts";
|
|
1
2
|
export * as brand from "./src/brand.ts";
|
|
2
3
|
export * as exec from "./src/exec.ts";
|
|
3
4
|
export * as file from "./src/file.ts";
|
|
5
|
+
export * as processLock from "./src/process-lock.ts";
|
|
4
6
|
export * as project from "./src/project.ts";
|
|
7
|
+
export type { BinContext, BinSelectionContext, BinSelector, BinOptions, BinUrl } from "./src/bin.ts";
|
|
5
8
|
export { BrandContextSchema, defaultBrandContext, parseBrandContext, brandContextJsonSchema, brandContextPrompt } from "./src/brand.ts";
|
|
6
9
|
export type { BrandContext, BrandContextInput } from "./src/brand.ts";
|
|
7
10
|
export { COMMAND_NOT_FOUND_EXIT_CODE } from "./src/exec.ts";
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
2
|
// Regenerated from the exporting modules in ./src.
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
export * as bin from "./src/bin.js";
|
|
4
5
|
export * as brand from "./src/brand.js";
|
|
5
6
|
export * as exec from "./src/exec.js";
|
|
6
7
|
export * as file from "./src/file.js";
|
|
8
|
+
export * as processLock from "./src/process-lock.js";
|
|
7
9
|
export * as project from "./src/project.js";
|
|
8
10
|
export { BrandContextSchema, defaultBrandContext, parseBrandContext, brandContextJsonSchema, brandContextPrompt } from "./src/brand.js";
|
|
9
11
|
export { COMMAND_NOT_FOUND_EXIT_CODE } from "./src/exec.js";
|
|
10
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
12
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssS0FBSyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxXQUFXLE1BQU0sdUJBQXVCLENBQUM7QUFDckQsT0FBTyxLQUFLLE9BQU8sTUFBTSxrQkFBa0IsQ0FBQztBQUU1QyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsbUJBQW1CLEVBQUUsaUJBQWlCLEVBQUUsc0JBQXNCLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUV4SSxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxlQUFlLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBHRU5FUkFURUQgYnkgcHJvamVuIHdhdGNoIC0gRE8gTk9UIEVESVQuXG4vLyBSZWdlbmVyYXRlZCBmcm9tIHRoZSBleHBvcnRpbmcgbW9kdWxlcyBpbiAuL3NyYy5cbi8vIEhhbmQgZWRpdHMgYXJlIG92ZXJ3cml0dGVuIG9uIHRoZSBuZXh0IHdhdGNoOyB0aGlzIGZpbGUgaXMgcmVhZC1vbmx5LlxuXG5leHBvcnQgKiBhcyBiaW4gZnJvbSBcIi4vc3JjL2Jpbi50c1wiO1xuZXhwb3J0ICogYXMgYnJhbmQgZnJvbSBcIi4vc3JjL2JyYW5kLnRzXCI7XG5leHBvcnQgKiBhcyBleGVjIGZyb20gXCIuL3NyYy9leGVjLnRzXCI7XG5leHBvcnQgKiBhcyBmaWxlIGZyb20gXCIuL3NyYy9maWxlLnRzXCI7XG5leHBvcnQgKiBhcyBwcm9jZXNzTG9jayBmcm9tIFwiLi9zcmMvcHJvY2Vzcy1sb2NrLnRzXCI7XG5leHBvcnQgKiBhcyBwcm9qZWN0IGZyb20gXCIuL3NyYy9wcm9qZWN0LnRzXCI7XG5leHBvcnQgdHlwZSB7IEJpbkNvbnRleHQsIEJpblNlbGVjdGlvbkNvbnRleHQsIEJpblNlbGVjdG9yLCBCaW5PcHRpb25zLCBCaW5VcmwgfSBmcm9tIFwiLi9zcmMvYmluLnRzXCI7XG5leHBvcnQgeyBCcmFuZENvbnRleHRTY2hlbWEsIGRlZmF1bHRCcmFuZENvbnRleHQsIHBhcnNlQnJhbmRDb250ZXh0LCBicmFuZENvbnRleHRKc29uU2NoZW1hLCBicmFuZENvbnRleHRQcm9tcHQgfSBmcm9tIFwiLi9zcmMvYnJhbmQudHNcIjtcbmV4cG9ydCB0eXBlIHsgQnJhbmRDb250ZXh0LCBCcmFuZENvbnRleHRJbnB1dCB9IGZyb20gXCIuL3NyYy9icmFuZC50c1wiO1xuZXhwb3J0IHsgQ09NTUFORF9OT1RfRk9VTkRfRVhJVF9DT0RFIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgRXhlY1N0ZGlvLCBMaW5lSGFuZGxlciwgU3RkaW9PcHRpb24sIEV4ZWNSZXN1bHQsIENoaWxkUHJvY2Vzc1Jlc3VsdCwgRXhlY09wdGlvbnMsIFN5bmNFeGVjU3RkaW8sIFN5bmNFeGVjT3B0aW9ucywgU3Bhd25BcmdzIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgUHJvamVjdENvbnRleHQgfSBmcm9tIFwiLi9zcmMvcHJvamVjdC50c1wiO1xuIl19
|
package/lib/src/bin.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Stable paths for an installed binary. */
|
|
2
|
+
export interface BinContext {
|
|
3
|
+
root: string;
|
|
4
|
+
binDir: string;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
/** Temporary download and extraction paths supplied to a custom selector. */
|
|
8
|
+
export interface BinSelectionContext {
|
|
9
|
+
destination: BinContext;
|
|
10
|
+
downloadPath: string;
|
|
11
|
+
source: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Select the executable from a download or unpacked archive. A selector may
|
|
15
|
+
* also prepare the file, such as applying its executable mode.
|
|
16
|
+
*/
|
|
17
|
+
export type BinSelector = (context: BinSelectionContext) => string | Promise<string>;
|
|
18
|
+
/** Options for {@link ensure}. */
|
|
19
|
+
export interface BinOptions {
|
|
20
|
+
autoUnpackage?: boolean;
|
|
21
|
+
selector?: BinSelector;
|
|
22
|
+
homeDir?: string;
|
|
23
|
+
}
|
|
24
|
+
/** A URL resolved only when the executable is not already installed. */
|
|
25
|
+
export type BinUrl = string | (() => string | Promise<string>);
|
|
26
|
+
/**
|
|
27
|
+
* Return an existing executable or install it atomically under
|
|
28
|
+
* `$HOME/.<name>/bin/<name>`. Installation uses a check-lock-check-load
|
|
29
|
+
* sequence so concurrent callers resolve and download the binary only once.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ensure(name: string, url: BinUrl, options?: BinOptions): Promise<BinContext>;
|
package/lib/src/bin.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install and reuse executable binaries under a per-tool home directory.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { access, chmod, copyFile, mkdir, mkdtemp, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
10
|
+
import { basename, join } from "node:path";
|
|
11
|
+
import extractZip from "extract-zip";
|
|
12
|
+
import { x as extractTar } from "tar";
|
|
13
|
+
import { withProcessLock } from "./process-lock.js";
|
|
14
|
+
function context(name, homeDir) {
|
|
15
|
+
if (!name || basename(name) !== name || name === "." || name === "..") {
|
|
16
|
+
throw new TypeError(`invalid binary name: ${name}`);
|
|
17
|
+
}
|
|
18
|
+
const root = join(homeDir, `.${name}`);
|
|
19
|
+
const binDir = join(root, "bin");
|
|
20
|
+
return { root, binDir, path: join(binDir, name) };
|
|
21
|
+
}
|
|
22
|
+
async function isExecutable(path) {
|
|
23
|
+
try {
|
|
24
|
+
if (!(await stat(path)).isFile())
|
|
25
|
+
return false;
|
|
26
|
+
await access(path, constants.F_OK | constants.X_OK);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function downloadName(url, name) {
|
|
34
|
+
try {
|
|
35
|
+
const candidate = basename(decodeURIComponent(new URL(url).pathname));
|
|
36
|
+
return candidate && Buffer.byteLength(candidate) <= 200 ? candidate : name;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function unpack(archive, destination) {
|
|
43
|
+
const filename = archive.toLowerCase();
|
|
44
|
+
if (filename.endsWith(".zip")) {
|
|
45
|
+
await extractZip(archive, { dir: destination });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (filename.endsWith(".tar") || filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
|
|
49
|
+
await extractTar({ file: archive, cwd: destination });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`unsupported binary archive: ${basename(archive)}`);
|
|
53
|
+
}
|
|
54
|
+
async function filesUnder(root) {
|
|
55
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
56
|
+
const files = [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const path = join(root, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
files.push(...(await filesUnder(path)));
|
|
61
|
+
}
|
|
62
|
+
else if (entry.isFile()) {
|
|
63
|
+
files.push(path);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
68
|
+
async function selectSingleFile(source) {
|
|
69
|
+
const files = await filesUnder(source);
|
|
70
|
+
const selected = files.at(0);
|
|
71
|
+
if (files.length !== 1 || !selected) {
|
|
72
|
+
throw new Error(`binary archive must contain one file, found ${files.length}`);
|
|
73
|
+
}
|
|
74
|
+
return selected;
|
|
75
|
+
}
|
|
76
|
+
async function selectedBin(destination, url, temp, options) {
|
|
77
|
+
const name = downloadName(url, basename(destination.path));
|
|
78
|
+
const downloadPath = join(temp, name);
|
|
79
|
+
const response = await fetch(url);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`binary download failed (${response.status})`);
|
|
82
|
+
}
|
|
83
|
+
await writeFile(downloadPath, Buffer.from(await response.arrayBuffer()), { mode: 0o755 });
|
|
84
|
+
let source = downloadPath;
|
|
85
|
+
if (options.autoUnpackage) {
|
|
86
|
+
source = join(temp, `unpacked-${randomUUID()}`);
|
|
87
|
+
await mkdir(source);
|
|
88
|
+
await unpack(downloadPath, source);
|
|
89
|
+
}
|
|
90
|
+
if (options.selector) {
|
|
91
|
+
return options.selector({ destination, downloadPath, source });
|
|
92
|
+
}
|
|
93
|
+
return options.autoUnpackage ? selectSingleFile(source) : source;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Return an existing executable or install it atomically under
|
|
97
|
+
* `$HOME/.<name>/bin/<name>`. Installation uses a check-lock-check-load
|
|
98
|
+
* sequence so concurrent callers resolve and download the binary only once.
|
|
99
|
+
*/
|
|
100
|
+
export async function ensure(name, url, options = {}) {
|
|
101
|
+
const destination = context(name, options.homeDir ?? homedir());
|
|
102
|
+
if (await isExecutable(destination.path))
|
|
103
|
+
return destination;
|
|
104
|
+
return withProcessLock(["bin.ensure", destination.path], async () => {
|
|
105
|
+
if (await isExecutable(destination.path))
|
|
106
|
+
return destination;
|
|
107
|
+
const resolvedUrl = typeof url === "function" ? await url() : url;
|
|
108
|
+
const temp = await mkdtemp(join(tmpdir(), `${name}-`));
|
|
109
|
+
let staged;
|
|
110
|
+
try {
|
|
111
|
+
const selected = await selectedBin(destination, resolvedUrl, temp, options);
|
|
112
|
+
if (!(await isExecutable(selected))) {
|
|
113
|
+
throw new Error(`selected binary is not executable: ${selected}`);
|
|
114
|
+
}
|
|
115
|
+
await mkdir(destination.binDir, { recursive: true });
|
|
116
|
+
staged = join(destination.binDir, `.${name}-${randomUUID()}`);
|
|
117
|
+
await copyFile(selected, staged);
|
|
118
|
+
await chmod(staged, (await stat(selected)).mode);
|
|
119
|
+
await rename(staged, destination.path);
|
|
120
|
+
staged = undefined;
|
|
121
|
+
return destination;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (staged)
|
|
125
|
+
await rm(staged, { force: true });
|
|
126
|
+
await rm(temp, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmluLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2Jpbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7OztHQUlHO0FBQ0gsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN6QyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3BDLE9BQU8sRUFDTCxNQUFNLEVBQ04sS0FBSyxFQUNMLFFBQVEsRUFDUixLQUFLLEVBQ0wsT0FBTyxFQUNQLE9BQU8sRUFDUCxNQUFNLEVBQ04sRUFBRSxFQUNGLElBQUksRUFDSixTQUFTLEdBQ1YsTUFBTSxrQkFBa0IsQ0FBQztBQUMxQixPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUMxQyxPQUFPLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUUzQyxPQUFPLFVBQVUsTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxFQUFFLENBQUMsSUFBSSxVQUFVLEVBQUUsTUFBTSxLQUFLLENBQUM7QUFFdEMsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBZ0NwRCxTQUFTLE9BQU8sQ0FBQyxJQUFZLEVBQUUsT0FBZTtJQUM1QyxJQUFJLENBQUMsSUFBSSxJQUFJLFFBQVEsQ0FBQyxJQUFJLENBQUMsS0FBSyxJQUFJLElBQUksSUFBSSxLQUFLLEdBQUcsSUFBSSxJQUFJLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDdEUsTUFBTSxJQUFJLFNBQVMsQ0FBQyx3QkFBd0IsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN0RCxDQUFDO0lBQ0QsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLE9BQU8sRUFBRSxJQUFJLElBQUksRUFBRSxDQUFDLENBQUM7SUFDdkMsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQztJQUNqQyxPQUFPLEVBQUUsSUFB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RSxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDNUUsSUFBSSxDQUFDLENBQUMsTUFBTSxZQUFZLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxDQUFDO2dCQUNwQyxNQUFNLElBQUksS0FBSyxDQUFDLHNDQUFzQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO1lBQ3BFLENBQUM7WUFFRCxNQUFNLEtBQUssQ0FBQyxXQUFXLENBQUMsTUFBTSxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7WUFDckQsTUFBTSxHQUFHLElBQUksQ0FBQyxXQUFXLENBQUMsTUFBTSxFQUFFLElBQUksSUFBSSxJQUFJLFVBQVUsRUFBRSxFQUFFLENBQUMsQ0FBQztZQUM5RCxNQUFNLFFBQVEsQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDakMsTUFBTSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsTUFBTSxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNqRCxNQUFNLE1BQU0sQ0FBQyxNQUFNLEVBQUUsV0FBVyxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ3ZDLE1BQU0sR0FBRyxTQUFTLENBQUM7WUFDbkIsT0FBTyxXQUFXLENBQUM7UUFDckIsQ0FBQztnQkFBUyxDQUFDO1lBQ1QsSUFBSSxNQUFNO2dCQUFFLE1BQU0sRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO1lBQzlDLE1BQU0sRUFBRSxDQUFDLElBQUksRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDbkQsQ0FBQztJQUNILENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogSW5zdGFsbCBhbmQgcmV1c2UgZXhlY3V0YWJsZSBiaW5hcmllcyB1bmRlciBhIHBlci10b29sIGhvbWUgZGlyZWN0b3J5LlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuaW1wb3J0IHsgcmFuZG9tVVVJRCB9IGZyb20gXCJub2RlOmNyeXB0b1wiO1xuaW1wb3J0IHsgY29uc3RhbnRzIH0gZnJvbSBcIm5vZGU6ZnNcIjtcbmltcG9ydCB7XG4gIGFjY2VzcyxcbiAgY2htb2QsXG4gIGNvcHlGaWxlLFxuICBta2RpcixcbiAgbWtkdGVtcCxcbiAgcmVhZGRpcixcbiAgcmVuYW1lLFxuICBybSxcbiAgc3RhdCxcbiAgd3JpdGVGaWxlLFxufSBmcm9tIFwibm9kZTpmcy9wcm9taXNlc1wiO1xuaW1wb3J0IHsgaG9tZWRpciwgdG1wZGlyIH0gZnJvbSBcIm5vZGU6b3NcIjtcbmltcG9ydCB7IGJhc2VuYW1lLCBqb2luIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuXG5pbXBvcnQgZXh0cmFjdFppcCBmcm9tIFwiZXh0cmFjdC16aXBcIjtcbmltcG9ydCB7IHggYXMgZXh0cmFjdFRhciB9IGZyb20gXCJ0YXJcIjtcblxuaW1wb3J0IHsgd2l0aFByb2Nlc3NMb2NrIH0gZnJvbSBcIi4vcHJvY2Vzcy1sb2NrLnRzXCI7XG5cbi8qKiBTdGFibGUgcGF0aHMgZm9yIGFuIGluc3RhbGxlZCBiaW5hcnkuICovXG5leHBvcnQgaW50ZXJmYWNlIEJpbkNvbnRleHQge1xuICByb290OiBzdHJpbmc7XG4gIGJpbkRpcjogc3RyaW5nO1xuICBwYXRoOiBzdHJpbmc7XG59XG5cbi8qKiBUZW1wb3JhcnkgZG93bmxvYWQgYW5kIGV4dHJhY3Rpb24gcGF0aHMgc3VwcGxpZWQgdG8gYSBjdXN0b20gc2VsZWN0b3IuICovXG5leHBvcnQgaW50ZXJmYWNlIEJpblNlbGVjdGlvbkNvbnRleHQge1xuICBkZXN0aW5hdGlvbjogQmluQ29udGV4dDtcbiAgZG93bmxvYWRQYXRoOiBzdHJpbmc7XG4gIHNvdXJjZTogc3RyaW5nO1xufVxuXG4vKipcbiAqIFNlbGVjdCB0aGUgZXhlY3V0YWJsZSBmcm9tIGEgZG93bmxvYWQgb3IgdW5wYWNrZWQgYXJjaGl2ZS4gQSBzZWxlY3RvciBtYXlcbiAqIGFsc28gcHJlcGFyZSB0aGUgZmlsZSwgc3VjaCBhcyBhcHBseWluZyBpdHMgZXhlY3V0YWJsZSBtb2RlLlxuICovXG5leHBvcnQgdHlwZSBCaW5TZWxlY3RvciA9IChjb250ZXh0OiBCaW5TZWxlY3Rpb25Db250ZXh0KSA9PiBzdHJpbmcgfCBQcm9taXNlPHN0cmluZz47XG5cbi8qKiBPcHRpb25zIGZvciB7QGxpbmsgZW5zdXJlfS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQmluT3B0aW9ucyB7XG4gIGF1dG9VbnBhY2thZ2U/OiBib29sZWFuO1xuICBzZWxlY3Rvcj86IEJpblNlbGVjdG9yO1xuICBob21lRGlyPzogc3RyaW5nO1xufVxuXG4vKiogQSBVUkwgcmVzb2x2ZWQgb25seSB3aGVuIHRoZSBleGVjdXRhYmxlIGlzIG5vdCBhbHJlYWR5IGluc3RhbGxlZC4gKi9cbmV4cG9ydCB0eXBlIEJpblVybCA9IHN0cmluZyB8ICgoKSA9PiBzdHJpbmcgfCBQcm9taXNlPHN0cmluZz4pO1xuXG5mdW5jdGlvbiBjb250ZXh0KG5hbWU6IHN0cmluZywgaG9tZURpcjogc3RyaW5nKTogQmluQ29udGV4dCB7XG4gIGlmICghbmFtZSB8fCBiYXNlbmFtZShuYW1lKSAhPT0gbmFtZSB8fCBuYW1lID09PSBcIi5cIiB8fCBuYW1lID09PSBcIi4uXCIpIHtcbiAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGBpbnZhbGlkIGJpbmFyeSBuYW1lOiAke25hbWV9YCk7XG4gIH1cbiAgY29uc3Qgcm9vdCA9IGpvaW4oaG9tZURpciwgYC4ke25hbWV9YCk7XG4gIGNvbnN0IGJpbkRpciA9IGpvaW4ocm9vdCwgXCJiaW5cIik7XG4gIHJldHVybiB7IHJvb3QsIGJpbkRpciwgcGF0aDogam9pbihiaW5EaXIsIG5hbWUpIH07XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGlzRXhlY3V0YWJsZShwYXRoOiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgdHJ5IHtcbiAgICBpZiAoIShhd2FpdCBzdGF0KHBhdGgpKS5pc0ZpbGUoKSkgcmV0dXJuIGZhbHNlO1xuICAgIGF3YWl0IGFjY2VzcyhwYXRoLCBjb25zdGFudHMuRl9PSyB8IGNvbnN0YW50cy5YX09LKTtcbiAgICByZXR1cm4gdHJ1ZTtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG59XG5cbmZ1bmN0aW9uIGRvd25sb2FkTmFtZSh1cmw6IHN0cmluZywgbmFtZTogc3RyaW5nKTogc3RyaW5nIHtcbiAgdHJ5IHtcbiAgICBjb25zdCBjYW5kaWRhdGUgPSBiYXNlbmFtZShkZWNvZGVVUklDb21wb25lbnQobmV3IFVSTCh1cmwpLnBhdGhuYW1lKSk7XG4gICAgcmV0dXJuIGNhbmRpZGF0ZSAmJiBCdWZmZXIuYnl0ZUxlbmd0aChjYW5kaWRhdGUpIDw9IDIwMCA/IGNhbmRpZGF0ZSA6IG5hbWU7XG4gIH0gY2F0Y2gge1xuICAgIHJldHVybiBuYW1lO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHVucGFjayhhcmNoaXZlOiBzdHJpbmcsIGRlc3RpbmF0aW9uOiBzdHJpbmcpOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgZmlsZW5hbWUgPSBhcmNoaXZlLnRvTG93ZXJDYXNlKCk7XG4gIGlmIChmaWxlbmFtZS5lbmRzV2l0aChcIi56aXBcIikpIHtcbiAgICBhd2FpdCBleHRyYWN0WmlwKGFyY2hpdmUsIHsgZGlyOiBkZXN0aW5hdGlvbiB9KTtcbiAgICByZXR1cm47XG4gIH1cbiAgaWYgKGZpbGVuYW1lLmVuZHNXaXRoKFwiLnRhclwiKSB8fCBmaWxlbmFtZS5lbmRzV2l0aChcIi50YXIuZ3pcIikgfHwgZmlsZW5hbWUuZW5kc1dpdGgoXCIudGd6XCIpKSB7XG4gICAgYXdhaXQgZXh0cmFjdFRhcih7IGZpbGU6IGFyY2hpdmUsIGN3ZDogZGVzdGluYXRpb24gfSk7XG4gICAgcmV0dXJuO1xuICB9XG4gIHRocm93IG5ldyBFcnJvcihgdW5zdXBwb3J0ZWQgYmluYXJ5IGFyY2hpdmU6ICR7YmFzZW5hbWUoYXJjaGl2ZSl9YCk7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGZpbGVzVW5kZXIocm9vdDogc3RyaW5nKTogUHJvbWlzZTxzdHJpbmdbXT4ge1xuICBjb25zdCBlbnRyaWVzID0gYXdhaXQgcmVhZGRpcihyb290LCB7IHdpdGhGaWxlVHlwZXM6IHRydWUgfSk7XG4gIGNvbnN0IGZpbGVzOiBzdHJpbmdbXSA9IFtdO1xuICBmb3IgKGNvbnN0IGVudHJ5IG9mIGVudHJpZXMpIHtcbiAgICBjb25zdCBwYXRoID0gam9pbihyb290LCBlbnRyeS5uYW1lKTtcbiAgICBpZiAoZW50cnkuaXNEaXJlY3RvcnkoKSkge1xuICAgICAgZmlsZXMucHVzaCguLi4oYXdhaXQgZmlsZXNVbmRlcihwYXRoKSkpO1xuICAgIH0gZWxzZSBpZiAoZW50cnkuaXNGaWxlKCkpIHtcbiAgICAgIGZpbGVzLnB1c2gocGF0aCk7XG4gICAgfVxuICB9XG4gIHJldHVybiBmaWxlcztcbn1cblxuYXN5bmMgZnVuY3Rpb24gc2VsZWN0U2luZ2xlRmlsZShzb3VyY2U6IHN0cmluZyk6IFByb21pc2U8c3RyaW5nPiB7XG4gIGNvbnN0IGZpbGVzID0gYXdhaXQgZmlsZXNVbmRlcihzb3VyY2UpO1xuICBjb25zdCBzZWxlY3RlZCA9IGZpbGVzLmF0KDApO1xuICBpZiAoZmlsZXMubGVuZ3RoICE9PSAxIHx8ICFzZWxlY3RlZCkge1xuICAgIHRocm93IG5ldyBFcnJvcihgYmluYXJ5IGFyY2hpdmUgbXVzdCBjb250YWluIG9uZSBmaWxlLCBmb3VuZCAke2ZpbGVzLmxlbmd0aH1gKTtcbiAgfVxuICByZXR1cm4gc2VsZWN0ZWQ7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHNlbGVjdGVkQmluKFxuICBkZXN0aW5hdGlvbjogQmluQ29udGV4dCxcbiAgdXJsOiBzdHJpbmcsXG4gIHRlbXA6IHN0cmluZyxcbiAgb3B0aW9uczogQmluT3B0aW9ucyxcbik6IFByb21pc2U8c3RyaW5nPiB7XG4gIGNvbnN0IG5hbWUgPSBkb3dubG9hZE5hbWUodXJsLCBiYXNlbmFtZShkZXN0aW5hdGlvbi5wYXRoKSk7XG4gIGNvbnN0IGRvd25sb2FkUGF0aCA9IGpvaW4odGVtcCwgbmFtZSk7XG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZmV0Y2godXJsKTtcbiAgaWYgKCFyZXNwb25zZS5vaykge1xuICAgIHRocm93IG5ldyBFcnJvcihgYmluYXJ5IGRvd25sb2FkIGZhaWxlZCAoJHtyZXNwb25zZS5zdGF0dXN9KWApO1xuICB9XG4gIGF3YWl0IHdyaXRlRmlsZShkb3dubG9hZFBhdGgsIEJ1ZmZlci5mcm9tKGF3YWl0IHJlc3BvbnNlLmFycmF5QnVmZmVyKCkpLCB7IG1vZGU6IDBvNzU1IH0pO1xuXG4gIGxldCBzb3VyY2UgPSBkb3dubG9hZFBhdGg7XG4gIGlmIChvcHRpb25zLmF1dG9VbnBhY2thZ2UpIHtcbiAgICBzb3VyY2UgPSBqb2luKHRlbXAsIGB1bnBhY2tlZC0ke3JhbmRvbVVVSUQoKX1gKTtcbiAgICBhd2FpdCBta2Rpcihzb3VyY2UpO1xuICAgIGF3YWl0IHVucGFjayhkb3dubG9hZFBhdGgsIHNvdXJjZSk7XG4gIH1cblxuICBpZiAob3B0aW9ucy5zZWxlY3Rvcikge1xuICAgIHJldHVybiBvcHRpb25zLnNlbGVjdG9yKHsgZGVzdGluYXRpb24sIGRvd25sb2FkUGF0aCwgc291cmNlIH0pO1xuICB9XG4gIHJldHVybiBvcHRpb25zLmF1dG9VbnBhY2thZ2UgPyBzZWxlY3RTaW5nbGVGaWxlKHNvdXJjZSkgOiBzb3VyY2U7XG59XG5cbi8qKlxuICogUmV0dXJuIGFuIGV4aXN0aW5nIGV4ZWN1dGFibGUgb3IgaW5zdGFsbCBpdCBhdG9taWNhbGx5IHVuZGVyXG4gKiBgJEhPTUUvLjxuYW1lPi9iaW4vPG5hbWU+YC4gSW5zdGFsbGF0aW9uIHVzZXMgYSBjaGVjay1sb2NrLWNoZWNrLWxvYWRcbiAqIHNlcXVlbmNlIHNvIGNvbmN1cnJlbnQgY2FsbGVycyByZXNvbHZlIGFuZCBkb3dubG9hZCB0aGUgYmluYXJ5IG9ubHkgb25jZS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGVuc3VyZShcbiAgbmFtZTogc3RyaW5nLFxuICB1cmw6IEJpblVybCxcbiAgb3B0aW9uczogQmluT3B0aW9ucyA9IHt9LFxuKTogUHJvbWlzZTxCaW5Db250ZXh0PiB7XG4gIGNvbnN0IGRlc3RpbmF0aW9uID0gY29udGV4dChuYW1lLCBvcHRpb25zLmhvbWVEaXIgPz8gaG9tZWRpcigpKTtcbiAgaWYgKGF3YWl0IGlzRXhlY3V0YWJsZShkZXN0aW5hdGlvbi5wYXRoKSkgcmV0dXJuIGRlc3RpbmF0aW9uO1xuXG4gIHJldHVybiB3aXRoUHJvY2Vzc0xvY2soW1wiYmluLmVuc3VyZVwiLCBkZXN0aW5hdGlvbi5wYXRoXSwgYXN5bmMgKCkgPT4ge1xuICAgIGlmIChhd2FpdCBpc0V4ZWN1dGFibGUoZGVzdGluYXRpb24ucGF0aCkpIHJldHVybiBkZXN0aW5hdGlvbjtcblxuICAgIGNvbnN0IHJlc29sdmVkVXJsID0gdHlwZW9mIHVybCA9PT0gXCJmdW5jdGlvblwiID8gYXdhaXQgdXJsKCkgOiB1cmw7XG4gICAgY29uc3QgdGVtcCA9IGF3YWl0IG1rZHRlbXAoam9pbih0bXBkaXIoKSwgYCR7bmFtZX0tYCkpO1xuICAgIGxldCBzdGFnZWQ6IHN0cmluZyB8IHVuZGVmaW5lZDtcbiAgICB0cnkge1xuICAgICAgY29uc3Qgc2VsZWN0ZWQgPSBhd2FpdCBzZWxlY3RlZEJpbihkZXN0aW5hdGlvbiwgcmVzb2x2ZWRVcmwsIHRlbXAsIG9wdGlvbnMpO1xuICAgICAgaWYgKCEoYXdhaXQgaXNFeGVjdXRhYmxlKHNlbGVjdGVkKSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBzZWxlY3RlZCBiaW5hcnkgaXMgbm90IGV4ZWN1dGFibGU6ICR7c2VsZWN0ZWR9YCk7XG4gICAgICB9XG5cbiAgICAgIGF3YWl0IG1rZGlyKGRlc3RpbmF0aW9uLmJpbkRpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG4gICAgICBzdGFnZWQgPSBqb2luKGRlc3RpbmF0aW9uLmJpbkRpciwgYC4ke25hbWV9LSR7cmFuZG9tVVVJRCgpfWApO1xuICAgICAgYXdhaXQgY29weUZpbGUoc2VsZWN0ZWQsIHN0YWdlZCk7XG4gICAgICBhd2FpdCBjaG1vZChzdGFnZWQsIChhd2FpdCBzdGF0KHNlbGVjdGVkKSkubW9kZSk7XG4gICAgICBhd2FpdCByZW5hbWUoc3RhZ2VkLCBkZXN0aW5hdGlvbi5wYXRoKTtcbiAgICAgIHN0YWdlZCA9IHVuZGVmaW5lZDtcbiAgICAgIHJldHVybiBkZXN0aW5hdGlvbjtcbiAgICB9IGZpbmFsbHkge1xuICAgICAgaWYgKHN0YWdlZCkgYXdhaXQgcm0oc3RhZ2VkLCB7IGZvcmNlOiB0cnVlIH0pO1xuICAgICAgYXdhaXQgcm0odGVtcCwgeyByZWN1cnNpdmU6IHRydWUsIGZvcmNlOiB0cnVlIH0pO1xuICAgIH1cbiAgfSk7XG59XG4iXX0=
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyed mutual exclusion across the main thread and its worker threads.
|
|
3
|
+
*
|
|
4
|
+
* {@link withProcessLock} serializes callbacks that share a key: one runs at a
|
|
5
|
+
* time, the rest queue in arrival order, and different keys proceed
|
|
6
|
+
* concurrently. Unlike a plain in-module `Promise` chain, the queue is shared by
|
|
7
|
+
* every thread wired up through {@link processLockWorkerOptions}, so a worker
|
|
8
|
+
* pool cannot run two callbacks for the same key at once.
|
|
9
|
+
*
|
|
10
|
+
* The name says `process`: this coordinates the THREADS of one Node process. It
|
|
11
|
+
* is not cross-process and not cross-host - a second `node` invocation, or a
|
|
12
|
+
* second app replica, has its own coordinator and shares nothing. When the scope
|
|
13
|
+
* is a deployment rather than a process, use
|
|
14
|
+
* `@dbx-tools/postgres`'s `withAdvisoryLock`, which puts the arbiter in
|
|
15
|
+
* PostgreSQL where every replica can see it.
|
|
16
|
+
*
|
|
17
|
+
* The main thread owns the only coordinator. Each participating thread gets one
|
|
18
|
+
* `MessagePort` to it, and a lock is granted by a message back over that port.
|
|
19
|
+
* Consequently:
|
|
20
|
+
*
|
|
21
|
+
* - the lock is ADVISORY, like Postgres advisory locks - it protects a
|
|
22
|
+
* critical section only insofar as every writer takes the same key;
|
|
23
|
+
* - fairness is FIFO per key, since the coordinator queues waiters in the
|
|
24
|
+
* order their requests arrive;
|
|
25
|
+
* - a thread that dies while holding a lock releases it, because its port
|
|
26
|
+
* closing is what hands the key to the next waiter (see
|
|
27
|
+
* {@link LockCoordinator.removePort}).
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { type Worker, type WorkerOptions } from "node:worker_threads";
|
|
32
|
+
/**
|
|
33
|
+
* Run `fn` while holding the lock named by `key`, releasing it when `fn`
|
|
34
|
+
* settles.
|
|
35
|
+
*
|
|
36
|
+
* Callers sharing a key are serialized across the main thread and every worker
|
|
37
|
+
* started through {@link processLockWorkerOptions}; distinct keys never block
|
|
38
|
+
* each other. Returns whatever `fn` returns and propagates what it throws, so it
|
|
39
|
+
* drops into an existing expression without restructuring.
|
|
40
|
+
*
|
|
41
|
+
* `key` is any value with a stable identity - a string, a `["invoice", id]`
|
|
42
|
+
* tuple, a config object - canonicalized by `object.toStableKey`, the same rule
|
|
43
|
+
* `@dbx-tools/postgres` uses for advisory-lock ids and channel names. Structure
|
|
44
|
+
* is part of the identity: `["invoice", 7]` and `"invoice_7"` are different
|
|
45
|
+
* locks.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* await withProcessLock(["cache", name], async () => {
|
|
49
|
+
* if (!(await exists(name))) await build(name);
|
|
50
|
+
* });
|
|
51
|
+
*/
|
|
52
|
+
export declare function withProcessLock<T>(key: unknown, fn: () => T | Promise<T>): Promise<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Add the coordinator port to a `Worker`'s options so the worker can lock
|
|
55
|
+
* immediately - during module initialization, before any message is handled.
|
|
56
|
+
*
|
|
57
|
+
* Preserves the caller's `workerData` and `transferList`; the port is
|
|
58
|
+
* transferred, as `MessagePort` cannot be cloned.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* new Worker(url, processLockWorkerOptions({ workerData: { tenant } }));
|
|
62
|
+
*/
|
|
63
|
+
export declare function processLockWorkerOptions(options?: WorkerOptions): WorkerOptions;
|
|
64
|
+
/**
|
|
65
|
+
* Wire an ALREADY-RUNNING worker into the lock, for a worker this code did not
|
|
66
|
+
* construct (a pool from a library, say).
|
|
67
|
+
*
|
|
68
|
+
* Prefer {@link processLockWorkerOptions}: the port arrives with a message, so
|
|
69
|
+
* the worker cannot lock during module initialization and must await
|
|
70
|
+
* {@link processLockAttached} first. The worker side needs no other change -
|
|
71
|
+
* `withProcessLock` works normally once the port lands.
|
|
72
|
+
*/
|
|
73
|
+
export declare function attachProcessLock(worker: Worker): void;
|
|
74
|
+
/**
|
|
75
|
+
* In a worker, resolve once the {@link attachProcessLock} port has arrived.
|
|
76
|
+
*
|
|
77
|
+
* Only needed on the attach path, and safe to await regardless: it returns
|
|
78
|
+
* immediately when the worker already has a port (the
|
|
79
|
+
* {@link processLockWorkerOptions} case) or when called on the main thread, so
|
|
80
|
+
* shared worker code does not branch on how it was started.
|
|
81
|
+
*
|
|
82
|
+
* A `parentPort` listener keeps the worker alive, which is CORRECT here and
|
|
83
|
+
* deliberately not unref'd: this promise is pending work, and a worker allowed to
|
|
84
|
+
* exit while awaiting its port would die silently instead of locking. The
|
|
85
|
+
* listener is removed as soon as the port lands, so the worker is free to exit
|
|
86
|
+
* again the moment the wait is over.
|
|
87
|
+
*/
|
|
88
|
+
export declare function processLockAttached(): Promise<void>;
|