Skip to content

Commit 103c122

Browse files
committed
chore: add the RWLock
1 parent 6479d4e commit 103c122

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

bun.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@opentelemetry/sdk-trace-base": "^2.1.0",
4848
"@opentelemetry/sdk-trace-node": "^2.1.0",
4949
"@opentelemetry/semantic-conventions": "^1.37.0",
50+
"@rocicorp/lock": "^1.0.4",
5051
"@types/marked-terminal": "^6.1.1",
5152
"@types/react": "^19.2.2",
5253
"@types/ws": "^8.18.1",
@@ -710,6 +711,10 @@
710711

711712
"@radix-ui/react-use-layout-effect": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
712713

714+
"@rocicorp/lock": ["@rocicorp/[email protected]", "", { "dependencies": { "@rocicorp/resolver": "^1.0.2" } }, "sha512-FavTiO8ETXFXDVfA87IThGduTTTR8iqzBnr/c60gUUmbk7knGEXPmf2B+yiNuluJD0ku0fL2V2r62UXnsLXl6w=="],
715+
716+
"@rocicorp/resolver": ["@rocicorp/[email protected]", "", {}, "sha512-TfjMTQp9cNNqNtHFfa+XHEGdA7NnmDRu+ZJH4YF3dso0Xk/b9DMhg/sl+b6CR4ThFZArXXDsG1j8Mwl34wcOZQ=="],
717+
713718
"@rolldown/binding-android-arm64": ["@rolldown/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-TP8bcPOb1s6UmY5syhXrDn9k0XkYcw+XaoylTN4cJxf0JOVS2j682I3aTcpfT51hOFGr2bRwNKN9RZ19XxeQbA=="],
714719

715720
"@rolldown/binding-darwin-arm64": ["@rolldown/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kuVWnZsE4vEjMF/10SbSUyzucIW2zmdsqFghYMqy+fsjXnRHg0luTU6qWF8IqJf4Cbpm9NEZRnjIEPpAbdiSNQ=="],

packages/blink/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"@opentelemetry/sdk-trace-base": "^2.1.0",
101101
"@opentelemetry/sdk-trace-node": "^2.1.0",
102102
"@opentelemetry/semantic-conventions": "^1.37.0",
103+
"@rocicorp/lock": "^1.0.4",
103104
"@types/marked-terminal": "^6.1.1",
104105
"@types/react": "^19.2.2",
105106
"@types/ws": "^8.18.1",
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { expect, test } from "bun:test";
2+
import { RWLock } from "./rw-lock";
3+
4+
// Note: The actual lock logic is handled by the @rocicorp/lock library.
5+
// These tests verify that our wrapper correctly implements Symbol.asyncDispose
6+
// and that basic read/write lock semantics work as expected.
7+
test("RWLock: write lock blocks readers", async () => {
8+
const lock = new RWLock();
9+
const events: string[] = [];
10+
11+
let writerAcquired: () => void = () => {
12+
throw new Error("writerAcquired not set");
13+
};
14+
let releaseWriter: () => void = () => {
15+
throw new Error("releaseWriter not set");
16+
};
17+
const writerHasAcquired = new Promise<void>((resolve) => {
18+
writerAcquired = resolve;
19+
});
20+
const writerCanRelease = new Promise<void>((resolve) => {
21+
releaseWriter = resolve;
22+
});
23+
24+
// Acquire write lock
25+
const writerPromise = (async () => {
26+
using writeLock = await lock.write();
27+
events.push("writer-acquired");
28+
writerAcquired();
29+
await writerCanRelease;
30+
// give a chance for a bug to happen. it shouldn't!
31+
await new Promise((resolve) => setTimeout(resolve, 10));
32+
events.push("writer-releasing");
33+
})();
34+
35+
// Wait for writer to actually acquire the lock
36+
await writerHasAcquired;
37+
38+
// Try to acquire read lock (should wait)
39+
const readerPromise = (async () => {
40+
events.push("reader-waiting");
41+
using readLock = await lock.read();
42+
events.push("reader-acquired");
43+
})();
44+
45+
releaseWriter();
46+
47+
await Promise.all([writerPromise, readerPromise]);
48+
49+
// Reader should wait for writer to release
50+
expect(events).toEqual([
51+
"writer-acquired",
52+
"reader-waiting",
53+
"writer-releasing",
54+
"reader-acquired",
55+
]);
56+
});
57+
58+
test("RWLock: readers block write lock", async () => {
59+
const lock = new RWLock();
60+
const events: string[] = [];
61+
62+
let readerAcquired: () => void = () => {
63+
throw new Error("readerAcquired not set");
64+
};
65+
let releaseReader: () => void = () => {
66+
throw new Error("releaseReader not set");
67+
};
68+
const readerHasAcquired = new Promise<void>((resolve) => {
69+
readerAcquired = resolve;
70+
});
71+
const readerCanRelease = new Promise<void>((resolve) => {
72+
releaseReader = resolve;
73+
});
74+
75+
// Acquire read lock
76+
const readerPromise = (async () => {
77+
using readLock = await lock.read();
78+
events.push("reader-acquired");
79+
readerAcquired();
80+
await readerCanRelease;
81+
// give a chance for a bug to happen. it shouldn't!
82+
await new Promise((resolve) => setTimeout(resolve, 10));
83+
events.push("reader-releasing");
84+
})();
85+
86+
// Wait for reader to actually acquire the lock
87+
await readerHasAcquired;
88+
89+
// Try to acquire write lock (should wait)
90+
const writerPromise = (async () => {
91+
events.push("writer-waiting");
92+
using writeLock = await lock.write();
93+
events.push("writer-acquired");
94+
})();
95+
96+
releaseReader();
97+
98+
await Promise.all([readerPromise, writerPromise]);
99+
100+
// Writer should wait for reader to release
101+
expect(events).toEqual([
102+
"reader-acquired",
103+
"writer-waiting",
104+
"reader-releasing",
105+
"writer-acquired",
106+
]);
107+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { RWLock as RocicorpRWLock } from "@rocicorp/lock";
2+
3+
export class ReadLock {
4+
constructor(private onRelease: () => void) {}
5+
6+
[Symbol.dispose](): void {
7+
this.onRelease();
8+
}
9+
}
10+
11+
export class WriteLock {
12+
constructor(private onRelease: () => void) {}
13+
14+
[Symbol.dispose](): void {
15+
this.onRelease();
16+
}
17+
}
18+
19+
/**
20+
* RWLock is a read/write lock that allows multiple concurrent readers
21+
* or a single writer. Writers wait for all readers to finish before acquiring
22+
* the lock.
23+
*
24+
* This is designed for in-memory, single-process synchronization using the
25+
* explicit resource management pattern (Symbol.dispose).
26+
*
27+
* Example usage:
28+
* ```typescript
29+
* const lock = new RWLock();
30+
*
31+
* // Multiple readers can acquire the lock concurrently
32+
* using readLock = await lock.read();
33+
* // ... do read operations ...
34+
*
35+
* // Writers wait for all readers to finish
36+
* using writeLock = await lock.write();
37+
* // ... do write operations ...
38+
* ```
39+
*/
40+
export class RWLock {
41+
private _lock = new RocicorpRWLock();
42+
43+
/**
44+
* Acquire a read lock. Multiple readers can hold the lock concurrently.
45+
* If a writer is waiting, new readers will wait until the writer completes.
46+
*
47+
* The lock is automatically released when the returned object is disposed.
48+
*/
49+
async read(): Promise<ReadLock> {
50+
const release = await this._lock.read();
51+
return new ReadLock(release);
52+
}
53+
54+
/**
55+
* Acquire a write lock. Waits for all readers to finish before acquiring.
56+
* Only one writer can hold the lock at a time.
57+
*
58+
* The lock is automatically released when the returned object is disposed.
59+
*/
60+
async write(): Promise<WriteLock> {
61+
const release = await this._lock.write();
62+
return new WriteLock(release);
63+
}
64+
}

0 commit comments

Comments
 (0)