@zukhruf/mutex 0.3.40 → 0.3.42
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 +18 -10
- package/dist/lock-stores/docker/docker-engine-store.d.ts +39 -0
- package/dist/lock-stores/docker/docker-engine-store.d.ts.map +1 -0
- package/dist/lock-stores/docker/docker-engine-store.js +339 -0
- package/dist/lock-stores/docker/docker-engine-store.js.map +1 -0
- package/dist/lock-stores/docker/index.d.ts +2 -0
- package/dist/lock-stores/docker/index.d.ts.map +1 -0
- package/dist/lock-stores/docker/index.js +2 -0
- package/dist/lock-stores/docker/index.js.map +1 -0
- package/dist/lock-stores/mariadb/index.d.ts +2 -0
- package/dist/lock-stores/mariadb/index.d.ts.map +1 -0
- package/dist/lock-stores/mariadb/index.js +2 -0
- package/dist/lock-stores/mariadb/index.js.map +1 -0
- package/dist/lock-stores/mariadb/mariadb-store.d.ts +33 -0
- package/dist/lock-stores/mariadb/mariadb-store.d.ts.map +1 -0
- package/dist/lock-stores/mariadb/mariadb-store.js +36 -0
- package/dist/lock-stores/mariadb/mariadb-store.js.map +1 -0
- package/dist/lock-stores/mysql/get-lock-dialect.d.ts +46 -0
- package/dist/lock-stores/mysql/get-lock-dialect.d.ts.map +1 -0
- package/dist/lock-stores/mysql/get-lock-dialect.js +80 -0
- package/dist/lock-stores/mysql/get-lock-dialect.js.map +1 -0
- package/dist/lock-stores/mysql/index.d.ts +2 -0
- package/dist/lock-stores/mysql/index.d.ts.map +1 -0
- package/dist/lock-stores/mysql/index.js +2 -0
- package/dist/lock-stores/mysql/index.js.map +1 -0
- package/dist/lock-stores/mysql/mysql-store.d.ts +37 -0
- package/dist/lock-stores/mysql/mysql-store.d.ts.map +1 -0
- package/dist/lock-stores/mysql/mysql-store.js +35 -0
- package/dist/lock-stores/mysql/mysql-store.js.map +1 -0
- package/dist/lock-stores/postgres/postgres-store.d.ts +2 -8
- package/dist/lock-stores/postgres/postgres-store.d.ts.map +1 -1
- package/dist/lock-stores/postgres/postgres-store.js +55 -226
- package/dist/lock-stores/postgres/postgres-store.js.map +1 -1
- package/dist/lock-stores/session/session-lock-store.d.ts +72 -0
- package/dist/lock-stores/session/session-lock-store.d.ts.map +1 -0
- package/dist/lock-stores/session/session-lock-store.js +221 -0
- package/dist/lock-stores/session/session-lock-store.js.map +1 -0
- package/dist/lock-stores/sqlite/sqlite-store.d.ts.map +1 -1
- package/dist/lock-stores/sqlite/sqlite-store.js +6 -25
- package/dist/lock-stores/sqlite/sqlite-store.js.map +1 -1
- package/dist/lock-stores/sqlserver/index.d.ts +2 -0
- package/dist/lock-stores/sqlserver/index.d.ts.map +1 -0
- package/dist/lock-stores/sqlserver/index.js +2 -0
- package/dist/lock-stores/sqlserver/index.js.map +1 -0
- package/dist/lock-stores/sqlserver/sqlserver-store.d.ts +40 -0
- package/dist/lock-stores/sqlserver/sqlserver-store.d.ts.map +1 -0
- package/dist/lock-stores/sqlserver/sqlserver-store.js +104 -0
- package/dist/lock-stores/sqlserver/sqlserver-store.js.map +1 -0
- package/dist/shared/in-process-turns.d.ts +17 -0
- package/dist/shared/in-process-turns.d.ts.map +1 -0
- package/dist/shared/in-process-turns.js +39 -0
- package/dist/shared/in-process-turns.js.map +1 -0
- package/dist/shared/key-digest.d.ts +8 -0
- package/dist/shared/key-digest.d.ts.map +1 -0
- package/dist/shared/key-digest.js +11 -0
- package/dist/shared/key-digest.js.map +1 -0
- package/package.json +21 -8
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { untilAborted } from '@zukhruf/async';
|
|
2
|
+
import { keyDigest } from "../../shared/key-digest.js";
|
|
3
|
+
/**
|
|
4
|
+
* The name of the user lock, made on the server from the database of the
|
|
5
|
+
* session and the digest of the key (the one `?`). A user lock belongs to
|
|
6
|
+
* the whole server, but the token source belongs to one database, so the
|
|
7
|
+
* lock belongs to one database too. SHA-256 gives 64 lowercase hex
|
|
8
|
+
* characters: the limit of MySQL, and the same under a comparison with or
|
|
9
|
+
* without case. With no database, the name is NULL, and the statement fails.
|
|
10
|
+
*/
|
|
11
|
+
export const lockName = "SHA2(CONCAT(DATABASE(), ':', ?), 256)";
|
|
12
|
+
/** `GET_LOCK` gives 1 for a grant and 0 for a timeout; NULL is an error, for example a killed statement. */
|
|
13
|
+
function granted(row, server) {
|
|
14
|
+
const locked = row?.['locked'];
|
|
15
|
+
if (locked === null || locked === undefined) {
|
|
16
|
+
throw new Error(`GET_LOCK of ${server} gave NULL.`);
|
|
17
|
+
}
|
|
18
|
+
return Number(locked) === 1;
|
|
19
|
+
}
|
|
20
|
+
function isRow(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The dialect of a user lock (`GET_LOCK`) for each key. The name of each lock
|
|
25
|
+
* includes the database of the session, so the locks belong to one database.
|
|
26
|
+
* The client sends the key digest as 64 lowercase hex characters.
|
|
27
|
+
*/
|
|
28
|
+
export function getLockDialect(server) {
|
|
29
|
+
const runOn = (session) => async (sql, values) => {
|
|
30
|
+
const result = await server.run(session, sql, values);
|
|
31
|
+
if (!Array.isArray(result))
|
|
32
|
+
return undefined;
|
|
33
|
+
const first = result[0];
|
|
34
|
+
return isRow(first) ? first : undefined;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
server: server.server,
|
|
38
|
+
lockName: (key) => keyDigest(key).toString('hex'),
|
|
39
|
+
// A timeout of `GET_LOCK` (MariaDB's longest is one year) waits again.
|
|
40
|
+
wait: async (session, { name }, signal) => {
|
|
41
|
+
for (;;) {
|
|
42
|
+
const row = await untilAborted(runOn(session)(server.waitStatement, [name]), signal);
|
|
43
|
+
if (granted(row, server.server))
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
try: async (session, { name }) => granted(await runOn(session)(`SELECT GET_LOCK(${lockName}, 0) AS locked`, [
|
|
48
|
+
name,
|
|
49
|
+
]), server.server),
|
|
50
|
+
mint: (session) => server.mint(runOn(session)),
|
|
51
|
+
// `IS_USED_LOCK` never acquires the lock. It gives NULL for a free lock.
|
|
52
|
+
holds: async (session, { name }) => {
|
|
53
|
+
const row = await runOn(session)(`SELECT IS_USED_LOCK(${lockName}) = CONNECTION_ID() AS held`, [name]);
|
|
54
|
+
return Number(row?.['held']) === 1;
|
|
55
|
+
},
|
|
56
|
+
release: async (session, { name }) => {
|
|
57
|
+
const row = await runOn(session)(`SELECT RELEASE_LOCK(${lockName}) AS released`, [name]);
|
|
58
|
+
return Number(row?.['released']) === 1;
|
|
59
|
+
},
|
|
60
|
+
/** Reads `IS_USED_LOCK`, which never acquires the lock. A session with no database fails. */
|
|
61
|
+
isHeld: async (session, { name }) => {
|
|
62
|
+
const row = await runOn(session)(`SELECT IS_USED_LOCK(${lockName}) AS holder, DATABASE() AS db`, [name]);
|
|
63
|
+
if (row?.['db'] === null) {
|
|
64
|
+
throw new Error(`The ${server.server} session has no database.`);
|
|
65
|
+
}
|
|
66
|
+
return row?.['holder'] !== null;
|
|
67
|
+
},
|
|
68
|
+
detach: (session, { error, end }) => {
|
|
69
|
+
session.off('error', error);
|
|
70
|
+
session.off('end', end);
|
|
71
|
+
},
|
|
72
|
+
giveBack: (session) => server.giveBack(session),
|
|
73
|
+
destroy: (session) => session.destroy(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Quotes one identifier for MySQL and MariaDB. */
|
|
77
|
+
export function quoteName(name) {
|
|
78
|
+
return `\`${name.replaceAll('`', '``')}\``;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=get-lock-dialect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-lock-dialect.js","sourceRoot":"","sources":["../../../src/lock-stores/mysql/get-lock-dialect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/mysql/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lock-stores/mysql/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,GAGX,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SessionLockStore } from '../session/session-lock-store.ts';
|
|
2
|
+
import { type GetLockSession } from './get-lock-dialect.ts';
|
|
3
|
+
/**
|
|
4
|
+
* One connection to the MySQL server: only the methods that the store calls.
|
|
5
|
+
* A `PoolConnection` of `mysql2/promise` fits it.
|
|
6
|
+
*/
|
|
7
|
+
export interface MysqlSession extends GetLockSession {
|
|
8
|
+
query(sql: string, values: unknown[]): Promise<[unknown, unknown]>;
|
|
9
|
+
/** Gives the session back to its pool. */
|
|
10
|
+
release(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface MysqlStoreOptions {
|
|
13
|
+
/** Gives a new session. The store owns each session that it gets. */
|
|
14
|
+
connect(): Promise<MysqlSession>;
|
|
15
|
+
/**
|
|
16
|
+
* The name of an existing table in the database of the session, with one
|
|
17
|
+
* row and one `BIGINT UNSIGNED` column `id`, for the fencing tokens. The
|
|
18
|
+
* store runs no DDL.
|
|
19
|
+
*/
|
|
20
|
+
table: string;
|
|
21
|
+
/**
|
|
22
|
+
* Milliseconds between two heartbeats of a holder, and the time limit of
|
|
23
|
+
* each heartbeat: the longest time that a holder can work after its session
|
|
24
|
+
* ended, before its signal aborts.
|
|
25
|
+
*/
|
|
26
|
+
heartbeatInterval: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Holds a MySQL user lock (`GET_LOCK`) for each key, on a session of its own.
|
|
30
|
+
* The server releases the lock when the session ends, so a holder that stops
|
|
31
|
+
* never leaves a key locked. Every process on every machine that reaches one
|
|
32
|
+
* MySQL database shares the locks.
|
|
33
|
+
*/
|
|
34
|
+
export declare class MysqlStore extends SessionLockStore<MysqlSession> {
|
|
35
|
+
constructor({ connect, table, heartbeatInterval }: MysqlStoreOptions);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=mysql-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/mysql/mysql-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAEL,KAAK,cAAc,EAIpB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACnE,0CAA0C;IAC1C,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IACjC;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,gBAAgB,CAAC,YAAY,CAAC;gBAChD,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,EAAE,iBAAiB;CAMrE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SessionLockStore } from "../session/session-lock-store.js";
|
|
2
|
+
import { getLockDialect, lockName, quoteName, } from "./get-lock-dialect.js";
|
|
3
|
+
/**
|
|
4
|
+
* Holds a MySQL user lock (`GET_LOCK`) for each key, on a session of its own.
|
|
5
|
+
* The server releases the lock when the session ends, so a holder that stops
|
|
6
|
+
* never leaves a key locked. Every process on every machine that reaches one
|
|
7
|
+
* MySQL database shares the locks.
|
|
8
|
+
*/
|
|
9
|
+
export class MysqlStore extends SessionLockStore {
|
|
10
|
+
constructor({ connect, table, heartbeatInterval }) {
|
|
11
|
+
super({ connect, heartbeatInterval }, getLockDialect(mysqlServer(quoteName(table))));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function mysqlServer(table) {
|
|
15
|
+
return {
|
|
16
|
+
server: 'MySQL',
|
|
17
|
+
// A negative timeout waits with no limit. The hint lifts the session's
|
|
18
|
+
// max_execution_time, which interrupts a waiting GET_LOCK.
|
|
19
|
+
waitStatement: `SELECT /*+ SET_VAR(max_execution_time = 0) */ GET_LOCK(${lockName}, -1) AS locked`,
|
|
20
|
+
run: async (session, sql, values) => (await session.query(sql, values))[0],
|
|
21
|
+
giveBack: (session) => session.release(),
|
|
22
|
+
// The sequence of the MySQL manual (LAST_INSERT_ID(expr)). An UPDATE that
|
|
23
|
+
// changes no row leaves LAST_INSERT_ID() at the session's last token.
|
|
24
|
+
mint: async (run) => {
|
|
25
|
+
await run(`UPDATE ${table} SET id = LAST_INSERT_ID(id + 1)`, []);
|
|
26
|
+
const row = await run('SELECT CAST(LAST_INSERT_ID() AS CHAR) AS token, ROW_COUNT() AS updated', []);
|
|
27
|
+
const updated = Number(row?.['updated']);
|
|
28
|
+
if (updated !== 1 || typeof row?.['token'] !== 'string') {
|
|
29
|
+
throw new Error(`The token table ${table} must have exactly one row; the update changed ${updated}.`);
|
|
30
|
+
}
|
|
31
|
+
return BigInt(row['token']);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=mysql-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql-store.js","sourceRoot":"","sources":["../../../src/lock-stores/mysql/mysql-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAGL,cAAc,EACd,QAAQ,EACR,SAAS,GACV,MAAM,uBAAuB,CAAC;AA6B/B;;;;;GAKG;AACH,MAAM,OAAO,UAAW,SAAQ,gBAA8B;IAC5D,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAqB;QAClE,KAAK,CACH,EAAE,OAAO,EAAE,iBAAiB,EAAE,EAC9B,cAAc,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAC9C,CAAC;IACJ,CAAC;CACF;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO;QACL,MAAM,EAAE,OAAO;QACf,uEAAuE;QACvE,2DAA2D;QAC3D,aAAa,EAAE,0DAA0D,QAAQ,iBAAiB;QAClG,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QACxC,0EAA0E;QAC1E,sEAAsE;QACtE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAClB,MAAM,GAAG,CAAC,UAAU,KAAK,kCAAkC,EAAE,EAAE,CAAC,CAAC;YACjE,MAAM,GAAG,GAAG,MAAM,GAAG,CACnB,wEAAwE,EACxE,EAAE,CACH,CAAC;YACF,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;YACzC,IAAI,OAAO,KAAK,CAAC,IAAI,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACxD,MAAM,IAAI,KAAK,CACb,mBAAmB,KAAK,kDAAkD,OAAO,GAAG,CACrF,CAAC;YACJ,CAAC;YACD,OAAO,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { AcquireOptions, LockStore } from '../../mutex/lock-store.ts';
|
|
1
|
+
import { SessionLockStore } from '../session/session-lock-store.ts';
|
|
3
2
|
/**
|
|
4
3
|
* One connection to the Postgres server: only the methods that the store
|
|
5
4
|
* calls. A `PoolClient` of `pg` (node-postgres) fits it.
|
|
@@ -36,12 +35,7 @@ export interface PostgresStoreOptions {
|
|
|
36
35
|
* first one gets a session: one session per key per process, however many
|
|
37
36
|
* callers wait, and first-come order within the process.
|
|
38
37
|
*/
|
|
39
|
-
export declare class PostgresStore
|
|
40
|
-
#private;
|
|
38
|
+
export declare class PostgresStore extends SessionLockStore<PostgresSession> {
|
|
41
39
|
constructor(options: PostgresStoreOptions);
|
|
42
|
-
acquire(key: string, options?: AcquireOptions): Promise<LockHandle>;
|
|
43
|
-
tryAcquire(key: string): Promise<LockHandle | undefined>;
|
|
44
|
-
/** Reads `pg_locks` on a short session, and never runs an advisory lock function. */
|
|
45
|
-
isHeld(key: string): Promise<boolean>;
|
|
46
40
|
}
|
|
47
41
|
//# sourceMappingURL=postgres-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/postgres/postgres-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/postgres/postgres-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,gBAAgB,EACjB,MAAM,kCAAkC,CAAC;AAE1C;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CACH,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,OAAO,EAAE,GAChB,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IAChD,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC;IAC9D,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IAChD,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC;IACvE,sEAAsE;IACtE,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAeD;;;;;;;;;GASG;AACH,qBAAa,aAAc,SAAQ,gBAAgB,CAAC,eAAe,CAAC;gBACtD,OAAO,EAAE,oBAAoB;CAG1C"}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
1
|
import { untilAborted } from '@zukhruf/async';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
SELECT 1 FROM pg_catalog.pg_locks
|
|
2
|
+
import { keyDigest } from "../../shared/key-digest.js";
|
|
3
|
+
import { SessionLockStore, } from "../session/session-lock-store.js";
|
|
4
|
+
/** The granted advisory locks of a lock number in the database of the session. */
|
|
5
|
+
const lockRows = `FROM pg_catalog.pg_locks
|
|
9
6
|
WHERE locktype = 'advisory'
|
|
10
7
|
AND database = (SELECT oid FROM pg_catalog.pg_database WHERE datname = pg_catalog.current_database())
|
|
11
8
|
AND objsubid = 1
|
|
12
9
|
AND ((classid::bigint << 32) | objid::bigint) = $1::bigint
|
|
13
|
-
AND granted
|
|
14
|
-
) AS held`;
|
|
10
|
+
AND granted`;
|
|
11
|
+
const heldQuery = `SELECT EXISTS (SELECT 1 ${lockRows}) AS held`;
|
|
12
|
+
/** Whether the session of the heartbeat holds the lock. */
|
|
13
|
+
const holdsQuery = `SELECT EXISTS (SELECT 1 ${lockRows} AND pid = pg_catalog.pg_backend_pid()) AS held`;
|
|
15
14
|
/**
|
|
16
15
|
* Holds a session-level advisory lock of Postgres for each key, on a session
|
|
17
16
|
* of its own. The server releases the lock when the session ends, so a
|
|
@@ -22,233 +21,63 @@ const heldQuery = `SELECT EXISTS (
|
|
|
22
21
|
* first one gets a session: one session per key per process, however many
|
|
23
22
|
* callers wait, and first-come order within the process.
|
|
24
23
|
*/
|
|
25
|
-
export class PostgresStore {
|
|
26
|
-
#inProcess = new MemoryStore();
|
|
27
|
-
#options;
|
|
24
|
+
export class PostgresStore extends SessionLockStore {
|
|
28
25
|
constructor(options) {
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
async acquire(key, options) {
|
|
32
|
-
const signal = options?.signal;
|
|
33
|
-
await using turn = new AsyncDisposableStack();
|
|
34
|
-
// Joining the in-process queue is synchronous, so the order is the order of the calls.
|
|
35
|
-
turn.use(await this.#inProcess.acquire(key, { signal }));
|
|
36
|
-
const session = await this.#session(key, signal);
|
|
37
|
-
await session.wait(signal);
|
|
38
|
-
return withTurn(await session.hold(), turn);
|
|
39
|
-
}
|
|
40
|
-
async tryAcquire(key) {
|
|
41
|
-
await using turn = new AsyncDisposableStack();
|
|
42
|
-
const inProcess = await this.#inProcess.tryAcquire(key);
|
|
43
|
-
if (!inProcess)
|
|
44
|
-
return undefined;
|
|
45
|
-
turn.use(inProcess);
|
|
46
|
-
const session = await this.#session(key, undefined);
|
|
47
|
-
if (!(await session.try()))
|
|
48
|
-
return undefined;
|
|
49
|
-
return withTurn(await session.hold(), turn);
|
|
50
|
-
}
|
|
51
|
-
/** Reads `pg_locks` on a short session, and never runs an advisory lock function. */
|
|
52
|
-
async isHeld(key) {
|
|
53
|
-
const session = new OwnedSession(await this.#options.connect());
|
|
54
|
-
try {
|
|
55
|
-
const [row] = await session.query(heldQuery, [lockNumber(key)]);
|
|
56
|
-
session.giveBack();
|
|
57
|
-
return row?.held === true;
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
session.destroy();
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/** Gets a session for `key`. A cancel never waits for the pool: a session that comes later goes back at once. */
|
|
65
|
-
async #session(key, signal) {
|
|
66
|
-
const connecting = this.#options.connect();
|
|
67
|
-
try {
|
|
68
|
-
return new KeySession(new OwnedSession(await untilAborted(connecting, signal)), key, this.#options);
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (signal?.aborted && error === signal.reason) {
|
|
72
|
-
void connecting.then((late) => late.release(false), () => { });
|
|
73
|
-
}
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
26
|
+
super(options, postgresDialect(options));
|
|
76
27
|
}
|
|
77
28
|
}
|
|
78
29
|
/**
|
|
79
|
-
* The number of the advisory lock of `key`: the first 8 bytes of the
|
|
80
|
-
* digest
|
|
81
|
-
*
|
|
82
|
-
* numbers. Sent as text, because a JavaScript number cannot hold 64 bits.
|
|
30
|
+
* The number of the advisory lock of `key`: the first 8 bytes of the key
|
|
31
|
+
* digest, as a signed 64-bit integer. Sent as text, because a JavaScript
|
|
32
|
+
* number cannot hold 64 bits.
|
|
83
33
|
*/
|
|
84
34
|
function lockNumber(key) {
|
|
85
|
-
return
|
|
86
|
-
.update(Buffer.from(key, 'utf16le'))
|
|
87
|
-
.digest()
|
|
88
|
-
.readBigInt64BE(0)
|
|
89
|
-
.toString();
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* A session from `connect` that the store owns until it gives it back or
|
|
93
|
-
* destroys it, once. Its `ended` signal aborts when the session emits `error`
|
|
94
|
-
* or `end`. Its listeners stay on a destroyed session, because node-postgres
|
|
95
|
-
* can emit `error` again after the close.
|
|
96
|
-
*/
|
|
97
|
-
class OwnedSession {
|
|
98
|
-
#session;
|
|
99
|
-
#ended = new AbortController();
|
|
100
|
-
#owned = new AbortController();
|
|
101
|
-
#onError = (error) => this.#ended.abort(error);
|
|
102
|
-
#onEnd = () => this.#ended.abort(new Error('The Postgres session ended.'));
|
|
103
|
-
constructor(session) {
|
|
104
|
-
this.#session = session;
|
|
105
|
-
session.on('error', this.#onError);
|
|
106
|
-
session.on('end', this.#onEnd);
|
|
107
|
-
}
|
|
108
|
-
get ended() {
|
|
109
|
-
return this.#ended.signal;
|
|
110
|
-
}
|
|
111
|
-
async query(text, values) {
|
|
112
|
-
return (await this.#session.query(text, values)).rows;
|
|
113
|
-
}
|
|
114
|
-
/** The session holds nothing: it goes back to the pool with its own settings. */
|
|
115
|
-
giveBack() {
|
|
116
|
-
if (this.#owned.signal.aborted)
|
|
117
|
-
return;
|
|
118
|
-
this.#owned.abort();
|
|
119
|
-
this.#session.off('error', this.#onError);
|
|
120
|
-
this.#session.off('end', this.#onEnd);
|
|
121
|
-
this.#session.release(false);
|
|
122
|
-
}
|
|
123
|
-
/** The lock state of the session is not known: the server releases each lock of a session that ends. */
|
|
124
|
-
destroy() {
|
|
125
|
-
if (this.#owned.signal.aborted)
|
|
126
|
-
return;
|
|
127
|
-
this.#owned.abort();
|
|
128
|
-
this.#session.release(true);
|
|
129
|
-
}
|
|
35
|
+
return keyDigest(key).readBigInt64BE(0).toString();
|
|
130
36
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
#lease;
|
|
138
|
-
/** Aborts when the store stops holding: a release, or a lost session. */
|
|
139
|
-
#done = new AbortController();
|
|
140
|
-
constructor(session, key, options) {
|
|
141
|
-
this.#session = session;
|
|
142
|
-
this.#key = key;
|
|
143
|
-
this.#number = lockNumber(key);
|
|
144
|
-
this.#options = options;
|
|
145
|
-
this.#lease = new LeaseController(key);
|
|
146
|
-
}
|
|
147
|
-
/** Waits in Postgres' lock queue. A cancel closes the session, and the end of the session releases a late grant. */
|
|
148
|
-
async wait(signal) {
|
|
149
|
-
try {
|
|
37
|
+
const rowsOf = async (session, text, values) => (await session.query(text, values)).rows;
|
|
38
|
+
function postgresDialect(options) {
|
|
39
|
+
return {
|
|
40
|
+
server: 'Postgres',
|
|
41
|
+
lockName: lockNumber,
|
|
42
|
+
wait: async (session, { name }, signal) => {
|
|
150
43
|
// A timeout of the pool must not stop the wait.
|
|
151
|
-
await
|
|
152
|
-
await untilAborted(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
async try() {
|
|
162
|
-
try {
|
|
163
|
-
const [row] = await this.#session.query('SELECT pg_try_advisory_lock($1::bigint) AS locked', [this.#number]);
|
|
164
|
-
if (row?.locked === true)
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
this.#session.destroy();
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
this.#session.giveBack();
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
/** Mints the token of a granted lock and starts the heartbeat. A failed mint releases the key. */
|
|
175
|
-
async hold() {
|
|
176
|
-
let token;
|
|
177
|
-
try {
|
|
178
|
-
const [row] = await this.#session.query('SELECT nextval($1::regclass)::text AS token', [this.#options.sequence]);
|
|
44
|
+
await session.query('SET statement_timeout = 0; SET lock_timeout = 0', []);
|
|
45
|
+
await untilAborted(session.query('SELECT pg_advisory_lock($1::bigint)', [name]), signal);
|
|
46
|
+
},
|
|
47
|
+
try: async (session, { name }) => {
|
|
48
|
+
const [row] = await rowsOf(session, 'SELECT pg_try_advisory_lock($1::bigint) AS locked', [name]);
|
|
49
|
+
return row?.locked === true;
|
|
50
|
+
},
|
|
51
|
+
mint: async (session) => {
|
|
52
|
+
const [row] = await rowsOf(session, 'SELECT nextval($1::regclass)::text AS token', [options.sequence]);
|
|
179
53
|
if (typeof row?.token !== 'string') {
|
|
180
|
-
throw new Error(`nextval of sequence ${JSON.stringify(
|
|
181
|
-
}
|
|
182
|
-
token = new FencingToken(BigInt(row.token));
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
await this.#release();
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
const ended = this.#session.ended;
|
|
189
|
-
ended.addEventListener('abort', () => this.#lose(ended.reason), {
|
|
190
|
-
once: true,
|
|
191
|
-
signal: this.#done.signal,
|
|
192
|
-
});
|
|
193
|
-
void this.#beat();
|
|
194
|
-
return {
|
|
195
|
-
token,
|
|
196
|
-
signal: this.#lease.signal,
|
|
197
|
-
[Symbol.asyncDispose]: () => this.#release(),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
/** Sends `SELECT 1` each interval. A failed or late answer loses the lease. */
|
|
201
|
-
async #beat() {
|
|
202
|
-
const interval = this.#options.heartbeatInterval;
|
|
203
|
-
const done = this.#done.signal;
|
|
204
|
-
try {
|
|
205
|
-
for (;;) {
|
|
206
|
-
await delay(interval, undefined, { signal: done });
|
|
207
|
-
await untilAborted(this.#session.query('SELECT 1', []), AbortSignal.any([done, AbortSignal.timeout(interval)]));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch (cause) {
|
|
211
|
-
if (!done.aborted)
|
|
212
|
-
this.#lose(cause);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
/** Another holder can have the key now. */
|
|
216
|
-
#lose(cause) {
|
|
217
|
-
if (this.#done.signal.aborted)
|
|
218
|
-
return;
|
|
219
|
-
this.#done.abort();
|
|
220
|
-
this.#lease.lose(cause);
|
|
221
|
-
this.#session.destroy();
|
|
222
|
-
}
|
|
223
|
-
async #release() {
|
|
224
|
-
if (this.#done.signal.aborted)
|
|
225
|
-
return;
|
|
226
|
-
this.#done.abort();
|
|
227
|
-
try {
|
|
228
|
-
const [row] = await this.#session.query('SELECT pg_advisory_unlock($1::bigint) AS released', [this.#number]);
|
|
229
|
-
if (row?.released !== true) {
|
|
230
|
-
throw new Error(`The session did not hold the lock of ${JSON.stringify(this.#key)}.`);
|
|
54
|
+
throw new Error(`nextval of sequence ${JSON.stringify(options.sequence)} gave no number.`);
|
|
231
55
|
}
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
56
|
+
return BigInt(row.token);
|
|
57
|
+
},
|
|
58
|
+
holds: async (session, { name }) => {
|
|
59
|
+
const [row] = await rowsOf(session, holdsQuery, [name]);
|
|
60
|
+
return row?.held === true;
|
|
61
|
+
},
|
|
62
|
+
release: async (session, { name }) => {
|
|
63
|
+
const [row] = await rowsOf(session, 'SELECT pg_advisory_unlock($1::bigint) AS released', [name]);
|
|
64
|
+
if (row?.released !== true)
|
|
65
|
+
return false;
|
|
66
|
+
await session.query('RESET statement_timeout; RESET lock_timeout', []);
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
/** Reads `pg_locks`, and never runs an advisory lock function. */
|
|
70
|
+
isHeld: async (session, { name }) => {
|
|
71
|
+
const [row] = await rowsOf(session, heldQuery, [name]);
|
|
72
|
+
return row?.held === true;
|
|
73
|
+
},
|
|
74
|
+
detach: (session, { error, end }) => {
|
|
75
|
+
session.off('error', error);
|
|
76
|
+
session.off('end', end);
|
|
77
|
+
},
|
|
78
|
+
// The session holds nothing: it goes back to the pool with its own settings.
|
|
79
|
+
giveBack: (session) => session.release(false),
|
|
80
|
+
destroy: (session) => session.release(true),
|
|
252
81
|
};
|
|
253
82
|
}
|
|
254
83
|
//# sourceMappingURL=postgres-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-store.js","sourceRoot":"","sources":["../../../src/lock-stores/postgres/postgres-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"postgres-store.js","sourceRoot":"","sources":["../../../src/lock-stores/postgres/postgres-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { LockHandle } from '../../mutex/lease.ts';
|
|
2
|
+
import type { AcquireOptions, LockStore } from '../../mutex/lock-store.ts';
|
|
3
|
+
/** The events of a session that tell the store that the session ended. */
|
|
4
|
+
export interface SessionEvents {
|
|
5
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
6
|
+
on(event: 'end', listener: () => void): unknown;
|
|
7
|
+
}
|
|
8
|
+
/** The listeners that the store puts on each session that it owns. */
|
|
9
|
+
export interface SessionListeners {
|
|
10
|
+
error: (error: Error) => void;
|
|
11
|
+
end: () => void;
|
|
12
|
+
}
|
|
13
|
+
/** The lock of one key on the server. */
|
|
14
|
+
export interface SessionLock {
|
|
15
|
+
key: string;
|
|
16
|
+
/** The name of the lock on the server, made from the key digest. */
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
/** What differs between the servers of the SQL lock stores, and between their drivers. */
|
|
20
|
+
export interface SessionLockDialect<Session extends SessionEvents> {
|
|
21
|
+
/** The name of the server in error messages. */
|
|
22
|
+
readonly server: string;
|
|
23
|
+
/** The name of the lock of `key` on the server. */
|
|
24
|
+
lockName(key: string): string;
|
|
25
|
+
/** Waits in the server's queue until the session holds the lock. A cancel of `signal` rejects at once. */
|
|
26
|
+
wait(session: Session, lock: SessionLock, signal: AbortSignal | undefined): Promise<void>;
|
|
27
|
+
/** Takes the lock only when no other session holds it. */
|
|
28
|
+
try(session: Session, lock: SessionLock): Promise<boolean>;
|
|
29
|
+
/** Gives the next fencing token. */
|
|
30
|
+
mint(session: Session): Promise<bigint>;
|
|
31
|
+
/** Whether this session holds the lock now. It never acquires the lock. */
|
|
32
|
+
holds(session: Session, lock: SessionLock): Promise<boolean>;
|
|
33
|
+
/** Releases the lock; `false` when the session did not hold it. */
|
|
34
|
+
release(session: Session, lock: SessionLock): Promise<boolean>;
|
|
35
|
+
/** Whether any session holds the lock now. It never acquires the lock. */
|
|
36
|
+
isHeld(session: Session, lock: SessionLock): Promise<boolean>;
|
|
37
|
+
/** Takes the store's listeners off a session that goes back to its pool. */
|
|
38
|
+
detach(session: Session, listeners: SessionListeners): void;
|
|
39
|
+
/** Gives back a session that holds nothing: to its pool, or by a close. */
|
|
40
|
+
giveBack(session: Session): void;
|
|
41
|
+
/** Ends the connection: the server then releases each lock of the session. */
|
|
42
|
+
destroy(session: Session): void;
|
|
43
|
+
}
|
|
44
|
+
export interface SessionLockStoreOptions<Session extends SessionEvents> {
|
|
45
|
+
/** Gives a new session. The store owns each session that it gets. */
|
|
46
|
+
connect(): Promise<Session>;
|
|
47
|
+
/**
|
|
48
|
+
* Milliseconds between two heartbeats of a holder, and the time limit of
|
|
49
|
+
* each heartbeat: the longest time that a holder can work after its session
|
|
50
|
+
* ended, before its signal aborts.
|
|
51
|
+
*/
|
|
52
|
+
heartbeatInterval: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Holds a session-level lock of a SQL server for each key, on a session of
|
|
56
|
+
* its own. The server releases the lock when the session ends, so a holder
|
|
57
|
+
* that stops never leaves a key locked. Every process on every machine that
|
|
58
|
+
* reaches one database shares the locks.
|
|
59
|
+
*
|
|
60
|
+
* Callers in this process first line up in a queue in memory, so only the
|
|
61
|
+
* first one gets a session: one session per key per process, however many
|
|
62
|
+
* callers wait, and first-come order within the process.
|
|
63
|
+
*/
|
|
64
|
+
export declare class SessionLockStore<Session extends SessionEvents> implements LockStore {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(options: SessionLockStoreOptions<Session>, dialect: SessionLockDialect<Session>);
|
|
67
|
+
acquire(key: string, options?: AcquireOptions): Promise<LockHandle>;
|
|
68
|
+
tryAcquire(key: string): Promise<LockHandle | undefined>;
|
|
69
|
+
/** Asks the server on a short session, and never acquires the lock. */
|
|
70
|
+
isHeld(key: string): Promise<boolean>;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=session-lock-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-lock-store.d.ts","sourceRoot":"","sources":["../../../src/lock-stores/session/session-lock-store.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAG3E,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC;IAC9D,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CACjD;AAED,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0FAA0F;AAC1F,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,aAAa;IAC/D,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,0GAA0G;IAC1G,IAAI,CACF,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,0DAA0D;IAC1D,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,oCAAoC;IACpC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,2EAA2E;IAC3E,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,mEAAmE;IACnE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,0EAA0E;IAC1E,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,4EAA4E;IAC5E,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC5D,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,8EAA8E;IAC9E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,uBAAuB,CAAC,OAAO,SAAS,aAAa;IACpE,qEAAqE;IACrE,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAgB,CAC3B,OAAO,SAAS,aAAa,CAC7B,YAAW,SAAS;;gBAMlB,OAAO,EAAE,uBAAuB,CAAC,OAAO,CAAC,EACzC,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC;IAMtC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC;IASnE,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAQxD,uEAAuE;IACjE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAwC5C"}
|