Skip to content

Commit e8a6a44

Browse files
authored
More observable Wasm (#75)
1 parent 1e47d0a commit e8a6a44

File tree

13 files changed

+195
-52
lines changed

13 files changed

+195
-52
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: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ authors = [
3232
[workspace.dependencies]
3333
# Internal crates
3434
automerge_sedimentree_wasm = { version = "0.5.0", path = "automerge_sedimentree_wasm" }
35+
automerge_subduction_wasm = { version = "0.3.1", path = "automerge_subduction_wasm", default-features = false }
3536
bijou64 = { version = "0.1.0", path = "bijou64", default-features = false }
3637
sedimentree_core = { version = "0.5.0", path = "sedimentree_core", default-features = false }
3738
sedimentree_fs_storage = { version = "0.5.0", path = "sedimentree_fs_storage" }
38-
sedimentree_wasm = { version = "0.5.0", path = "sedimentree_wasm", default-features = false }
39+
sedimentree_wasm = { version = "0.5.1", path = "sedimentree_wasm", default-features = false }
3940
subduction_core = { version = "0.5.0", path = "subduction_core", default-features = false }
4041
subduction_crypto = { version = "0.2.0", path = "subduction_crypto", default-features = false }
4142
subduction_http_longpoll = { version = "0.3.0", path = "subduction_http_longpoll", default-features = false }
4243
subduction_iroh = { version = "0.2.0", path = "subduction_iroh" }
43-
subduction_wasm = { version = "0.5.0", path = "subduction_wasm", default-features = false }
44+
subduction_wasm = { version = "0.5.1", path = "subduction_wasm", default-features = false }
4445
subduction_websocket = { version = "0.5.0", path = "subduction_websocket", default-features = false }
4546

4647
# External crates
@@ -164,6 +165,22 @@ opt-level = 3
164165
[profile.release.package.tokio]
165166
opt-level = 3
166167

168+
# Wasm crates: preserve Wasm name section for readable stack traces in
169+
# production. Only strips DWARF debug info, not function names (~5-15%
170+
# larger than strip="symbols", but panics show real function names
171+
# instead of wasm-function[4616]).
172+
[profile.release.package.sedimentree_wasm]
173+
strip = "debuginfo"
174+
175+
[profile.release.package.subduction_wasm]
176+
strip = "debuginfo"
177+
178+
[profile.release.package.automerge_sedimentree_wasm]
179+
strip = "debuginfo"
180+
181+
[profile.release.package.automerge_subduction_wasm]
182+
strip = "debuginfo"
183+
167184
# Benchmarks inherit from release, but the workspace release profile is tuned
168185
# for Wasm (opt-level="z", strip="symbols"). Override for accurate benchmarks
169186
# with debug info sufficient for flamegraphs.

automerge_subduction_wasm/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "automerge_subduction_wasm"
3-
version = "0.3.0"
3+
version = "0.3.1"
44
description = "Wasm bindings for syncing Automerge documents via Subduction"
55
categories = ["web-programming"]
66
keywords = ["automerge", "sedimentree", "wasm"]
@@ -33,8 +33,12 @@ wasm_refgen = { workspace = true }
3333
getrandom_03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
3434

3535
[features]
36-
default = ["console_error_panic_hook", "std", "wasm-tracing"]
36+
default = ["console_error_panic_hook", "standalone", "std", "wasm-tracing"]
3737
console_error_panic_hook = ["dep:console_error_panic_hook"]
38+
# Enable the #[wasm_bindgen(start)] entry point. Disable when using
39+
# this crate as a dependency of another cdylib crate that defines
40+
# its own start function (wasm-bindgen allows only one per module).
41+
standalone = []
3842
std = [
3943
"automerge_sedimentree_wasm/std",
4044
"subduction_wasm/std",

automerge_subduction_wasm/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,13 @@ pub fn set_panic_hook() {
254254
}
255255
}
256256

257-
/// Entry point called when the wasm module is instantiated.
257+
/// Entry point called when the Wasm module is instantiated.
258+
///
259+
/// Only compiled when the `standalone` feature is active. Downstream cdylib
260+
/// crates that define their own `#[wasm_bindgen(start)]` should depend on
261+
/// `automerge_subduction_wasm` with `default-features = false` and call
262+
/// [`set_panic_hook`] from their own start function.
263+
#[cfg(feature = "standalone")]
258264
#[wasm_bindgen(start)]
259265
pub fn start() {
260266
set_panic_hook();

flake.lock

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

sedimentree_wasm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sedimentree_wasm"
3-
version = "0.5.0"
3+
version = "0.5.1"
44
description = "Wasm/JavaScript bindings for Sedimentree (browser and Node.js)"
55
categories = ["web-programming"]
66
keywords = ["sedimentree", "wasm"]

sedimentree_wasm/src/storage.rs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,18 +152,27 @@ impl core::fmt::Debug for JsStorage {
152152
}
153153

154154
/// Parse a JS array of digests.
155-
fn parse_digest_array<T: Encode + Decode>(js_value: &JsValue) -> Set<Digest<T>> {
155+
///
156+
/// Returns an error if any element cannot be cast to a `Digest`.
157+
fn parse_digest_array<T: Encode + Decode>(
158+
js_value: &JsValue,
159+
) -> Result<Set<Digest<T>>, JsStorageError> {
156160
let array = js_sys::Array::from(js_value);
157161
let mut result = Set::new();
158162

159163
for i in 0..array.length() {
160164
let item = array.get(i);
161-
let js_digest: JsDigest = JsCast::unchecked_into(item);
165+
let js_digest: JsDigest =
166+
item.dyn_into()
167+
.map_err(|value| JsStorageError::UnexpectedJsType {
168+
expected: "Digest",
169+
value,
170+
})?;
162171
let digest: Digest<T> = WasmDigest::from(&js_digest).into();
163172
result.insert(digest);
164173
}
165174

166-
result
175+
Ok(result)
167176
}
168177

169178
impl Storage<Local> for JsStorage {
@@ -265,7 +274,13 @@ impl Storage<Local> for JsStorage {
265274
return Ok(None);
266275
}
267276

268-
let commit_with_blob: JsCommitWithBlob = js_value.unchecked_into();
277+
let commit_with_blob: JsCommitWithBlob =
278+
js_value
279+
.dyn_into()
280+
.map_err(|value| JsStorageError::UnexpectedJsType {
281+
expected: "CommitWithBlob",
282+
value,
283+
})?;
269284
let wasm_commit: WasmCommitWithBlob = (&commit_with_blob).into();
270285
let signed: Signed<LooseCommit> = wasm_commit.signed().into();
271286
let blob = Blob::new(wasm_commit.blob().to_vec());
@@ -286,7 +301,7 @@ impl Storage<Local> for JsStorage {
286301
let js_value = JsFuture::from(js_promise)
287302
.await
288303
.map_err(JsStorageError::JsError)?;
289-
Ok(parse_digest_array(&js_value))
304+
parse_digest_array(&js_value)
290305
})
291306
}
292307

@@ -307,7 +322,12 @@ impl Storage<Local> for JsStorage {
307322

308323
for i in 0..array.length() {
309324
let item = array.get(i);
310-
let commit_with_blob: JsCommitWithBlob = item.unchecked_into();
325+
let commit_with_blob: JsCommitWithBlob =
326+
item.dyn_into()
327+
.map_err(|value| JsStorageError::UnexpectedJsType {
328+
expected: "CommitWithBlob",
329+
value,
330+
})?;
311331
let wasm_commit: WasmCommitWithBlob = (&commit_with_blob).into();
312332
let signed: Signed<LooseCommit> = wasm_commit.signed().into();
313333
let blob = Blob::new(wasm_commit.blob().to_vec());
@@ -397,7 +417,13 @@ impl Storage<Local> for JsStorage {
397417
return Ok(None);
398418
}
399419

400-
let fragment_with_blob: JsFragmentWithBlob = js_value.unchecked_into();
420+
let fragment_with_blob: JsFragmentWithBlob =
421+
js_value
422+
.dyn_into()
423+
.map_err(|value| JsStorageError::UnexpectedJsType {
424+
expected: "FragmentWithBlob",
425+
value,
426+
})?;
401427
let wasm_fragment: WasmFragmentWithBlob = (&fragment_with_blob).into();
402428
let signed: Signed<Fragment> = wasm_fragment.signed().into();
403429
let blob = Blob::new(wasm_fragment.blob().to_vec());
@@ -418,7 +444,7 @@ impl Storage<Local> for JsStorage {
418444
let js_value = JsFuture::from(js_promise)
419445
.await
420446
.map_err(JsStorageError::JsError)?;
421-
Ok(parse_digest_array(&js_value))
447+
parse_digest_array(&js_value)
422448
})
423449
}
424450

@@ -439,7 +465,12 @@ impl Storage<Local> for JsStorage {
439465

440466
for i in 0..array.length() {
441467
let item = array.get(i);
442-
let fragment_with_blob: JsFragmentWithBlob = item.unchecked_into();
468+
let fragment_with_blob: JsFragmentWithBlob =
469+
item.dyn_into()
470+
.map_err(|value| JsStorageError::UnexpectedJsType {
471+
expected: "FragmentWithBlob",
472+
value,
473+
})?;
443474
let wasm_fragment: WasmFragmentWithBlob = (&fragment_with_blob).into();
444475
let signed: Signed<Fragment> = wasm_fragment.signed().into();
445476
let blob = Blob::new(wasm_fragment.blob().to_vec());
@@ -550,6 +581,15 @@ pub enum JsStorageError {
550581
/// The `JsValue` could not be converted into an array of `SedimentreeId`s.
551582
#[error("Value was not an array of SedimentreeIds: {0:?}")]
552583
NotSedimentreeIdArray(#[from] WasmConvertJsValueToSedimentreeIdArrayError),
584+
585+
/// A JS value could not be cast to the expected type.
586+
#[error("JS type cast failed: expected {expected}, got {value:?}")]
587+
UnexpectedJsType {
588+
/// The type name that was expected.
589+
expected: &'static str,
590+
/// The actual JS value received.
591+
value: JsValue,
592+
},
553593
}
554594

555595
impl From<JsStorageError> for JsValue {

subduction_wasm/Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "subduction_wasm"
3-
version = "0.5.0"
3+
version = "0.5.1"
44
description = "Wasm/JavaScript bindings for Subduction (browser and Node.js)"
55
categories = ["web-programming"]
66
keywords = ["subduction", "websocket"]
@@ -80,9 +80,12 @@ getrandom_03 = { package = "getrandom", version = "0.3", features = ["wasm_js"]
8080
wasm-bindgen-test = "0.2"
8181

8282
[features]
83-
default = []
84-
# Enable for better panic messages in dev builds (adds ~30KB due to std)
83+
default = ["console_error_panic_hook", "standalone"]
8584
console_error_panic_hook = ["dep:console_error_panic_hook"]
85+
# Enable the #[wasm_bindgen(start)] entry point. Disable when using
86+
# subduction_wasm as a dependency of another cdylib crate that defines
87+
# its own start function (wasm-bindgen allows only one per module).
88+
standalone = []
8689
# IndexedDB storage backend
8790
idb = ["sedimentree_wasm/idb"]
8891
std = [

subduction_wasm/src/connection.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,13 @@ impl Connection<Local> for JsConnection {
137137
let js_value = JsFuture::from(self.js_recv())
138138
.await
139139
.map_err(JsConnectionError::Recv)?;
140-
let js_msg: JsMessage = JsCast::unchecked_into(js_value);
140+
let js_msg: JsMessage =
141+
js_value
142+
.dyn_into()
143+
.map_err(|value| JsConnectionError::UnexpectedJsType {
144+
expected: "Message",
145+
value,
146+
})?;
141147
let wasm_msg = WasmMessage::from(&js_msg);
142148
Ok(wasm_msg.into())
143149
}
@@ -149,8 +155,11 @@ impl Connection<Local> for JsConnection {
149155
#[allow(clippy::expect_used)]
150156
let js_value = JsFuture::from(self.js_next_request_id())
151157
.await
152-
.expect("nextRequestId should not fail");
153-
let js_req_id: JsRequestId = js_value.unchecked_into();
158+
.expect("JsConnection.nextRequestId() promise rejected");
159+
#[allow(clippy::expect_used)]
160+
let js_req_id: JsRequestId = js_value
161+
.dyn_into()
162+
.expect("JsConnection.nextRequestId() did not return a RequestId");
154163
let wasm_req_id = WasmRequestId::from(&js_req_id);
155164
wasm_req_id.into()
156165
}
@@ -169,7 +178,13 @@ impl Connection<Local> for JsConnection {
169178
let js_value = JsFuture::from(self.js_call(wasm_req, timeout_ms))
170179
.await
171180
.map_err(JsConnectionError::Call)?;
172-
let js_resp: JsBatchSyncResponse = JsCast::unchecked_into(js_value);
181+
let js_resp: JsBatchSyncResponse =
182+
js_value
183+
.dyn_into()
184+
.map_err(|value| JsConnectionError::UnexpectedJsType {
185+
expected: "BatchSyncResponse",
186+
value,
187+
})?;
173188
let wasm_resp = WasmBatchSyncResponse::from(&js_resp);
174189
Ok(wasm_resp.into())
175190
}
@@ -195,6 +210,15 @@ pub enum JsConnectionError {
195210
/// An error that occurred during a synchronous call.
196211
#[error("Call error")]
197212
Call(JsValue),
213+
214+
/// A JS value could not be cast to the expected type.
215+
#[error("JS type cast failed: expected {expected}, got {value:?}")]
216+
UnexpectedJsType {
217+
/// The type name that was expected.
218+
expected: &'static str,
219+
/// The actual JS value received.
220+
value: JsValue,
221+
},
198222
}
199223

200224
impl From<JsConnectionError> for JsValue {

subduction_wasm/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@ extern crate std;
99

1010
extern crate alloc;
1111

12+
/// Install the console error panic hook so that panics produce readable
13+
/// error messages in the browser console.
14+
///
15+
/// Called automatically when the `standalone` feature is enabled.
16+
/// When `subduction_wasm` is used as a dependency of another cdylib crate,
17+
/// call this from that crate's own start function instead.
18+
pub fn set_panic_hook() {
19+
#[cfg(feature = "console_error_panic_hook")]
20+
console_error_panic_hook::set_once();
21+
}
22+
23+
/// Entry point called when the Wasm module is instantiated.
24+
///
25+
/// Only compiled when the `standalone` feature is active. Downstream cdylib
26+
/// crates that define their own `#[wasm_bindgen(start)]` should depend on
27+
/// `subduction_wasm` with `default-features = false` and call
28+
/// [`set_panic_hook`] from their own start function.
29+
#[cfg(feature = "standalone")]
30+
#[wasm_bindgen::prelude::wasm_bindgen(start)]
31+
pub fn start() {
32+
set_panic_hook();
33+
}
34+
1235
pub mod connection;
1336
pub mod connection_id;
1437
pub mod error;

0 commit comments

Comments
 (0)