this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Drop the dead Option wrapping on WASM handles

`WasmOpakeHandle::inner: Rc<Mutex<Option<WasmOpake>>>` and
`WasmFileManagerHandle::context: Option<FileContext>` — neither was
ever set to `None` anywhere in the crate. The Option wrappers and the
defensive "handle already consumed" plumbing were leftover from an
earlier design where `.cabinet()` and `.workspace()` consumed the
parent. Both methods are non-consuming now, and `wipeState()` drains
the keepers rather than the Opake itself, so no code path flipped the
Option — every `is_none()` branch and every
`.as_ref().ok_or_else(...)` was defending an unreachable state.

Collapsing:

- Fields: `Rc<Mutex<Option<WasmOpake>>>` → `Rc<Mutex<WasmOpake>>`;
`context: Option<FileContext>` → `context: FileContext`.
- `OpakeGuard`: was a newtype over `MutexGuard<'_, Option<WasmOpake>>`
with hand-rolled `Deref`/`DerefMut` that unwrapped on every access.
Now a type alias over `MutexGuard<'_, WasmOpake>` — the plain
`MutexGuard` already derefs correctly.
- Seven different error strings gone: "Opake context already consumed",
"already consumed", "already finished", "FileManager already
finished", "Opake not available", and the `FileManager context not
available` branches. They were all defending against None states
that can't occur.
- `setIndexerUrl` loses its `Result` — the lock acquisition can't
fail in single-threaded WASM; a `Promise<void>` JS signature is
preserved.

Legitimate error strings stay: the `try_lock()` "Opake is busy" and
"Opake is busy — an operation is in progress" paths signal real
mutex contention, not impossible states. Identity-missing branches
(`no identity`, `identity has no signing key`) stay too — those are
real user-facing conditions on a freshly-paired device.

Net: -130 / +53 lines. Every access path is simpler, and the API
surface is honest about what can and can't fail.

+53 -130
+8 -26
crates/opake-wasm/src/file_manager_wasm.rs
··· 1 1 // WasmFileManagerHandle — file operations within a cabinet or workspace. 2 2 // 3 - // Holds an Rc clone of the parent OpakeContext's async Mutex. 4 - // If the OpakeContext's Option is set to None, FileManager methods 5 - // fail cleanly with "Opake not available". 6 - // 7 - // All async methods lock the Mutex for the duration of the operation. 8 - // Concurrent calls (e.g., upload in-flight + token refresh) queue rather 9 - // than panic — the Mutex handles serialization. 3 + // Holds an Rc clone of the parent OpakeContext's async Mutex over the 4 + // same WasmOpake. All async methods lock the Mutex for the duration of 5 + // the operation; concurrent calls (e.g., upload in-flight + token 6 + // refresh) queue rather than panic. 10 7 // 11 8 // Methods take &self (not &mut self) — the Mutex provides interior 12 9 // mutability, avoiding wasm-bindgen's borrow tracking which panics ··· 29 26 /// WASM FileManager handle. 30 27 #[wasm_bindgen(js_name = FileManager)] 31 28 pub struct WasmFileManagerHandle { 32 - pub(crate) opake: Rc<Mutex<Option<WasmOpake>>>, 29 + pub(crate) opake: Rc<Mutex<WasmOpake>>, 33 30 /// Shared TreeKeeper cloned from the parent OpakeContext. Used for 34 31 /// SSE-driven watcher registration. 35 32 pub(crate) tree_keeper: Rc<Mutex<TreeKeeper>>, 36 - pub(crate) context: Option<FileContext>, 33 + pub(crate) context: FileContext, 37 34 } 38 35 39 36 #[wasm_bindgen(js_class = FileManager)] ··· 461 458 .opake 462 459 .try_lock() 463 460 .ok_or_else(|| JsError::new("Opake is busy — an operation is in progress"))?; 464 - let opake = guard 465 - .as_ref() 466 - .ok_or_else(|| JsError::new("already finished"))?; 467 - let ctx = self 468 - .context 469 - .as_ref() 470 - .ok_or_else(|| JsError::new("already finished"))?; 471 - Ok(opake.did() == ctx.owner_did()) 461 + Ok(guard.did() == self.context.owner_did()) 472 462 } 473 463 474 464 /// Lock the Mutex and return the Opake + FileContext. 475 465 async fn parts(&self) -> Result<(OpakeGuard<'_>, &FileContext), JsError> { 476 - let guard = self.opake.lock().await; 477 - if guard.is_none() { 478 - return Err(JsError::new("Opake not available")); 479 - } 480 - let ctx = self 481 - .context 482 - .as_ref() 483 - .ok_or_else(|| JsError::new("FileManager already finished"))?; 484 - Ok((OpakeGuard(guard), ctx)) 466 + Ok((self.opake.lock().await, &self.context)) 485 467 } 486 468 }
+23 -49
crates/opake-wasm/src/opake_wasm.rs
··· 2 2 // 3 3 // JS callers construct an OpakeContext, then either: 4 4 // - Call workspace management methods directly (createWorkspace, listWorkspaces, etc.) 5 - // - Call .cabinet() or .workspace() to get a FileManager for file operations 5 + // - Call .cabinet() or .workspaceByUri() to get a FileManager for file operations 6 6 // 7 7 // FileManager shares access via Rc<Mutex<>>. Both OpakeContext and 8 - // FileManager hold Rc clones. If the inner Option is set to None, 9 - // FileManager methods fail cleanly. 8 + // FileManager hold Rc clones of the same WasmOpake — the Mutex serializes 9 + // concurrent async operations. 10 10 // 11 11 // The inner Mutex (futures_util::lock::Mutex) replaces RefCell. RefCell 12 12 // panics when borrowed concurrently across async boundaries (the JS event ··· 42 42 43 43 #[wasm_bindgen(js_name = OpakeContext)] 44 44 pub struct WasmOpakeHandle { 45 - pub(crate) inner: Rc<Mutex<Option<WasmOpake>>>, 45 + pub(crate) inner: Rc<Mutex<WasmOpake>>, 46 46 /// Persistent tree state + SSE watcher registry. Held behind its own 47 47 /// Mutex (separate from the Opake mutex) so SSE event application and 48 48 /// file operations don't block each other. ··· 78 78 let opake = make_opake_from_storage(did.as_deref(), storage_adapter).await?; 79 79 let did_owned = opake.did().to_string(); 80 80 Ok(Self { 81 - inner: Rc::new(Mutex::new(Some(opake))), 81 + inner: Rc::new(Mutex::new(opake)), 82 82 tree_keeper: Rc::new(Mutex::new(TreeKeeper::new(did_owned))), 83 83 workspace_keeper: Rc::new(Mutex::new(WorkspaceKeeper::new())), 84 84 inbox_keeper: Rc::new(Mutex::new(InboxKeeper::new())), ··· 89 89 /// Create a cabinet FileManager. Non-consuming — the OpakeContext 90 90 /// remains usable after the FileManager is freed. 91 91 pub async fn cabinet(&self) -> Result<WasmFileManagerHandle, JsError> { 92 - let guard = self.inner.lock().await; 93 - let opake = guard 94 - .as_ref() 95 - .ok_or_else(|| JsError::new("Opake context already consumed"))?; 96 - let context = cabinet_context(opake)?; 97 - drop(guard); 92 + let context = { 93 + let guard = self.inner.lock().await; 94 + cabinet_context(&guard)? 95 + }; 98 96 99 97 Ok(WasmFileManagerHandle { 100 98 opake: Rc::clone(&self.inner), 101 99 tree_keeper: Rc::clone(&self.tree_keeper), 102 - context: Some(context), 100 + context, 103 101 }) 104 102 } 105 103 ··· 124 122 Ok(WasmFileManagerHandle { 125 123 opake: Rc::clone(&self.inner), 126 124 tree_keeper: Rc::clone(&self.tree_keeper), 127 - context: Some(context), 125 + context, 128 126 }) 129 127 } 130 128 ··· 598 596 /// this value via `set_account_config` — so a user-configured 599 597 /// indexer (written via settings) wins over the host default. 600 598 #[wasm_bindgen(js_name = setIndexerUrl)] 601 - pub async fn set_indexer_url(&self, url: String) -> Result<(), JsError> { 602 - let mut guard = self.inner.lock().await; 603 - let opake = guard 604 - .as_mut() 605 - .ok_or_else(|| JsError::new("Opake context already consumed"))?; 606 - opake.set_indexer_url(url); 607 - Ok(()) 599 + pub async fn set_indexer_url(&self, url: String) { 600 + self.inner.lock().await.set_indexer_url(url); 608 601 } 609 602 610 603 /// Verify the session is usable by touching the account config record. ··· 879 872 .inner 880 873 .try_lock() 881 874 .ok_or_else(|| JsError::new("Opake is busy"))?; 882 - let opake = guard 883 - .as_ref() 884 - .ok_or_else(|| JsError::new("already consumed"))?; 885 - Ok(opake.did().to_string()) 875 + Ok(guard.did().to_string()) 886 876 } 887 877 888 878 /// Get the token expiry timestamp without exposing the full session. ··· 899 889 let Some(guard) = self.inner.try_lock() else { 900 890 return -1.0; 901 891 }; 902 - let Some(opake) = guard.as_ref() else { 903 - return -1.0; 904 - }; 905 - opake 892 + guard 906 893 .session() 907 894 .and_then(|s| s.expires_at()) 908 895 .map(|t| t as f64) 909 896 .unwrap_or(-1.0) 910 897 } 911 898 899 + /// Acquire the Opake mutex. 900 + /// 901 + /// Result-returning for API continuity with the callsites that predate 902 + /// the Option removal; the lock acquisition itself cannot fail in 903 + /// single-threaded WASM. 912 904 async fn opake(&self) -> Result<OpakeGuard<'_>, JsError> { 913 - let guard = self.inner.lock().await; 914 - if guard.is_none() { 915 - return Err(JsError::new("Opake context already consumed")); 916 - } 917 - Ok(OpakeGuard(guard)) 905 + Ok(self.inner.lock().await) 918 906 } 919 907 } 920 908 921 - /// Newtype over MutexGuard that derefs to WasmOpake (unwraps the Option). 922 - /// The Option is checked in `opake()` — callers can use this like `&mut WasmOpake`. 923 - pub(crate) struct OpakeGuard<'a>(pub(crate) futures_util::lock::MutexGuard<'a, Option<WasmOpake>>); 924 - 925 - impl std::ops::Deref for OpakeGuard<'_> { 926 - type Target = WasmOpake; 927 - fn deref(&self) -> &WasmOpake { 928 - self.0.as_ref().unwrap() 929 - } 930 - } 931 - 932 - impl std::ops::DerefMut for OpakeGuard<'_> { 933 - fn deref_mut(&mut self) -> &mut WasmOpake { 934 - self.0.as_mut().unwrap() 935 - } 936 - } 909 + /// Shorthand for "locked guard onto the shared WasmOpake." 910 + pub(crate) type OpakeGuard<'a> = futures_util::lock::MutexGuard<'a, WasmOpake>; 937 911 938 912 // FileManager is in file_manager_wasm.rs
+22 -55
crates/opake-wasm/src/sse_wasm.rs
··· 211 211 let cb = js_watcher_callback(callback); 212 212 213 213 // Pick scope based on the FileManager's context. 214 - let handle = match self.context.as_ref() { 215 - Some(opake_core::manager::FileContext::Cabinet(_)) => { 214 + let handle = match &self.context { 215 + opake_core::manager::FileContext::Cabinet(_) => { 216 216 keeper.watch_cabinet(directory_uri, cb) 217 217 } 218 - Some(opake_core::manager::FileContext::Workspace(ws)) => { 218 + opake_core::manager::FileContext::Workspace(ws) => { 219 219 keeper.watch_workspace(ws.uri.clone(), directory_uri, cb) 220 220 } 221 - None => return Err(JsError::new("FileManager context not available")), 222 221 }; 223 222 224 223 Ok(WasmDirectoryWatcher { ··· 236 235 // Fast path: already installed? Check without holding the opake lock. 237 236 { 238 237 let keeper = self.tree_keeper.lock().await; 239 - let already = match self.context.as_ref() { 240 - Some(opake_core::manager::FileContext::Cabinet(_)) => { 238 + let already = match &self.context { 239 + opake_core::manager::FileContext::Cabinet(_) => { 241 240 keeper.cabinet_tree().is_some() 242 241 } 243 - Some(opake_core::manager::FileContext::Workspace(ws)) => { 242 + opake_core::manager::FileContext::Workspace(ws) => { 244 243 keeper.workspace_tree(&ws.uri).is_some() 245 244 } 246 - None => return Err(JsError::new("FileManager context not available")), 247 245 }; 248 246 if already { 249 247 return Ok(()); ··· 253 251 // Slow path: load the tree via the existing FileManager path, then install. 254 252 let (tree, scope) = { 255 253 let mut guard = self.opake.lock().await; 256 - let opake = guard 257 - .as_mut() 258 - .ok_or_else(|| JsError::new("Opake context already consumed"))?; 254 + let context = &self.context; 259 255 260 - let context = self 261 - .context 262 - .as_ref() 263 - .ok_or_else(|| JsError::new("FileManager context not available"))?; 264 - 265 - let mut mgr = opake.file_manager(context); 256 + let mut mgr = guard.file_manager(context); 266 257 let tree = mgr.load_tree().await.map_err(wasm_err)?; 267 258 268 259 // Extract the scope info before dropping mgr + guard. ··· 279 270 match scope { 280 271 TreeInstall::Cabinet => { 281 272 let guard = self.opake.lock().await; 282 - let opake = guard 283 - .as_ref() 284 - .ok_or_else(|| JsError::new("Opake context already consumed"))?; 285 - let identity = opake 273 + let identity = guard 286 274 .identity() 287 275 .ok_or_else(|| JsError::new("no identity"))?; 288 276 let private_key = *identity.private_key_bytes().map_err(wasm_err)?; ··· 336 324 // without a default still surfaces the error cleanly. 337 325 let resolved_url = { 338 326 let mut guard = self.inner.lock().await; 339 - let opake = guard 340 - .as_mut() 341 - .ok_or_else(|| JsError::new("Opake context already consumed"))?; 342 327 if let Some(url) = indexer_url { 343 - opake.set_indexer_url(url); 328 + guard.set_indexer_url(url); 344 329 } 345 - opake.resolve_indexer_url() 330 + guard.resolve_indexer_url() 346 331 }; 347 332 348 333 if self.sse_started.get() { ··· 502 487 503 488 /// Build a token fetcher closure that uses the shared Opake to request 504 489 /// a fresh SSE token on every connect attempt. 505 - fn make_token_fetcher(opake_rc: Rc<Mutex<Option<WasmOpake>>>, indexer_url: String) -> TokenFetcher { 490 + fn make_token_fetcher(opake_rc: Rc<Mutex<WasmOpake>>, indexer_url: String) -> TokenFetcher { 506 491 Box::new(move || { 507 492 let opake_rc = Rc::clone(&opake_rc); 508 493 let indexer_url = indexer_url.clone(); 509 494 Box::pin(async move { 510 495 let guard = opake_rc.lock().await; 511 - let opake = guard 512 - .as_ref() 513 - .ok_or_else(|| opake_core::error::Error::Sse("opake consumed".into()))?; 514 - let did = opake.did().to_string(); 515 - let identity = opake 496 + let did = guard.did().to_string(); 497 + let identity = guard 516 498 .identity() 517 499 .ok_or_else(|| opake_core::error::Error::Sse("no identity".into()))?; 518 500 // Ed25519 signing key — used for indexer auth signatures. ··· 546 528 547 529 /// Schedule a debounced workspace sync. Fire-and-forget: returns before 548 530 /// the task spawns so the SSE consumer loop keeps pulling events. 549 - fn schedule_proposal_sync(opake_rc: Rc<Mutex<Option<WasmOpake>>>, keyring_uri: String) { 531 + fn schedule_proposal_sync(opake_rc: Rc<Mutex<WasmOpake>>, keyring_uri: String) { 550 532 let generation = PROPOSAL_DEBOUNCE_GENERATIONS.with(|state| { 551 533 let mut state = state.borrow_mut(); 552 534 let entry = state.entry(keyring_uri.clone()).or_insert(0); ··· 582 564 583 565 /// Acquire the Opake lock and call `sync_workspace_by_uri`. Called only 584 566 /// from the debounced scheduler — never from the consumer loop directly. 585 - async fn dispatch_proposal_sync(opake_rc: &Rc<Mutex<Option<WasmOpake>>>, keyring_uri: &str) { 567 + async fn dispatch_proposal_sync(opake_rc: &Rc<Mutex<WasmOpake>>, keyring_uri: &str) { 586 568 let mut guard = opake_rc.lock().await; 587 - let Some(opake) = guard.as_mut() else { 588 - log::warn!("[sse] proposal sync: opake unavailable"); 589 - return; 590 - }; 591 - match opake.sync_workspace_by_uri(keyring_uri).await { 569 + match guard.sync_workspace_by_uri(keyring_uri).await { 592 570 Ok(Some(result)) => { 593 571 if result.proposals_applied > 0 { 594 572 log::info!( ··· 643 621 /// keeper acquire; this is benign — the next SSE event or the keeper's 644 622 /// idempotent upsert self-corrects. 645 623 async fn apply_keyring_to_workspace_keeper( 646 - opake_rc: &Rc<Mutex<Option<WasmOpake>>>, 624 + opake_rc: &Rc<Mutex<WasmOpake>>, 647 625 workspace_keeper_rc: &Rc<Mutex<WorkspaceKeeper>>, 648 626 started_flag: &Rc<std::cell::Cell<bool>>, 649 627 event: &SseEvent, ··· 653 631 // Build the entry under the opake lock only. 654 632 let maybe_entry = { 655 633 let guard = opake_rc.lock().await; 656 - let Some(opake) = guard.as_ref() else { 657 - log::warn!("[sse] workspace upsert: opake unavailable"); 658 - return; 659 - }; 660 - let did = opake.did().to_string(); 661 - let Some(identity) = opake.identity() else { 634 + let did = guard.did().to_string(); 635 + let Some(identity) = guard.identity() else { 662 636 log::warn!("[sse] workspace upsert: no identity"); 663 637 return; 664 638 }; ··· 695 669 /// `GrantUpsert`: build an entry filtered by the caller's DID. 696 670 /// `GrantDelete`: delete by URI. Other events are no-ops. 697 671 async fn apply_grant_to_inbox_keeper( 698 - opake_rc: &Rc<Mutex<Option<WasmOpake>>>, 672 + opake_rc: &Rc<Mutex<WasmOpake>>, 699 673 inbox_keeper_rc: &Rc<Mutex<InboxKeeper>>, 700 674 started_flag: &Rc<std::cell::Cell<bool>>, 701 675 event: &SseEvent, ··· 705 679 // Fetch the DID under the opake lock, then drop it before 706 680 // acquiring the keeper lock (matches the workspace keeper 707 681 // pattern — keeps the opake mutex free for SSE throughput). 708 - let my_did = { 709 - let guard = opake_rc.lock().await; 710 - let Some(opake) = guard.as_ref() else { 711 - log::warn!("[sse] inbox upsert: opake unavailable"); 712 - return; 713 - }; 714 - opake.did().to_string() 715 - }; 682 + let my_did = opake_rc.lock().await.did().to_string(); 716 683 let Some(entry) = ik::try_build_entry_from_sse_record(record, &my_did) else { 717 684 // Not for us — silently drop. 718 685 return;