Skip to content

Commit a657592

Browse files
committed
feat: add waitForLockRelease
1 parent 55666da commit a657592

File tree

3 files changed

+86
-3
lines changed

3 files changed

+86
-3
lines changed

.eslintrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@
6868
"@typescript-eslint/parser": [".ts"]
6969
},
7070
"jsdoc": {
71-
"exemptDestructuredRootsFromChecks": true
71+
"exemptDestructuredRootsFromChecks": true,
72+
"tagNamePreference": {
73+
"hidden": "hidden"
74+
}
7275
}
7376
},
7477
"rules": {

src/withLock.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ const locks = new Map<any, Map<string, Promise<any>>>();
55
*/
66
export async function withLock<ReturnType>(scope: any, key: string, callback: () => Promise<ReturnType>): Promise<ReturnType> {
77
while (locks.get(scope)?.has(key)) {
8-
await locks.get(scope)?.get(key);
8+
try {
9+
await locks.get(scope)?.get(key);
10+
} catch (err) {
11+
// we only need to wait here for the promise to resolve, we don't care about the result
12+
}
913
}
1014

1115
const promise = callback();
@@ -60,6 +64,30 @@ export async function acquireLock<S = any, K extends string = string>(scope: S,
6064
};
6165
}
6266

67+
/**
68+
* Wait for a lock to be released for a given `scope` and `key`.
69+
*/
70+
export async function waitForLockRelease(scope: any, key: string): Promise<void> {
71+
// eslint-disable-next-line no-constant-condition
72+
while (true) {
73+
try {
74+
await locks.get(scope)?.get(key);
75+
} catch (err) {
76+
// we only need to wait here for the promise to resolve, we don't care about the result
77+
}
78+
79+
if (locks.get(scope)?.has(key))
80+
continue;
81+
82+
await Promise.resolve(); // wait for a microtask to run, so other pending locks can be registered
83+
84+
if (locks.get(scope)?.has(key))
85+
continue;
86+
87+
return;
88+
}
89+
}
90+
6391
export type Lock<S = any, K extends string = string> = {
6492
scope: S,
6593
key: K,

test/withLock.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {describe, expect, test} from "vitest";
2-
import {acquireLock, isLockActive, withLock} from "../src/index.js";
2+
import {acquireLock, isLockActive, waitForLockRelease, withLock} from "../src/index.js";
33

44
describe("withLock", () => {
55
test("lock works", async () => {
@@ -103,4 +103,56 @@ describe("withLock", () => {
103103

104104
expect(res).toEqual([1, 2, 3]);
105105
});
106+
107+
test("waitForLockRelease", async () => {
108+
const scope1 = {};
109+
const key1 = "key";
110+
111+
const waitForEnoughMicrotasks = async () => {
112+
for (let i = 0; i < 10; i++)
113+
await Promise.resolve();
114+
};
115+
116+
const lock1 = await acquireLock(scope1, key1);
117+
118+
const lockWithError2 = withLock(scope1, key1, async () => {
119+
throw new Error("some error");
120+
});
121+
122+
let lockReleased = false;
123+
void (async () => {
124+
await waitForLockRelease(scope1, key1);
125+
lockReleased = true;
126+
})();
127+
128+
const lock3Promise = acquireLock(scope1, key1);
129+
130+
expect(lockReleased).toBe(false);
131+
await waitForEnoughMicrotasks();
132+
expect(lockReleased).toBe(false);
133+
134+
lock1.dispose();
135+
expect(lockReleased).toBe(false);
136+
await waitForEnoughMicrotasks();
137+
expect(lockReleased).toBe(false);
138+
139+
try {
140+
await lockWithError2;
141+
expect.unreachable("lockWithError2 should throw");
142+
} catch (err) {
143+
expect(lockReleased).toBe(false);
144+
await waitForEnoughMicrotasks();
145+
expect(lockReleased).toBe(false);
146+
}
147+
148+
const lock2 = await lock3Promise;
149+
expect(lockReleased).toBe(false);
150+
await waitForEnoughMicrotasks();
151+
expect(lockReleased).toBe(false);
152+
153+
lock2.dispose();
154+
155+
await waitForEnoughMicrotasks();
156+
expect(lockReleased).toBe(true);
157+
});
106158
});

0 commit comments

Comments
 (0)