this repo has no description
1
fork

Configure Feed

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

Reorganize indexer-shaped modules under opake-core/src/indexer/

Move everything that depends on the hosted indexer service into a single
namespace. Pure code motion + import path updates; no API behavior change.

Module moves:
- crates/opake-core/src/client/indexer.rs → indexer/client.rs
- crates/opake-core/src/client/indexer_auth.rs → indexer/auth.rs
- crates/opake-core/src/client/indexer_types.rs → indexer/types.rs
- crates/opake-core/src/sse/ → indexer/sse/
- crates/opake-core/src/daemon.rs → indexer/daemon.rs
- crates/opake-core/src/tree_keeper/ → indexer/tree_keeper/
- crates/opake-core/src/workspace_keeper/ → indexer/workspace_keeper/
- crates/opake-core/src/inbox_keeper/ → indexer/inbox_keeper/

New indexer/mod.rs declares the submodules and re-exports common items
(fetch_inbox, sign_indexer_request, TreeDelta, etc.) so callers use short
paths like opake_core::indexer::fetch_inbox instead of
opake_core::indexer::client::fetch_inbox.

Rationale: opake-core had accumulated a second job — ~5,000 lines of the
~10,000 total were indexer-coupled (HTTP client, SSE consumer, live state
stores). Co-locating them under indexer/ makes the architectural split
legible without crate-level extraction. A future crate split becomes
mechanical; a third-party that only needs crypto + records + XRPC primitives
can opt out at that point.

Keepers (tree_keeper, workspace_keeper, inbox_keeper) moved with the indexer
because their only patch sources are SSE dispatch and indexer list-endpoint
bootstraps — the core FileManager has no Keeper references (it uses the
Storage trait for caching), and the CLI explicitly doesn't use Keepers
(daemon/mod.rs: "The CLI has no TreeKeeper").

Build verified: cargo check --workspace --all-targets clean; cargo test
556 pass; cargo check -p opake-wasm --target wasm32-unknown-unknown clean;
regenerated wasm; @opake/sdk + @opake/react builds + tests clean; apps/web
tsc clean.

+194 -150
+1
.envrc
··· 5 5 # Indexer URL for CLI commands 6 6 # Production: https://indexer.opake.app 7 7 export OPAKE_INDEXER_URL=http://localhost:6100 8 + export VITE_INDEXER_URL=$OPAKE_INDEXER_URL
+5 -5
apps/cli/src/commands/daemon/mod.rs
··· 8 8 use log::{info, warn}; 9 9 use opake_core::client::ReqwestTransport; 10 10 use opake_core::crypto::{OsRng, RngCore}; 11 - use opake_core::daemon::{self, TASKS}; 11 + use opake_core::indexer::daemon::{self, TASKS}; 12 + use opake_core::indexer::sse::consumer::{JitterRng, SleepFn, SseConsumer, TokenFetcher}; 13 + use opake_core::indexer::sse::events::SseEvent; 14 + use opake_core::indexer::sse::reqwest_connection::ReqwestSseTransport; 12 15 use opake_core::opake::Opake; 13 - use opake_core::sse::consumer::{JitterRng, SleepFn, SseConsumer, TokenFetcher}; 14 - use opake_core::sse::events::SseEvent; 15 - use opake_core::sse::reqwest_connection::ReqwestSseTransport; 16 16 use tokio::sync::{Mutex, Notify}; 17 17 use tokio::task::LocalSet; 18 18 ··· 424 424 // --------------------------------------------------------------------------- 425 425 426 426 async fn list_tasks(storage: &FileStorage) -> Result<()> { 427 - use opake_core::daemon::{DaemonTaskKind, TaskStatus}; 427 + use opake_core::indexer::daemon::{DaemonTaskKind, TaskStatus}; 428 428 429 429 let tasks = storage.load_tasks(); 430 430
+2 -1
apps/cli/src/commands/inbox.rs
··· 1 1 use anyhow::Result; 2 2 use clap::Args; 3 - use opake_core::client::{InboxGrant, Session}; 3 + use opake_core::client::Session; 4 + use opake_core::indexer::InboxGrant; 4 5 5 6 use crate::commands::Execute; 6 7 use crate::session::CommandContext;
+3 -3
apps/cli/src/config.rs
··· 135 135 // -- Task helpers (standalone, not on the Storage trait) -------------------- 136 136 137 137 #[allow(dead_code)] // wired in daemon migration (Phase 1) 138 - pub fn save_task(&self, task: &opake_core::daemon::DaemonTask) -> anyhow::Result<()> { 138 + pub fn save_task(&self, task: &opake_core::indexer::daemon::DaemonTask) -> anyhow::Result<()> { 139 139 let tasks_path = self.base_dir.join("tasks.json"); 140 140 let mut tasks = self.load_tasks_inner(); 141 141 tasks.retain(|t| t.id != task.id); ··· 145 145 Ok(()) 146 146 } 147 147 148 - pub fn load_tasks(&self) -> Vec<opake_core::daemon::DaemonTask> { 148 + pub fn load_tasks(&self) -> Vec<opake_core::indexer::daemon::DaemonTask> { 149 149 self.load_tasks_inner() 150 150 } 151 151 ··· 159 159 Ok(()) 160 160 } 161 161 162 - fn load_tasks_inner(&self) -> Vec<opake_core::daemon::DaemonTask> { 162 + fn load_tasks_inner(&self) -> Vec<opake_core::indexer::daemon::DaemonTask> { 163 163 let tasks_path = self.base_dir.join("tasks.json"); 164 164 match fs::read_to_string(&tasks_path) { 165 165 Ok(json) => serde_json::from_str(&json).unwrap_or_default(),
+10 -10
crates/opake-core/src/client/indexer.rs crates/opake-core/src/indexer/client.rs
··· 3 3 // Uses the Transport trait for WASM compatibility. Signs each request 4 4 // with the caller's Ed25519 key via sign_indexer_request. 5 5 6 - use crate::client::indexer_auth::sign_indexer_request; 7 - use crate::client::indexer_types::{ 6 + use crate::client::{HttpMethod, HttpRequest, Transport}; 7 + use crate::error::Error; 8 + use crate::indexer::auth::sign_indexer_request; 9 + use crate::indexer::types::{ 8 10 InboxGrant, InboxResponse, KeyringsResponse, TreeDelta, WorkspaceDocument, WorkspaceResponse, 9 11 }; 10 - use crate::client::transport::{HttpMethod, HttpRequest, Transport}; 11 - use crate::error::Error; 12 12 13 13 /// Check an indexer JSON response for errors. 14 14 fn check_indexer_response(status: u16, body: &[u8]) -> Result<(), Error> { ··· 39 39 cursor: Option<&str>, 40 40 ) -> Result<InboxResponse, Error> { 41 41 let path = "/api/inbox"; 42 - let timestamp = super::time::unix_now() as u64; 42 + let timestamp = crate::client::time::unix_now() as u64; 43 43 let auth = sign_indexer_request("GET", path, did, signing_key, timestamp); 44 44 45 45 let mut params = Vec::new(); ··· 118 118 let mut cursor: Option<String> = None; 119 119 120 120 loop { 121 - let timestamp = super::time::unix_now() as u64; 121 + let timestamp = crate::client::time::unix_now() as u64; 122 122 let auth = sign_indexer_request("GET", path, did, signing_key, timestamp); 123 123 124 124 let mut query = format!("keyringUri={keyring_uri}"); ··· 167 167 let mut cursor: Option<String> = None; 168 168 169 169 loop { 170 - let timestamp = super::time::unix_now() as u64; 170 + let timestamp = crate::client::time::unix_now() as u64; 171 171 let auth = sign_indexer_request("GET", path, did, signing_key, timestamp); 172 172 173 173 let url = match &cursor { ··· 217 217 signing_key: &[u8; 32], 218 218 query: &str, 219 219 ) -> Result<Vec<u8>, Error> { 220 - let timestamp = super::time::unix_now() as u64; 220 + let timestamp = crate::client::time::unix_now() as u64; 221 221 let auth = sign_indexer_request("GET", path, did, signing_key, timestamp); 222 222 let url = if query.is_empty() { 223 223 format!("{indexer_url}{path}") ··· 342 342 signing_key: &[u8; 32], 343 343 ) -> Result<String, Error> { 344 344 let path = "/api/events/token"; 345 - let timestamp = super::time::unix_now() as u64; 345 + let timestamp = crate::client::time::unix_now() as u64; 346 346 let auth = sign_indexer_request("POST", path, did, signing_key, timestamp); 347 347 348 348 let request = HttpRequest { ··· 372 372 #[cfg(test)] 373 373 mod tests { 374 374 use super::*; 375 - use crate::client::transport::HttpResponse; 375 + use crate::client::HttpResponse; 376 376 use crate::test_utils::MockTransport; 377 377 378 378 fn inbox_json(grants: &[&str], cursor: Option<&str>) -> Vec<u8> {
crates/opake-core/src/client/indexer_auth.rs crates/opake-core/src/indexer/auth.rs
+1 -1
crates/opake-core/src/client/indexer_types.rs crates/opake-core/src/indexer/types.rs
··· 210 210 /// next sync passes this value back as `since`, and the server handles 211 211 /// the ISO8601 format. The millis value is just a local staleness marker. 212 212 pub fn fetched_at_millis(&self) -> u64 { 213 - super::time::unix_now_millis() 213 + crate::client::time::unix_now_millis() 214 214 } 215 215 216 216 /// The server timestamp to pass as `since` on the next sync request.
-6
crates/opake-core/src/client/mod.rs
··· 2 2 #[cfg(feature = "dns")] 3 3 mod dns; 4 4 pub mod dpop; 5 - mod indexer; 6 - mod indexer_auth; 7 - mod indexer_types; 8 5 mod list; 9 6 pub mod oauth_discovery; 10 7 pub mod oauth_token; ··· 20 17 pub use did::*; 21 18 #[cfg(feature = "dns")] 22 19 pub use dns::resolve_handle_dns; 23 - pub use indexer::*; 24 - pub use indexer_auth::*; 25 - pub use indexer_types::*; 26 20 pub use list::*; 27 21 #[cfg(feature = "reqwest-transport")] 28 22 pub use reqwest_transport::ReqwestTransport;
crates/opake-core/src/daemon.rs crates/opake-core/src/indexer/daemon.rs
+2 -2
crates/opake-core/src/directories/tree.rs
··· 9 9 use log::trace; 10 10 11 11 use crate::atproto; 12 - use crate::client::TreeDirectory; 13 12 use crate::crypto::{self, ContentKey, DirectoryMetadata, X25519PrivateKey}; 14 13 use crate::documents::DOCUMENT_COLLECTION; 15 14 use crate::error::Error; 15 + use crate::indexer::sse::events::SseDirectoryRecord; 16 + use crate::indexer::TreeDirectory; 16 17 use crate::records::{Directory, EncryptedMetadata, KeyWrapping}; 17 - use crate::sse::events::SseDirectoryRecord; 18 18 use crate::storage::CachedRecord; 19 19 20 20 use super::{DIRECTORY_COLLECTION, ROOT_DIRECTORY_NAME, ROOT_DIRECTORY_RKEY};
+1 -1
crates/opake-core/src/directories/tree_tests.rs
··· 597 597 // Incremental mutation via apply_directory_delta (SSE path) 598 598 // --------------------------------------------------------------------------- 599 599 600 - use crate::sse::events::SseDirectoryRecord; 600 + use crate::indexer::sse::events::SseDirectoryRecord; 601 601 602 602 /// Convert a test `Directory` into the SSE payload shape. Mirrors how the 603 603 /// indexer broadcaster formats directory records — `key_wrapping` and
+4 -4
crates/opake-core/src/inbox_keeper/mod.rs crates/opake-core/src/indexer/inbox_keeper/mod.rs
··· 21 21 //! cross-PDS fetch via [`Opake::resolve_grant_metadata`], but that's 22 22 //! the consumer's job, not the keeper's. 23 23 //! 24 - //! [`WorkspaceKeeper`]: crate::workspace_keeper::WorkspaceKeeper 24 + //! [`WorkspaceKeeper`]: crate::indexer::workspace_keeper::WorkspaceKeeper 25 25 //! [`Opake::resolve_grant_metadata`]: crate::opake::Opake::resolve_grant_metadata 26 26 27 27 use std::collections::HashMap; ··· 201 201 /// where the caller is NOT the recipient (the broadcaster already 202 202 /// routes by DID topic, but defense-in-depth is cheap here). 203 203 pub fn try_build_entry_from_sse_record( 204 - record: &crate::sse::events::SseGrantRecord, 204 + record: &crate::indexer::sse::events::SseGrantRecord, 205 205 recipient_did: &str, 206 206 ) -> Option<InboxEntry> { 207 207 // If the grant event carries an explicit recipient, verify it matches. ··· 222 222 223 223 /// Convenience wrapper: build an entry from an indexer [`InboxGrant`]. 224 224 /// 225 - /// [`InboxGrant`]: crate::client::InboxGrant 226 - pub fn entry_from_indexer_grant(grant: &crate::client::InboxGrant) -> InboxEntry { 225 + /// [`InboxGrant`]: crate::indexer::InboxGrant 226 + pub fn entry_from_indexer_grant(grant: &crate::indexer::InboxGrant) -> InboxEntry { 227 227 InboxEntry { 228 228 uri: grant.uri.clone(), 229 229 owner_did: grant.owner_did.clone(),
+2 -2
crates/opake-core/src/inbox_keeper/tests.rs crates/opake-core/src/indexer/inbox_keeper/tests.rs
··· 149 149 150 150 #[test] 151 151 fn try_build_entry_filters_non_recipient() { 152 - let record = crate::sse::events::SseGrantRecord { 152 + let record = crate::indexer::sse::events::SseGrantRecord { 153 153 uri: "at://a/app.opake.grant/g1".to_string(), 154 154 owner_did: "did:plc:alice".to_string(), 155 155 recipient_did: Some("did:plc:bob".to_string()), ··· 165 165 166 166 #[test] 167 167 fn try_build_entry_defaults_created_at_when_absent() { 168 - let record = crate::sse::events::SseGrantRecord { 168 + let record = crate::indexer::sse::events::SseGrantRecord { 169 169 uri: "at://a/app.opake.grant/g1".to_string(), 170 170 owner_did: "did:plc:alice".to_string(), 171 171 recipient_did: None,
+30
crates/opake-core/src/indexer/mod.rs
··· 1 + // Indexer client, SSE consumer, and live state keepers. 2 + // 3 + // This module is the boundary between opake-core's domain primitives and 4 + // the hosted Opake indexer service. Everything here depends on the indexer 5 + // being reachable over HTTP + SSE. Consumers that only need local PDS 6 + // operations (crypto, records, XRPC, FileManager) can ignore this module. 7 + // 8 + // Submodule layout: 9 + // - `client` — HTTP JSON API (inbox, workspace discovery, sync) 10 + // - `auth` — Opake-Ed25519 request signing 11 + // - `types` — response shapes from the JSON API 12 + // - `sse` — live event stream consumer (reconnect, parser, transport) 13 + // - `daemon` — long-lived consumer loop entry points 14 + // - `tree_keeper`, `workspace_keeper`, `inbox_keeper` — in-memory state 15 + // stores patched by SSE events + bootstrapped from list endpoints 16 + 17 + pub mod auth; 18 + pub mod client; 19 + pub mod daemon; 20 + pub mod inbox_keeper; 21 + pub mod sse; 22 + pub mod tree_keeper; 23 + pub mod types; 24 + pub mod workspace_keeper; 25 + 26 + // Convenience re-exports so callers can say `opake_core::indexer::fetch_inbox` 27 + // instead of `opake_core::indexer::client::fetch_inbox`. 28 + pub use auth::*; 29 + pub use client::*; 30 + pub use types::*;
+2 -2
crates/opake-core/src/keyrings/mod.rs
··· 62 62 /// Returns `None` if the DID isn't a member, deserialization fails, 63 63 /// unwrapping fails, or metadata decryption fails. 64 64 pub fn decrypt_indexer_keyring_name( 65 - keyring: &crate::client::IndexerKeyring, 65 + keyring: &crate::indexer::IndexerKeyring, 66 66 did: &str, 67 67 private_key: &X25519PrivateKey, 68 68 ) -> Option<String> { ··· 87 87 #[cfg(test)] 88 88 mod indexer_keyring_tests { 89 89 use super::*; 90 - use crate::client::IndexerKeyring; 91 90 use crate::crypto::{ 92 91 generate_content_key, wrap_key, OsRng, X25519DalekPublicKey, X25519DalekStaticSecret, 93 92 X25519PrivateKey, X25519PublicKey, 94 93 }; 94 + use crate::indexer::IndexerKeyring; 95 95 use crate::records::KeyringMember; 96 96 97 97 fn test_keypair() -> (X25519PublicKey, X25519PrivateKey) {
+1 -5
crates/opake-core/src/lib.rs
··· 25 25 pub mod cabinet; 26 26 pub mod client; 27 27 pub mod crypto; 28 - pub mod daemon; 29 28 pub mod directories; 30 29 pub mod documents; 31 30 pub mod error; 32 - pub mod inbox_keeper; 31 + pub mod indexer; 33 32 pub mod keyrings; 34 33 pub mod manager; 35 34 pub mod metadata; ··· 41 40 pub mod resolve; 42 41 pub mod scope; 43 42 pub mod sharing; 44 - pub mod sse; 45 43 pub mod storage; 46 44 pub mod tid; 47 - pub mod tree_keeper; 48 45 pub mod workspace; 49 - pub mod workspace_keeper; 50 46 51 47 #[cfg(any(test, feature = "test-utils"))] 52 48 pub mod test_utils;
+1 -1
crates/opake-core/src/manager/mod.rs
··· 26 26 }; 27 27 28 28 use crate::client::Transport; 29 - use crate::client::{DocumentProposal, KeyringProposal, TreeProposal}; 30 29 use crate::crypto::{CryptoRng, RngCore}; 30 + use crate::indexer::{DocumentProposal, KeyringProposal, TreeProposal}; 31 31 use crate::opake::Opake; 32 32 use crate::storage::Storage; 33 33
+14 -19
crates/opake-core/src/manager/tree.rs
··· 4 4 5 5 use crate::atproto; 6 6 use crate::client::Transport; 7 - use crate::client::TreeDelta; 8 7 use crate::crypto::{self, CryptoRng, RngCore}; 9 8 use crate::directories::{DirectoryTree, EntryKind, ResolvedPath}; 10 9 use crate::documents::DOCUMENT_COLLECTION; 11 10 use crate::error::Error; 11 + use crate::indexer::TreeDelta; 12 12 use crate::records::{Document, Encryption}; 13 13 use crate::storage::{CachedCollection, CachedRecord, Storage}; 14 14 ··· 390 390 async fn try_sync_deltas( 391 391 &self, 392 392 cached: CachedCollection, 393 - ) -> Result<(Vec<CachedRecord>, Vec<crate::client::TreeProposal>), Error> { 394 - let indexer_url = match &self.opake.indexer_url { 395 - Some(url) => url.clone(), 396 - None => return Ok((cached.records, Vec::new())), 393 + ) -> Result<(Vec<CachedRecord>, Vec<crate::indexer::TreeProposal>), Error> { 394 + let indexer_url = match self.opake.resolve_indexer_url(None) { 395 + Ok(url) => url, 396 + Err(_) => return Ok((cached.records, Vec::new())), 397 397 }; 398 398 let signing_key = match self.opake.require_identity() { 399 399 Ok(id) => match id.signing_key_bytes() { ··· 413 413 let delta_result = match self.context { 414 414 FileContext::Cabinet(_) => match since { 415 415 Some(s) => { 416 - crate::client::fetch_cabinet_sync( 416 + crate::indexer::fetch_cabinet_sync( 417 417 self.opake.client.transport(), 418 418 &indexer_url, 419 419 &self.opake.did, ··· 423 423 .await 424 424 } 425 425 None => { 426 - crate::client::fetch_cabinet_snapshot( 426 + crate::indexer::fetch_cabinet_snapshot( 427 427 self.opake.client.transport(), 428 428 &indexer_url, 429 429 &self.opake.did, ··· 434 434 }, 435 435 FileContext::Workspace(ws) => match since { 436 436 Some(s) => { 437 - crate::client::fetch_workspace_sync( 437 + crate::indexer::fetch_workspace_sync( 438 438 self.opake.client.transport(), 439 439 &indexer_url, 440 440 &self.opake.did, ··· 445 445 .await 446 446 } 447 447 None => { 448 - crate::client::fetch_workspace_snapshot( 448 + crate::indexer::fetch_workspace_snapshot( 449 449 self.opake.client.transport(), 450 450 &indexer_url, 451 451 &self.opake.did, ··· 542 542 /// Fetches a full snapshot from the Indexer, caches it locally, 543 543 /// and returns a tree built from the cached records. 544 544 async fn bootstrap_tree(&mut self) -> Result<DirectoryTree, Error> { 545 - let indexer_url = self 546 - .opake 547 - .indexer_url 548 - .as_ref() 549 - .ok_or_else(|| { 550 - Error::Storage("Indexer URL required — configure one or self-host".into()) 551 - })? 552 - .clone(); 545 + let indexer_url = self.opake.resolve_indexer_url(None).map_err(|_| { 546 + Error::Storage("Indexer URL required — configure one or self-host".into()) 547 + })?; 553 548 554 549 let identity = self.opake.require_identity()?; 555 550 let signing_key = identity ··· 558 553 559 554 let snapshot = match self.context { 560 555 FileContext::Cabinet(_) => { 561 - crate::client::fetch_cabinet_snapshot( 556 + crate::indexer::fetch_cabinet_snapshot( 562 557 self.opake.client.transport(), 563 558 &indexer_url, 564 559 &self.opake.did, ··· 567 562 .await? 568 563 } 569 564 FileContext::Workspace(ws) => { 570 - crate::client::fetch_workspace_snapshot( 565 + crate::indexer::fetch_workspace_snapshot( 571 566 self.opake.client.transport(), 572 567 &indexer_url, 573 568 &self.opake.did,
+68 -42
crates/opake-core/src/opake.rs
··· 39 39 pub(crate) storage: S, 40 40 pub(crate) now_fn: fn() -> String, 41 41 pub(crate) now_micros_fn: fn() -> u64, 42 - /// Cached indexer URL from account config (fetched once at construction). 43 - pub(crate) indexer_url: Option<String>, 42 + /// Host-set runtime override — highest priority. Populated via 43 + /// `set_indexer_url` at boot (CLI: `OPAKE_INDEXER_URL` env var; 44 + /// web: `VITE_INDEXER_URL`). Lets devs point at localhost regardless 45 + /// of what's on PDS. 46 + pub(crate) runtime_indexer_url: Option<String>, 47 + /// User-configured indexer URL, mirrored from 48 + /// `accountConfig.indexerUrl` on PDS. Second priority; falls back to 49 + /// `DEFAULT_INDEXER_URL` when unset. 50 + pub(crate) config_indexer_url: Option<String>, 44 51 } 45 52 46 53 /// Indexer URL baked into the binary at compile time. ··· 70 77 storage, 71 78 now_fn: now, 72 79 now_micros_fn: now_micros, 73 - indexer_url: None, 80 + runtime_indexer_url: None, 81 + config_indexer_url: None, 74 82 } 75 83 } 76 84 ··· 123 131 }; 124 132 125 133 let client = XrpcClient::with_session(transport, account.pds_url.clone(), session); 126 - let mut opake = Self::new(client, target_did, identity, rng, storage, now, now_micros); 127 - opake.indexer_url = Some(DEFAULT_INDEXER_URL.to_string()); 134 + let opake = Self::new(client, target_did, identity, rng, storage, now, now_micros); 135 + // Indexer URL resolution happens lazily in `resolve_indexer_url`: 136 + // runtime override → PDS config → compile-time default. No seeding 137 + // needed here — the priority chain has a const fallback. 128 138 Ok(opake) 129 139 } 130 140 ··· 464 474 /// failing workspace doesn't block the rest. 465 475 pub async fn sync_owned_workspaces_detailed( 466 476 &mut self, 467 - ) -> Result<Vec<crate::daemon::WorkspaceSyncResult>, Error> { 477 + ) -> Result<Vec<crate::indexer::daemon::WorkspaceSyncResult>, Error> { 468 478 log::trace!("sync: discovering workspaces for {}", self.did); 469 479 let indexer_keyrings = self.discover_member_keyrings(None).await?; 470 480 log::trace!("sync: found {} workspaces", indexer_keyrings.len()); ··· 488 498 pub async fn sync_workspace_by_uri( 489 499 &mut self, 490 500 keyring_uri: &str, 491 - ) -> Result<Option<crate::daemon::WorkspaceSyncResult>, Error> { 501 + ) -> Result<Option<crate::indexer::daemon::WorkspaceSyncResult>, Error> { 492 502 let indexer_keyrings = self.discover_member_keyrings(None).await?; 493 503 let target = indexer_keyrings.iter().find(|kr| kr.uri == keyring_uri); 494 504 let Some(kr) = target else { return Ok(None) }; ··· 506 516 /// propagating — the caller can continue with remaining workspaces. 507 517 async fn sync_single_workspace( 508 518 &mut self, 509 - kr: &crate::client::IndexerKeyring, 519 + kr: &crate::indexer::IndexerKeyring, 510 520 private_key: &crate::crypto::X25519PrivateKey, 511 - ) -> crate::daemon::WorkspaceSyncResult { 512 - use crate::daemon::WorkspaceSyncResult; 521 + ) -> crate::indexer::daemon::WorkspaceSyncResult { 522 + use crate::indexer::daemon::WorkspaceSyncResult; 513 523 514 524 let is_owner = kr.owner_did == self.did; 515 525 log::trace!("sync: processing {} (owner={is_owner})", kr.uri); ··· 620 630 async fn apply_keyring_proposals( 621 631 &mut self, 622 632 keyring_uri: &str, 623 - proposals: &[crate::client::KeyringProposal], 633 + proposals: &[crate::indexer::KeyringProposal], 624 634 ) -> Result<usize, Error> { 625 635 use crate::crypto; 626 636 use crate::records::{self, keyring_update}; ··· 817 827 /// Only the workspace owner processes these — they own the document records. 818 828 async fn apply_document_proposals( 819 829 &mut self, 820 - proposals: &[crate::client::DocumentProposal], 830 + proposals: &[crate::indexer::DocumentProposal], 821 831 ) -> Result<usize, Error> { 822 832 let mut applied = 0; 823 833 ··· 861 871 /// re-hosts it on the owner's PDS, and updates the document record. 862 872 async fn apply_single_document_proposal( 863 873 &mut self, 864 - proposal: &crate::client::DocumentProposal, 874 + proposal: &crate::indexer::DocumentProposal, 865 875 ) -> Result<(), Error> { 866 876 use crate::client::get_record_public; 867 877 use crate::records; ··· 945 955 /// just skips them on re-processing. 946 956 async fn cleanup_own_applied_keyring_proposals( 947 957 &mut self, 948 - proposals: &[crate::client::KeyringProposal], 958 + proposals: &[crate::indexer::KeyringProposal], 949 959 member_dids: &std::collections::HashSet<&str>, 950 960 ) -> usize { 951 961 use crate::client::ApplyWriteOp; ··· 1394 1404 1395 1405 // -- Indexer helpers -- 1396 1406 1397 - /// Override the Indexer URL unconditionally (runtime env var override). 1407 + /// Set the host-level runtime override for the indexer URL. 1408 + /// 1409 + /// Highest priority in `resolve_indexer_url`. Hosts call this at boot 1410 + /// to inject a deployment-specific URL (CLI reads `OPAKE_INDEXER_URL`; 1411 + /// web reads `VITE_INDEXER_URL`). Runtime overrides win over PDS 1412 + /// account config — dev builds pointing at localhost keep working 1413 + /// even when the account's PDS config points at prod. 1398 1414 pub fn set_indexer_url(&mut self, url: String) { 1399 - self.indexer_url = Some(url); 1415 + self.runtime_indexer_url = Some(url); 1400 1416 } 1401 1417 1402 - /// Resolve the indexer URL: cached config → caller default → error. 1403 1418 /// Resolve the indexer URL to use for a request. 1404 1419 /// 1405 - /// Returns the URL stored on this Opake instance (loaded from config 1406 - /// during `init`), falling back to the provided `default` if no URL 1407 - /// is stored. Returns `NotFound` if neither source has a URL. 1420 + /// Priority (highest first): 1421 + /// 1. Runtime override (`set_indexer_url`) 1422 + /// 2. PDS account config (`config_indexer_url`, mirrored from 1423 + /// `accountConfig.indexerUrl`) 1424 + /// 3. Caller-provided fallback (legacy arg, usually `None`) 1425 + /// 4. Compile-time `DEFAULT_INDEXER_URL` 1408 1426 /// 1409 - /// This is the shared helper behind every indexer-touching method 1410 - /// (`request_sse_token`, `list_inbox`, `discover_member_keyrings`, 1411 - /// etc.) and also used by WASM bindings that need to auto-resolve 1412 - /// the URL before starting long-lived tasks like the SSE consumer. 1427 + /// Always resolves — the compile-time default is a non-empty const, 1428 + /// so the `Result` shape is preserved for API compatibility but the 1429 + /// `NotFound` arm is unreachable under normal operation. 1413 1430 pub fn resolve_indexer_url(&self, default: Option<&str>) -> Result<String, Error> { 1414 - self.indexer_url 1431 + self.runtime_indexer_url 1415 1432 .clone() 1433 + .or_else(|| self.config_indexer_url.clone()) 1416 1434 .or_else(|| default.map(|s| s.to_string())) 1417 - .ok_or_else(|| { 1418 - Error::NotFound( 1419 - "no indexer URL — set VITE_INDEXER_URL or configure in settings".into(), 1420 - ) 1421 - }) 1435 + .or_else(|| Some(DEFAULT_INDEXER_URL.to_string())) 1436 + .ok_or_else(|| Error::NotFound("no indexer URL configured".into())) 1422 1437 } 1423 1438 1424 1439 // -- SSE token (for EventSource auth) -- ··· 1437 1452 .ok_or_else(|| Error::Auth("no signing key for SSE token request".into()))?; 1438 1453 1439 1454 let url = self.resolve_indexer_url(default_indexer_url)?; 1440 - crate::client::request_sse_token(self.client.transport(), &url, &self.did, &signing_key) 1455 + crate::indexer::request_sse_token(self.client.transport(), &url, &self.did, &signing_key) 1441 1456 .await 1442 1457 } 1443 1458 ··· 1449 1464 pub async fn list_inbox( 1450 1465 &mut self, 1451 1466 default_indexer_url: Option<&str>, 1452 - ) -> Result<Vec<crate::client::InboxGrant>, Error> { 1467 + ) -> Result<Vec<crate::indexer::InboxGrant>, Error> { 1453 1468 let identity = match self.identity.as_ref() { 1454 1469 Some(id) => id, 1455 1470 None => return Ok(vec![]), ··· 1461 1476 1462 1477 let url = self.resolve_indexer_url(default_indexer_url)?; 1463 1478 1464 - crate::client::fetch_inbox_all(self.client.transport(), &url, &self.did, &signing_key).await 1479 + crate::indexer::fetch_inbox_all(self.client.transport(), &url, &self.did, &signing_key) 1480 + .await 1465 1481 } 1466 1482 1467 1483 /// Fetch workspace documents from the Indexer. ··· 1471 1487 &mut self, 1472 1488 keyring_uri: &str, 1473 1489 default_indexer_url: Option<&str>, 1474 - ) -> Result<Vec<crate::client::WorkspaceDocument>, Error> { 1490 + ) -> Result<Vec<crate::indexer::WorkspaceDocument>, Error> { 1475 1491 let identity = match self.identity.as_ref() { 1476 1492 Some(id) => id, 1477 1493 None => return Ok(vec![]), ··· 1483 1499 1484 1500 let url = self.resolve_indexer_url(default_indexer_url)?; 1485 1501 1486 - crate::client::fetch_workspace_documents( 1502 + crate::indexer::fetch_workspace_documents( 1487 1503 self.client.transport(), 1488 1504 &url, 1489 1505 &self.did, ··· 1499 1515 pub async fn discover_member_keyrings( 1500 1516 &mut self, 1501 1517 default_indexer_url: Option<&str>, 1502 - ) -> Result<Vec<crate::client::IndexerKeyring>, Error> { 1518 + ) -> Result<Vec<crate::indexer::IndexerKeyring>, Error> { 1503 1519 let identity = match self.identity.as_ref() { 1504 1520 Some(id) => id, 1505 1521 None => return Ok(vec![]), ··· 1511 1527 1512 1528 let url = self.resolve_indexer_url(default_indexer_url)?; 1513 1529 1514 - crate::client::fetch_member_keyrings(self.client.transport(), &url, &self.did, &signing_key) 1515 - .await 1530 + crate::indexer::fetch_member_keyrings( 1531 + self.client.transport(), 1532 + &url, 1533 + &self.did, 1534 + &signing_key, 1535 + ) 1536 + .await 1516 1537 } 1517 1538 1518 1539 // -- Sharing (pending shares) -- ··· 1571 1592 } 1572 1593 1573 1594 /// Write the account config record (upsert). 1595 + /// 1596 + /// Mirrors `config.indexer_url` into `self.config_indexer_url` — this is 1597 + /// the source-of-truth field for the PDS-configured URL. Host-level 1598 + /// overrides set via `set_indexer_url` live in a separate field and 1599 + /// continue to win priority in `resolve_indexer_url`, so a dev pointing 1600 + /// `VITE_INDEXER_URL` at localhost isn't disturbed by account-config 1601 + /// writes that happen to include `indexerUrl: None` (e.g. `check_session` 1602 + /// touching `modified_at` on a fresh record). 1574 1603 pub async fn set_account_config( 1575 1604 &mut self, 1576 1605 config: &crate::records::AccountConfigRecord, 1577 1606 ) -> Result<String, Error> { 1578 - // Propagate whatever the user last committed to their PDS — including 1579 - // explicit clears (None). The compile-time default is seeded separately 1580 - // via `set_indexer_url` and lives below this in priority order. 1581 - self.indexer_url = config.indexer_url.clone(); 1607 + self.config_indexer_url = config.indexer_url.clone(); 1582 1608 let result = crate::account_config::publish_account_config(&mut self.client, config).await; 1583 1609 self.signoff(result).await 1584 1610 }
+2 -1
crates/opake-core/src/opake_tests.rs
··· 1 1 use super::*; 2 - use crate::client::{HttpResponse, KeyringProposal, LegacySession, Session, XrpcClient}; 2 + use crate::client::{HttpResponse, LegacySession, Session, XrpcClient}; 3 3 use crate::crypto::{ 4 4 self, generate_content_key, DidMember, KeyringMetadata, OsRng, X25519DalekPublicKey, 5 5 X25519DalekStaticSecret, 6 6 }; 7 + use crate::indexer::KeyringProposal; 7 8 use crate::records::{keyring_update, Keyring, KeyringMember}; 8 9 use crate::storage::{Identity, NoopStorage}; 9 10 use crate::test_utils::MockTransport;
+1 -1
crates/opake-core/src/reencryption.rs
··· 11 11 use crate::atproto; 12 12 use crate::client::Transport; 13 13 use crate::crypto::{self, ContentKey}; 14 - use crate::daemon::REENCRYPTION_BATCH_SIZE_BYTES; 15 14 use crate::error::Error; 15 + use crate::indexer::daemon::REENCRYPTION_BATCH_SIZE_BYTES; 16 16 use crate::records; 17 17 18 18 const DOCUMENT_COLLECTION: &str = "app.opake.document";
+5 -5
crates/opake-core/src/sse/consumer.rs crates/opake-core/src/indexer/sse/consumer.rs
··· 19 19 use std::time::Duration; 20 20 21 21 use crate::error::Error; 22 - use crate::sse::events::SseEvent; 23 - use crate::sse::reconnect::BackoffPolicy; 24 - use crate::sse::transport::{SseConnection, SseTransport}; 22 + use crate::indexer::sse::events::SseEvent; 23 + use crate::indexer::sse::reconnect::BackoffPolicy; 24 + use crate::indexer::sse::transport::{SseConnection, SseTransport}; 25 25 26 26 /// A future-returning token fetcher. Called before every connect attempt. 27 27 /// Own-your-captures: the closure holds clones of whatever state it needs ··· 187 187 #[cfg(test)] 188 188 mod tests { 189 189 use super::*; 190 - use crate::sse::events::{SseDeletePayload, SseEvent}; 191 - use crate::sse::mock::MockSseTransport; 190 + use crate::indexer::sse::events::{SseDeletePayload, SseEvent}; 191 + use crate::indexer::sse::mock::MockSseTransport; 192 192 use std::cell::RefCell; 193 193 use std::rc::Rc; 194 194
crates/opake-core/src/sse/events.rs crates/opake-core/src/indexer/sse/events.rs
+3 -3
crates/opake-core/src/sse/mock.rs crates/opake-core/src/indexer/sse/mock.rs
··· 9 9 // touching opake-core internals. 10 10 11 11 use crate::error::Error; 12 - use crate::sse::events::SseEvent; 13 - use crate::sse::transport::{SseConnection, SseTransport}; 12 + use crate::indexer::sse::events::SseEvent; 13 + use crate::indexer::sse::transport::{SseConnection, SseTransport}; 14 14 use std::cell::RefCell; 15 15 use std::collections::VecDeque; 16 16 use std::rc::Rc; ··· 128 128 #[cfg(test)] 129 129 mod tests { 130 130 use super::*; 131 - use crate::sse::events::SseDeletePayload; 131 + use crate::indexer::sse::events::SseDeletePayload; 132 132 133 133 fn delete_event(uri: &str) -> SseEvent { 134 134 SseEvent::KeyringDelete(SseDeletePayload {
crates/opake-core/src/sse/mod.rs crates/opake-core/src/indexer/sse/mod.rs
+1 -1
crates/opake-core/src/sse/parser.rs crates/opake-core/src/indexer/sse/parser.rs
··· 19 19 // over bytes from `reqwest::Response::bytes_stream()`. 20 20 21 21 use crate::error::Error; 22 - use crate::sse::events::SseEvent; 22 + use crate::indexer::sse::events::SseEvent; 23 23 24 24 /// Accumulates SSE field lines into complete events. Call [`feed_line`] 25 25 /// for each `\n`-stripped line; it returns `Some(event)` when a blank line
crates/opake-core/src/sse/reconnect.rs crates/opake-core/src/indexer/sse/reconnect.rs
+4 -4
crates/opake-core/src/sse/reqwest_connection.rs crates/opake-core/src/indexer/sse/reqwest_connection.rs
··· 2 2 // 3 3 // Reqwest's streaming API gives us `impl Stream<Item = Result<Bytes>>`. 4 4 // Each chunk may split mid-line, so we feed it into an 5 - // [`SseLineAccumulator`](crate::sse::parser::SseLineAccumulator) which 5 + // [`SseLineAccumulator`](crate::indexer::sse::parser::SseLineAccumulator) which 6 6 // buffers partial lines and emits complete events as they parse. 7 7 // 8 8 // Unlike the WASM side (browser EventSource auto-reconnects, we just ··· 16 16 use reqwest::Client; 17 17 18 18 use crate::error::Error; 19 - use crate::sse::events::SseEvent; 20 - use crate::sse::parser::SseLineAccumulator; 21 - use crate::sse::transport::{SseConnection, SseTransport}; 19 + use crate::indexer::sse::events::SseEvent; 20 + use crate::indexer::sse::parser::SseLineAccumulator; 21 + use crate::indexer::sse::transport::{SseConnection, SseTransport}; 22 22 23 23 type BytesStream = Pin<Box<dyn Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Send>>; 24 24
+1 -1
crates/opake-core/src/sse/transport.rs crates/opake-core/src/indexer/sse/transport.rs
··· 11 11 // Reconnect events. 12 12 13 13 use crate::error::Error; 14 - use crate::sse::events::SseEvent; 14 + use crate::indexer::sse::events::SseEvent; 15 15 use std::future::Future; 16 16 17 17 /// Opens SSE connections against the indexer's `/api/events` endpoint.
+2 -2
crates/opake-core/src/sse/wasm_connection.rs crates/opake-core/src/indexer/sse/wasm_connection.rs
··· 25 25 use web_sys::{Event, EventSource, MessageEvent}; 26 26 27 27 use crate::error::Error; 28 - use crate::sse::events::SseEvent; 29 - use crate::sse::transport::{SseConnection, SseTransport}; 28 + use crate::indexer::sse::events::SseEvent; 29 + use crate::indexer::sse::transport::{SseConnection, SseTransport}; 30 30 31 31 /// Every named event the broadcaster can emit. Registered as individual 32 32 /// listeners because `onmessage` only fires for untyped (`event: message`)
+1 -1
crates/opake-core/src/tree_keeper/mod.rs crates/opake-core/src/indexer/tree_keeper/mod.rs
··· 28 28 use crate::crypto::{ContentKey, X25519PrivateKey}; 29 29 use crate::directories::{DecryptionCtx, DirectoryTree, TreeChange}; 30 30 use crate::error::Error; 31 - use crate::sse::events::{SseDirectoryRecord, SseEvent}; 31 + use crate::indexer::sse::events::{SseDirectoryRecord, SseEvent}; 32 32 33 33 /// Callback fired when a watched directory's tree state changes. 34 34 ///
+3 -3
crates/opake-core/src/tree_keeper/tests.rs crates/opake-core/src/indexer/tree_keeper/tests.rs
··· 4 4 use super::*; 5 5 use crate::crypto::ContentKey; 6 6 use crate::directories::tests::{dummy_directory_with_entries, test_keypair, TEST_DID}; 7 - use crate::sse::events::{SseDeletePayload, SseDirectoryRecord, SseEvent}; 7 + use crate::indexer::sse::events::{SseDeletePayload, SseDirectoryRecord, SseEvent}; 8 8 9 9 const ROOT_URI: &str = "at://did:plc:test/app.opake.directory/self"; 10 10 const DIR_PHOTOS_URI: &str = "at://did:plc:test/app.opake.directory/photos"; ··· 305 305 } 306 306 307 307 fn sse_doc_upsert(uri: &str, keyring_uri: Option<&str>) -> SseEvent { 308 - use crate::sse::events::SseDocumentRecord; 308 + use crate::indexer::sse::events::SseDocumentRecord; 309 309 SseEvent::DocumentUpsert(SseDocumentRecord { 310 310 document_uri: uri.into(), 311 311 owner_did: TEST_DID.into(), ··· 320 320 } 321 321 322 322 fn sse_doc_delete(uri: &str) -> SseEvent { 323 - use crate::sse::events::SseDeletePayload; 323 + use crate::indexer::sse::events::SseDeletePayload; 324 324 SseEvent::DocumentDelete(SseDeletePayload { 325 325 uri: None, 326 326 directory_uri: None,
+7 -7
crates/opake-core/src/workspace_keeper/mod.rs crates/opake-core/src/indexer/workspace_keeper/mod.rs
··· 29 29 //! workspace stays visible and self-corrects on the next event. 30 30 //! See [`apply_keyring_record`] for the canonical dispatch logic. 31 31 //! 32 - //! [`TreeKeeper`]: crate::tree_keeper::TreeKeeper 33 - //! [`SseEvent::KeyringUpsert`]: crate::sse::events::SseEvent::KeyringUpsert 34 - //! [`SseEvent::KeyringDelete`]: crate::sse::events::SseEvent::KeyringDelete 32 + //! [`TreeKeeper`]: crate::indexer::tree_keeper::TreeKeeper 33 + //! [`SseEvent::KeyringUpsert`]: crate::indexer::sse::events::SseEvent::KeyringUpsert 34 + //! [`SseEvent::KeyringDelete`]: crate::indexer::sse::events::SseEvent::KeyringDelete 35 35 36 36 use std::collections::HashMap; 37 37 ··· 335 335 336 336 /// Convenience wrapper: build an entry from an [`IndexerKeyring`]. 337 337 /// 338 - /// [`IndexerKeyring`]: crate::client::IndexerKeyring 338 + /// [`IndexerKeyring`]: crate::indexer::IndexerKeyring 339 339 pub fn try_build_entry_from_indexer_keyring( 340 - keyring: &crate::client::IndexerKeyring, 340 + keyring: &crate::indexer::IndexerKeyring, 341 341 my_did: &str, 342 342 private_key: &X25519PrivateKey, 343 343 ) -> Option<WorkspaceEntry> { ··· 359 359 /// well-formed broadcaster always emits it, but the field is `Option` 360 360 /// in the wire type so we handle the gap defensively). 361 361 /// 362 - /// [`SseKeyringRecord`]: crate::sse::events::SseKeyringRecord 362 + /// [`SseKeyringRecord`]: crate::indexer::sse::events::SseKeyringRecord 363 363 pub fn try_build_entry_from_sse_record( 364 - record: &crate::sse::events::SseKeyringRecord, 364 + record: &crate::indexer::sse::events::SseKeyringRecord, 365 365 my_did: &str, 366 366 private_key: &X25519PrivateKey, 367 367 ) -> Option<WorkspaceEntry> {
crates/opake-core/src/workspace_keeper/tests.rs crates/opake-core/src/indexer/workspace_keeper/tests.rs
+1 -1
crates/opake-wasm/src/daemon.rs
··· 10 10 use serde::Serialize; 11 11 use wasm_bindgen::prelude::*; 12 12 13 - use opake_core::daemon; 13 + use opake_core::indexer::daemon; 14 14 15 15 use crate::wasm_util; 16 16
+1 -1
crates/opake-wasm/src/file_manager_wasm.rs
··· 15 15 use std::rc::Rc; 16 16 17 17 use futures_util::lock::Mutex; 18 + use opake_core::indexer::tree_keeper::TreeKeeper; 18 19 use opake_core::manager::{FileContext, UploadRequest}; 19 - use opake_core::tree_keeper::TreeKeeper; 20 20 use serde::Serialize; 21 21 use wasm_bindgen::prelude::*; 22 22
+1 -1
crates/opake-wasm/src/lib.rs
··· 448 448 let key: [u8; 32] = signing_key 449 449 .try_into() 450 450 .map_err(|_| JsError::new("signing key must be exactly 32 bytes"))?; 451 - Ok(opake_core::client::sign_indexer_request( 451 + Ok(opake_core::indexer::sign_indexer_request( 452 452 method, 453 453 path, 454 454 did,
+7 -7
crates/opake-wasm/src/opake_wasm.rs
··· 23 23 use std::rc::Rc; 24 24 25 25 use futures_util::lock::Mutex; 26 - use opake_core::inbox_keeper::{self as ik, InboxKeeper}; 27 - use opake_core::tree_keeper::TreeKeeper; 28 - use opake_core::workspace_keeper::WorkspaceKeeper; 26 + use opake_core::indexer::inbox_keeper::{self as ik, InboxKeeper}; 27 + use opake_core::indexer::tree_keeper::TreeKeeper; 28 + use opake_core::indexer::workspace_keeper::WorkspaceKeeper; 29 29 use serde::Serialize; 30 30 use wasm_bindgen::prelude::*; 31 31 ··· 185 185 // Optimistic insert — the sidebar shows the new workspace immediately 186 186 // rather than waiting 1–4s for the SSE echo to arrive. The echo 187 187 // produces an equal entry and the keeper's dedup short-circuits. 188 - let optimistic = opake_core::workspace_keeper::WorkspaceEntry { 188 + let optimistic = opake_core::indexer::workspace_keeper::WorkspaceEntry { 189 189 uri: keyring_uri.clone(), 190 190 owner_did: opake.did().to_string(), 191 191 rotation: 1, ··· 244 244 // helper so this path and the SSE event path produce identical 245 245 // WorkspaceEntry values — any shape divergence between them 246 246 // would cause spurious watcher re-fires after SSE echoes. 247 - let entries: Vec<opake_core::workspace_keeper::WorkspaceEntry> = keyrings 247 + let entries: Vec<opake_core::indexer::workspace_keeper::WorkspaceEntry> = keyrings 248 248 .iter() 249 249 .filter_map(|kr| { 250 - opake_core::workspace_keeper::try_build_entry_from_indexer_keyring( 250 + opake_core::indexer::workspace_keeper::try_build_entry_from_indexer_keyring( 251 251 kr, 252 252 &did, 253 253 &private_key, ··· 747 747 .map_err(wasm_err)?; 748 748 drop(opake); 749 749 750 - let entries: Vec<opake_core::inbox_keeper::InboxEntry> = 750 + let entries: Vec<opake_core::indexer::inbox_keeper::InboxEntry> = 751 751 grants.iter().map(ik::entry_from_indexer_grant).collect(); 752 752 753 753 {
+7 -7
crates/opake-wasm/src/sse_wasm.rs
··· 21 21 use std::time::Duration; 22 22 23 23 use futures_util::lock::Mutex; 24 - use opake_core::client::request_sse_token; 25 24 use opake_core::directories::DirectoryTree; 26 - use opake_core::inbox_keeper::{ 25 + use opake_core::indexer::inbox_keeper::{ 27 26 self as ik, InboxKeeper, InboxSnapshot, InboxWatcherCallback, InboxWatcherHandle, 28 27 }; 29 - use opake_core::sse::consumer::{JitterRng, SleepFn, SseConsumer, TokenFetcher}; 30 - use opake_core::sse::events::SseEvent; 31 - use opake_core::sse::wasm_connection::WasmSseTransport; 32 - use opake_core::tree_keeper::{TreeKeeper, WatcherCallback, WatcherHandle}; 33 - use opake_core::workspace_keeper::{ 28 + use opake_core::indexer::request_sse_token; 29 + use opake_core::indexer::sse::consumer::{JitterRng, SleepFn, SseConsumer, TokenFetcher}; 30 + use opake_core::indexer::sse::events::SseEvent; 31 + use opake_core::indexer::sse::wasm_connection::WasmSseTransport; 32 + use opake_core::indexer::tree_keeper::{TreeKeeper, WatcherCallback, WatcherHandle}; 33 + use opake_core::indexer::workspace_keeper::{ 34 34 self as wk, WorkspaceKeeper, WorkspaceSnapshot, WorkspaceWatcherCallback, 35 35 WorkspaceWatcherHandle, 36 36 };