Skip to content

Commit dee507a

Browse files
authored
feat(ext/web_locks): weblocks api (#142)
* feat(ext/web_locks): weblocks api * chore: bump version
1 parent 173d8f4 commit dee507a

File tree

9 files changed

+1085
-6
lines changed

9 files changed

+1085
-6
lines changed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = ["the Andromeda team"]
77
edition = "2024"
88
license = "Mozilla Public License 2.0"
99
repository = "https://github.com/tryandromeda/andromeda"
10-
version = "0.1.0-draft37"
10+
version = "0.1.0-draft38"
1111

1212
[workspace.dependencies]
1313
andromeda-core = { path = "core" }

runtime/src/event_loop.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
use crate::ext::{cron::CronId, interval::IntervalId, timeout::TimeoutId};
5+
use crate::ext::{LockMode, cron::CronId, interval::IntervalId, timeout::TimeoutId};
66
use nova_vm::{ecmascript::types::Value, engine::Global};
77
use tokio::net::TcpStream;
88
use tokio_rustls::client::TlsStream;
@@ -30,4 +30,15 @@ pub enum RuntimeMacroTask {
3030
RejectPromise(Global<Value<'static>>, String),
3131
/// Register a TLS stream into the runtime resource table and resolve a promise with its rid.
3232
RegisterTlsStream(Global<Value<'static>>, Box<TlsStream<TcpStream>>),
33+
/// Acquire a lock and resolve the promise with the lock result.
34+
AcquireLock {
35+
promise: Global<Value<'static>>,
36+
lock_id: u64,
37+
name: String,
38+
mode: LockMode,
39+
},
40+
/// Release a lock and process any pending requests.
41+
ReleaseLock { name: String, lock_id: u64 },
42+
/// Abort a pending lock request.
43+
AbortLockRequest { name: String, lock_id: u64 },
3344
}

runtime/src/ext/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod time;
2727
pub mod tls;
2828
mod url;
2929
mod web;
30+
mod web_locks;
3031

3132
pub use broadcast_channel::*;
3233
#[cfg(feature = "storage")]
@@ -53,3 +54,4 @@ pub use time::*;
5354
pub use tls::*;
5455
pub use url::*;
5556
pub use web::*;
57+
pub use web_locks::*;

runtime/src/ext/web/web_locks.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
interface LockOptions {
6+
/**
7+
* The mode of the lock. Default is "exclusive".
8+
* - "exclusive": Only one holder allowed at a time
9+
* - "shared": Multiple holders allowed simultaneously
10+
*/
11+
mode?: "exclusive" | "shared";
12+
13+
/**
14+
* If true, the request will fail if the lock cannot be granted immediately.
15+
* The callback will be invoked with null.
16+
*/
17+
ifAvailable?: boolean;
18+
19+
/**
20+
* If true, any held locks with the same name will be released,
21+
* and the request will be granted, preempting any queued requests.
22+
*/
23+
steal?: boolean;
24+
25+
/**
26+
* An AbortSignal that can be used to abort the lock request.
27+
*/
28+
signal?: AbortSignal;
29+
}
30+
31+
/**
32+
* Information about a lock for query results
33+
*/
34+
interface LockInfo {
35+
/** The name of the lock */
36+
name: string;
37+
/** The mode of the lock */
38+
mode: "exclusive" | "shared";
39+
/** An identifier for the client holding or requesting the lock */
40+
clientId?: string;
41+
}
42+
43+
/**
44+
* Result of a query operation
45+
*/
46+
interface LockManagerSnapshot {
47+
/** Currently held locks */
48+
held: LockInfo[];
49+
/** Pending lock requests */
50+
pending: LockInfo[];
51+
}
52+
53+
/**
54+
* Represents a granted lock
55+
*/
56+
class Lock {
57+
#name: string;
58+
#mode: "exclusive" | "shared";
59+
60+
constructor(name: string, mode: "exclusive" | "shared") {
61+
this.#name = name;
62+
this.#mode = mode;
63+
}
64+
65+
/**
66+
* The name of the lock
67+
*/
68+
get name(): string {
69+
return this.#name;
70+
}
71+
72+
/**
73+
* The mode of the lock
74+
*/
75+
get mode(): "exclusive" | "shared" {
76+
return this.#mode;
77+
}
78+
}
79+
80+
/**
81+
* The LockManager interface provides methods for requesting locks and querying lock state
82+
*/
83+
class LockManager {
84+
/**
85+
* Request a lock and execute a callback while holding it
86+
* @param name The name of the lock
87+
* @param callback The callback to execute while holding the lock
88+
* @param options Options for the lock request
89+
* @returns A promise that resolves with the return value of the callback
90+
*/
91+
async request(
92+
name: string,
93+
callback: (lock: Lock | null) => unknown,
94+
options: LockOptions = {},
95+
): Promise<unknown> {
96+
// Validate name (no leading '-')
97+
if (typeof name !== "string" || name.startsWith("-")) {
98+
throw new DOMException("Invalid lock name", "NotSupportedError");
99+
}
100+
101+
// Validate options
102+
if (options.ifAvailable && options.steal) {
103+
throw new DOMException(
104+
"Cannot specify both 'ifAvailable' and 'steal' options",
105+
"NotSupportedError",
106+
);
107+
}
108+
109+
if (typeof callback !== "function") {
110+
throw new DOMException("Callback must be a function", "TypeError");
111+
}
112+
113+
// Check for AbortSignal
114+
if (options.signal && options.signal.aborted) {
115+
throw new DOMException("The operation was aborted", "AbortError");
116+
}
117+
118+
const mode = options.mode || "exclusive";
119+
const ifAvailable = options.ifAvailable || false;
120+
const steal = options.steal || false;
121+
122+
try {
123+
const lockIdResult = await __andromeda__.internal_locks_request(
124+
name,
125+
mode,
126+
ifAvailable,
127+
steal,
128+
);
129+
130+
// Handle error responses
131+
if (lockIdResult.startsWith("error:")) {
132+
const errorMessage = lockIdResult.substring(6); // Remove 'error:' prefix
133+
134+
if (errorMessage === "Invalid lock name") {
135+
throw new DOMException(
136+
"Invalid lock name",
137+
"NotSupportedError",
138+
);
139+
} else {
140+
throw new Error(errorMessage);
141+
}
142+
}
143+
144+
if (lockIdResult === "not_available") {
145+
// Lock not available and ifAvailable was true
146+
return callback(null);
147+
}
148+
149+
const lock = new Lock(name, mode);
150+
151+
// Set up AbortSignal listener if provided
152+
let abortHandler: (() => void) | null = null;
153+
if (options.signal) {
154+
abortHandler = () => {
155+
__andromeda__.internal_locks_abort(name, lockIdResult);
156+
throw new DOMException(
157+
"The operation was aborted",
158+
"AbortError",
159+
);
160+
};
161+
options.signal.addEventListener("abort", abortHandler);
162+
}
163+
164+
try {
165+
return await callback(lock);
166+
} finally {
167+
if (options.signal && abortHandler) {
168+
options.signal.removeEventListener("abort", abortHandler);
169+
}
170+
try {
171+
await __andromeda__.internal_locks_release(
172+
name,
173+
lockIdResult,
174+
);
175+
} catch (releaseError) {
176+
console.error(`Failed to release lock: ${releaseError}`);
177+
}
178+
}
179+
} catch (error) {
180+
throw error;
181+
}
182+
}
183+
184+
/**
185+
* Query the current state of locks
186+
* @returns A promise that resolves with information about held and pending locks
187+
*/
188+
async query(): Promise<LockManagerSnapshot> {
189+
try {
190+
const resultJson = await __andromeda__.internal_locks_query();
191+
const result = JSON.parse(resultJson);
192+
193+
return {
194+
held: result.held || [],
195+
pending: result.pending || [],
196+
};
197+
} catch (error) {
198+
throw new Error(`Failed to query locks: ${error}`);
199+
}
200+
}
201+
}
202+
203+
const lockManager = new LockManager();
204+
205+
// @ts-ignore - Adding locks property to navigator
206+
navigator.locks = lockManager;
207+
208+
// @ts-ignore - Adding LockManager to global scope
209+
globalThis.LockManager = LockManager;
210+
// @ts-ignore - Adding Lock to global scope
211+
globalThis.Lock = Lock;

0 commit comments

Comments
 (0)