Skip to content

Commit 7c0e18a

Browse files
authored
Add more threading utilities (#283)
## 🎟️ Tracking <!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. --> ## 📔 Objective Add sleep and cancellation token ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes
1 parent 297a114 commit 7c0e18a

File tree

11 files changed

+205
-2
lines changed

11 files changed

+205
-2
lines changed

Cargo.lock

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

crates/bitwarden-threading/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ serde = { workspace = true }
1616
serde_json = { workspace = true }
1717
thiserror = { workspace = true }
1818
tokio = { features = ["sync", "time", "rt"], workspace = true }
19+
tokio-util = { version = "0.7.15" }
1920

2021
[target.'cfg(target_arch="wasm32")'.dependencies]
22+
gloo-timers = { version = "0.3.0", features = ["futures"] }
2123
js-sys = { workspace = true }
2224
tsify-next = { workspace = true }
2325
wasm-bindgen = { workspace = true }
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
pub use tokio_util::sync::CancellationToken;
2+
3+
#[cfg(target_arch = "wasm32")]
4+
pub mod wasm {
5+
use wasm_bindgen::prelude::*;
6+
7+
use super::*;
8+
9+
#[wasm_bindgen]
10+
extern "C" {
11+
#[wasm_bindgen]
12+
#[derive(Clone)]
13+
pub type AbortController;
14+
15+
#[wasm_bindgen(constructor)]
16+
pub fn new() -> AbortController;
17+
18+
#[wasm_bindgen(method, getter)]
19+
pub fn signal(this: &AbortController) -> AbortSignal;
20+
21+
#[wasm_bindgen(method, js_name = abort)]
22+
pub fn abort(this: &AbortController, reason: JsValue);
23+
24+
#[wasm_bindgen]
25+
pub type AbortSignal;
26+
27+
#[wasm_bindgen(method, getter)]
28+
pub fn aborted(this: &AbortSignal) -> bool;
29+
30+
#[wasm_bindgen(method, js_name = addEventListener)]
31+
pub fn add_event_listener(
32+
this: &AbortSignal,
33+
event_type: &str,
34+
callback: &Closure<dyn FnMut()>,
35+
);
36+
}
37+
38+
pub trait AbortControllerExt {
39+
/// Converts an `AbortController` to a `CancellationToken`.
40+
/// The signal only travels in one direction: `AbortController` -> `CancellationToken`,
41+
/// i.e. the `CancellationToken` will be cancelled when the `AbortController` is aborted
42+
/// but not the other way around.
43+
fn to_cancellation_token(&self) -> CancellationToken;
44+
}
45+
46+
impl AbortControllerExt for AbortController {
47+
fn to_cancellation_token(&self) -> CancellationToken {
48+
self.signal().to_cancellation_token()
49+
}
50+
}
51+
52+
pub trait AbortSignalExt {
53+
/// Converts an `AbortSignal` to a `CancellationToken`.
54+
/// The signal only travels in one direction: `AbortSignal` -> `CancellationToken`,
55+
/// i.e. the `CancellationToken` will be cancelled when the `AbortSignal` is aborted
56+
/// but not the other way around.
57+
fn to_cancellation_token(&self) -> CancellationToken;
58+
}
59+
60+
impl AbortSignalExt for AbortSignal {
61+
fn to_cancellation_token(&self) -> CancellationToken {
62+
let token = CancellationToken::new();
63+
64+
let token_clone = token.clone();
65+
let closure = Closure::new(move || {
66+
token_clone.cancel();
67+
});
68+
self.add_event_listener("abort", &closure);
69+
closure.forget(); // Transfer ownership to the JS runtime
70+
71+
token
72+
}
73+
}
74+
}

crates/bitwarden-threading/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod cancellation_token;
12
mod thread_bound_runner;
3+
pub mod time;
24

35
pub use thread_bound_runner::ThreadBoundRunner;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use std::time::Duration;
2+
3+
#[cfg(not(target_arch = "wasm32"))]
4+
pub async fn sleep(duration: Duration) {
5+
tokio::time::sleep(duration).await;
6+
}
7+
8+
#[cfg(target_arch = "wasm32")]
9+
pub async fn sleep(duration: Duration) {
10+
use gloo_timers::future::sleep;
11+
12+
sleep(duration).await;
13+
}
14+
15+
#[cfg(test)]
16+
mod test {
17+
use wasm_bindgen_test::wasm_bindgen_test;
18+
19+
#[wasm_bindgen_test]
20+
#[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test`
21+
async fn should_sleep_wasm() {
22+
use js_sys::Date;
23+
24+
use super::*;
25+
26+
console_error_panic_hook::set_once();
27+
let start = Date::now();
28+
29+
sleep(Duration::from_millis(100)).await;
30+
31+
let end = Date::now();
32+
let elapsed = end - start;
33+
34+
assert!(elapsed >= 90.0, "Elapsed time was less than expected");
35+
}
36+
37+
#[tokio::test]
38+
async fn should_sleep_tokio() {
39+
use std::time::Instant;
40+
41+
use super::*;
42+
43+
let start = Instant::now();
44+
45+
sleep(Duration::from_millis(100)).await;
46+
47+
let end = Instant::now();
48+
let elapsed = end.duration_since(start);
49+
50+
assert!(
51+
elapsed >= Duration::from_millis(90),
52+
"Elapsed time was less than expected"
53+
);
54+
}
55+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#[cfg(target_arch = "wasm32")]
2+
mod wasm;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![allow(unused_imports)] // Rust analyzer doesn't understand the `wasm_bindgen_test` macro
2+
use std::time::Duration;
3+
4+
use bitwarden_threading::{
5+
cancellation_token::wasm::{AbortController, AbortControllerExt},
6+
time::sleep,
7+
};
8+
use wasm_bindgen_test::wasm_bindgen_test;
9+
10+
mod to_cancellation_token {
11+
use super::*;
12+
13+
#[wasm_bindgen_test]
14+
#[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test`
15+
#[cfg(target_arch = "wasm32")]
16+
async fn rust_cancel_does_not_propagate_to_js() {
17+
console_error_panic_hook::set_once();
18+
19+
let controller = AbortController::new();
20+
let token = controller.clone().to_cancellation_token();
21+
22+
assert!(!token.is_cancelled());
23+
assert!(!controller.signal().aborted());
24+
25+
token.cancel();
26+
// Give the cancellation some time to propagate
27+
sleep(Duration::from_millis(100)).await;
28+
29+
assert!(token.is_cancelled());
30+
assert!(!controller.signal().aborted());
31+
}
32+
33+
#[wasm_bindgen_test]
34+
#[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test`
35+
#[cfg(target_arch = "wasm32")]
36+
async fn js_abort_propagate_to_rust() {
37+
console_error_panic_hook::set_once();
38+
39+
let controller = AbortController::new();
40+
let token = controller.clone().to_cancellation_token();
41+
42+
assert!(!token.is_cancelled());
43+
assert!(!controller.signal().aborted());
44+
45+
controller.abort(wasm_bindgen::JsValue::from("Test reason"));
46+
// Give the cancellation some time to propagate
47+
sleep(Duration::from_millis(100)).await;
48+
49+
assert!(token.is_cancelled());
50+
assert!(controller.signal().aborted());
51+
}
52+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
mod standard_tokio;
2-
mod wasm;
1+
mod cancellation_token;
2+
mod thread_bound_runner;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mod standard_tokio;
2+
mod wasm;

0 commit comments

Comments
 (0)