@dbx-tools/core 0.6.65 → 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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # @dbx-tools/core
2
2
 
3
- Node-only core helpers for process execution and project discovery.
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssS0FBSyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxFQUFFLGtCQUFrQixFQUFFLG1CQUFtQixFQUFFLGlCQUFpQixFQUFFLHNCQUFzQixFQUFFLGtCQUFrQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFFeEksT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sZUFBZSxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gR0VORVJBVEVEIGJ5IHByb2plbiB3YXRjaCAtIERPIE5PVCBFRElULlxuLy8gUmVnZW5lcmF0ZWQgZnJvbSB0aGUgZXhwb3J0aW5nIG1vZHVsZXMgaW4gLi9zcmMuXG4vLyBIYW5kIGVkaXRzIGFyZSBvdmVyd3JpdHRlbiBvbiB0aGUgbmV4dCB3YXRjaDsgdGhpcyBmaWxlIGlzIHJlYWQtb25seS5cblxuZXhwb3J0ICogYXMgYnJhbmQgZnJvbSBcIi4vc3JjL2JyYW5kLnRzXCI7XG5leHBvcnQgKiBhcyBleGVjIGZyb20gXCIuL3NyYy9leGVjLnRzXCI7XG5leHBvcnQgKiBhcyBmaWxlIGZyb20gXCIuL3NyYy9maWxlLnRzXCI7XG5leHBvcnQgKiBhcyBwcm9qZWN0IGZyb20gXCIuL3NyYy9wcm9qZWN0LnRzXCI7XG5leHBvcnQgeyBCcmFuZENvbnRleHRTY2hlbWEsIGRlZmF1bHRCcmFuZENvbnRleHQsIHBhcnNlQnJhbmRDb250ZXh0LCBicmFuZENvbnRleHRKc29uU2NoZW1hLCBicmFuZENvbnRleHRQcm9tcHQgfSBmcm9tIFwiLi9zcmMvYnJhbmQudHNcIjtcbmV4cG9ydCB0eXBlIHsgQnJhbmRDb250ZXh0LCBCcmFuZENvbnRleHRJbnB1dCB9IGZyb20gXCIuL3NyYy9icmFuZC50c1wiO1xuZXhwb3J0IHsgQ09NTUFORF9OT1RfRk9VTkRfRVhJVF9DT0RFIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgRXhlY1N0ZGlvLCBMaW5lSGFuZGxlciwgU3RkaW9PcHRpb24sIEV4ZWNSZXN1bHQsIENoaWxkUHJvY2Vzc1Jlc3VsdCwgRXhlY09wdGlvbnMsIFN5bmNFeGVjU3RkaW8sIFN5bmNFeGVjT3B0aW9ucywgU3Bhd25BcmdzIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgUHJvamVjdENvbnRleHQgfSBmcm9tIFwiLi9zcmMvcHJvamVjdC50c1wiO1xuIl19
12
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssS0FBSyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxXQUFXLE1BQU0sdUJBQXVCLENBQUM7QUFDckQsT0FBTyxLQUFLLE9BQU8sTUFBTSxrQkFBa0IsQ0FBQztBQUU1QyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsbUJBQW1CLEVBQUUsaUJBQWlCLEVBQUUsc0JBQXNCLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUV4SSxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxlQUFlLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBHRU5FUkFURUQgYnkgcHJvamVuIHdhdGNoIC0gRE8gTk9UIEVESVQuXG4vLyBSZWdlbmVyYXRlZCBmcm9tIHRoZSBleHBvcnRpbmcgbW9kdWxlcyBpbiAuL3NyYy5cbi8vIEhhbmQgZWRpdHMgYXJlIG92ZXJ3cml0dGVuIG9uIHRoZSBuZXh0IHdhdGNoOyB0aGlzIGZpbGUgaXMgcmVhZC1vbmx5LlxuXG5leHBvcnQgKiBhcyBiaW4gZnJvbSBcIi4vc3JjL2Jpbi50c1wiO1xuZXhwb3J0ICogYXMgYnJhbmQgZnJvbSBcIi4vc3JjL2JyYW5kLnRzXCI7XG5leHBvcnQgKiBhcyBleGVjIGZyb20gXCIuL3NyYy9leGVjLnRzXCI7XG5leHBvcnQgKiBhcyBmaWxlIGZyb20gXCIuL3NyYy9maWxlLnRzXCI7XG5leHBvcnQgKiBhcyBwcm9jZXNzTG9jayBmcm9tIFwiLi9zcmMvcHJvY2Vzcy1sb2NrLnRzXCI7XG5leHBvcnQgKiBhcyBwcm9qZWN0IGZyb20gXCIuL3NyYy9wcm9qZWN0LnRzXCI7XG5leHBvcnQgdHlwZSB7IEJpbkNvbnRleHQsIEJpblNlbGVjdGlvbkNvbnRleHQsIEJpblNlbGVjdG9yLCBCaW5PcHRpb25zLCBCaW5VcmwgfSBmcm9tIFwiLi9zcmMvYmluLnRzXCI7XG5leHBvcnQgeyBCcmFuZENvbnRleHRTY2hlbWEsIGRlZmF1bHRCcmFuZENvbnRleHQsIHBhcnNlQnJhbmRDb250ZXh0LCBicmFuZENvbnRleHRKc29uU2NoZW1hLCBicmFuZENvbnRleHRQcm9tcHQgfSBmcm9tIFwiLi9zcmMvYnJhbmQudHNcIjtcbmV4cG9ydCB0eXBlIHsgQnJhbmRDb250ZXh0LCBCcmFuZENvbnRleHRJbnB1dCB9IGZyb20gXCIuL3NyYy9icmFuZC50c1wiO1xuZXhwb3J0IHsgQ09NTUFORF9OT1RfRk9VTkRfRVhJVF9DT0RFIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgRXhlY1N0ZGlvLCBMaW5lSGFuZGxlciwgU3RkaW9PcHRpb24sIEV4ZWNSZXN1bHQsIENoaWxkUHJvY2Vzc1Jlc3VsdCwgRXhlY09wdGlvbnMsIFN5bmNFeGVjU3RkaW8sIFN5bmNFeGVjT3B0aW9ucywgU3Bhd25BcmdzIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgUHJvamVjdENvbnRleHQgfSBmcm9tIFwiLi9zcmMvcHJvamVjdC50c1wiO1xuIl19
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmluLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2Jpbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7OztHQUlHO0FBQ0gsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN6QyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3BDLE9BQU8sRUFDTCxNQUFNLEVBQ04sS0FBSyxFQUNMLFFBQVEsRUFDUixLQUFLLEVBQ0wsT0FBTyxFQUNQLE9BQU8sRUFDUCxNQUFNLEVBQ04sRUFBRSxFQUNGLElBQUksRUFDSixTQUFTLEdBQ1YsTUFBTSxrQkFBa0IsQ0FBQztBQUMxQixPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUMxQyxPQUFPLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUUzQyxPQUFPLFVBQVUsTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxFQUFFLENBQUMsSUFBSSxVQUFVLEVBQUUsTUFBTSxLQUFLLENBQUM7QUFFdEMsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBZ0NwRCxTQUFTLE9BQU8sQ0FBQyxJQUFZLEVBQUUsT0FBZTtJQUM1QyxJQUFJLENBQUMsSUFBSSxJQUFJLFFBQVEsQ0FBQyxJQUFJLENBQUMsS0FBSyxJQUFJLElBQUksSUFBSSxLQUFLLEdBQUcsSUFBSSxJQUFJLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDdEUsTUFBTSxJQUFJLFNBQVMsQ0FBQyx3QkFBd0IsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN0RCxDQUFDO0lBQ0QsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLE9BQU8sRUFBRSxJQUFJLElBQUksRUFBRSxDQUFDLENBQUM7SUFDdkMsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsQ0FBQztJQUNqQyxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsRUFBRSxDQUFDO0FBQ3BELENBQUM7QUFFRCxLQUFLLFVBQVUsWUFBWSxDQUFDLElBQVk7SUFDdEMsSUFBSSxDQUFDO1FBQ0gsSUFBSSxDQUFDLENBQUMsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxNQUFNLEVBQUU7WUFBRSxPQUFPLEtBQUssQ0FBQztRQUMvQyxNQUFNLE1BQU0sQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLElBQUksR0FBRyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDcEQsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0FBQ0gsQ0FBQztBQUVELFNBQVMsWUFBWSxDQUFDLEdBQVcsRUFBRSxJQUFZO0lBQzdDLElBQUksQ0FBQztRQUNILE1BQU0sU0FBUyxHQUFHLFFBQVEsQ0FBQyxrQkFBa0IsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDO1FBQ3RFLE9BQU8sU0FBUyxJQUFJLE1BQU0sQ0FBQyxVQUFVLENBQUMsU0FBUyxDQUFDLElBQUksR0FBRyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztJQUM3RSxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1AsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0FBQ0gsQ0FBQztBQUVELEtBQUssVUFBVSxNQUFNLENBQUMsT0FBZSxFQUFFLFdBQW1CO0lBQ3hELE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxXQUFXLEVBQUUsQ0FBQztJQUN2QyxJQUFJLFFBQVEsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUM5QixNQUFNLFVBQVUsQ0FBQyxPQUFPLEVBQUUsRUFBRSxHQUFHLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQztRQUNoRCxPQUFPO0lBQ1QsQ0FBQztJQUNELElBQUksUUFBUSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsSUFBSSxRQUFRLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxJQUFJLFFBQVEsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUMzRixNQUFNLFVBQVUsQ0FBQyxFQUFFLElBQUksRUFBRSxPQUFPLEVBQUUsR0FBRyxFQUFFLFdBQVcsRUFBRSxDQUFDLENBQUM7UUFDdEQsT0FBTztJQUNULENBQUM7SUFDRCxNQUFNLElBQUksS0FBSyxDQUFDLCtCQUErQixRQUFRLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQ3RFLENBQUM7QUFFRCxLQUFLLFVBQVUsVUFBVSxDQUFDLElBQVk7SUFDcEMsTUFBTSxPQUFPLEdBQUcsTUFBTSxPQUFPLENBQUMsSUFBSSxFQUFFLEVBQUUsYUFBYSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7SUFDN0QsTUFBTSxLQUFLLEdBQWEsRUFBRSxDQUFDO0lBQzNCLEtBQUssTUFBTSxLQUFLLElBQUksT0FBTyxFQUFFLENBQUM7UUFDNUIsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDcEMsSUFBSSxLQUFLLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQztZQUN4QixLQUFLLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLFVBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDMUMsQ0FBQzthQUFNLElBQUksS0FBSyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUM7WUFDMUIsS0FBSyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNuQixDQUFDO0lBQ0gsQ0FBQztJQUNELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQztBQUVELEtBQUssVUFBVSxnQkFBZ0IsQ0FBQyxNQUFjO0lBQzVDLE1BQU0sS0FBSyxHQUFHLE1BQU0sVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3ZDLE1BQU0sUUFBUSxHQUFHLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDN0IsSUFBSSxLQUFLLENBQUMsTUFBTSxLQUFLLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ3BDLE1BQU0sSUFBSSxLQUFLLENBQUMsK0NBQStDLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO0lBQ2pGLENBQUM7SUFDRCxPQUFPLFFBQVEsQ0FBQztBQUNsQixDQUFDO0FBRUQsS0FBSyxVQUFVLFdBQVcsQ0FDeEIsV0FBdUIsRUFDdkIsR0FBVyxFQUNYLElBQVksRUFDWixPQUFtQjtJQUVuQixNQUFNLElBQUksR0FBRyxZQUFZLENBQUMsR0FBRyxFQUFFLFFBQVEsQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUMzRCxNQUFNLFlBQVksR0FBRyxJQUFJLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxDQUFDO0lBQ3RDLE1BQU0sUUFBUSxHQUFHLE1BQU0sS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2xDLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDakIsTUFBTSxJQUFJLEtBQUssQ0FBQywyQkFBMkIsUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDakUsQ0FBQztJQUNELE1BQU0sU0FBUyxDQUFDLFlBQVksRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sUUFBUSxDQUFDLFdBQVcsRUFBRSxDQUFDLEVBQUUsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztJQUUxRixJQUFJLE1BQU0sR0FBRyxZQUFZLENBQUM7SUFDMUIsSUFBSSxPQUFPLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDMUIsTUFBTSxHQUFHLElBQUksQ0FBQyxJQUFJLEVBQUUsWUFBWSxVQUFVLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDaEQsTUFBTSxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDcEIsTUFBTSxNQUFNLENBQUMsWUFBWSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ3JDLENBQUM7SUFFRCxJQUFJLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNyQixPQUFPLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUM7SUFDakUsQ0FBQztJQUNELE9BQU8sT0FBTyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUNuRSxDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsTUFBTSxDQUMxQixJQUFZLEVBQ1osR0FBVyxFQUNYLFVBQXNCLEVBQUU7SUFFeEIsTUFBTSxXQUFXLEdBQUcsT0FBTyxDQUFDLElBQUksRUFBRSxPQUFPLENBQUMsT0FBTyxJQUFJLE9BQU8sRUFBRSxDQUFDLENBQUM7SUFDaEUsSUFBSSxNQUFNLFlBQVksQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDO1FBQUUsT0FBTyxXQUFXLENBQUM7SUFFN0QsT0FBTyxlQUFlLENBQUMsQ0FBQyxZQUFZLEVBQUUsV0FBVyxDQUFDLElBQUksQ0FBQyxFQUFFLEtBQUssSUFBSSxFQUFFO1FBQ2xFLElBQUksTUFBTSxZQUFZLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQztZQUFFLE9BQU8sV0FBVyxDQUFDO1FBRTdELE1BQU0sV0FBVyxHQUFHLE9BQU8sR0FBRyxLQUFLLFVBQVUsQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLEVBQUUsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDO1FBQ2xFLE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsRUFBRSxHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsQ0FBQztRQUN2RCxJQUFJLE1BQTBCLENBQUM7UUFDL0IsSUFBSSxDQUFDO1lBQ0gsTUFBTSxRQUFRLEdBQUcsTUFBTSxXQUFXLENBQUMsV0FBVyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDNUUsSUFBSSxDQUFDLENBQUMsTUFBTSxZQUFZLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxDQUFDO2dCQUNwQyxNQUFNLElBQUksS0FBSyxDQUFDLHNDQUFzQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO1lBQ3BFLENBQUM7WUFFRCxNQUFNLEtBQUssQ0FBQyxXQUFXLENBQUMsTUFBTSxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7WUFDckQsTUFBTSxHQUFHLElBQUksQ0FBQyxXQUFXLENBQUMsTUFBTSxFQUFFLElBQUksSUFBSSxJQUFJLFVBQVUsRUFBRSxFQUFFLENBQUMsQ0FBQztZQUM5RCxNQUFNLFFBQVEsQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDakMsTUFBTSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsTUFBTSxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNqRCxNQUFNLE1BQU0sQ0FBQyxNQUFNLEVBQUUsV0FBVyxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ3ZDLE1BQU0sR0FBRyxTQUFTLENBQUM7WUFDbkIsT0FBTyxXQUFXLENBQUM7UUFDckIsQ0FBQztnQkFBUyxDQUFDO1lBQ1QsSUFBSSxNQUFNO2dCQUFFLE1BQU0sRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO1lBQzlDLE1BQU0sRUFBRSxDQUFDLElBQUksRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDbkQsQ0FBQztJQUNILENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogSW5zdGFsbCBhbmQgcmV1c2UgZXhlY3V0YWJsZSBiaW5hcmllcyB1bmRlciBhIHBlci10b29sIGhvbWUgZGlyZWN0b3J5LlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuaW1wb3J0IHsgcmFuZG9tVVVJRCB9IGZyb20gXCJub2RlOmNyeXB0b1wiO1xuaW1wb3J0IHsgY29uc3RhbnRzIH0gZnJvbSBcIm5vZGU6ZnNcIjtcbmltcG9ydCB7XG4gIGFjY2VzcyxcbiAgY2htb2QsXG4gIGNvcHlGaWxlLFxuICBta2RpcixcbiAgbWtkdGVtcCxcbiAgcmVhZGRpcixcbiAgcmVuYW1lLFxuICBybSxcbiAgc3RhdCxcbiAgd3JpdGVGaWxlLFxufSBmcm9tIFwibm9kZTpmcy9wcm9taXNlc1wiO1xuaW1wb3J0IHsgaG9tZWRpciwgdG1wZGlyIH0gZnJvbSBcIm5vZGU6b3NcIjtcbmltcG9ydCB7IGJhc2VuYW1lLCBqb2luIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuXG5pbXBvcnQgZXh0cmFjdFppcCBmcm9tIFwiZXh0cmFjdC16aXBcIjtcbmltcG9ydCB7IHggYXMgZXh0cmFjdFRhciB9IGZyb20gXCJ0YXJcIjtcblxuaW1wb3J0IHsgd2l0aFByb2Nlc3NMb2NrIH0gZnJvbSBcIi4vcHJvY2Vzcy1sb2NrLnRzXCI7XG5cbi8qKiBTdGFibGUgcGF0aHMgZm9yIGFuIGluc3RhbGxlZCBiaW5hcnkuICovXG5leHBvcnQgaW50ZXJmYWNlIEJpbkNvbnRleHQge1xuICByb290OiBzdHJpbmc7XG4gIGJpbkRpcjogc3RyaW5nO1xuICBwYXRoOiBzdHJpbmc7XG59XG5cbi8qKiBUZW1wb3JhcnkgZG93bmxvYWQgYW5kIGV4dHJhY3Rpb24gcGF0aHMgc3VwcGxpZWQgdG8gYSBjdXN0b20gc2VsZWN0b3IuICovXG5leHBvcnQgaW50ZXJmYWNlIEJpblNlbGVjdGlvbkNvbnRleHQge1xuICBkZXN0aW5hdGlvbjogQmluQ29udGV4dDtcbiAgZG93bmxvYWRQYXRoOiBzdHJpbmc7XG4gIHNvdXJjZTogc3RyaW5nO1xufVxuXG4vKipcbiAqIFNlbGVjdCB0aGUgZXhlY3V0YWJsZSBmcm9tIGEgZG93bmxvYWQgb3IgdW5wYWNrZWQgYXJjaGl2ZS4gQSBzZWxlY3RvciBtYXlcbiAqIGFsc28gcHJlcGFyZSB0aGUgZmlsZSwgc3VjaCBhcyBhcHBseWluZyBpdHMgZXhlY3V0YWJsZSBtb2RlLlxuICovXG5leHBvcnQgdHlwZSBCaW5TZWxlY3RvciA9IChjb250ZXh0OiBCaW5TZWxlY3Rpb25Db250ZXh0KSA9PiBzdHJpbmcgfCBQcm9taXNlPHN0cmluZz47XG5cbi8qKiBPcHRpb25zIGZvciB7QGxpbmsgZW5zdXJlfS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQmluT3B0aW9ucyB7XG4gIGF1dG9VbnBhY2thZ2U/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>;