@effectstream/db 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/assets-config.json +28 -0
  2. package/migrations/assets.ts +35 -0
  3. package/migrations/readme.md +37 -0
  4. package/migrations/system-down-v-0.0.0.sql +2 -0
  5. package/migrations/system-down-v-0.0.1.sql +14 -0
  6. package/migrations/system-up-v-0.0.0.sql +40 -0
  7. package/migrations/system-up-v-0.0.1.sql +117 -0
  8. package/migrations/system-up-v-0.0.2.sql +22 -0
  9. package/migrations/system-up-v-0.8.1.sql +5 -0
  10. package/migrations/system-version.ts +147 -0
  11. package/package.json +33 -0
  12. package/pgtypedconfig.json +21 -0
  13. package/scripts/Dockerfile +13 -0
  14. package/scripts/apply-migrations.ts +49 -0
  15. package/scripts/pgtyped-internal.ts +43 -0
  16. package/scripts/pgtyped-update.ts +119 -0
  17. package/scripts/start-pglite.ts +71 -0
  18. package/scripts/wait-for-db.ts +43 -0
  19. package/src/dynamic-tables.ts +110 -0
  20. package/src/event-indexing.ts +31 -0
  21. package/src/migrations.ts +28 -0
  22. package/src/mod.ts +65 -0
  23. package/src/pg-connection.test.ts +99 -0
  24. package/src/pg-connection.ts +211 -0
  25. package/src/register-events.ts +40 -0
  26. package/src/scheduled-constructors.ts +117 -0
  27. package/src/snapshot-handler.ts +164 -0
  28. package/src/sql/accounts.queries.ts +350 -0
  29. package/src/sql/accounts.sql +77 -0
  30. package/src/sql/achievements.queries.ts +74 -0
  31. package/src/sql/achievements.sql +17 -0
  32. package/src/sql/block-heights.queries.ts +205 -0
  33. package/src/sql/block-heights.sql +45 -0
  34. package/src/sql/events.queries.ts +201 -0
  35. package/src/sql/events.sql +41 -0
  36. package/src/sql/nonces.queries.ts +85 -0
  37. package/src/sql/nonces.sql +11 -0
  38. package/src/sql/primitives.queries.ts +35 -0
  39. package/src/sql/primitives.sql +4 -0
  40. package/src/sql/rollup_inputs.queries.ts +654 -0
  41. package/src/sql/rollup_inputs.sql +227 -0
  42. package/src/sql/statistics.queries.ts +160 -0
  43. package/src/sql/statistics.sql +55 -0
  44. package/src/sql/sync-protocols/page.queries.ts +140 -0
  45. package/src/sql/sync-protocols/page.sql +39 -0
  46. package/src/sql/system.queries.ts +356 -0
  47. package/src/sql/system.sql +75 -0
  48. package/src/sql/tables.queries.ts +89 -0
  49. package/src/sql/tables.sql +17 -0
  50. package/src/system.ts +41 -0
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Runs database startup and pgtyped generation concurrently
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const __dirname = dirname(import.meta.url?.startsWith("file:") ? fileURLToPath(import.meta.url) : import.meta.url.replace("file://", ""));
11
+
12
+ interface ProcessInfo {
13
+ name: string;
14
+ command: string;
15
+ args: string[];
16
+ wait: boolean;
17
+ }
18
+
19
+ function createPrefix(name: string): string {
20
+ const colors = {
21
+ "db:up": "\x1b[36m", // cyan
22
+ "pgtyped:internal": "\x1b[33m", // yellow
23
+ };
24
+ const reset = "\x1b[0m";
25
+ const color = colors[name as keyof typeof colors] || "\x1b[37m"; // white
26
+ return `${color}[${name}]${reset} `;
27
+ }
28
+
29
+ async function runProcess(processInfo: ProcessInfo): Promise<void> {
30
+ const prefix = createPrefix(processInfo.name);
31
+
32
+ console.log(`${prefix}Starting...`);
33
+
34
+ const child = spawn(processInfo.command, processInfo.args, {
35
+ stdio: ["inherit", "pipe", "pipe"],
36
+ shell: processInfo.command === "npx",
37
+ });
38
+
39
+ const stdoutPromise = new Promise<void>((resolve) => {
40
+ child.stdout?.on("data", (chunk: Buffer | string) => {
41
+ const text = chunk.toString();
42
+ text.split("\n").forEach((line) => {
43
+ if (line.trim()) console.log(`${prefix}${line}`);
44
+ });
45
+ });
46
+ child.stdout?.on("end", resolve);
47
+ if (!child.stdout) resolve();
48
+ });
49
+
50
+ const stderrPromise = new Promise<void>((resolve) => {
51
+ child.stderr?.on("data", (chunk: Buffer | string) => {
52
+ const text = chunk.toString();
53
+ text.split("\n").forEach((line) => {
54
+ if (line.trim()) console.error(`${prefix}${line}`);
55
+ });
56
+ });
57
+ child.stderr?.on("end", resolve);
58
+ if (!child.stderr) resolve();
59
+ });
60
+
61
+ const exitCode = new Promise<number | null>((resolve) => {
62
+ child.on("close", (code) => resolve(code));
63
+ });
64
+
65
+ if (processInfo.wait) {
66
+ const [code] = await Promise.all([exitCode, stdoutPromise, stderrPromise]);
67
+ if (code === 0) {
68
+ console.log(`${prefix}✅ Completed successfully`);
69
+ } else {
70
+ console.error(`${prefix}❌ Failed with exit code ${code}`);
71
+ throw new Error(`Process ${processInfo.name} failed`);
72
+ }
73
+ }
74
+ }
75
+
76
+ async function main() {
77
+ const processes: ProcessInfo[] = [
78
+ {
79
+ name: "db:up",
80
+ command: "bun",
81
+ args: ["run", join(__dirname, "start-pglite.ts")],
82
+ wait: false,
83
+ },
84
+ {
85
+ name: "db:wait",
86
+ command: "bun",
87
+ args: ["run", join(__dirname, "wait-for-db.ts")],
88
+ wait: true,
89
+ },
90
+ {
91
+ name: "apply-migrations",
92
+ command: "bun",
93
+ args: ["run", join(__dirname, "apply-migrations.ts")],
94
+ wait: true,
95
+ },
96
+ {
97
+ name: "pgtyped",
98
+ command: "npx",
99
+ args: ["pgtyped", "-c", "./pgtypedconfig.json"],
100
+ wait: true,
101
+ },
102
+ ];
103
+
104
+ console.log("🚀 Starting concurrent processes...\n");
105
+
106
+ try {
107
+ for (const process of processes) {
108
+ await runProcess(process);
109
+ }
110
+ console.log("\n✅ All processes completed successfully");
111
+ } catch (error) {
112
+ console.error("\n❌ One or more processes failed:", error);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ if (import.meta.main) {
118
+ await main();
119
+ }
@@ -0,0 +1,71 @@
1
+ import { type DebugLevel, PGlite } from "@electric-sql/pglite";
2
+ // TODO This is not working, so we load the pg_ivm extension from the node_modules folder
3
+ // import { pg_ivm } from "@electric-sql/pglite/pg_ivm";
4
+ import net from "node:net";
5
+ import { statSync } from "node:fs";
6
+ import { fromNodeSocket } from "pg-gateway/node";
7
+ import { ENV } from "@effectstream/utils/node-env";
8
+ import { args, cwd } from "@effectstream/utils/runtime";
9
+
10
+ // TODO PORT be a ENV variable
11
+ // Get port from arguments.
12
+ const portArgName = "--port";
13
+ const argv = args();
14
+ const portArgIndex = argv.indexOf(portArgName);
15
+ const portValue = portArgIndex !== -1 ? argv[portArgIndex + 1] : "5432";
16
+ const port = parseInt(portValue);
17
+ if (isNaN(port)) {
18
+ throw new Error(`Port argument ${portArgName} is not a number`);
19
+ }
20
+
21
+ // Resolve pg_ivm extension path via import.meta.resolve (works with bun's hoisting)
22
+ const pgliteEntry = import.meta.resolve("@electric-sql/pglite");
23
+ const pgliteDir = pgliteEntry.replace(/\/dist\/[^/]+$/, "");
24
+ const pgIvmUrl = new URL(`${pgliteDir}/dist/pg_ivm.tar.gz`);
25
+
26
+ const db = new PGlite(
27
+ ENV.PGLITE_DATA_DIR,
28
+ {
29
+ username: ENV.DB_USER,
30
+ database: ENV.DB_NAME,
31
+ extensions: {
32
+ pg_ivm: pgIvmUrl,
33
+ },
34
+ debug: (ENV.DEBUG_PGLITE as DebugLevel) || 0,
35
+ },
36
+ );
37
+
38
+ {
39
+ // TODO: consider switching to pglite-socket once it works
40
+ // https://discord.com/channels/933657521581858818/1371976702674075780/1371992712076595250
41
+ const server = net.createServer(async (socket) => {
42
+ await fromNodeSocket(socket, {
43
+ serverVersion: "16.3",
44
+
45
+ auth: {
46
+ // No password required
47
+ method: "trust",
48
+ },
49
+
50
+ async onStartup() {
51
+ // Wait for PGlite to be ready before further processing
52
+ await db.waitReady;
53
+ },
54
+
55
+ // Hook into each client message
56
+ async onMessage(data, { isAuthenticated }) {
57
+ // Only forward messages to PGlite after authentication
58
+ if (!isAuthenticated) {
59
+ return;
60
+ }
61
+
62
+ // Forward raw message to PGlite and send response to client
63
+ return await db.execProtocolRaw(data);
64
+ },
65
+ });
66
+ });
67
+
68
+ server.listen(port, () => {
69
+ console.info(`database: server listening on port ${port}`);
70
+ });
71
+ }
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from "node:child_process";
3
+ import { args, exit } from "@effectstream/utils/runtime";
4
+
5
+ // Get port from arguments.
6
+ const portArgName = "--port";
7
+ const argv = args();
8
+ const portArgIndex = argv.indexOf(portArgName);
9
+ const portValue = portArgIndex !== -1 ? argv[portArgIndex + 1] : "5432";
10
+ const port = parseInt(portValue);
11
+ if (isNaN(port)) {
12
+ throw new Error(`Port argument ${portArgName} is not a number`);
13
+ }
14
+
15
+ async function waitForDb() {
16
+ try {
17
+ console.log("waiting for db on port", port);
18
+ const child = spawn("npx", ["wait-on", `tcp:${port}`], {
19
+ stdio: "inherit",
20
+ shell: true,
21
+ });
22
+
23
+ const code = await new Promise<number | null>((resolve) => {
24
+ child.on("close", (code, _sig) => resolve(code));
25
+ });
26
+
27
+ if (code === 0) {
28
+ console.log("✅ Database is ready on port 5432");
29
+ } else {
30
+ console.error("❌ Failed to connect to database on port 5432");
31
+ exit(code ?? 1);
32
+ }
33
+ } catch (error) {
34
+ console.error("❌ Error waiting for database:", error);
35
+ exit(1);
36
+ }
37
+ }
38
+
39
+ export { waitForDb };
40
+
41
+ if (import.meta.main) {
42
+ await waitForDb();
43
+ }
@@ -0,0 +1,110 @@
1
+ import { until } from "effection";
2
+
3
+ // This import causes a circular dependency with the sync package.
4
+ // import type { AllSyncProtocols } from "@effectstream/sync";
5
+ import type { PoolClient } from "pg";
6
+ import type { VersionInfo } from "../migrations/system-version.ts";
7
+ import { applyMigrations } from "../scripts/apply-migrations.ts";
8
+ import { findMigrationByName } from "./sql/system.queries.ts";
9
+
10
+ /**
11
+ * Creates dynamic tables for the given sync protocols.
12
+ * This is used to create the IVMs for the given sync protocols.
13
+ *
14
+ * For example for ERC20 it creates a table that holds the balance for each address.
15
+ * For ERC721 it creates a table that holds the token id for each address.
16
+ *
17
+ * @param dbConn - The database connection.
18
+ * @param syncProtocols - The sync protocols.
19
+ */
20
+ export function* createDynamicTables(
21
+ versionInfo: VersionInfo,
22
+ lastBlockHeight: number,
23
+ dbConn: PoolClient,
24
+ syncProtocols: any[], // AllSyncProtocols[],
25
+ ) {
26
+ for (const syncProtocol of syncProtocols) {
27
+ for (const primitive of syncProtocol.config.primitives) {
28
+ yield* createDynamicTableForPrimitive(primitive, lastBlockHeight, dbConn);
29
+ }
30
+ }
31
+ }
32
+
33
+ function* createDynamicTableForPrimitive(
34
+ p: {
35
+ primitive: {
36
+ type: string;
37
+ name: string;
38
+ };
39
+ },
40
+ lastBlockHeight: number,
41
+ dbConn: PoolClient,
42
+ ) {
43
+ const type = p.primitive.type;
44
+ const name = p.primitive.name;
45
+
46
+ // const primitive = PrimitiveRegistry.getPrimitive(name);
47
+ const primitive = (globalThis as any).EFFECTSTREAM_REGISTRY[name];
48
+ if (!primitive) {
49
+ // This should never happen.
50
+ console.error(`Cannot find primitive ${name} in the registry. If this is custom primitive, ensure it's registered.`);
51
+ throw new Error(`Primitive ${name} not found`);
52
+ }
53
+ const sqlFunction = primitive.getDynamicTables;
54
+
55
+ // This primitive does not have dynamic tables.
56
+ if (!sqlFunction) return;
57
+ const code = sqlFunction(name);
58
+ if (!code) return;
59
+
60
+ const migrationName = `dynamic-tables-${type}-${name}`;
61
+ const [migration] = yield* until(findMigrationByName.run({
62
+ name: migrationName,
63
+ isSystemMigration: true,
64
+ }, dbConn));
65
+
66
+ // This particular migration has been applied, so we can skip it.
67
+ if (migration) return;
68
+
69
+ yield* until(applyMigrations(
70
+ dbConn as any, // Client,
71
+ lastBlockHeight,
72
+ migrationName,
73
+ code,
74
+ true,
75
+ ));
76
+ }
77
+
78
+ /**
79
+ * Returns the prefix for the given primitive name.
80
+ * This is useful to query the table given you know the primitive name.
81
+ * @param primitiveType - The type of the primitive.
82
+ * @returns The prefix for the given primitive name.
83
+ */
84
+ export function getPrimitivePrefix(
85
+ primitiveType: string,
86
+ ): string[] {
87
+ const expectedPrimitive: any = Object.values(
88
+ (globalThis as any).EFFECTSTREAM_REGISTRY,
89
+ ).find((primitive: any) => primitive.internalTypeName === primitiveType);
90
+ return expectedPrimitive?.getViewPrefix() || [];
91
+ }
92
+
93
+ /**
94
+ * The intermediate prefix is used to query the intermediate table for the given primitive type.
95
+ * The intermediate table is used to store the intermediate data for the given primitive type.
96
+ *
97
+ * For example for ERC20 it stores the balance for each address.
98
+ * For ERC721 it stores the token id for each address.
99
+ *
100
+ * @param primitiveType - The type of the primitive.
101
+ * @returns The intermediate prefix for the given primitive type.
102
+ */
103
+ export function getPrimitiveIntermediatePrefix(
104
+ primitiveType: string,
105
+ ): string[] {
106
+ const expectedPrimitive: any = Object.values(
107
+ (globalThis as any).EFFECTSTREAM_REGISTRY,
108
+ ).find((primitive: any) => primitive.internalTypeName === primitiveType);
109
+ return expectedPrimitive?.getIntermediatePrefix() || [];
110
+ }
@@ -0,0 +1,31 @@
1
+ import type { PoolClient } from "pg";
2
+
3
+ export async function createIndexesForEvents(
4
+ pool: PoolClient,
5
+ eventDescriptions: { topic: string; fieldName: string; indexed: boolean }[],
6
+ ): Promise<boolean> {
7
+ for (const event of eventDescriptions) {
8
+ const indexName = `index_${
9
+ event.topic.slice(0, 20)
10
+ }_${event.fieldName.toLowerCase()}`;
11
+
12
+ const checkQuery =
13
+ `SELECT * FROM pg_indexes WHERE indexname = '${indexName}';`;
14
+
15
+ const result = await pool.query(checkQuery);
16
+
17
+ const createQuery =
18
+ `CREATE INDEX ${indexName} ON event(topic, (data->>'${event.fieldName}'));`;
19
+ const deleteQuery = `DROP INDEX ${indexName};`;
20
+
21
+ if (result.rowCount === 0 && event.indexed) {
22
+ await pool.query(createQuery);
23
+ }
24
+
25
+ if (result.rowCount !== 0 && !event.indexed) {
26
+ await pool.query(deleteQuery);
27
+ }
28
+ }
29
+
30
+ return true;
31
+ }
@@ -0,0 +1,28 @@
1
+ // TODO: Circular dependency.
2
+ // import type { DBMigrations } from "@effectstream/runtime";
3
+
4
+ /**
5
+ * This function is used by Paima Engine to apply the migration at the correct block heights.
6
+ * It returns the migration script for the given block height.
7
+ * @param startBlockHeight - The paima block height to start applying the migrations from (inclusive).
8
+ * @param endBlockHeight - The paima block height to stop applying the migrations at (inclusive).
9
+ * @returns The migration script for the given block height.
10
+ */
11
+ export function getMigrationsForBlockHeight(
12
+ migrationTable: /*DBMigrations[]*/ any[] | undefined,
13
+ startBlockHeight: number,
14
+ endBlockHeight: number,
15
+ ): /*DBMigrations[]*/ any[] {
16
+ if (!migrationTable) {
17
+ return [];
18
+ }
19
+ const migrationsToApply = migrationTable
20
+ .filter((migration) => {
21
+ const targetBlockHeight = migration.blockHeight ?? 1;
22
+ return (
23
+ targetBlockHeight >= startBlockHeight &&
24
+ targetBlockHeight <= endBlockHeight
25
+ );
26
+ });
27
+ return migrationsToApply;
28
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,65 @@
1
+ // https://github.com/denoland/deno/issues/26611#issuecomment-2492987153
2
+
3
+ // https://github.com/adelsz/pgtyped/issues/565
4
+ export type { Json } from "./sql/events.queries.ts";
5
+
6
+ export {
7
+ getAchievementProgress,
8
+ setAchievementProgress,
9
+ } from "./sql/achievements.queries.ts";
10
+ export type {
11
+ IGetAchievementProgressParams,
12
+ IGetAchievementProgressResult,
13
+ ISetAchievementProgressParams,
14
+ ISetAchievementProgressResult,
15
+ } from "./sql/achievements.queries.ts";
16
+ // https://github.com/adelsz/pgtyped/issues/565
17
+ export {
18
+ blockHeightDone,
19
+ getBlockByHash,
20
+ getBlockHeights,
21
+ getBlockSeeds,
22
+ getLatestProcessedBlockHeight,
23
+ saveLastBlock,
24
+ } from "./sql/block-heights.queries.ts";
25
+ export type {
26
+ IBlockHeightDoneParams,
27
+ IBlockHeightDoneResult,
28
+ IGetBlockByHashParams,
29
+ IGetBlockByHashResult,
30
+ IGetBlockHeightsParams,
31
+ IGetBlockHeightsResult,
32
+ IGetBlockSeedsParams,
33
+ IGetBlockSeedsResult,
34
+ IGetLatestProcessedBlockHeightParams,
35
+ IGetLatestProcessedBlockHeightResult,
36
+ ISaveLastBlockParams,
37
+ ISaveLastBlockResult,
38
+ } from "./sql/block-heights.queries.ts";
39
+ export * from "./sql/statistics.queries.ts";
40
+ export * from "./sql/nonces.queries.ts";
41
+ export * from "./sql/rollup_inputs.queries.ts";
42
+ export * from "./sql/accounts.queries.ts";
43
+ export * from "./sql/events.queries.ts";
44
+ export * from "./sql/sync-protocols/page.queries.ts";
45
+ export * from "./sql/primitives.queries.ts";
46
+ export * from "./sql/system.queries.ts";
47
+ export * from "./sql/tables.queries.ts";
48
+
49
+ export * from "./event-indexing.ts";
50
+ export * from "./register-events.ts";
51
+ export * from "./pg-connection.ts";
52
+ export * from "./scheduled-constructors.ts";
53
+ export * from "./system.ts";
54
+
55
+ export {
56
+ createDynamicTables,
57
+ getPrimitiveIntermediatePrefix,
58
+ getPrimitivePrefix,
59
+ } from "./dynamic-tables.ts";
60
+ // export { startPGlite } from "./start-pglite.ts";
61
+
62
+ export { getMigrationsForBlockHeight } from "./migrations.ts";
63
+
64
+ export { createSnapshot, runSnapshotLoop } from "./snapshot-handler.ts";
65
+ export type { SnapshotConfig, SnapshotRetentionConfig } from "./snapshot-handler.ts";
@@ -0,0 +1,99 @@
1
+ import { test, expect } from "bun:test";
2
+ import {
3
+ acquireDBMutex,
4
+ releaseDBMutex,
5
+ waitUntilFree,
6
+ } from "./pg-connection.ts";
7
+ import { run, sleep, spawn } from "effection";
8
+ import { setEnv } from "@effectstream/utils/runtime";
9
+
10
+ // Force PGLITE env var to true for testing mutex logic
11
+ setEnv("PGLITE", "true");
12
+
13
+ test("DB Mutex - acquires and releases lock", async () => {
14
+ await run(function* () {
15
+ const lockName = "test-lock";
16
+
17
+ yield* acquireDBMutex(lockName);
18
+ let state = waitUntilFree();
19
+ expect(state.db_mutex).toEqual("locked");
20
+ expect(state.running.name).toEqual(lockName);
21
+
22
+ releaseDBMutex(lockName);
23
+ state = waitUntilFree();
24
+ expect(state.db_mutex).toEqual("free");
25
+ expect(state.running.name).toEqual("");
26
+ });
27
+ });
28
+
29
+ test("DB Mutex - queues requests", async () => {
30
+ await run(function* () {
31
+ const lock1 = "lock-1";
32
+ const lock2 = "lock-2";
33
+
34
+ yield* acquireDBMutex(lock1);
35
+
36
+ // Spawn a second task that tries to acquire lock
37
+ let lock2Acquired = false;
38
+ yield* spawn(function* () {
39
+ yield* acquireDBMutex(lock2);
40
+ lock2Acquired = true;
41
+ releaseDBMutex(lock2);
42
+ });
43
+
44
+ yield* sleep(50);
45
+ // Lock 2 should be waiting
46
+ expect(lock2Acquired).toEqual(false);
47
+ let state = waitUntilFree();
48
+ expect(state.waiting.length).toEqual(1);
49
+ expect(state.waiting[0].name).toEqual(lock2);
50
+
51
+ // Release lock 1
52
+ releaseDBMutex(lock1);
53
+
54
+ yield* sleep(50);
55
+ // Lock 2 should now be acquired (and released immediately in this test flow, but let's check flow)
56
+ expect(lock2Acquired).toEqual(true);
57
+
58
+ state = waitUntilFree();
59
+ expect(state.db_mutex).toEqual("free");
60
+ });
61
+ });
62
+
63
+ test("DB Mutex - respects priority", async () => {
64
+ await run(function* () {
65
+ const mainLock = "main";
66
+ const lowPrio = "low";
67
+ const highPrio = "high";
68
+
69
+ yield* acquireDBMutex(mainLock);
70
+
71
+ const acquireOrder: string[] = [];
72
+
73
+ yield* spawn(function* () {
74
+ yield* acquireDBMutex(lowPrio, "low");
75
+ acquireOrder.push(lowPrio);
76
+ releaseDBMutex(lowPrio);
77
+ });
78
+
79
+ yield* sleep(10);
80
+
81
+ yield* spawn(function* () {
82
+ yield* acquireDBMutex(highPrio, "high");
83
+ acquireOrder.push(highPrio);
84
+ releaseDBMutex(highPrio);
85
+ });
86
+
87
+ yield* sleep(10);
88
+
89
+ let state = waitUntilFree();
90
+ expect(state.waiting.length).toEqual(2);
91
+
92
+ releaseDBMutex(mainLock);
93
+ yield* sleep(50);
94
+
95
+ // High priority should have been acquired first
96
+ expect(acquireOrder[0]).toEqual(highPrio);
97
+ expect(acquireOrder[1]).toEqual(lowPrio);
98
+ });
99
+ });