@dbx-tools/postgres 0.6.62
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 +225 -0
- package/index.ts +10 -0
- package/lib/exports.d.ts +1 -0
- package/lib/exports.js +6 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +8 -0
- package/lib/src/advisory-lock.d.ts +51 -0
- package/lib/src/advisory-lock.js +111 -0
- package/lib/src/topic-bus.d.ts +272 -0
- package/lib/src/topic-bus.js +506 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +74 -0
- package/src/advisory-lock.ts +148 -0
- package/src/topic-bus.ts +655 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# `@dbx-tools/postgres`
|
|
2
|
+
|
|
3
|
+
Connection-correct PostgreSQL primitives for Node.js: advisory locks that hold the
|
|
4
|
+
connection they lock, and a structured topic bus over `LISTEN`/`NOTIFY`.
|
|
5
|
+
|
|
6
|
+
Both work against a plain `pg.Pool` or anything structurally compatible with one,
|
|
7
|
+
including the pool AppKit's Lakebase plugin exports — so a Databricks App gets
|
|
8
|
+
them without a second database client or connection pool.
|
|
9
|
+
|
|
10
|
+
## Key Features
|
|
11
|
+
|
|
12
|
+
- Converts an arbitrary structured key (string, array, object, `Date`, or an
|
|
13
|
+
explicit `bigint`) into a stable signed 64-bit advisory-lock identifier.
|
|
14
|
+
- Derives a legal Postgres channel name from whatever identifies a channel, so no
|
|
15
|
+
call site has to sanitize an app name or a tenant id into an identifier.
|
|
16
|
+
- Holds a session lock on one dedicated pooled connection for the whole callback,
|
|
17
|
+
and hands that connection to the callback so protected work runs on it.
|
|
18
|
+
- Holds a transaction lock released atomically by `COMMIT` or `ROLLBACK`, which is
|
|
19
|
+
what one-time schema installation needs.
|
|
20
|
+
- Broadcasts a `type`/`metadata`/`body` envelope to every listening process over
|
|
21
|
+
`pg_notify`, filtered by topic in-process.
|
|
22
|
+
- Fills in project, machine, process, deployment, and optional AppKit sender
|
|
23
|
+
context automatically, with caller metadata always winning.
|
|
24
|
+
- Rejects a payload that would not survive a JSON round trip unchanged, at the
|
|
25
|
+
call site rather than on the wire.
|
|
26
|
+
- Reconnects a dropped listener with bounded backoff while subscribers remain.
|
|
27
|
+
|
|
28
|
+
## Advisory Locks
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { withAdvisoryLock } from "@dbx-tools/postgres";
|
|
32
|
+
import { Pool } from "pg";
|
|
33
|
+
|
|
34
|
+
const pool = new Pool();
|
|
35
|
+
|
|
36
|
+
await withAdvisoryLock(pool, ["invoice", invoiceId], async (client) => {
|
|
37
|
+
await client.query("UPDATE invoices SET status = 'sent' WHERE id = $1", [invoiceId]);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Run the protected work on the `client` the callback receives. A `pool.query()`
|
|
42
|
+
inside the callback may land on a different connection, which is not the one
|
|
43
|
+
holding the lock.
|
|
44
|
+
|
|
45
|
+
Use the transaction-scoped helper for one-time database work:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { withAdvisoryTransactionLock } from "@dbx-tools/postgres";
|
|
49
|
+
|
|
50
|
+
await withAdvisoryTransactionLock(pool, { schema: "my_feature", version: 1 }, async (client) => {
|
|
51
|
+
await client.query("CREATE TABLE IF NOT EXISTS my_feature.events (id bigint primary key)");
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`advisoryLockId(key)` exposes the same reduction on its own. A `bigint` key is
|
|
56
|
+
used directly (narrowed to 64 bits) rather than hashed, so a lock can interoperate
|
|
57
|
+
with another implementation that publishes its numeric lock id — `pgmq`'s
|
|
58
|
+
installer lock, for instance. Anything else is canonicalized with
|
|
59
|
+
`object.toStableKey` from `@dbx-tools/shared-core` and hashed, so key order in an
|
|
60
|
+
object does not matter while a `1` and a `"1"` stay different locks. An array is
|
|
61
|
+
read as several parts, so `["invoice", 7]` and `"invoice_7"` are different locks. A
|
|
62
|
+
non-finite number or a cyclic key throws `TypeError` rather than yielding an
|
|
63
|
+
identity two callers could disagree about.
|
|
64
|
+
|
|
65
|
+
## Topic Bus
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { PostgresTopicBus } from "@dbx-tools/postgres";
|
|
69
|
+
|
|
70
|
+
const bus = new PostgresTopicBus(pool, {
|
|
71
|
+
metadata: async () => ({ publicIp: await resolvePublicIp() }),
|
|
72
|
+
onError: (cause) => logger.error("bus", { cause }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const unsubscribe = await bus.listen("orders", ({ type, metadata, body }) => {
|
|
76
|
+
console.log(type, metadata.project, body);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await bus.broadcast("orders", {
|
|
80
|
+
type: "order.updated",
|
|
81
|
+
metadata: { traceId: "abc-123" },
|
|
82
|
+
body: { orderId: "123", status: "ready" },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await unsubscribe();
|
|
86
|
+
await bus.close();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every process listening on the channel receives every message; there are no
|
|
90
|
+
competing consumers. That makes the bus right for live fan-out — an SSE stream per
|
|
91
|
+
browser tab, a cache invalidation, a presence ping — and wrong for work
|
|
92
|
+
distribution. Use a table or a queue when a subscriber needs acks or replay.
|
|
93
|
+
|
|
94
|
+
### The Envelope
|
|
95
|
+
|
|
96
|
+
| Field | Source | Notes |
|
|
97
|
+
| ------------- | ------ | ------------------------------------------------------------------ |
|
|
98
|
+
| `id` | bus | Unique per message across instances. Use it to dedupe or as `id:`. |
|
|
99
|
+
| `topic` | bus | The `broadcast` topic. Listeners on other topics never see it. |
|
|
100
|
+
| `type` | caller | Event name, e.g. `order.updated`. Required, non-blank. |
|
|
101
|
+
| `metadata` | merged | Automatic context under the caller's. See below. |
|
|
102
|
+
| `body` | caller | Passed through unchanged. |
|
|
103
|
+
| `publishedAt` | bus | ISO-8601, from the publisher's clock — not the database's. |
|
|
104
|
+
|
|
105
|
+
`publishedAt` and `id` are for display and dedupe. Neither orders messages from
|
|
106
|
+
different instances reliably.
|
|
107
|
+
|
|
108
|
+
### Metadata Precedence
|
|
109
|
+
|
|
110
|
+
Weakest to strongest: automatic machine context, then AppKit sender identity, then
|
|
111
|
+
the bus's `metadata` option, then the per-message `metadata`. A key present at a
|
|
112
|
+
stronger layer is never overwritten, so passing `project` explicitly replaces the
|
|
113
|
+
inferred one.
|
|
114
|
+
|
|
115
|
+
Automatic keys, each omitted when it resolves to nothing: `project` (from
|
|
116
|
+
`DATABRICKS_APP_NAME`, `DATABRICKS_BUNDLE_NAME`, `PROJECT_NAME`, or
|
|
117
|
+
`npm_package_name`), `publicIp` (from `PUBLIC_IP`, `DATABRICKS_PUBLIC_IP`, or
|
|
118
|
+
`HOST_IP`), `hostname`, `cwd`, `platform`, `pid`, `environment`, `appName`,
|
|
119
|
+
`deploymentId`, and `databricksHost`. CPU architecture, runtime name, and runtime
|
|
120
|
+
version are left out on purpose: they are constant per deployment and every
|
|
121
|
+
message would pay for them against the payload limit.
|
|
122
|
+
|
|
123
|
+
`publicIp` is only read from the environment. The bus makes no network call to
|
|
124
|
+
discover one — pass a `metadata` function for that, which is also how to attach
|
|
125
|
+
context a process learns after construction. The function runs on every broadcast,
|
|
126
|
+
so memoize anything expensive.
|
|
127
|
+
|
|
128
|
+
With the optional `@databricks/appkit` peer installed and a user execution context
|
|
129
|
+
active, `senderId`, `senderName`, and `senderEmail` are added too. AppKit is
|
|
130
|
+
imported lazily and never required; outside a request, or without the package, the
|
|
131
|
+
bus just skips those keys.
|
|
132
|
+
|
|
133
|
+
### What A Message May Contain
|
|
134
|
+
|
|
135
|
+
The body and metadata must survive `JSON.parse(JSON.stringify(x))` unchanged, which
|
|
136
|
+
is stricter than `JSON.stringify` not throwing. A `Date` becomes a string, `NaN`
|
|
137
|
+
and `Infinity` become `null`, a `Map` becomes `{}`, and `undefined` vanishes — each
|
|
138
|
+
reaches the listener as something other than what was sent, so all of them throw
|
|
139
|
+
`TypeError` at the call instead. Cycles, functions, symbols, bigints, and class
|
|
140
|
+
instances are rejected for the same reason.
|
|
141
|
+
|
|
142
|
+
The rule lives in `@dbx-tools/shared-core` as `object.isSerializableValue` and
|
|
143
|
+
`object.SerializableValue`, so a route can apply the same check to an untrusted
|
|
144
|
+
request body and answer 400 rather than 500, and a browser-side caller can share
|
|
145
|
+
the type.
|
|
146
|
+
|
|
147
|
+
The encoded envelope is capped at 7900 bytes, under PostgreSQL's 8000-byte
|
|
148
|
+
`NOTIFY` limit, and a larger one throws `RangeError`. Automatic metadata counts
|
|
149
|
+
against that, so broadcast a reference and let the receiver fetch anything big.
|
|
150
|
+
|
|
151
|
+
### Connections, Lifecycle, And Failure
|
|
152
|
+
|
|
153
|
+
Publishing borrows a pooled connection per call. Listening cannot: `LISTEN` is
|
|
154
|
+
session state, so the bus checks out ONE dedicated client and keeps it for as long
|
|
155
|
+
as it has subscribers, however many topics and listeners are registered. Size the
|
|
156
|
+
pool with that long-lived checkout in mind.
|
|
157
|
+
|
|
158
|
+
Nothing connects until the first `listen`, or an explicit `start()` when you would
|
|
159
|
+
rather find out about a connection problem at boot. `close()` releases the client
|
|
160
|
+
and is required — register it with the host's shutdown hook. A closed bus stays
|
|
161
|
+
closed and throws instead of silently reconnecting.
|
|
162
|
+
|
|
163
|
+
When the notification connection drops, the bus reconnects on its own: immediately
|
|
164
|
+
first, then doubling from 250ms to a 5s ceiling, indefinitely, for as long as
|
|
165
|
+
subscribers remain. A Postgres restart or a rotated Lakebase credential recovers
|
|
166
|
+
without intervention. Messages published during the gap are lost. Every failed
|
|
167
|
+
attempt, every dropped connection, and every listener that throws is reported
|
|
168
|
+
through `onError`, which defaults to swallowing them — wire it to a logger in
|
|
169
|
+
anything long-running. A failing listener never affects the publisher or the other
|
|
170
|
+
listeners.
|
|
171
|
+
|
|
172
|
+
One channel carries many topics, so adding a topic costs nothing; give a genuinely
|
|
173
|
+
high-volume unrelated stream its own channel instead, since every listening
|
|
174
|
+
session decodes every message on the channel.
|
|
175
|
+
|
|
176
|
+
### Naming A Channel
|
|
177
|
+
|
|
178
|
+
`channel` takes whatever identifies the channel, not a pre-sanitized identifier —
|
|
179
|
+
a name, a tenant id, a `[env, feature]` pair, a config object. One value or many:
|
|
180
|
+
an array is read as several parts, anything else as one.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
new PostgresTopicBus(pool, { channel: "billing-events" }).channelName;
|
|
184
|
+
// "billing_events_3x55ck"
|
|
185
|
+
new PostgresTopicBus(pool, { channel: ["billing", "production"] }).channelName;
|
|
186
|
+
// "billing_production_008jgf"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The parts are tokenized into the readable half and a short hash of their canonical
|
|
190
|
+
form (`object.toStableKey`) is appended. The hash is what makes the mapping
|
|
191
|
+
trustworthy: tokenizing alone is lossy, so `my-app`, `my_app`, and `myApp` would
|
|
192
|
+
collapse onto one channel, and a name long enough to hit Postgres's 63-character
|
|
193
|
+
limit would collide with anything sharing its leading tokens. With the suffix the
|
|
194
|
+
readable part stays readable and distinct inputs stay distinct — including
|
|
195
|
+
`["billing", "prod"]` versus `"billing_prod"`, which differ in structure.
|
|
196
|
+
|
|
197
|
+
Derivation is deterministic across processes and runs, so every participant that
|
|
198
|
+
passes equivalent parts lands on the same channel without coordinating. Object key
|
|
199
|
+
order does not matter; a different spelling does. Read `bus.channelName` to see
|
|
200
|
+
what a set of parts resolved to, or to confirm two processes agree. Defaults to a
|
|
201
|
+
shared `dbx_tools_topic_bus` channel.
|
|
202
|
+
|
|
203
|
+
## Why A Separate Package?
|
|
204
|
+
|
|
205
|
+
Advisory locks are connection-scoped, and that is easy to get wrong invisibly:
|
|
206
|
+
taking the lock with `pool.query()` and doing the protected work with another
|
|
207
|
+
`pool.query()` can use two different connections, so the lock protects nothing and
|
|
208
|
+
the code looks correct. The same applies to `LISTEN`, which is session state a
|
|
209
|
+
pooled query cannot hold. This package owns both lifecycles once, with no
|
|
210
|
+
dependency beyond `pg`, so a consumer that only needs a lock does not pull in a
|
|
211
|
+
message bus, an AppKit runtime, or a queue extension.
|
|
212
|
+
|
|
213
|
+
AppKit exposes Lakebase but has no lock helper and no message bus, so there is no
|
|
214
|
+
native surface to prefer here.
|
|
215
|
+
|
|
216
|
+
## Module Map
|
|
217
|
+
|
|
218
|
+
| Module | Purpose |
|
|
219
|
+
| -------------- | ------------------------------------------------------------------ |
|
|
220
|
+
| `advisoryLock` | Stable lock IDs plus session- and transaction-scoped lock helpers. |
|
|
221
|
+
| `topicBus` | Structured topic broadcast/listen over PostgreSQL `NOTIFY`. |
|
|
222
|
+
|
|
223
|
+
Both modules are also flattened onto the package root, so
|
|
224
|
+
`import { withAdvisoryLock, PostgresTopicBus } from "@dbx-tools/postgres"` works
|
|
225
|
+
without the namespace.
|
package/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
|
|
5
|
+
export * as advisoryLock from "./src/advisory-lock.ts";
|
|
6
|
+
export * as topicBus from "./src/topic-bus.ts";
|
|
7
|
+
export type { AdvisoryLockKey, PgPoolLike, PgQueryable } from "./src/advisory-lock.ts";
|
|
8
|
+
export { PostgresTopicBus } from "./src/topic-bus.ts";
|
|
9
|
+
export type { TopicMetadata, TopicMessage, TopicPublishInput, TopicListener, TopicMetadataProvider, PostgresTopicBusOptions } from "./src/topic-bus.ts";
|
|
10
|
+
export * from "./exports.ts";
|
package/lib/exports.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { advisoryLockId, withAdvisoryLock, withAdvisoryTransactionLock, } from "./src/advisory-lock.ts";
|
package/lib/exports.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Flat re-exports layered onto the generated `index.ts` barrel, which publishes
|
|
2
|
+
// each module as a namespace (`advisoryLock`, `topicBus`) and does not lift
|
|
3
|
+
// functions to the package root. These are the call-site names, so they are
|
|
4
|
+
// importable directly from `@dbx-tools/postgres`.
|
|
5
|
+
export { advisoryLockId, withAdvisoryLock, withAdvisoryTransactionLock, } from "./src/advisory-lock.js";
|
|
6
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhwb3J0cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL2V4cG9ydHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsZ0ZBQWdGO0FBQ2hGLDRFQUE0RTtBQUM1RSw0RUFBNEU7QUFDNUUsa0RBQWtEO0FBQ2xELE9BQU8sRUFDTCxjQUFjLEVBQ2QsZ0JBQWdCLEVBQ2hCLDJCQUEyQixHQUM1QixNQUFNLHdCQUF3QixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gRmxhdCByZS1leHBvcnRzIGxheWVyZWQgb250byB0aGUgZ2VuZXJhdGVkIGBpbmRleC50c2AgYmFycmVsLCB3aGljaCBwdWJsaXNoZXNcbi8vIGVhY2ggbW9kdWxlIGFzIGEgbmFtZXNwYWNlIChgYWR2aXNvcnlMb2NrYCwgYHRvcGljQnVzYCkgYW5kIGRvZXMgbm90IGxpZnRcbi8vIGZ1bmN0aW9ucyB0byB0aGUgcGFja2FnZSByb290LiBUaGVzZSBhcmUgdGhlIGNhbGwtc2l0ZSBuYW1lcywgc28gdGhleSBhcmVcbi8vIGltcG9ydGFibGUgZGlyZWN0bHkgZnJvbSBgQGRieC10b29scy9wb3N0Z3Jlc2AuXG5leHBvcnQge1xuICBhZHZpc29yeUxvY2tJZCxcbiAgd2l0aEFkdmlzb3J5TG9jayxcbiAgd2l0aEFkdmlzb3J5VHJhbnNhY3Rpb25Mb2NrLFxufSBmcm9tIFwiLi9zcmMvYWR2aXNvcnktbG9jay50c1wiO1xuIl19
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * as advisoryLock from "./src/advisory-lock.ts";
|
|
2
|
+
export * as topicBus from "./src/topic-bus.ts";
|
|
3
|
+
export type { AdvisoryLockKey, PgPoolLike, PgQueryable } from "./src/advisory-lock.ts";
|
|
4
|
+
export { PostgresTopicBus } from "./src/topic-bus.ts";
|
|
5
|
+
export type { TopicMetadata, TopicMessage, TopicPublishInput, TopicListener, TopicMetadataProvider, PostgresTopicBusOptions } from "./src/topic-bus.ts";
|
|
6
|
+
export * from "./exports.ts";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
export * as advisoryLock from "./src/advisory-lock.js";
|
|
5
|
+
export * as topicBus from "./src/topic-bus.js";
|
|
6
|
+
export { PostgresTopicBus } from "./src/topic-bus.js";
|
|
7
|
+
export * from "./exports.js";
|
|
8
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssWUFBWSxNQUFNLHdCQUF3QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxRQUFRLE1BQU0sb0JBQW9CLENBQUM7QUFFL0MsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFdEQsY0FBYyxjQUFjLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBHRU5FUkFURUQgYnkgcHJvamVuIHdhdGNoIC0gRE8gTk9UIEVESVQuXG4vLyBSZWdlbmVyYXRlZCBmcm9tIHRoZSBleHBvcnRpbmcgbW9kdWxlcyBpbiAuL3NyYy5cbi8vIEhhbmQgZWRpdHMgYXJlIG92ZXJ3cml0dGVuIG9uIHRoZSBuZXh0IHdhdGNoOyB0aGlzIGZpbGUgaXMgcmVhZC1vbmx5LlxuXG5leHBvcnQgKiBhcyBhZHZpc29yeUxvY2sgZnJvbSBcIi4vc3JjL2Fkdmlzb3J5LWxvY2sudHNcIjtcbmV4cG9ydCAqIGFzIHRvcGljQnVzIGZyb20gXCIuL3NyYy90b3BpYy1idXMudHNcIjtcbmV4cG9ydCB0eXBlIHsgQWR2aXNvcnlMb2NrS2V5LCBQZ1Bvb2xMaWtlLCBQZ1F1ZXJ5YWJsZSB9IGZyb20gXCIuL3NyYy9hZHZpc29yeS1sb2NrLnRzXCI7XG5leHBvcnQgeyBQb3N0Z3Jlc1RvcGljQnVzIH0gZnJvbSBcIi4vc3JjL3RvcGljLWJ1cy50c1wiO1xuZXhwb3J0IHR5cGUgeyBUb3BpY01ldGFkYXRhLCBUb3BpY01lc3NhZ2UsIFRvcGljUHVibGlzaElucHV0LCBUb3BpY0xpc3RlbmVyLCBUb3BpY01ldGFkYXRhUHJvdmlkZXIsIFBvc3RncmVzVG9waWNCdXNPcHRpb25zIH0gZnJvbSBcIi4vc3JjL3RvcGljLWJ1cy50c1wiO1xuZXhwb3J0ICogZnJvbSBcIi4vZXhwb3J0cy50c1wiO1xuIl19
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory-lock helpers for any `pg.Pool`-compatible pool.
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL advisory locks belong to a connection, not a pool. These helpers
|
|
5
|
+
* reserve one pooled client for the full callback, acquire the lock on that
|
|
6
|
+
* client, and release both in the correct order.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import type { Pool, PoolClient, QueryResult, QueryResultRow } from "pg";
|
|
11
|
+
/**
|
|
12
|
+
* What names a lock. Anything reducible to a stable identity: a string, an id, a
|
|
13
|
+
* `["invoice", id]` pair, a config object, or an explicit `bigint` to interoperate
|
|
14
|
+
* with another implementation's published lock id.
|
|
15
|
+
*
|
|
16
|
+
* One value or many: an array is read as multiple parts, anything else as a single
|
|
17
|
+
* part. So `["invoice", 7]` and `"invoice_7"` are different locks, since the
|
|
18
|
+
* canonical form sees different structure.
|
|
19
|
+
*/
|
|
20
|
+
export type AdvisoryLockKey = unknown;
|
|
21
|
+
/** Structural pool shape accepted by the lock helpers. */
|
|
22
|
+
export type PgPoolLike = Pick<Pool, "connect">;
|
|
23
|
+
/** Structural query shape shared by `pg.PoolClient` and AppKit Lakebase. */
|
|
24
|
+
export interface PgQueryable {
|
|
25
|
+
query<T extends QueryResultRow = QueryResultRow>(text: string, values?: unknown[]): Promise<QueryResult<T>>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Convert an arbitrary structured key into PostgreSQL's signed 64-bit advisory
|
|
29
|
+
* lock namespace. A bigint is preserved directly so callers can interoperate
|
|
30
|
+
* with another implementation that publishes its lock ID.
|
|
31
|
+
*
|
|
32
|
+
* Everything else is canonicalized with `object.toStableKey` and hashed, so key
|
|
33
|
+
* order in an object does not matter while a `1` and a `"1"` stay different locks.
|
|
34
|
+
* A cycle, a non-finite number, or a function/symbol key throws `TypeError`
|
|
35
|
+
* rather than yielding an identity two callers could disagree about.
|
|
36
|
+
*/
|
|
37
|
+
export declare function advisoryLockId(key: AdvisoryLockKey): bigint;
|
|
38
|
+
/**
|
|
39
|
+
* Hold a session advisory lock for the duration of `fn`.
|
|
40
|
+
*
|
|
41
|
+
* The callback receives the dedicated `PoolClient` that owns the lock. Use it
|
|
42
|
+
* for any operation that must be protected by the lock.
|
|
43
|
+
*/
|
|
44
|
+
export declare function withAdvisoryLock<T>(pool: PgPoolLike, key: AdvisoryLockKey, fn: (client: PoolClient) => Promise<T> | T): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Run `fn` in a transaction while holding a transaction advisory lock.
|
|
47
|
+
*
|
|
48
|
+
* The lock is released atomically by `COMMIT` or `ROLLBACK`, making this the
|
|
49
|
+
* right primitive for one-time schema installation and migrations.
|
|
50
|
+
*/
|
|
51
|
+
export declare function withAdvisoryTransactionLock<T>(pool: PgPoolLike, key: AdvisoryLockKey, fn: (client: PoolClient) => Promise<T> | T): Promise<T>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory-lock helpers for any `pg.Pool`-compatible pool.
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL advisory locks belong to a connection, not a pool. These helpers
|
|
5
|
+
* reserve one pooled client for the full callback, acquire the lock on that
|
|
6
|
+
* client, and release both in the correct order.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { object } from "@dbx-tools/shared-core";
|
|
12
|
+
const SIGNED_BIGINT_BITS = 64;
|
|
13
|
+
/**
|
|
14
|
+
* Convert an arbitrary structured key into PostgreSQL's signed 64-bit advisory
|
|
15
|
+
* lock namespace. A bigint is preserved directly so callers can interoperate
|
|
16
|
+
* with another implementation that publishes its lock ID.
|
|
17
|
+
*
|
|
18
|
+
* Everything else is canonicalized with `object.toStableKey` and hashed, so key
|
|
19
|
+
* order in an object does not matter while a `1` and a `"1"` stay different locks.
|
|
20
|
+
* A cycle, a non-finite number, or a function/symbol key throws `TypeError`
|
|
21
|
+
* rather than yielding an identity two callers could disagree about.
|
|
22
|
+
*/
|
|
23
|
+
export function advisoryLockId(key) {
|
|
24
|
+
if (typeof key === "bigint")
|
|
25
|
+
return BigInt.asIntN(SIGNED_BIGINT_BITS, key);
|
|
26
|
+
const parts = object.toOneOrMany(key);
|
|
27
|
+
const digest = createHash("sha256")
|
|
28
|
+
.update(parts.map((part) => object.toStableKey(part)).join("\u0000"))
|
|
29
|
+
.digest();
|
|
30
|
+
return digest.readBigInt64BE(0);
|
|
31
|
+
}
|
|
32
|
+
async function acquire(client, id, transaction) {
|
|
33
|
+
const fn = transaction ? "pg_advisory_xact_lock" : "pg_advisory_lock";
|
|
34
|
+
await client.query(`SELECT ${fn}($1::bigint)`, [id.toString()]);
|
|
35
|
+
}
|
|
36
|
+
async function unlock(client, id) {
|
|
37
|
+
const result = await client.query("SELECT pg_advisory_unlock($1::bigint) AS unlocked", [id.toString()]);
|
|
38
|
+
if (result.rows[0]?.unlocked !== true) {
|
|
39
|
+
throw new Error(`Postgres advisory lock ${id} was not held by this connection`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Hold a session advisory lock for the duration of `fn`.
|
|
44
|
+
*
|
|
45
|
+
* The callback receives the dedicated `PoolClient` that owns the lock. Use it
|
|
46
|
+
* for any operation that must be protected by the lock.
|
|
47
|
+
*/
|
|
48
|
+
export async function withAdvisoryLock(pool, key, fn) {
|
|
49
|
+
const id = advisoryLockId(key);
|
|
50
|
+
const client = await pool.connect();
|
|
51
|
+
let acquired = false;
|
|
52
|
+
let failed = false;
|
|
53
|
+
let failure;
|
|
54
|
+
let value;
|
|
55
|
+
try {
|
|
56
|
+
await acquire(client, id, false);
|
|
57
|
+
acquired = true;
|
|
58
|
+
value = await fn(client);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
failed = true;
|
|
62
|
+
failure = error;
|
|
63
|
+
}
|
|
64
|
+
let unlockFailure;
|
|
65
|
+
if (acquired) {
|
|
66
|
+
try {
|
|
67
|
+
await unlock(client, id);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
unlockFailure = error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
client.release(unlockFailure instanceof Error ? unlockFailure : undefined);
|
|
74
|
+
if (failed)
|
|
75
|
+
throw failure;
|
|
76
|
+
if (unlockFailure !== undefined)
|
|
77
|
+
throw unlockFailure;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run `fn` in a transaction while holding a transaction advisory lock.
|
|
82
|
+
*
|
|
83
|
+
* The lock is released atomically by `COMMIT` or `ROLLBACK`, making this the
|
|
84
|
+
* right primitive for one-time schema installation and migrations.
|
|
85
|
+
*/
|
|
86
|
+
export async function withAdvisoryTransactionLock(pool, key, fn) {
|
|
87
|
+
const id = advisoryLockId(key);
|
|
88
|
+
const client = await pool.connect();
|
|
89
|
+
let releaseError;
|
|
90
|
+
try {
|
|
91
|
+
await client.query("BEGIN");
|
|
92
|
+
await acquire(client, id, true);
|
|
93
|
+
const value = await fn(client);
|
|
94
|
+
await client.query("COMMIT");
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
try {
|
|
99
|
+
await client.query("ROLLBACK");
|
|
100
|
+
}
|
|
101
|
+
catch (rollbackError) {
|
|
102
|
+
releaseError =
|
|
103
|
+
rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError));
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
client.release(releaseError);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWR2aXNvcnktbG9jay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hZHZpc29yeS1sb2NrLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7OztHQVFHO0FBRUgsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN6QyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFHaEQsTUFBTSxrQkFBa0IsR0FBRyxFQUFFLENBQUM7QUEwQjlCOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sVUFBVSxjQUFjLENBQUMsR0FBb0I7SUFDakQsSUFBSSxPQUFPLEdBQUcsS0FBSyxRQUFRO1FBQUUsT0FBTyxNQUFNLENBQUMsTUFBTSxDQUFDLGtCQUFrQixFQUFFLEdBQUcsQ0FBQyxDQUFDO0lBQzNFLE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDdEMsTUFBTSxNQUFNLEdBQUcsVUFBVSxDQUFDLFFBQVEsQ0FBQztTQUNoQyxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztTQUNwRSxNQUFNLEVBQUUsQ0FBQztJQUNaLE9BQU8sTUFBTSxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUNsQyxDQUFDO0FBRUQsS0FBSyxVQUFVLE9BQU8sQ0FBQyxNQUFtQixFQUFFLEVBQVUsRUFBRSxXQUFvQjtJQUMxRSxNQUFNLEVBQUUsR0FBRyxXQUFXLENBQUMsQ0FBQyxDQUFDLHVCQUF1QixDQUFDLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQztJQUN0RSxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsVUFBVSxFQUFFLGNBQWMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxD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QUFZO2dCQUNWLGFBQWEsWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsSUFBSSxLQUFLLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUM7UUFDdEYsQ0FBQztRQUNELE1BQU0sS0FBSyxDQUFDO0lBQ2QsQ0FBQztZQUFTLENBQUM7UUFDVCxNQUFNLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQy9CLENBQUM7QUFDSCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBBZHZpc29yeS1sb2NrIGhlbHBlcnMgZm9yIGFueSBgcGcuUG9vbGAtY29tcGF0aWJsZSBwb29sLlxuICpcbiAqIFBvc3RncmVTUUwgYWR2aXNvcnkgbG9ja3MgYmVsb25nIHRvIGEgY29ubmVjdGlvbiwgbm90IGEgcG9vbC4gVGhlc2UgaGVscGVyc1xuICogcmVzZXJ2ZSBvbmUgcG9vbGVkIGNsaWVudCBmb3IgdGhlIGZ1bGwgY2FsbGJhY2ssIGFjcXVpcmUgdGhlIGxvY2sgb24gdGhhdFxuICogY2xpZW50LCBhbmQgcmVsZWFzZSBib3RoIGluIHRoZSBjb3JyZWN0IG9yZGVyLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBjcmVhdGVIYXNoIH0gZnJvbSBcIm5vZGU6Y3J5cHRvXCI7XG5pbXBvcnQgeyBvYmplY3QgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHR5cGUgeyBQb29sLCBQb29sQ2xpZW50LCBRdWVyeVJlc3VsdCwgUXVlcnlSZXN1bHRSb3cgfSBmcm9tIFwicGdcIjtcblxuY29uc3QgU0lHTkVEX0JJR0lOVF9CSVRTID0gNjQ7XG5cbi8qKlxuICogV2hhdCBuYW1lcyBhIGxvY2suIEFueXRoaW5nIHJlZHVjaWJsZSB0byBhIHN0YWJsZSBpZGVudGl0eTogYSBzdHJpbmcsIGFuIGlkLCBhXG4gKiBgW1wiaW52b2ljZVwiLCBpZF1gIHBhaXIsIGEgY29uZmlnIG9iamVjdCwgb3IgYW4gZXhwbGljaXQgYGJpZ2ludGAgdG8gaW50ZXJvcGVyYXRlXG4gKiB3aXRoIGFub3RoZXIgaW1wbGVtZW50YXRpb24ncyBwdWJsaXNoZWQgbG9jayBpZC5cbiAqXG4gKiBPbmUgdmFsdWUgb3IgbWFueTogYW4gYXJyYXkgaXMgcmVhZCBhcyBtdWx0aXBsZSBwYXJ0cywgYW55dGhpbmcgZWxzZSBhcyBhIHNpbmdsZVxuICogcGFydC4gU28gYFtcImludm9pY2VcIiwgN11gIGFuZCBgXCJpbnZvaWNlXzdcImAgYXJlIGRpZmZlcmVudCBsb2Nrcywgc2luY2UgdGhlXG4gKiBjYW5vbmljYWwgZm9ybSBzZWVzIGRpZmZlcmVudCBzdHJ1Y3R1cmUuXG4gKi9cbmV4cG9ydCB0eXBlIEFkdmlzb3J5TG9ja0tleSA9IHVua25vd247XG5cbi8qKiBTdHJ1Y3R1cmFsIHBvb2wgc2hhcGUgYWNjZXB0ZWQgYnkgdGhlIGxvY2sgaGVscGVycy4gKi9cbmV4cG9ydCB0eXBlIFBnUG9vbExpa2UgPSBQaWNrPFBvb2wsIFwiY29ubmVjdFwiPjtcblxuLyoqIFN0cnVjdHVyYWwgcXVlcnkgc2hhcGUgc2hhcmVkIGJ5IGBwZy5Qb29sQ2xpZW50YCBhbmQgQXBwS2l0IExha2ViYXNlLiAqL1xuZXhwb3J0IGludGVyZmFjZSBQZ1F1ZXJ5YWJsZSB7XG4gIHF1ZXJ5PFQgZXh0ZW5kcyBRdWVyeVJlc3VsdFJvdyA9IFF1ZXJ5UmVzdWx0Um93PihcbiAgICB0ZXh0OiBzdHJpbmcsXG4gICAgdmFsdWVzPzogdW5rbm93bltdLFxuICApOiBQcm9taXNlPFF1ZXJ5UmVzdWx0PFQ+Pjtcbn1cblxudHlwZSBVbmxvY2tSb3cgPSBRdWVyeVJlc3VsdFJvdyAmIHsgdW5sb2NrZWQ6IGJvb2xlYW4gfTtcblxuLyoqXG4gKiBDb252ZXJ0IGFuIGFyYml0cmFyeSBzdHJ1Y3R1cmVkIGtleSBpbnRvIFBvc3RncmVTUUwncyBzaWduZWQgNjQtYml0IGFkdmlzb3J5XG4gKiBsb2NrIG5hbWVzcGFjZS4gQSBiaWdpbnQgaXMgcHJlc2VydmVkIGRpcmVjdGx5IHNvIGNhbGxlcnMgY2FuIGludGVyb3BlcmF0ZVxuICogd2l0aCBhbm90aGVyIGltcGxlbWVudGF0aW9uIHRoYXQgcHVibGlzaGVzIGl0cyBsb2NrIElELlxuICpcbiAqIEV2ZXJ5dGhpbmcgZWxzZSBpcyBjYW5vbmljYWxpemVkIHdpdGggYG9iamVjdC50b1N0YWJsZUtleWAgYW5kIGhhc2hlZCwgc28ga2V5XG4gKiBvcmRlciBpbiBhbiBvYmplY3QgZG9lcyBub3QgbWF0dGVyIHdoaWxlIGEgYDFgIGFuZCBhIGBcIjFcImAgc3RheSBkaWZmZXJlbnQgbG9ja3MuXG4gKiBBIGN5Y2xlLCBhIG5vbi1maW5pdGUgbnVtYmVyLCBvciBhIGZ1bmN0aW9uL3N5bWJvbCBrZXkgdGhyb3dzIGBUeXBlRXJyb3JgXG4gKiByYXRoZXIgdGhhbiB5aWVsZGluZyBhbiBpZGVudGl0eSB0d28gY2FsbGVycyBjb3VsZCBkaXNhZ3JlZSBhYm91dC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFkdmlzb3J5TG9ja0lkKGtleTogQWR2aXNvcnlMb2NrS2V5KTogYmlnaW50IHtcbiAgaWYgKHR5cGVvZiBrZXkgPT09IFwiYmlnaW50XCIpIHJldHVybiBCaWdJbnQuYXNJbnROKFNJR05FRF9CSUdJTlRfQklUUywga2V5KTtcbiAgY29uc3QgcGFydHMgPSBvYmplY3QudG9PbmVPck1hbnkoa2V5KTtcbiAgY29uc3QgZGlnZXN0ID0gY3JlYXRlSGFzaChcInNoYTI1NlwiKVxuICAgIC51cGRhdGUocGFydHMubWFwKChwYXJ0KSA9PiBvYmplY3QudG9TdGFibGVLZXkocGFydCkpLmpvaW4oXCJcXHUwMDAwXCIpKVxuICAgIC5kaWdlc3QoKTtcbiAgcmV0dXJuIGRpZ2VzdC5yZWFkQmlnSW50NjRCRSgwKTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gYWNxdWlyZShjbGllbnQ6IFBnUXVlcnlhYmxlLCBpZDogYmlnaW50LCB0cmFuc2FjdGlvbjogYm9vbGVhbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBmbiA9IHRyYW5zYWN0aW9uID8gXCJwZ19hZHZpc29yeV94YWN0X2xvY2tcIiA6IFwicGdfYWR2aXNvcnlfbG9ja1wiO1xuICBhd2FpdCBjbGllbnQucXVlcnkoYFNFTEVDVCAke2ZufSgkMTo6YmlnaW50KWAsIFtpZC50b1N0cmluZygpXSk7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHVubG9jayhjbGllbnQ6IFBnUXVlcnlhYmxlLCBpZDogYmlnaW50KTogUHJvbWlzZTx2b2lkPiB7XG4gIGNvbnN0IHJlc3VsdCA9IGF3YWl0IGNsaWVudC5xdWVyeTxVbmxvY2tSb3c+KFxuICAgIFwiU0VMRUNUIHBnX2Fkdmlzb3J5X3VubG9jaygkMTo6YmlnaW50KSBBUyB1bmxvY2tlZFwiLFxuICAgIFtpZC50b1N0cmluZygpXSxcbiAgKTtcbiAgaWYgKHJlc3VsdC5yb3dzWzBdPy51bmxvY2tlZCAhPT0gdHJ1ZSkge1xuICAgIHRocm93IG5ldyBFcnJvcihgUG9zdGdyZXMgYWR2aXNvcnkgbG9jayAke2lkfSB3YXMgbm90IGhlbGQgYnkgdGhpcyBjb25uZWN0aW9uYCk7XG4gIH1cbn1cblxuLyoqXG4gKiBIb2xkIGEgc2Vzc2lvbiBhZHZpc29yeSBsb2NrIGZvciB0aGUgZHVyYXRpb24gb2YgYGZuYC5cbiAqXG4gKiBUaGUgY2FsbGJhY2sgcmVjZWl2ZXMgdGhlIGRlZGljYXRlZCBgUG9vbENsaWVudGAgdGhhdCBvd25zIHRoZSBsb2NrLiBVc2UgaXRcbiAqIGZvciBhbnkgb3BlcmF0aW9uIHRoYXQgbXVzdCBiZSBwcm90ZWN0ZWQgYnkgdGhlIGxvY2suXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiB3aXRoQWR2aXNvcnlMb2NrPFQ+KFxuICBwb29sOiBQZ1Bvb2xMaWtlLFxuICBrZXk6IEFkdmlzb3J5TG9ja0tleSxcbiAgZm46IChjbGllbnQ6IFBvb2xDbGllbnQpID0+IFByb21pc2U8VD4gfCBULFxuKTogUHJvbWlzZTxUPiB7XG4gIGNvbnN0IGlkID0gYWR2aXNvcnlMb2NrSWQoa2V5KTtcbiAgY29uc3QgY2xpZW50ID0gYXdhaXQgcG9vbC5jb25uZWN0KCk7XG4gIGxldCBhY3F1aXJlZCA9IGZhbHNlO1xuICBsZXQgZmFpbGVkID0gZmFsc2U7XG4gIGxldCBmYWlsdXJlOiB1bmtub3duO1xuICBsZXQgdmFsdWU6IFQgfCB1bmRlZmluZWQ7XG5cbiAgdHJ5IHtcbiAgICBhd2FpdCBhY3F1aXJlKGNsaWVudCwgaWQsIGZhbHNlKTtcbiAgICBhY3F1aXJlZCA9IHRydWU7XG4gICAgdmFsdWUgPSBhd2FpdCBmbihjbGllbnQpO1xuICB9IGNhdGNoIChlcnJvcikge1xuICAgIGZhaWxlZCA9IHRydWU7XG4gICAgZmFpbHVyZSA9IGVycm9yO1xuICB9XG5cbiAgbGV0IHVubG9ja0ZhaWx1cmU6IHVua25vd247XG4gIGlmIChhY3F1aXJlZCkge1xuICAgIHRyeSB7XG4gICAgICBhd2FpdCB1bmxvY2soY2xpZW50LCBpZCk7XG4gICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgIHVubG9ja0ZhaWx1cmUgPSBlcnJvcjtcbiAgICB9XG4gIH1cbiAgY2xpZW50LnJlbGVhc2UodW5sb2NrRmFpbHVyZSBpbnN0YW5jZW9mIEVycm9yID8gdW5sb2NrRmFpbHVyZSA6IHVuZGVmaW5lZCk7XG5cbiAgaWYgKGZhaWxlZCkgdGhyb3cgZmFpbHVyZTtcbiAgaWYgKHVubG9ja0ZhaWx1cmUgIT09IHVuZGVmaW5lZCkgdGhyb3cgdW5sb2NrRmFpbHVyZTtcbiAgcmV0dXJuIHZhbHVlIGFzIFQ7XG59XG5cbi8qKlxuICogUnVuIGBmbmAgaW4gYSB0cmFuc2FjdGlvbiB3aGlsZSBob2xkaW5nIGEgdHJhbnNhY3Rpb24gYWR2aXNvcnkgbG9jay5cbiAqXG4gKiBUaGUgbG9jayBpcyByZWxlYXNlZCBhdG9taWNhbGx5IGJ5IGBDT01NSVRgIG9yIGBST0xMQkFDS2AsIG1ha2luZyB0aGlzIHRoZVxuICogcmlnaHQgcHJpbWl0aXZlIGZvciBvbmUtdGltZSBzY2hlbWEgaW5zdGFsbGF0aW9uIGFuZCBtaWdyYXRpb25zLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gd2l0aEFkdmlzb3J5VHJhbnNhY3Rpb25Mb2NrPFQ+KFxuICBwb29sOiBQZ1Bvb2xMaWtlLFxuICBrZXk6IEFkdmlzb3J5TG9ja0tleSxcbiAgZm46IChjbGllbnQ6IFBvb2xDbGllbnQpID0+IFByb21pc2U8VD4gfCBULFxuKTogUHJvbWlzZTxUPiB7XG4gIGNvbnN0IGlkID0gYWR2aXNvcnlMb2NrSWQoa2V5KTtcbiAgY29uc3QgY2xpZW50ID0gYXdhaXQgcG9vbC5jb25uZWN0KCk7XG4gIGxldCByZWxlYXNlRXJyb3I6IEVycm9yIHwgdW5kZWZpbmVkO1xuICB0cnkge1xuICAgIGF3YWl0IGNsaWVudC5xdWVyeShcIkJFR0lOXCIpO1xuICAgIGF3YWl0IGFjcXVpcmUoY2xpZW50LCBpZCwgdHJ1ZSk7XG4gICAgY29uc3QgdmFsdWUgPSBhd2FpdCBmbihjbGllbnQpO1xuICAgIGF3YWl0IGNsaWVudC5xdWVyeShcIkNPTU1JVFwiKTtcbiAgICByZXR1cm4gdmFsdWU7XG4gIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IGNsaWVudC5xdWVyeShcIlJPTExCQUNLXCIpO1xuICAgIH0gY2F0Y2ggKHJvbGxiYWNrRXJyb3IpIHtcbiAgICAgIHJlbGVhc2VFcnJvciA9XG4gICAgICAgIHJvbGxiYWNrRXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IHJvbGxiYWNrRXJyb3IgOiBuZXcgRXJyb3IoU3RyaW5nKHJvbGxiYWNrRXJyb3IpKTtcbiAgICB9XG4gICAgdGhyb3cgZXJyb3I7XG4gIH0gZmluYWxseSB7XG4gICAgY2xpZW50LnJlbGVhc2UocmVsZWFzZUVycm9yKTtcbiAgfVxufVxuIl19
|