pub use std::time::Duration;
use std::{
hash::{DefaultHasher, Hash, Hasher},
sync::atomic::{AtomicUsize, Ordering},
};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
use wasm_safe_thread::Builder as WasmThreadBuilder;
#[cfg(not(target_arch = "wasm32"))]
pub type Thread = std::thread::Thread;
#[cfg(target_arch = "wasm32")]
pub type Thread = wasm_safe_thread::Thread;
#[cfg(not(target_arch = "wasm32"))]
pub type ThreadId = std::thread::ThreadId;
#[cfg(target_arch = "wasm32")]
pub type ThreadId = wasm_safe_thread::ThreadId;
#[cfg(not(target_arch = "wasm32"))]
#[inline]
pub fn yield_now() {
std::thread::yield_now();
}
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn yield_now() {}
#[cfg(target_arch = "wasm32")]
#[inline]
#[must_use]
pub fn is_worker_thread() -> bool {
js_sys::global()
.dyn_into::<web_sys::DedicatedWorkerGlobalScope>()
.is_ok()
}
#[cfg(target_arch = "wasm32")]
#[inline]
#[must_use]
pub fn is_main_thread() -> bool {
!is_worker_thread()
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
#[must_use]
pub fn is_worker_thread() -> bool {
false
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
#[must_use]
pub fn is_main_thread() -> bool {
true
}
#[inline]
pub fn assert_main_thread(_label: &str) {
#[cfg(target_arch = "wasm32")]
if !is_main_thread() {
panic!("main-thread-only call executed on worker thread: {_label}");
}
}
#[inline]
pub fn assert_not_main_thread(_label: &str) {
#[cfg(target_arch = "wasm32")]
if is_main_thread() {
panic!("worker-thread-only call executed on main thread: {_label}");
}
}
#[cfg(not(target_arch = "wasm32"))]
pub type JoinHandle<T> = std::thread::JoinHandle<T>;
#[cfg(target_arch = "wasm32")]
pub type JoinHandle<T> = wasm_safe_thread::JoinHandle<T>;
#[cfg(not(target_arch = "wasm32"))]
#[inline]
#[must_use]
pub fn current() -> Thread {
std::thread::current()
}
#[cfg(target_arch = "wasm32")]
#[inline]
#[must_use]
pub fn current() -> Thread {
wasm_safe_thread::current()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
std::thread::spawn(f)
}
static ACTIVE_NAMED_THREADS: AtomicUsize = AtomicUsize::new(0);
#[must_use]
pub fn active_named_thread_count() -> usize {
ACTIVE_NAMED_THREADS.load(Ordering::Acquire)
}
fn counted<F, T>(f: F) -> impl FnOnce() -> T + Send + 'static
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
ACTIVE_NAMED_THREADS.fetch_add(1, Ordering::Release);
move || {
let result = f();
ACTIVE_NAMED_THREADS.fetch_sub(1, Ordering::Release);
result
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn spawn_named<F, T, N: Into<String>>(name: N, f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
std::thread::Builder::new()
.name(name.into())
.spawn(counted(f))
.expect(
"BUG: spawn_named must succeed; thread::Builder only fails on OS resource exhaustion",
)
}
#[cfg(target_arch = "wasm32")]
pub fn spawn_named<F, T, N: Into<String>>(name: N, f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let _name = name.into();
spawn(counted(f))
}
#[cfg(target_arch = "wasm32")]
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
const SHIM_NAME: &str = "kithara-wasm";
WasmThreadBuilder::new()
.shim_name(SHIM_NAME.to_owned())
.spawn(move || {
console_error_panic_hook::set_once();
f()
})
.expect("BUG: WASM Worker spawn must succeed; only fails on OS resource exhaustion")
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
pub fn sleep(duration: Duration) {
std::thread::sleep(duration);
}
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn sleep(duration: Duration) {
wasm_safe_thread::sleep(duration);
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
pub fn park() {
std::thread::park();
}
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn park() {
wasm_safe_thread::park();
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
pub fn park_timeout(duration: Duration) {
std::thread::park_timeout(duration);
}
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn park_timeout(duration: Duration) {
wasm_safe_thread::park_timeout(duration);
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
#[must_use]
pub fn current_thread_id() -> u64 {
let id = current().id();
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}
#[cfg(target_arch = "wasm32")]
#[inline]
#[must_use]
pub fn current_thread_id() -> u64 {
let id = current().id();
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
#[must_use]
pub fn available_parallelism() -> Option<std::num::NonZeroUsize> {
std::thread::available_parallelism().ok()
}
#[cfg(target_arch = "wasm32")]
#[inline]
#[must_use]
pub fn available_parallelism() -> Option<std::num::NonZeroUsize> {
wasm_safe_thread::available_parallelism().ok()
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use kithara_test_utils::kithara;
use super::*;
#[kithara::test]
fn native_thread_detectors_are_consistent() {
#[cfg(not(target_arch = "wasm32"))]
{
assert!(is_main_thread());
assert!(!is_worker_thread());
assert_main_thread("native-main");
assert_not_main_thread("native-main");
}
}
#[kithara::test]
fn park_timeout_returns_after_unpark() {
#[cfg(not(target_arch = "wasm32"))]
{
let parked = current();
let start = Instant::now();
let join = spawn(move || {
sleep(Duration::from_millis(5));
parked.unpark();
});
park_timeout(Duration::from_secs(1));
join.join()
.expect("BUG: wake-helper thread joined cleanly without panicking");
assert!(start.elapsed() < Duration::from_millis(250));
}
}
}