eidetica/instance/mod.rs
1//!
2//! Provides the main database structures (`Instance` and `Database`).
3//!
4//! `Instance` manages multiple `Database` instances and interacts with the storage `Database`.
5//! `Database` represents a single, independent history of data entries, analogous to a table or branch.
6
7use std::{
8 collections::HashMap,
9 future::Future,
10 path::PathBuf,
11 pin::Pin,
12 sync::{
13 Arc, Mutex, Weak,
14 atomic::{AtomicU64, Ordering},
15 },
16};
17
18use handle_trait::Handle;
19
20use crate::{
21 Clock, Database, Entry, Result, SystemClock,
22 auth::crypto::{PrivateKey, PublicKey},
23 backend::{BackendImpl, InstanceMetadata, InstanceSecrets, VerificationStatus},
24 entry::ID,
25 snapshot::Snapshot,
26 sync::Sync,
27 user::User,
28};
29#[cfg(all(unix, feature = "service"))]
30use crate::{auth::SigKey, service::client::RemoteConnection};
31
32pub mod backend;
33pub mod errors;
34pub mod new_user;
35pub mod settings_merge;
36pub mod url;
37
38#[cfg(test)]
39mod tests;
40
41// Re-export main types for easier access
42#[cfg(all(unix, feature = "service"))]
43use backend::RemoteBackend;
44use backend::{Backend, LocalBackend};
45pub use errors::InstanceError;
46pub use new_user::NewUser;
47
48/// Indicates whether an entry write originated locally or from a remote source (e.g., sync).
49///
50/// This distinction allows different callbacks to be triggered based on the write source,
51/// enabling behaviors like "only trigger sync for local writes" or "only update UI for remote writes".
52///
53/// Marked `#[non_exhaustive]` so additional source variants can be added in the
54/// future (e.g. a distinct `Promoted` for verify-pass fires that surface
55/// already-stored entries) without breaking exhaustive `match` arms in user
56/// code. Always include a wildcard arm when matching.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
58#[non_exhaustive]
59pub enum WriteSource {
60 /// Write originated from a local transaction commit
61 Local,
62 /// Write originated from a remote source (e.g., sync, replication)
63 Remote,
64}
65
66/// A cursor-advance notification delivered to a write callback.
67///
68/// A `WriteEvent` carries *no entry payloads* — only the cursor brackets
69/// (`previous_tips` → `post_tips`) and the [`WriteSource`]. Callbacks that
70/// only care that *something* changed (cache invalidation, UI wake-ups)
71/// can act on the event directly without touching the wire or the DAG.
72/// Callbacks that need to enumerate or fetch the new entries call
73/// [`Database::ids_added`](crate::Database::ids_added) with the brackets:
74///
75/// ```rust,no_run
76/// # use eidetica::{instance::WriteEvent, Database, Result};
77/// # async fn example(event: &WriteEvent, db: &Database) -> Result<()> {
78/// // Enumerate the IDs added between the two cursors
79/// let new_ids = db.ids_added(event.previous_tips(), event.post_tips()).await?;
80/// for id in new_ids {
81/// // … fetch entry bodies via db.get_entry(id) if needed …
82/// }
83/// # Ok(()) }
84/// ```
85///
86/// Cursor semantics: `previous_tips` is this callback's frontier *before*
87/// this fire — the user-supplied initial tips on the first fire, then the
88/// preceding fire's `post_tips` on each subsequent fire. The cursor
89/// advances to `post_tips` synchronously *before* the user closure is
90/// awaited, so the next fire is guaranteed to bracket against the latest
91/// observed frontier even if the closure is slow.
92///
93/// Triggered only by settled-state (Verified) writes, but the cursors are
94/// raw DAG frontiers — the bracket can span `Unverified`/`Failed` entries,
95/// which [`Database::ids_added`](crate::Database::ids_added) will
96/// enumerate. See
97/// [`Notification::DatabaseWrite`](crate::service::protocol::Notification::DatabaseWrite)
98/// rustdoc for the full verification contract.
99#[derive(Debug, Clone)]
100pub struct WriteEvent {
101 /// The database state this callback was last delivered at — its
102 /// cursor before this fire, as a canonical [`Snapshot`]. Subsequent
103 /// fires for the same callback will have `previous_tips = this fire's
104 /// post_tips`.
105 previous_tips: Snapshot,
106 /// The database state after this write, as a canonical [`Snapshot`].
107 /// Equal to this callback's cursor *after* the fire. Useful for
108 /// "what's the frontier I'm now caught up to" without an extra read.
109 post_tips: Snapshot,
110 /// Whether this write originated locally or from a remote sync.
111 source: WriteSource,
112}
113
114impl WriteEvent {
115 /// Get the database state at this callback's cursor *before* this fire.
116 ///
117 /// The first fire on a freshly-registered callback returns the
118 /// initial snapshot passed at registration time. Subsequent fires
119 /// return the previous fire's `post_tips`.
120 pub fn previous_tips(&self) -> &Snapshot {
121 &self.previous_tips
122 }
123
124 /// Get the database state at this callback's cursor *after* this fire.
125 ///
126 /// The cursor advances to this value before the callback is awaited,
127 /// so the next fire on the same callback will have
128 /// `previous_tips() == this fire's post_tips()`.
129 pub fn post_tips(&self) -> &Snapshot {
130 &self.post_tips
131 }
132
133 /// The source of this write (local commit or remote sync).
134 pub fn source(&self) -> WriteSource {
135 self.source
136 }
137}
138
139/// Boxed future returned by the internal async callback dispatcher.
140/// The future a callback returns is `'static` — it may borrow the `&WriteEvent`
141/// / `&Database` only for the synchronous prefix of the call, never past the
142/// returned future. Both registration paths already require `Fut: 'static`, so
143/// this is not a new constraint on callers; it lets `spawn_write_callbacks`
144/// invoke the callback *synchronously in cursor-advance order* (running any
145/// synchronous side effect — e.g. the service subscription's frame send — in
146/// canonical order under the tree lock) and then spawn only the returned future.
147pub(crate) type AsyncWriteCallbackFuture =
148 Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>>;
149
150/// Internal async callback function type. The user-facing callback contract
151/// is documented on [`Database::on_write`](crate::Database::on_write).
152pub(crate) type AsyncWriteCallbackFn = Arc<
153 dyn for<'a> Fn(&'a WriteEvent, &'a Database) -> AsyncWriteCallbackFuture
154 + Send
155 + std::marker::Sync,
156>;
157
158/// Opaque identifier for a registered callback. Stable for the life of the registration.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
160pub(crate) struct CallbackId(u64);
161
162/// One per-database callback registration plus the cursor that tracks the
163/// frontier this specific callback has observed.
164///
165/// Each fire reads the cursor, builds a [`WriteEvent`] with the cursor
166/// as `previous_tips`, advances the cursor to the post-write tips, and
167/// then invokes the callback. The cursor mutex is held synchronously
168/// during the read/advance — never across the user callback's `.await`.
169///
170/// Stored in `Vec<Arc<PerDbCallbackEntry>>` on each tree so
171/// `fire_write_callbacks` can snapshot Arcs under the registry mutex
172/// and run the dispatches outside it.
173pub(crate) struct PerDbCallbackEntry {
174 pub(crate) id: CallbackId,
175 /// Cursor — the post-write [`Snapshot`] of the most recent event this
176 /// callback has been delivered, or the user-provided initial snapshot
177 /// from the registration call before any event has fired.
178 pub(crate) last_tips: std::sync::Mutex<Snapshot>,
179 pub(crate) callback: AsyncWriteCallbackFn,
180}
181
182/// Type alias for the per-database callback list on a tree.
183type PerDbCallbackVec = Vec<Arc<PerDbCallbackEntry>>;
184
185/// Type alias for the global write callback list. Globals fire for every
186/// write on every tree (used by sync today). No cursor — globals are
187/// tree-agnostic and don't have a meaningful per-tree frontier to track,
188/// so they continue to use whatever `previous_tips` the caller passes in.
189type GlobalCallbackVec = Vec<(CallbackId, AsyncWriteCallbackFn)>;
190
191/// Handle to a registered write callback. **Drop to unregister.**
192///
193/// Returned by [`Database::on_write`](crate::Database::on_write). While this
194/// value is alive the callback fires on writes; dropping it removes the
195/// registration. Use [`detach`](Self::detach) to keep the callback registered
196/// for the life of the [`Instance`] when you don't want to manage the lifetime
197/// yourself.
198///
199/// Holds a weak reference to the [`Instance`], so a `WriteCallback` will not
200/// keep the Instance alive on its own.
201#[must_use = "dropping a WriteCallback unregisters it; call .detach() to keep the callback registered"]
202pub struct WriteCallback {
203 instance: WeakInstance,
204 tree_id: ID,
205 id: CallbackId,
206 detached: bool,
207}
208
209impl WriteCallback {
210 pub(crate) fn new_per_database(instance: WeakInstance, tree_id: ID, id: CallbackId) -> Self {
211 Self {
212 instance,
213 tree_id,
214 id,
215 detached: false,
216 }
217 }
218
219 /// Consume the handle without unregistering. The callback remains active
220 /// for the life of the [`Instance`].
221 ///
222 /// Implementation note: this sets a flag rather than calling `mem::forget`
223 /// so that field destructors (the `WeakInstance`'s weak count, the
224 /// `tree_id`'s heap allocation) still run — only our `Drop` impl is
225 /// short-circuited.
226 pub fn detach(mut self) {
227 self.detached = true;
228 }
229}
230
231impl std::fmt::Debug for WriteCallback {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 f.debug_struct("WriteCallback")
234 .field("id", &self.id)
235 .field("tree_id", &self.tree_id)
236 .field("detached", &self.detached)
237 .finish()
238 }
239}
240
241impl Drop for WriteCallback {
242 fn drop(&mut self) {
243 if self.detached {
244 return;
245 }
246 if let Some(instance) = self.instance.upgrade() {
247 // On a connected instance, dropping the last local callback
248 // for this tree transitions the wire-side subscription to
249 // `Idle`. Daemon-side stays subscribed through a grace
250 // window so a quick re-registration is a no-op; the sweep
251 // task in the connection unsubscribes after the window
252 // elapses.
253 //
254 // The local-instance build doesn't read the `was_last`
255 // signal; bind it under the `cfg` so the inactive build
256 // doesn't carry a dead variable.
257 #[cfg(all(unix, feature = "service"))]
258 {
259 let was_last = instance.remove_write_callback(&self.tree_id, self.id);
260 if was_last && let Some(conn) = instance.remote_connection() {
261 conn.transition_to_idle(&self.tree_id);
262 }
263 }
264 #[cfg(not(all(unix, feature = "service")))]
265 {
266 let _ = instance.remove_write_callback(&self.tree_id, self.id);
267 }
268 }
269 }
270}
271
272/// Internal state for Instance
273///
274/// This structure holds the actual implementation data for Instance.
275/// Instance itself is just a cheap-to-clone handle wrapping Arc<InstanceInternal>.
276pub(crate) struct InstanceInternal {
277 /// The database storage backend
278 backend: Arc<dyn Backend>,
279 /// Time provider for timestamps
280 clock: Arc<dyn Clock>,
281 /// Synchronization module for this database instance
282 /// TODO: Overengineered, Sync can be created by default but disabled
283 sync: std::sync::OnceLock<Arc<Sync>>,
284 /// Public instance metadata (device identity, system database IDs)
285 metadata: InstanceMetadata,
286 /// Private instance secrets (None for remote instances without key access)
287 secrets: Option<InstanceSecrets>,
288 /// JSON snapshot file path for an in-memory backend constructed via
289 /// `memory:///path.json` (or set explicitly through
290 /// [`Instance::snapshot_to_path`]). [`Instance::flush`] and the
291 /// [`Drop`] safety net write through this. `None` on any non-snapshot
292 /// backend.
293 ///
294 /// The mutex serves double duty: it guards the path slot itself (so
295 /// `set_snapshot_path` doesn't race with readers) AND serializes the
296 /// actual write so concurrent callers from `flush` / `snapshot_to_path`
297 /// / `Drop` don't race on the shared `<path>.tmp` staging file in
298 /// [`InMemory::save_to_file`]. Held across sync I/O only — never
299 /// across an `.await`. Poison-tolerant: a panic mid-write leaves the
300 /// on-disk snapshot unchanged but must not strand the [`Instance`].
301 snapshot_path: Mutex<Option<PathBuf>>,
302 /// Per-database callbacks keyed by tree_id. Each entry carries its own
303 /// cursor (`last_tips`) so fires can build a callback-specific
304 /// `previous_tips` regardless of when the callback registered or when
305 /// the most recent fire actually advanced its frontier. Consumers
306 /// branch on [`WriteEvent::source`] if they only care about one
307 /// source.
308 write_callbacks: Mutex<HashMap<ID, PerDbCallbackVec>>,
309 /// Global callbacks fired for every write across every database.
310 /// Tree-agnostic — no per-callback cursor.
311 global_write_callbacks: Mutex<GlobalCallbackVec>,
312 /// Monotonic id source for [`CallbackId`].
313 next_callback_id: AtomicU64,
314 /// Per-tree async locks serializing the
315 /// `snapshot` → backend write → callback dispatch sequence so
316 /// `WriteEvent::previous_tips` is consistent for concurrent writers
317 /// to the same tree.
318 tree_locks: Mutex<HashMap<ID, Arc<tokio::sync::Mutex<()>>>>,
319}
320
321impl std::fmt::Debug for InstanceInternal {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 f.debug_struct("InstanceInternal")
324 .field("backend", &"<BackendDB>")
325 .field("clock", &self.clock)
326 .field("sync", &self.sync)
327 .field("metadata", &self.metadata)
328 .field("secrets", &self.secrets.is_some())
329 .field(
330 "write_callbacks",
331 &format!(
332 "<{} per-db callbacks>",
333 self.write_callbacks
334 .lock()
335 .unwrap_or_else(|p| p.into_inner())
336 .len()
337 ),
338 )
339 .field(
340 "global_write_callbacks",
341 &format!(
342 "<{} global callbacks>",
343 self.global_write_callbacks
344 .lock()
345 .unwrap_or_else(|p| p.into_inner())
346 .len()
347 ),
348 )
349 .field(
350 "next_callback_id",
351 &self.next_callback_id.load(Ordering::Relaxed),
352 )
353 .finish()
354 }
355}
356
357impl InstanceInternal {
358 /// Synchronously write a JSON snapshot of the underlying backend to `path`.
359 ///
360 /// Returns [`InstanceError::SnapshotNotSupported`] for any backend other
361 /// than the local in-memory backend. Shared by [`Instance::snapshot_to_path`],
362 /// [`Instance::flush`], and the [`Drop`] fallback so the three can't drift.
363 ///
364 /// **Caller must hold the [`snapshot_path`](Self::snapshot_path) mutex.**
365 /// That lock serializes the write — without it, concurrent callers
366 /// would race on the shared `<path>.tmp` staging file in
367 /// [`InMemory::save_to_file`]. The critical section is fully sync; no
368 /// `.await` happens while the lock is held.
369 fn save_snapshot_locked(&self, path: &std::path::Path) -> Result<()> {
370 use crate::backend::database::InMemory;
371 let engine = self
372 .backend
373 .local_engine()
374 .ok_or(InstanceError::SnapshotNotSupported)?;
375 let in_memory = engine
376 .as_any()
377 .downcast_ref::<InMemory>()
378 .ok_or(InstanceError::SnapshotNotSupported)?;
379 in_memory.save_to_file(path)
380 }
381}
382
383/// Best-effort snapshot save on the *last* `InstanceInternal` drop.
384///
385/// Fires when the `Arc<InstanceInternal>` reaches refcount 0 and a snapshot
386/// path is armed (i.e. the `Instance` was constructed via a
387/// `memory:///path.json` URL). [`Instance::flush`] does **not** clear the
388/// snapshot path, so Drop fires even after a successful `flush()` — the
389/// write is idempotent (same atomic tmp+rename), so the worst case is one
390/// extra write of unchanged JSON.
391///
392/// **Errors are logged via `tracing::error!`, not surfaced** — `Drop` can't
393/// return a `Result` and panicking would be worse than logging. Apps that
394/// care about snapshot durability should call [`Instance::flush`] at
395/// well-defined checkpoints and inspect its `Result`; Drop is a safety net,
396/// not the primary persistence path. If `flush()` failed with a permanent
397/// error (e.g. nonexistent parent directory), Drop will fail the same way
398/// and emit a second log line — accept this redundancy as the cost of a
399/// best-effort fallback.
400///
401/// **Blocking I/O warning:** the snapshot write is synchronous
402/// (`std::fs::write` + `rename`). If the `Instance` is dropped on a tokio
403/// worker thread, this blocks that worker for the duration of the write —
404/// negligible for small snapshots, but pathological for very large ones.
405/// Prefer `flush().await` (which still blocks briefly, but does so under
406/// explicit caller control).
407impl Drop for InstanceInternal {
408 fn drop(&mut self) {
409 // Drop runs at Arc refcount 0, so no other handle can race here —
410 // any in-flight `flush()` future holds `&self` and thus an Arc
411 // clone, which would have prevented Drop from firing. We still
412 // acquire the lock for the write so the locking discipline in
413 // `save_snapshot_locked`'s doc-comment holds uniformly. The lock
414 // is uncontended at this point.
415 let mut guard = match self.snapshot_path.lock() {
416 Ok(g) => g,
417 Err(p) => p.into_inner(),
418 };
419 let Some(path) = guard.take() else { return };
420
421 if let Err(e) = self.save_snapshot_locked(&path) {
422 tracing::error!(
423 snapshot_path = %path.display(),
424 error = %e,
425 "Drop: snapshot save failed. Call `Instance::flush().await` at \
426 checkpoints to inspect the error via Result; Drop is a safety net only.",
427 );
428 }
429 }
430}
431/// Database implementation on top of the storage backend.
432///
433/// Instance manages infrastructure only:
434/// - Backend storage and device identity
435/// - System databases (_users, _databases, _sync)
436/// - User account management (create, login, list)
437///
438/// All database creation and key operations happen through User after login.
439///
440/// Instance is a cheap-to-clone handle around `Arc<InstanceInternal>`.
441///
442/// ## Example
443///
444/// ```
445/// # use eidetica::{Instance, NewUser, crdt::Doc};
446/// # #[tokio::main]
447/// # async fn main() -> eidetica::Result<()> {
448/// // Bootstrap a fresh instance with an initial admin user. The first user
449/// // created on an instance is automatically granted Admin on the system
450/// // databases.
451/// let (instance, maybe_user) = Instance::connect_or_create(
452/// "memory://",
453/// NewUser::passwordless("alice"),
454/// ).await?;
455/// let mut user = maybe_user.expect("memory:// is always fresh");
456///
457/// // Use User API for operations
458/// let mut settings = Doc::new();
459/// settings.set("name", "my_database");
460/// let default_key = user.get_default_key()?;
461/// let db = user.create_database(settings, &default_key).await?;
462/// # Ok(())
463/// # }
464/// ```
465#[derive(Clone, Debug, Handle)]
466pub struct Instance {
467 inner: Arc<InstanceInternal>,
468}
469
470/// Weak reference to an Instance.
471///
472/// This is a weak handle that does not prevent the Instance from being dropped.
473/// Dependent objects (Database, Sync, BackgroundSync) hold weak references to avoid
474/// circular reference cycles that would leak memory.
475///
476/// Use `upgrade()` to convert to a strong `Instance` reference.
477#[derive(Clone, Debug, Handle)]
478pub struct WeakInstance {
479 inner: Weak<InstanceInternal>,
480}
481
482impl Instance {
483 /// Open a connection to an eidetica instance described by a connection URL.
484 ///
485 /// Strict load: returns [`InstanceError::NotInitialized`] when the URL
486 /// points at an embedded backend (`sqlite://`, `postgres://`, `memory://`)
487 /// that has no eidetica metadata yet. Use
488 /// [`Instance::connect_or_create`] to bootstrap an embedded backend on
489 /// first run.
490 ///
491 /// Supported URL schemes:
492 /// - `sqlite://./app.db` — embedded sqlite backend; URL is passed through
493 /// to `sqlx::sqlite`, so any sqlx-accepted form works
494 /// (`?mode=rwc&journal_mode=WAL` etc.).
495 /// - `postgres://user:pwd@host/db` — embedded postgres backend; URL is
496 /// passed through to `sqlx::postgres`.
497 /// - `unix:///run/eidetica/sock` — thin client to a running daemon.
498 /// - `memory://` — empty in-memory backend. Strict load against an
499 /// empty in-memory backend always errors `NotInitialized`; use
500 /// `connect_or_create` for a fresh in-memory instance.
501 /// - `memory:///path/to/snap.json` — in-memory backend with a JSON
502 /// snapshot file (load-on-start; snapshot writes via
503 /// [`Instance::flush`] / [`Instance::snapshot_to_path`] / Drop fallback).
504 ///
505 /// See [`crate::instance::url`] for the full URL grammar.
506 ///
507 /// # Example
508 ///
509 /// `connect()` only succeeds against an already-initialised backend.
510 /// The two-phase pattern below bootstraps once, then re-opens with
511 /// the strict load:
512 ///
513 /// ```
514 /// # #[tokio::main]
515 /// # async fn main() -> eidetica::Result<()> {
516 /// use eidetica::{Instance, NewUser};
517 ///
518 /// let temp = tempfile::tempdir()?;
519 /// let snapshot = temp.path().join("app.json");
520 /// let url = format!("memory://{}", snapshot.display());
521 ///
522 /// // First run: bootstrap and flush a snapshot to disk.
523 /// {
524 /// let (instance, maybe_user) =
525 /// Instance::connect_or_create(&url, NewUser::passwordless("alice")).await?;
526 /// let _user = maybe_user.expect("fresh bootstrap on first run");
527 /// instance.flush()?;
528 /// }
529 ///
530 /// // Later: strict connect against the persisted snapshot.
531 /// let instance = Instance::connect(&url).await?;
532 /// let _user = instance.login_user("alice", None).await?;
533 /// # Ok(())
534 /// # }
535 /// ```
536 ///
537 /// Calling `connect()` on a backend with no eidetica metadata returns
538 /// [`InstanceError::NotInitialized`]; reach for
539 /// [`Instance::connect_or_create`] when first-run bootstrap is part
540 /// of the expected lifecycle.
541 pub async fn connect(url: impl AsRef<str>) -> Result<Self> {
542 Self::connect_impl(url.as_ref(), Arc::new(SystemClock)).await
543 }
544
545 /// Open or initialise an eidetica instance described by a connection URL.
546 ///
547 /// On the load arm: identical to [`Instance::connect`]; `initial` is
548 /// silently ignored and the second tuple element is `None`.
549 ///
550 /// On the bootstrap arm: initialises the backend at the URL with the
551 /// supplied [`NewUser`] as the first admin and returns
552 /// `(Instance, Some(User))`. Only embedded backends
553 /// (sqlite/postgres/memory) ever take the bootstrap arm — `unix://` URLs
554 /// degrade to `connect` (the daemon owns its own initialisation), so the
555 /// returned `Option<User>` is always `None` for `unix://`.
556 ///
557 /// # Example
558 /// ```
559 /// # use eidetica::{Instance, NewUser};
560 /// # #[tokio::main]
561 /// # async fn main() -> eidetica::Result<()> {
562 /// let (instance, maybe_user) = Instance::connect_or_create(
563 /// "memory://",
564 /// NewUser::passwordless("alice"),
565 /// ).await?;
566 /// let mut user = match maybe_user {
567 /// Some(u) => u,
568 /// None => instance.login_user("alice", None).await?,
569 /// };
570 /// # let _ = user.get_default_key()?;
571 /// # Ok(())
572 /// # }
573 /// ```
574 pub async fn connect_or_create(
575 url: impl AsRef<str>,
576 initial: NewUser,
577 ) -> Result<(Self, Option<User>)> {
578 Self::connect_or_create_impl(url.as_ref(), initial, Arc::new(SystemClock)).await
579 }
580
581 /// Escape hatch: open or initialise an eidetica instance against a
582 /// pre-built [`BackendImpl`] (sqlite, postgres, in-memory, or custom).
583 ///
584 /// Same load-or-bootstrap semantics as [`Instance::connect_or_create`]
585 /// but skips URL parsing. Useful for tests, embedded apps that want to
586 /// configure the backend's pool/runtime manually, or backends not yet
587 /// exposed via a URL scheme.
588 pub async fn connect_or_create_backend(
589 backend: Box<dyn BackendImpl>,
590 initial: NewUser,
591 ) -> Result<(Self, Option<User>)> {
592 Self::connect_or_create_backend_impl(
593 Arc::from(backend),
594 initial,
595 Arc::new(SystemClock),
596 None,
597 )
598 .await
599 }
600
601 /// Strict-load escape hatch: open an eidetica instance against a
602 /// pre-built [`BackendImpl`] that's already been initialised. Mirrors
603 /// [`Instance::connect`]'s strict semantics for the URL-less case.
604 ///
605 /// Errors with [`InstanceError::NotInitialized`] if the backend has no
606 /// instance metadata; use [`Instance::connect_or_create_backend`] when you want
607 /// to bootstrap on an empty backend.
608 pub async fn open_backend(backend: Box<dyn BackendImpl>) -> Result<Self> {
609 Self::open_impl(backend, Arc::new(SystemClock)).await
610 }
611
612 /// Test variant of [`Instance::open_backend`] with an injectable clock.
613 #[cfg(any(test, feature = "testing"))]
614 pub async fn open_backend_with_clock(
615 backend: Box<dyn BackendImpl>,
616 clock: Arc<dyn Clock>,
617 ) -> Result<Self> {
618 Self::open_impl(backend, clock).await
619 }
620
621 /// Strict-create escape hatch: initialise an eidetica instance on a
622 /// fresh pre-built [`BackendImpl`] and bootstrap an initial admin user.
623 ///
624 /// Errors with [`InstanceError::InstanceAlreadyExists`] if the backend
625 /// is already initialised; use [`Instance::connect_or_create_backend`] when the
626 /// caller doesn't want to choose between load and create up front.
627 pub async fn create_backend(
628 backend: Box<dyn BackendImpl>,
629 initial: NewUser,
630 ) -> Result<(Self, User)> {
631 Self::create_backend_impl(backend, initial, Arc::new(SystemClock)).await
632 }
633
634 /// Test variant of [`Instance::create_backend`] with an injectable clock.
635 ///
636 /// Arg order: backend, clock, initial — clock goes in the middle so
637 /// migrating from the prior `create_with_clock` is a pure rename.
638 #[cfg(any(test, feature = "testing"))]
639 pub async fn create_backend_with_clock(
640 backend: Box<dyn BackendImpl>,
641 clock: Arc<dyn Clock>,
642 initial: NewUser,
643 ) -> Result<(Self, User)> {
644 Self::create_backend_impl(backend, initial, clock).await
645 }
646
647 async fn create_backend_impl(
648 backend: Box<dyn BackendImpl>,
649 initial: NewUser,
650 clock: Arc<dyn Clock>,
651 ) -> Result<(Self, User)> {
652 let backend: Arc<dyn BackendImpl> = Arc::from(backend);
653 if backend.get_instance_metadata().await?.is_some() {
654 return Err(InstanceError::InstanceAlreadyExists.into());
655 }
656 Self::create_internal(backend, clock, initial).await
657 }
658
659 // Clock injection is exposed only through the pre-built-backend
660 // variants ([`open_backend_with_clock`] and [`create_backend_with_clock`]).
661 // The URL-based `connect_*` constructors deliberately have no
662 // `_with_clock` siblings: every existing test that needs deterministic
663 // timestamps already builds an `InMemory` backend directly, so a URL-
664 // shaped clock entry point would be dead weight.
665
666 // ============ Internal URL dispatchers ============
667
668 async fn connect_impl(url: &str, clock: Arc<dyn Clock>) -> Result<Self> {
669 let parsed = url::parse(url)?;
670 match parsed {
671 url::ConnectionUrl::Sqlite { url } => Self::connect_sqlite(&url, clock).await,
672 url::ConnectionUrl::Postgres { url } => Self::connect_postgres(&url, clock).await,
673 url::ConnectionUrl::Unix { socket_path } => {
674 Self::connect_unix_socket(socket_path, clock).await
675 }
676 url::ConnectionUrl::Memory { snapshot_path } => {
677 Self::connect_memory(snapshot_path, clock).await
678 }
679 }
680 }
681
682 async fn connect_or_create_impl(
683 url: &str,
684 initial: NewUser,
685 clock: Arc<dyn Clock>,
686 ) -> Result<(Self, Option<User>)> {
687 let parsed = url::parse(url)?;
688 match parsed {
689 url::ConnectionUrl::Sqlite { url } => {
690 let backend = open_sqlite_backend(&url).await?;
691 Self::connect_or_create_backend_impl(Arc::from(backend), initial, clock, None).await
692 }
693 url::ConnectionUrl::Postgres { url } => {
694 let backend = open_postgres_backend(&url).await?;
695 Self::connect_or_create_backend_impl(Arc::from(backend), initial, clock, None).await
696 }
697 url::ConnectionUrl::Unix { socket_path } => {
698 // Daemons own their own initialisation. connect_or_create
699 // against `unix://` degrades to a plain connect; `initial`
700 // is unused on this arm. Log it so the silent drop is
701 // discoverable when debugging "why didn't my initial user
702 // get created?" on a remote URL.
703 tracing::debug!(
704 socket_path = %socket_path.display(),
705 username = %initial.username,
706 "connect_or_create against `unix://` is degrading to `connect`; \
707 `initial` is ignored — daemons own their own initialisation. \
708 Run `eidetica daemon init` to bootstrap a daemon-side instance."
709 );
710 let instance = Self::connect_unix_socket(socket_path, clock).await?;
711 Ok((instance, None))
712 }
713 url::ConnectionUrl::Memory { snapshot_path } => {
714 use crate::backend::database::InMemory;
715 // Build the backend from a single `try_load_from_file` call.
716 // `Ok(None)` means the file didn't exist at read time (the
717 // bootstrap-friendly "first run" case → empty backend).
718 // `Ok(Some(loaded))` means the file existed and parsed; if
719 // it carries no instance metadata it's foreign data that
720 // happened to satisfy the `SerializableDatabase` shape, and
721 // we refuse to bootstrap over it (the next snapshot would
722 // silently overwrite the caller's file). Doing the existence
723 // test and the read in one call removes the TOCTOU window a
724 // separate `path.exists()` check would open.
725 let backend: Box<dyn BackendImpl> = match snapshot_path.as_deref() {
726 None => Box::new(InMemory::new()),
727 Some(path) => {
728 let loaded = InMemory::try_load_from_file(path).await.map_err(|e| {
729 InstanceError::InvalidSnapshot {
730 path: path.to_path_buf(),
731 reason: e.to_string(),
732 }
733 })?;
734 match loaded {
735 None => Box::new(InMemory::new()),
736 Some(loaded) => {
737 let boxed: Box<dyn BackendImpl> = Box::new(loaded);
738 if boxed.get_instance_metadata().await?.is_none() {
739 return Err(InstanceError::InvalidSnapshot {
740 path: path.to_path_buf(),
741 reason: "snapshot file exists but contains no instance \
742 metadata; refusing to bootstrap on top of foreign \
743 data. Delete or move the file to create a fresh \
744 instance at this path."
745 .into(),
746 }
747 .into());
748 }
749 boxed
750 }
751 }
752 }
753 };
754 Self::connect_or_create_backend_impl(
755 Arc::from(backend),
756 initial,
757 clock,
758 snapshot_path,
759 )
760 .await
761 }
762 }
763 }
764
765 /// Internal: load-or-bootstrap against a pre-built backend, optionally
766 /// remembering a snapshot path so Drop / flush can write to it.
767 async fn connect_or_create_backend_impl(
768 backend: Arc<dyn BackendImpl>,
769 initial: NewUser,
770 clock: Arc<dyn Clock>,
771 snapshot_path: Option<PathBuf>,
772 ) -> Result<(Self, Option<User>)> {
773 if let Some(metadata) = backend.get_instance_metadata().await? {
774 let instance = Self::open_impl_arc_with_metadata(backend, clock, metadata).await?;
775 instance.set_snapshot_path(snapshot_path);
776 Ok((instance, None))
777 } else {
778 let (instance, user) = Self::create_internal(backend, clock, initial).await?;
779 instance.set_snapshot_path(snapshot_path);
780 Ok((instance, Some(user)))
781 }
782 }
783
784 // ============ Backend connection helpers ============
785
786 #[cfg(all(unix, feature = "service"))]
787 async fn connect_unix_socket(socket_path: PathBuf, clock: Arc<dyn Clock>) -> Result<Self> {
788 let conn = crate::service::client::RemoteConnection::connect(&socket_path).await?;
789 // Keep a clone for the post-construction `attach_instance` call:
790 // the reader task spawned inside `RemoteConnection::connect` needs a
791 // `WeakInstance` to route push notifications into, and the Instance
792 // doesn't exist yet here.
793 let conn_for_attach = conn.clone();
794 let backend: Arc<dyn Backend> = Arc::new(RemoteBackend::new(conn, None));
795
796 // Load metadata from the remote backend
797 let metadata = backend
798 .get_instance_metadata()
799 .await?
800 .ok_or(InstanceError::DeviceKeyNotFound)?;
801
802 // No local secrets — keys are held server-side after login.
803 let inner = Arc::new(InstanceInternal {
804 backend,
805 clock,
806 sync: std::sync::OnceLock::new(),
807 metadata,
808 secrets: None,
809 snapshot_path: Mutex::new(None),
810 write_callbacks: Mutex::new(HashMap::new()),
811 global_write_callbacks: Mutex::new(Vec::new()),
812 next_callback_id: AtomicU64::new(0),
813 tree_locks: Mutex::new(HashMap::new()),
814 });
815 let instance = Self { inner };
816 // Hand the reader task a Weak reference so it can dispatch
817 // server-pushed `Notification::DatabaseWrite` frames into this
818 // Instance's callback registry. Must run *after* the Instance is
819 // constructed; until then the reader task drops notifications
820 // (none can arrive because the client only subscribes lazily on
821 // the first `Database::on_write` call).
822 conn_for_attach.attach_instance(instance.downgrade());
823 Ok(instance)
824 }
825
826 #[cfg(not(all(unix, feature = "service")))]
827 async fn connect_unix_socket(_socket_path: PathBuf, _clock: Arc<dyn Clock>) -> Result<Self> {
828 Err(InstanceError::BackendUnavailable {
829 scheme: "unix",
830 missing_feature: "service",
831 }
832 .into())
833 }
834
835 async fn connect_sqlite(url: &str, clock: Arc<dyn Clock>) -> Result<Self> {
836 let backend = open_sqlite_backend(url).await?;
837 Self::open_impl(backend, clock).await
838 }
839
840 async fn connect_postgres(url: &str, clock: Arc<dyn Clock>) -> Result<Self> {
841 let backend = open_postgres_backend(url).await?;
842 Self::open_impl(backend, clock).await
843 }
844
845 async fn connect_memory(snapshot_path: Option<PathBuf>, clock: Arc<dyn Clock>) -> Result<Self> {
846 use crate::backend::database::InMemory;
847 // Strict load: a snapshot URL that points at a non-existent file
848 // cannot satisfy `connect`'s "must already be initialised" contract.
849 // `try_load_from_file` returns `Ok(None)` when the file doesn't
850 // exist, which we translate into a pointed `InvalidSnapshot`. Using
851 // the same call for the existence test and the read removes the
852 // TOCTOU window a separate `path.exists()` check would open: if the
853 // file vanishes mid-call, the underlying `read_to_string` surfaces
854 // `NotFound` and lands us in the same `None` arm.
855 let backend: Box<dyn BackendImpl> = match snapshot_path.as_deref() {
856 None => Box::new(InMemory::new()),
857 Some(path) => {
858 let loaded = InMemory::try_load_from_file(path).await.map_err(|e| {
859 InstanceError::InvalidSnapshot {
860 path: path.to_path_buf(),
861 reason: e.to_string(),
862 }
863 })?;
864 match loaded {
865 Some(loaded) => Box::new(loaded),
866 None => {
867 return Err(InstanceError::InvalidSnapshot {
868 path: path.to_path_buf(),
869 reason: "snapshot file does not exist; \
870 use `Instance::connect_or_create` to bootstrap a new instance \
871 at this path, or pass `memory://` for an ephemeral instance"
872 .into(),
873 }
874 .into());
875 }
876 }
877 }
878 };
879 let instance = Self::open_impl(backend, clock).await?;
880 instance.set_snapshot_path(snapshot_path);
881 Ok(instance)
882 }
883
884 /// Flush deferred persistence state to disk.
885 ///
886 /// For an `Instance` constructed via a `memory:///path.json` URL, this
887 /// writes the current backend state to the snapshot path (atomic on
888 /// POSIX — `<path>.tmp` then rename). For sqlite/postgres/unix
889 /// backends this is a no-op; those storage layers handle persistence
890 /// inline.
891 ///
892 /// Idempotent and reentrant — call it as often as you like at
893 /// well-defined checkpoints. The snapshot path stays armed, so the
894 /// [`Drop`] fallback continues to fire on the last handle as a safety
895 /// net. The `Instance` (and any clones) remain fully usable after
896 /// `flush()` returns; this is not a shutdown.
897 ///
898 /// If `flush()` fails (e.g. nonexistent parent directory), the error
899 /// surfaces in the `Result`. Drop will later try the same write and
900 /// fail the same way, logging via `tracing::error!`. The duplicate
901 /// signal is intentional — Drop must report what it sees.
902 ///
903 /// **Blocking I/O note:** the snapshot write is synchronous
904 /// (`std::fs::write` + `rename`) and runs inline on the caller. Hence
905 /// the sync signature — there is no `.await` inside. If you're calling
906 /// from a tokio task, this briefly blocks the runtime worker;
907 /// negligible for small snapshots.
908 pub fn flush(&self) -> Result<()> {
909 // Acquire the snapshot_path lock once and hold it across the
910 // write — the lock both gates the path slot and serializes the
911 // sync I/O so concurrent flushes don't clobber each other's
912 // staging tempfile. The path stays armed (we read, don't take)
913 // so subsequent flushes and the Drop safety net keep working.
914 let guard = self
915 .inner
916 .snapshot_path
917 .lock()
918 .unwrap_or_else(|p| p.into_inner());
919 if let Some(path) = guard.as_deref() {
920 self.inner.save_snapshot_locked(path)?;
921 }
922 Ok(())
923 }
924
925 /// Write a JSON snapshot of the in-memory backend to `path`.
926 ///
927 /// The write goes to `<path>.tmp` and then renames into place. On POSIX
928 /// the rename is atomic; on Windows it is not atomic when the
929 /// destination already exists. Returns
930 /// [`InstanceError::SnapshotNotSupported`] on any backend other than
931 /// the in-memory backend.
932 pub fn snapshot_to_path(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
933 let _guard = self
934 .inner
935 .snapshot_path
936 .lock()
937 .unwrap_or_else(|p| p.into_inner());
938 self.inner.save_snapshot_locked(path.as_ref())
939 }
940
941 /// Stash the snapshot path on the InstanceInternal so Drop / close can
942 /// find it. Only meaningful for in-memory backends — no-op for others.
943 fn set_snapshot_path(&self, path: Option<PathBuf>) {
944 if path.is_none() {
945 return;
946 }
947 // Poison-tolerant: a panic in another holder must not strand the
948 // Instance — the snapshot path is a simple swappable Option.
949 let mut guard = self
950 .inner
951 .snapshot_path
952 .lock()
953 .unwrap_or_else(|p| p.into_inner());
954 *guard = path;
955 }
956
957 /// Internal load-only implementation that works with any clock.
958 async fn open_impl(backend: Box<dyn BackendImpl>, clock: Arc<dyn Clock>) -> Result<Self> {
959 let backend: Arc<dyn BackendImpl> = Arc::from(backend);
960
961 // Strict: require existing InstanceMetadata. Initialisation is the
962 // caller's responsibility (`connect_or_create` / `connect_or_create_backend`).
963 let metadata = backend
964 .get_instance_metadata()
965 .await?
966 .ok_or(InstanceError::NotInitialized)?;
967
968 // Load secrets (contains the private key)
969 let secrets = backend.get_instance_secrets().await?;
970
971 // If secrets are present, verify they match the metadata
972 if let Some(ref secrets) = secrets {
973 let derived_id = secrets.signing_key.public_key();
974 if derived_id != metadata.id {
975 return Err(InstanceError::DeviceKeyMismatch.into());
976 }
977 }
978
979 // Existing backend: load from metadata + secrets
980 let inner = Arc::new(InstanceInternal {
981 backend: Arc::new(LocalBackend::new(backend)),
982 clock,
983 sync: std::sync::OnceLock::new(),
984 metadata,
985 secrets,
986 snapshot_path: Mutex::new(None),
987 write_callbacks: Mutex::new(HashMap::new()),
988 global_write_callbacks: Mutex::new(Vec::new()),
989 next_callback_id: AtomicU64::new(0),
990 tree_locks: Mutex::new(HashMap::new()),
991 });
992 Ok(Self { inner })
993 }
994
995 /// Load-only helper that accepts an already-arc'd backend and the
996 /// already-fetched metadata. Used by `connect_or_create_backend_impl`,
997 /// which has already inspected metadata to choose between the load
998 /// and bootstrap arms — passing it through avoids a redundant
999 /// `get_instance_metadata` round-trip.
1000 async fn open_impl_arc_with_metadata(
1001 backend: Arc<dyn BackendImpl>,
1002 clock: Arc<dyn Clock>,
1003 metadata: InstanceMetadata,
1004 ) -> Result<Self> {
1005 let secrets = backend.get_instance_secrets().await?;
1006 if let Some(ref secrets) = secrets {
1007 let derived_id = secrets.signing_key.public_key();
1008 if derived_id != metadata.id {
1009 return Err(InstanceError::DeviceKeyMismatch.into());
1010 }
1011 }
1012 let inner = Arc::new(InstanceInternal {
1013 backend: Arc::new(LocalBackend::new(backend)),
1014 clock,
1015 sync: std::sync::OnceLock::new(),
1016 metadata,
1017 secrets,
1018 snapshot_path: Mutex::new(None),
1019 write_callbacks: Mutex::new(HashMap::new()),
1020 global_write_callbacks: Mutex::new(Vec::new()),
1021 next_callback_id: AtomicU64::new(0),
1022 tree_locks: Mutex::new(HashMap::new()),
1023 });
1024 Ok(Self { inner })
1025 }
1026
1027 /// Internal create implementation. Returns the new `Instance` along with
1028 /// the just-bootstrapped initial `User`, materialised directly from the
1029 /// keys we generated (no redundant login round-trip).
1030 pub(crate) async fn create_internal(
1031 backend: Arc<dyn BackendImpl>,
1032 clock: Arc<dyn Clock>,
1033 initial: NewUser,
1034 ) -> Result<(Self, User)> {
1035 use crate::user::system_databases::{create_databases_tracking, create_users_database};
1036
1037 // 1. Generate device key
1038 let device_key = PrivateKey::generate();
1039 let device_id = device_key.public_key();
1040
1041 // 2. Create system databases with device_key passed directly
1042 // Create a temporary Instance for database creation (databases will store full IDs later)
1043 //
1044 // SAFETY: The temporary instance has empty users_db_id and databases_db_id placeholders.
1045 // This is safe because:
1046 // 1. We only use it to create new system databases via Database::create()
1047 // 2. Database::create() doesn't access the instance's system database IDs
1048 // 3. The system databases don't exist yet, so their IDs can't be referenced
1049 // 4. The temporary instance is only used during initial setup and discarded
1050 // 5. The real instance is constructed afterward with the correct database IDs
1051 let temp_instance = Self {
1052 inner: Arc::new(InstanceInternal {
1053 backend: Arc::new(LocalBackend::new(Arc::clone(&backend))),
1054 clock: Arc::clone(&clock),
1055 sync: std::sync::OnceLock::new(),
1056 metadata: InstanceMetadata {
1057 id: device_id.clone(),
1058 users_db: ID::default(), // Placeholder - system DBs don't exist yet
1059 databases_db: ID::default(), // Placeholder - system DBs don't exist yet
1060 sync_db: None,
1061 },
1062 secrets: Some(InstanceSecrets {
1063 signing_key: device_key.clone(),
1064 }),
1065 snapshot_path: Mutex::new(None),
1066 write_callbacks: Mutex::new(HashMap::new()),
1067 global_write_callbacks: Mutex::new(Vec::new()),
1068 next_callback_id: AtomicU64::new(0),
1069 tree_locks: Mutex::new(HashMap::new()),
1070 }),
1071 };
1072 let users_db = create_users_database(&temp_instance, &device_key).await?;
1073 let databases_db = create_databases_tracking(&temp_instance, &device_key).await?;
1074
1075 // 3. Save metadata and secrets (marks instance as initialized)
1076 // NB: Ordering matters. Secrets are stored first, then Metadata.
1077 // The presence of the Metadata indicates the instance is fully initialized.
1078 let secrets = InstanceSecrets {
1079 signing_key: device_key,
1080 };
1081 backend.set_instance_secrets(&secrets).await?;
1082
1083 let metadata = InstanceMetadata {
1084 id: device_id,
1085 users_db: users_db.root_id().clone(),
1086 databases_db: databases_db.root_id().clone(),
1087 sync_db: None,
1088 };
1089 backend.set_instance_metadata(&metadata).await?;
1090
1091 // 4. Build real instance
1092 let inner = Arc::new(InstanceInternal {
1093 backend: Arc::new(LocalBackend::new(backend)),
1094 clock,
1095 sync: std::sync::OnceLock::new(),
1096 metadata,
1097 secrets: Some(secrets),
1098 snapshot_path: Mutex::new(None),
1099 write_callbacks: Mutex::new(HashMap::new()),
1100 global_write_callbacks: Mutex::new(Vec::new()),
1101 next_callback_id: AtomicU64::new(0),
1102 tree_locks: Mutex::new(HashMap::new()),
1103 });
1104
1105 let instance = Self { inner };
1106
1107 // 5. Bootstrap the initial user. The first user created on an
1108 // instance is automatically promoted to Admin on the system
1109 // databases by `system_databases::create_user`'s
1110 // first-user-becomes-admin logic.
1111 let users_db = instance.users_db().await?;
1112 let (user_uuid, user_info, root_key) = crate::user::system_databases::create_user(
1113 &users_db,
1114 &instance,
1115 &initial.username,
1116 initial.password.as_deref(),
1117 )
1118 .await?;
1119
1120 // 6. Materialise the User session directly from the keys we just
1121 // generated — skips a redundant `login_user` round-trip that would
1122 // otherwise re-derive the encryption key from the password.
1123 let user = crate::user::system_databases::build_user_session(
1124 &instance,
1125 &user_uuid,
1126 &user_info,
1127 root_key,
1128 initial.password.as_deref(),
1129 )
1130 .await?;
1131
1132 Ok((instance, user))
1133 }
1134
1135 /// Get a reference to the backend seam.
1136 pub fn backend(&self) -> &Arc<dyn Backend> {
1137 &self.inner.backend
1138 }
1139
1140 /// The concrete in-process storage engine, or [`OperationNotSupported`] on
1141 /// a remote instance.
1142 ///
1143 /// Off-seam local-only operations (instance secrets, verification-status
1144 /// mutation, `all_roots`/`get_tree` raw dumps, scope-keyed cache) are
1145 /// performed through this accessor, so they are reachable only where a
1146 /// concrete local backend exists.
1147 ///
1148 /// [`OperationNotSupported`]: InstanceError::OperationNotSupported
1149 pub(crate) fn require_local_engine(&self) -> Result<Arc<dyn BackendImpl>> {
1150 self.inner.backend.local_engine().ok_or_else(|| {
1151 InstanceError::OperationNotSupported {
1152 operation: "local backend engine on remote instance".to_string(),
1153 }
1154 .into()
1155 })
1156 }
1157
1158 /// The remote connection backing this instance, if it was created via
1159 /// [`connect`](Self::connect). Returns `None` for local instances.
1160 ///
1161 /// Useful for constructing a [`Database`](crate::Database) that routes
1162 /// reads through the Database-level wire API while sharing the same
1163 /// connection and session as the instance's write path.
1164 #[cfg(all(unix, feature = "service"))]
1165 pub fn remote_connection(&self) -> Option<RemoteConnection> {
1166 self.inner.backend.remote_connection()
1167 }
1168
1169 /// Check if an entry exists in storage.
1170 pub async fn has_entry(&self, id: &ID) -> bool {
1171 self.inner.backend.get(id).await.is_ok()
1172 }
1173
1174 /// Check if a database is present locally.
1175 ///
1176 /// This differs from `has_entry` in that it checks for the active tracking
1177 /// of the database by the Instance. This method checks if we're tracking
1178 /// the database's tip state.
1179 pub async fn has_database(&self, root_id: &ID) -> bool {
1180 match self.inner.backend.snapshot(root_id).await {
1181 Ok(snap) => !snap.is_empty(),
1182 Err(_) => false,
1183 }
1184 }
1185
1186 /// Get a reference to the clock.
1187 ///
1188 /// The clock is used for timestamps in height calculations and peer tracking.
1189 pub(crate) fn clock(&self) -> &dyn Clock {
1190 &*self.inner.clock
1191 }
1192
1193 /// Get a cloned Arc of the clock.
1194 ///
1195 /// Used when passing the clock to components that need ownership (e.g., HeightCalculator).
1196 pub(crate) fn clock_arc(&self) -> Arc<dyn Clock> {
1197 self.inner.clock.clone()
1198 }
1199
1200 // === Backend pass-through methods (pub(crate) for internal use) ===
1201
1202 /// Get an entry from the backend
1203 pub(crate) async fn get(&self, id: &crate::entry::ID) -> Result<crate::entry::Entry> {
1204 self.inner.backend.get(id).await
1205 }
1206
1207 /// Put an entry into the backend. Always stored Unverified — see
1208 /// [`crate::backend::BackendImpl::put`].
1209 pub(crate) async fn put(&self, entry: crate::entry::Entry) -> Result<()> {
1210 self.inner.backend.put(entry).await
1211 }
1212
1213 /// Returns the current [`crate::Snapshot`] of `tree` — its DAG tips. See
1214 /// [`Database::snapshot`] for the public entry point.
1215 pub(crate) async fn snapshot(
1216 &self,
1217 tree: &crate::entry::ID,
1218 ) -> Result<crate::snapshot::Snapshot> {
1219 self.inner.backend.snapshot(tree).await
1220 }
1221
1222 // === System database accessors ===
1223
1224 /// Get the _users database
1225 ///
1226 /// This constructs a Database instance on-the-fly to avoid circular references.
1227 /// On a local instance the device signing key is attached so users-table
1228 /// writes (e.g., the local `create_user` path) can sign. On a remote
1229 /// instance the device key lives on the daemon side and isn't available
1230 /// locally, so no key is attached — the returned handle is read-only.
1231 /// Write paths on a remote instance must instead go through
1232 /// [`Instance::users_db_for_session`], which attaches the caller's
1233 /// session signing key (e.g. admin's key on the `InstanceAdmin`
1234 /// `create_user` path) and routes through `Database::open_remote`.
1235 pub(crate) async fn users_db(&self) -> Result<Database> {
1236 let db = Database::open(self, &self.inner.metadata.users_db).await?;
1237 #[cfg(all(unix, feature = "service"))]
1238 if self.remote_connection().is_some() {
1239 return Ok(db);
1240 }
1241 Ok(db.with_key(self.signing_key()?.clone()))
1242 }
1243
1244 /// Open the _users system database with a specific signing key (not the device
1245 /// key). Used by the admin-session paths
1246 /// ([`InstanceAdmin`](crate::user::InstanceAdmin), `User::admin_check`) on
1247 /// remote instances where the device key is unavailable.
1248 pub(crate) async fn users_db_for_session(&self, signing_key: &PrivateKey) -> Result<Database> {
1249 self.open_system_db_for_session(&self.inner.metadata.users_db, signing_key)
1250 .await
1251 }
1252
1253 /// Open a system database for an authenticated session.
1254 ///
1255 /// On a remote instance this routes every read through the connection's
1256 /// Database wire protocol ([`Database::open_remote`], a per-handle
1257 /// `RemoteBackend`), gated by the session key's identity — the plain
1258 /// [`Database::open`] path instead clones the instance's session backend,
1259 /// so on a connected instance its reads carry the connection's login
1260 /// identity. On a local instance it opens against the local backend as
1261 /// before. The signing key is attached for writes.
1262 pub(crate) async fn open_system_db_for_session(
1263 &self,
1264 root_id: &ID,
1265 signing_key: &PrivateKey,
1266 ) -> Result<Database> {
1267 #[cfg(all(unix, feature = "service"))]
1268 if let Some(conn) = self.remote_connection() {
1269 // The daemon gates per-tree reads against the acting pubkey
1270 // from the request's identity hint, and the hint here is
1271 // `signing_key.public_key()` (the caller's chosen identity for
1272 // this DB). The hint must be in the connection's session keyset
1273 // — register it now so subsequent reads through the returned
1274 // `RemoteBackend` are accepted.
1275 conn.register_session_key(signing_key).await?;
1276 let identity = SigKey::from_pubkey(&signing_key.public_key());
1277 return Ok(Database::open_remote(self, conn, root_id, identity)
1278 .await?
1279 .with_key(signing_key.clone()));
1280 }
1281 Ok(Database::open(self, root_id)
1282 .await?
1283 .with_key(signing_key.clone()))
1284 }
1285
1286 /// Get the _databases tracking database
1287 ///
1288 /// Parallel to `users_db()` — opens the instance's database-registry
1289 /// system DB with the device signing key attached. Used by the
1290 /// instance-admin bootstrap path (`system_databases::create_user`) to add
1291 /// the first user's pubkey as `Admin(0)` on the registry, so subsequent
1292 /// admin-gated instance ops (e.g., `SetInstanceMetadata`) can authorize
1293 /// against the user's key instead of the device key.
1294 pub(crate) async fn databases_db(&self) -> Result<Database> {
1295 Ok(Database::open(self, &self.inner.metadata.databases_db)
1296 .await?
1297 .with_key(self.signing_key()?.clone()))
1298 }
1299
1300 /// Root id of the `_databases` system DB.
1301 ///
1302 /// The service daemon uses this to gate admin-only ops
1303 /// (e.g., `SetInstanceMetadata`) against `_databases.auth_settings`:
1304 /// an instance admin is a user with `Admin` on `_databases`.
1305 pub(crate) fn databases_db_id(&self) -> &ID {
1306 &self.inner.metadata.databases_db
1307 }
1308
1309 /// Root id of the `_users` system DB.
1310 ///
1311 /// Parallel to `databases_db_id()`. Lets an instance admin open `_users`
1312 /// keyed by their own signing key (rather than the device key that
1313 /// `users_db()` attaches), so admin-gated edits to `_users.auth_settings`
1314 /// resolve against the admin's identity.
1315 pub(crate) fn users_db_id(&self) -> &ID {
1316 &self.inner.metadata.users_db
1317 }
1318
1319 // === User Management ===
1320
1321 /// Login a user with flexible password handling.
1322 ///
1323 /// Returns a User session object that provides access to user operations.
1324 /// For password-protected users, provide the password. For passwordless users, pass None.
1325 ///
1326 /// # Arguments
1327 /// * `user_id` - User identifier (username)
1328 /// * `password` - Optional password. None for passwordless users.
1329 ///
1330 /// # Returns
1331 /// A Result containing the User session
1332 pub async fn login_user(&self, user_id: &str, password: Option<&str>) -> Result<User> {
1333 // On a remote instance, the `TrustedLogin*` handshake authenticates the
1334 // socket connection AND ships back the user's full `UserInfo` plus the
1335 // decrypted root signing key. Build the `User` session from those —
1336 // the per-tree gate means a freshly-logged-in user with no permissions
1337 // on `_users` couldn't re-read it over the wire anyway, so we don't try.
1338 #[cfg(all(unix, feature = "service"))]
1339 if let Some(conn) = self.remote_connection() {
1340 let (user_uuid, user_info, signing_key) = conn.trusted_login(user_id, password).await?;
1341 return crate::user::system_databases::build_user_session(
1342 self,
1343 &user_uuid,
1344 &user_info,
1345 signing_key,
1346 password,
1347 )
1348 .await;
1349 }
1350
1351 use crate::user::system_databases::login_user;
1352 let users_db = self.users_db().await?;
1353 login_user(&users_db, self, user_id, password).await
1354 }
1355
1356 // === User-Sync Integration ===
1357
1358 // === Device Identity Management ===
1359 //
1360 // The Instance's public identity is stored in InstanceMetadata, and the private
1361 // signing key is stored in InstanceSecrets. Both are cached in memory.
1362
1363 /// Get the device signing key.
1364 ///
1365 /// # Internal Use Only
1366 ///
1367 /// This method provides direct access to the instance's cryptographic identity
1368 /// and is intended for internal operations that require the device key (sync,
1369 /// system database creation, authentication validation, etc.).
1370 ///
1371 /// These operations should only be performed by the server/instance administrator,
1372 /// but we don't verify that yet. Future versions may add admin permission checks.
1373 ///
1374 /// Similar to `Database::open` (without a key), this is a controlled escape hatch
1375 /// for internal library operations. Use with care - prefer User API for normal operations.
1376 ///
1377 /// Returns an error if this is a remote Instance that does not have access to the
1378 /// device key (e.g., connected via RPC where secrets are never transmitted).
1379 #[cfg(not(any(test, feature = "testing")))]
1380 pub(crate) fn signing_key(&self) -> Result<&PrivateKey> {
1381 self.inner
1382 .secrets
1383 .as_ref()
1384 .map(|s| &s.signing_key)
1385 .ok_or_else(|| InstanceError::DeviceKeyNotFound.into())
1386 }
1387
1388 /// Test-only: Get the device signing key.
1389 ///
1390 /// This is exposed for testing purposes only. In production, use the User API.
1391 ///
1392 /// Returns an error if this is a remote Instance that does not have access to the
1393 /// device key.
1394 #[cfg(any(test, feature = "testing"))]
1395 pub fn signing_key(&self) -> Result<&PrivateKey> {
1396 self.inner
1397 .secrets
1398 .as_ref()
1399 .map(|s| &s.signing_key)
1400 .ok_or_else(|| InstanceError::DeviceKeyNotFound.into())
1401 }
1402
1403 /// Get the instance identity (public key).
1404 ///
1405 /// # Returns
1406 /// The instance's public key identity.
1407 pub fn id(&self) -> PublicKey {
1408 self.inner.metadata.id.clone()
1409 }
1410
1411 // === Synchronization Management ===
1412 //
1413 // These methods provide access to the Sync module for managing synchronization
1414 // settings and state for this database instance.
1415
1416 /// Initializes the Sync module for this instance.
1417 ///
1418 /// Enables synchronization operations for this instance. This method is idempotent;
1419 /// calling it multiple times has no effect.
1420 ///
1421 /// # Errors
1422 /// Returns an error if the sync settings database cannot be created or if device key
1423 /// generation/storage fails.
1424 pub async fn enable_sync(&self) -> Result<()> {
1425 // Check if there is an existing Sync already loaded
1426 if self.inner.sync.get().is_some() {
1427 return Ok(());
1428 }
1429
1430 // A remote Instance must not run sync client-side: building a Sync
1431 // here would spin up a background sync engine that drives RPCs against
1432 // the daemon's backend — duplicating (and racing) the daemon's own
1433 // sync. Sync is owned by the process that owns the Instance.
1434 //
1435 // Return `Ok(())` so callers on a connected instance get the same
1436 // no-op success they would on a local instance where sync is already
1437 // running daemon-side. Long-term this should become an admin-gated
1438 // operation that lets a client ask the daemon to enable its sync
1439 // subsystem; until that ships, the client-side `enable_sync` is
1440 // intentionally a silent no-op because the daemon either already
1441 // has sync running or it doesn't, and the client can't change that.
1442 //
1443 // TODO(service): expose an admin-gated `enable_sync` on
1444 // `InstanceAdmin` so a client can enable sync remotely.
1445 #[cfg(all(unix, feature = "service"))]
1446 if self.remote_connection().is_some() {
1447 return Ok(());
1448 }
1449
1450 // Check InstanceMetadata for existing sync_db
1451 let metadata = self
1452 .backend()
1453 .get_instance_metadata()
1454 .await?
1455 .ok_or(InstanceError::DeviceKeyNotFound)?; // Metadata must exist if instance is initialized
1456
1457 let sync = if let Some(ref sync_db) = metadata.sync_db {
1458 // Load existing sync tree
1459 Sync::load(self.clone(), sync_db).await?
1460 } else {
1461 // Create new sync tree
1462 let sync = Sync::new(self.clone()).await?;
1463
1464 // Save sync_db to metadata
1465 let mut new_metadata = metadata;
1466 new_metadata.sync_db = Some(sync.sync_tree_root_id().clone());
1467 self.backend().set_instance_metadata(&new_metadata).await?;
1468
1469 sync
1470 };
1471
1472 let sync_arc = Arc::new(sync);
1473
1474 // Initialize the sync engine (no transports registered yet)
1475 // Users should call register_transport() to add transports
1476 sync_arc.start_background_sync()?;
1477
1478 // Sync wants to observe writes across *every* tree, including
1479 // trees created after this point — there's no fixed tree set to
1480 // register per-db callbacks against, so this is one of the few
1481 // legitimate uses of `register_global_write_callback`. Idempotent
1482 // because `enable_sync` early-returns at the top if sync is
1483 // already initialized (`self.inner.sync.get().is_some()`); without
1484 // that guard this would register a duplicate hook every call.
1485 let sync_for_callback = Arc::clone(&sync_arc);
1486 self.register_global_write_callback(move |event, database| {
1487 let sync = Arc::clone(&sync_for_callback);
1488 let event = event.clone();
1489 let database = database.clone();
1490 async move {
1491 if event.source() != WriteSource::Local {
1492 return Ok(());
1493 }
1494 // Local writes always carry exactly one entry today, but
1495 // the loop in `Sync::on_local_write` is intentionally
1496 // ready for future multi-entry local events.
1497 sync.on_local_write(&event, &database).await
1498 }
1499 });
1500
1501 let _ = self.inner.sync.set(sync_arc);
1502 Ok(())
1503 }
1504
1505 /// Get a reference to the Sync module.
1506 ///
1507 /// Returns a cheap-to-clone Arc handle to the Sync module. The Sync module
1508 /// uses interior mutability (AtomicBool and OnceLock) so &self methods are sufficient.
1509 ///
1510 /// # Returns
1511 /// An `Option` containing an `Arc<Sync>` if the Sync module is initialized.
1512 pub fn sync(&self) -> Option<Arc<Sync>> {
1513 self.inner.sync.get().map(Arc::clone)
1514 }
1515
1516 /// Flush all pending sync operations.
1517 ///
1518 /// This is a convenience method that processes all queued entries and
1519 /// retries any failed sends. If sync is not enabled, returns Ok(()).
1520 ///
1521 /// This is useful to force pending syncs to complete, e.g. on program shutdown.
1522 ///
1523 /// # Returns
1524 /// `Ok(())` if sync is not enabled or all operations completed successfully,
1525 /// or an error if sends failed.
1526 pub async fn flush_sync(&self) -> Result<()> {
1527 if let Some(sync) = self.sync() {
1528 sync.flush().await
1529 } else {
1530 Ok(())
1531 }
1532 }
1533
1534 // === Entry Write Coordination ===
1535 //
1536 // All entry writes go through Instance::put_entry() which handles backend storage
1537 // and callback dispatch. This centralizes write coordination and ensures hooks fire.
1538
1539 /// Register a per-database callback. Fires for writes to `tree_id` on
1540 /// this Instance.
1541 ///
1542 /// `initial_tips` seeds the callback's cursor — the first
1543 /// [`WriteEvent`] this callback receives will have `previous_tips`
1544 /// equal to `initial_tips`, and the cursor advances on each
1545 /// subsequent fire to that fire's post-write snapshot. Callers that
1546 /// want "tell me about everything after the point I just read at"
1547 /// pass the snapshot they just read; callers that want "tell me about
1548 /// everything from this empty cursor forward" can pass
1549 /// [`Snapshot::EMPTY`] (the first fire's `previous_tips` will be
1550 /// empty, and the subscriber walks the DAG to discover the gap).
1551 ///
1552 /// Returns the [`CallbackId`] of the registration. Callers wrap
1553 /// this in a [`WriteCallback`] handle (see
1554 /// [`Database::on_write_at_tips`]) to manage lifetime.
1555 pub(crate) fn register_write_callback<F, Fut>(
1556 &self,
1557 tree_id: ID,
1558 initial_tips: Snapshot,
1559 callback: F,
1560 ) -> CallbackId
1561 where
1562 F: for<'a> Fn(&'a WriteEvent, &'a Database) -> Fut + Send + std::marker::Sync + 'static,
1563 Fut: Future<Output = Result<()>> + Send + 'static,
1564 {
1565 let id = CallbackId(self.inner.next_callback_id.fetch_add(1, Ordering::Relaxed));
1566 let cb: AsyncWriteCallbackFn = Arc::new(move |event: &WriteEvent, database: &Database| {
1567 let fut = callback(event, database);
1568 Box::pin(fut) as AsyncWriteCallbackFuture
1569 });
1570 let entry = Arc::new(PerDbCallbackEntry {
1571 id,
1572 last_tips: std::sync::Mutex::new(initial_tips),
1573 callback: cb,
1574 });
1575 self.inner
1576 .write_callbacks
1577 .lock()
1578 .unwrap_or_else(|p| p.into_inner())
1579 .entry(tree_id)
1580 .or_default()
1581 .push(entry);
1582 id
1583 }
1584
1585 /// Register a non-removable callback fired for **every** write on **every**
1586 /// database for the life of the Instance.
1587 ///
1588 /// This is purpose-built for hooks that need to observe writes across
1589 /// the entire Instance — including writes to trees created *after* the
1590 /// hook is registered. The only legitimate use is something that
1591 /// genuinely doesn't know its target tree set up front: today, just
1592 /// sync (which wants to react to every local write so it can propagate
1593 /// to peers, and registers its hook once during `enable_sync`).
1594 ///
1595 /// **Not the right primitive for connection-scoped fan-out.** If you
1596 /// know up front which trees a consumer cares about (e.g. service
1597 /// clients subscribing per-tree via `DatabaseOp::SubscribeWrites`),
1598 /// register per-database callbacks with [`Self::register_write_callback`]
1599 /// instead. Per-db callbacks have a removal path
1600 /// ([`Self::remove_write_callback`]) which lets you tear them down on
1601 /// disconnect; this API does not.
1602 ///
1603 /// Callers branch on [`WriteEvent::source`] inside the closure if they
1604 /// only care about one source. Caller is responsible for idempotency
1605 /// (registering N times produces N firings); the canonical pattern is
1606 /// to guard the registration site with a `OnceLock` so it cannot run
1607 /// twice on the same Instance.
1608 pub(crate) fn register_global_write_callback<F, Fut>(&self, callback: F)
1609 where
1610 F: for<'a> Fn(&'a WriteEvent, &'a Database) -> Fut + Send + std::marker::Sync + 'static,
1611 Fut: Future<Output = Result<()>> + Send + 'static,
1612 {
1613 let id = CallbackId(self.inner.next_callback_id.fetch_add(1, Ordering::Relaxed));
1614 let cb: AsyncWriteCallbackFn = Arc::new(move |event: &WriteEvent, database: &Database| {
1615 let fut = callback(event, database);
1616 Box::pin(fut) as AsyncWriteCallbackFuture
1617 });
1618 self.inner
1619 .global_write_callbacks
1620 .lock()
1621 .unwrap_or_else(|p| p.into_inner())
1622 .push((id, cb));
1623 }
1624
1625 /// Remove a per-database callback by id. Returns `true` iff the
1626 /// removal emptied the per-tree callback list (i.e. this was the
1627 /// last live callback for `tree_id` on this Instance). No-op if
1628 /// the id isn't registered.
1629 ///
1630 /// Callers use the `true` return to drive lifecycle hooks on the
1631 /// connection's subscription state: dropping the last local
1632 /// callback for a tree on a connected instance is the trigger to
1633 /// transition the wire subscription to `Idle` (see
1634 /// [`crate::service::client::RemoteConnection::transition_to_idle`]).
1635 pub(crate) fn remove_write_callback(&self, tree_id: &ID, id: CallbackId) -> bool {
1636 let mut callbacks = self
1637 .inner
1638 .write_callbacks
1639 .lock()
1640 .unwrap_or_else(|p| p.into_inner());
1641 if let Some(vec) = callbacks.get_mut(tree_id) {
1642 let before = vec.len();
1643 vec.retain(|entry| entry.id != id);
1644 let removed = vec.len() < before;
1645 if vec.is_empty() {
1646 callbacks.remove(tree_id);
1647 return removed;
1648 }
1649 }
1650 false
1651 }
1652
1653 /// Whether any per-database write callback is currently registered for
1654 /// `tree_id`.
1655 ///
1656 /// Used by [`crate::service::client::RemoteConnection::transition_to_idle`]
1657 /// to re-check the registry while holding the subscription-state lock,
1658 /// so a registration racing the last callback's drop can't leave a live
1659 /// callback stranded on an `Idle` wire subscription.
1660 #[cfg(all(unix, feature = "service"))]
1661 pub(crate) fn has_write_callbacks(&self, tree_id: &ID) -> bool {
1662 self.inner
1663 .write_callbacks
1664 .lock()
1665 .unwrap_or_else(|p| p.into_inner())
1666 .contains_key(tree_id)
1667 }
1668
1669 /// Acquire (or create) the per-tree async lock that serializes the
1670 /// `snapshot` → backend write → callback dispatch sequence.
1671 ///
1672 /// Without this, two concurrent writers to the same tree both snapshot
1673 /// `previous_tips` before either writes, so the second callback's
1674 /// `previous_tips` would not reflect the first write — breaking the
1675 /// "diff against current tips" contract documented on [`WriteEvent`].
1676 pub(crate) fn tree_lock(&self, tree_id: &ID) -> Arc<tokio::sync::Mutex<()>> {
1677 let mut locks = self
1678 .inner
1679 .tree_locks
1680 .lock()
1681 .unwrap_or_else(|p| p.into_inner());
1682 Arc::clone(
1683 locks
1684 .entry(tree_id.clone())
1685 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))),
1686 )
1687 }
1688
1689 /// Write an entry to the backend and dispatch callbacks.
1690 ///
1691 /// This is the central coordination point for all entry writes in the system.
1692 /// All writes must go through this method to ensure:
1693 /// - Entries are persisted to the backend
1694 /// - Appropriate callbacks are triggered based on write source
1695 /// - Hooks have full context (entry, database, instance)
1696 ///
1697 /// Serialized per-tree against [`Self::put_remote_entries`] so
1698 /// [`WriteEvent::previous_tips`] is consistent.
1699 ///
1700 /// # Arguments
1701 /// * `tree_id` - The root ID of the database being written to
1702 /// * `verification` - Authentication verification status of the entry
1703 /// * `entry` - The entry to write
1704 /// * `source` - Whether this is a local or remote write
1705 ///
1706 /// # Returns
1707 /// A Result indicating success or failure
1708 pub async fn put_entry(
1709 &self,
1710 tree_id: &ID,
1711 verification: crate::backend::VerificationStatus,
1712 entry: Entry,
1713 source: WriteSource,
1714 ) -> Result<()> {
1715 let lock = self.tree_lock(tree_id);
1716 let _guard = lock.lock().await;
1717
1718 // 1. Capture tips before the write so callbacks know what changed.
1719 //
1720 // On a connected (remote) instance, the daemon owns the canonical
1721 // DAG and the client's local backend has nothing to read; reading
1722 // tips here would also gate against the *connection's* login pubkey
1723 // (not the per-DB acting identity from the Database handle), which
1724 // breaks the legitimate "create-a-tree-with-a-non-login-key" flow.
1725 // The client also skips firing callbacks locally on this path (see
1726 // step 3 below) — the daemon round-trips a `Notification::DatabaseWrite`
1727 // back with its own canonical `previous_tips` and we fire from
1728 // there instead.
1729 #[cfg(all(unix, feature = "service"))]
1730 let is_connected = self.remote_connection().is_some();
1731 #[cfg(not(all(unix, feature = "service")))]
1732 let is_connected = false;
1733
1734 let previous_tips = if is_connected {
1735 Snapshot::EMPTY
1736 } else {
1737 self.snapshot(tree_id).await?
1738 };
1739
1740 // 2. Persist to backend storage (and notify server for remote backends)
1741 self.backend()
1742 .write_entry(verification, entry.clone(), source)
1743 .await?;
1744
1745 // 3. Build event and fire callbacks — but only on a local
1746 // instance, and only for entries that arrive `Verified`.
1747 //
1748 // **Connected instance**: the daemon is the sole publisher of
1749 // write events. It fires its own callback registry when it
1750 // stores the entry, then pushes a `Notification::DatabaseWrite`
1751 // back to every subscribed client (including this one). Firing
1752 // here too would double-deliver. See `Database::on_write` for
1753 // the timing contract.
1754 //
1755 // **Unverified path**: skipped on purpose. Only a Verified write
1756 // *triggers* a fire. An entry that arrives `Unverified` (over the
1757 // wire as a `SubmitSignedEntry` body, or via sync as a remote
1758 // batch) is ingested silently here; the subsequent local
1759 // verification pass (the caller's responsibility to schedule)
1760 // decides whether it ever becomes a fire-eligible Verified entry,
1761 // and if so fires from there.
1762 //
1763 // This gates the *trigger*, not the bracket: the cursors below are
1764 // raw backend snapshots, so an Unverified or Failed entry that is
1765 // a raw tip still falls inside a subsequent event's bracket and
1766 // `ids_added` will enumerate it. Narrowing that to the Verified
1767 // frontier needs an incremental frontier first — `verified_frontier`
1768 // is an O(N) walk, too expensive on the per-commit path.
1769 // coding: raw-frontier cursors; switch to the Verified frontier
1770 // once an incremental one exists.
1771 let joins = if !is_connected && verification == VerificationStatus::Verified {
1772 // Compute the post-write snapshot for cursor advance. Cheap:
1773 // just re-read the backend snapshot post-put. Each per-callback
1774 // cursor advances to this value.
1775 // Propagate rather than defaulting: `Snapshot::EMPTY` is a
1776 // *meaningful* cursor ("no initial state"), not a neutral
1777 // fallback. Masking an error here would fire this event with
1778 // `post = EMPTY` (so `ids_added` reports nothing and sync's
1779 // global hook queues nothing — a silently unsynced commit) and
1780 // leave every per-callback cursor at EMPTY, replaying the full
1781 // history to every subscriber on the next event.
1782 let post_tips = self.snapshot(tree_id).await?;
1783 // Commit cursor advances + spawn under the tree lock (ordering),
1784 // but drain after dropping the guard (below) — a callback that
1785 // reads tips can re-enter `verify()` → `tree_lock` and would
1786 // deadlock against a still-held `_guard`. See
1787 // `Instance::spawn_write_callbacks`.
1788 Some(
1789 self.spawn_write_callbacks(tree_id, &previous_tips, &post_tips, source)
1790 .await,
1791 )
1792 } else {
1793 None
1794 };
1795
1796 // Release the per-tree lock before awaiting user callbacks.
1797 drop(_guard);
1798
1799 if let Some(mut joins) = joins {
1800 while joins.join_next().await.is_some() {}
1801 }
1802
1803 Ok(())
1804 }
1805
1806 /// Store a batch of remotely-received entries and fire callbacks once.
1807 ///
1808 /// This is the correct way to ingest entries from sync. All entries are
1809 /// persisted first, then callbacks fire exactly once with the full batch
1810 /// and the tips from before ingestion. This ensures:
1811 ///
1812 /// - The database is fully consistent when callbacks execute
1813 /// - Callbacks fire once per sync exchange, not once per entry
1814 /// - `previous_tips` lets consumers reconstruct exactly what changed
1815 ///
1816 /// Entries that fail to store are logged and skipped — remaining entries
1817 /// are still stored and callbacks still fire for whatever was persisted.
1818 /// Returns the number of entries that were successfully persisted.
1819 ///
1820 /// Serialized per-tree against [`Self::put_entry`] and other concurrent
1821 /// `put_remote_entries` calls so `previous_tips` is consistent across
1822 /// writers.
1823 ///
1824 /// Entries are stored as [`VerificationStatus::Unverified`] without
1825 /// exception: they arrive from outside this node's local validation pass,
1826 /// so this node has not verified them and a peer cannot assert that it
1827 /// did. A later local re-verification pass may promote them.
1828 ///
1829 /// # Arguments
1830 /// * `tree_id` - The root ID of the database receiving the batch
1831 /// * `entries` - The entries to ingest
1832 pub(crate) async fn put_remote_entries(
1833 &self,
1834 tree_id: &ID,
1835 entries: Vec<Entry>,
1836 ) -> Result<usize> {
1837 if entries.is_empty() {
1838 return Ok(0);
1839 }
1840
1841 // Store the batch under the tree lock; release before calling
1842 // `verify`, which acquires its own lock for the pass + fire.
1843 let stored_count = {
1844 let lock = self.tree_lock(tree_id);
1845 let _guard = lock.lock().await;
1846 let mut stored = 0usize;
1847 for entry in entries {
1848 match self.backend().put(entry.clone()).await {
1849 Ok(_) => stored += 1,
1850 Err(e) => tracing::error!(
1851 tree_id = %tree_id,
1852 entry_id = %entry.id(),
1853 "Failed to store remote entry: {}", e
1854 ),
1855 }
1856 }
1857 stored
1858 };
1859
1860 // Run verify inline. `Database::verify` walks the Unverified
1861 // region in O(K), promotes whatever can be settled, and fires
1862 // one batched `Verified` event for the promotions. Sync-ingest
1863 // subscribers see the promotion without needing to schedule
1864 // their own verify pass.
1865 if stored_count > 0 {
1866 Database::open(self, tree_id).await?.verify().await?;
1867 }
1868
1869 Ok(stored_count)
1870 }
1871
1872 /// Demote `entry_id` to [`VerificationStatus::Unverified`] and
1873 /// cascade the demotion to every `Verified` descendant in
1874 /// `tree_id`'s DAG.
1875 ///
1876 /// **Why the cascade.** The `Verified` set on a tree is
1877 /// prefix-closed: an entry is `Verified` only if every one of its
1878 /// ancestors is. Demoting an ancestor without also demoting its
1879 /// `Verified` descendants breaks that invariant, which means
1880 /// `Database::verify`'s targeted walk-from-tips cannot find the
1881 /// demoted entry — it's hidden behind a still-`Verified` descendant
1882 /// and would be stranded. The cascade restores the invariant.
1883 ///
1884 /// **Scope.** Today's only callers are tests (and the v0
1885 /// re-verification scenarios they exercise). Production code does
1886 /// not demote `Verified` → `Unverified` at all under the current
1887 /// verify implementation. If a future demotion path appears (e.g.
1888 /// retroactive settings-change-driven invalidation) it should route
1889 /// through this method.
1890 ///
1891 /// O(N) per call: walks `get_tree` to build a children index. Cheap
1892 /// for the test sizes this targets; not a hot path.
1893 ///
1894 /// Exposed publicly under `cfg(test)` and the `testing` feature so
1895 /// integration tests in the `it` crate can use it; otherwise
1896 /// `pub(crate)`-equivalent.
1897 #[cfg(any(test, feature = "testing"))]
1898 pub async fn demote_to_unverified(&self, tree_id: &ID, entry_id: &ID) -> Result<()> {
1899 self.demote_to_unverified_impl(tree_id, entry_id).await
1900 }
1901
1902 #[cfg(not(any(test, feature = "testing")))]
1903 pub(crate) async fn demote_to_unverified(&self, tree_id: &ID, entry_id: &ID) -> Result<()> {
1904 self.demote_to_unverified_impl(tree_id, entry_id).await
1905 }
1906
1907 async fn demote_to_unverified_impl(&self, tree_id: &ID, entry_id: &ID) -> Result<()> {
1908 use std::collections::{HashMap, HashSet, VecDeque};
1909 let backend = self.require_local_engine()?;
1910 let entries = backend.get_tree(tree_id).await?;
1911
1912 // Build children index from each entry's parents.
1913 let mut children: HashMap<ID, Vec<ID>> = HashMap::new();
1914 for entry in &entries {
1915 for p in entry.parents().unwrap_or_default() {
1916 children.entry(p).or_default().push(entry.id());
1917 }
1918 }
1919
1920 // BFS from the target. The target itself always gets demoted
1921 // (caller's intent); descendants get demoted only if they are
1922 // currently `Verified`. `Failed` or `Unverified` descendants
1923 // are left as-is — `Failed` is terminal, `Unverified` is
1924 // already at the target state.
1925 let mut queue: VecDeque<ID> = VecDeque::new();
1926 queue.push_back(entry_id.clone());
1927 let mut visited: HashSet<ID> = HashSet::new();
1928 while let Some(id) = queue.pop_front() {
1929 if !visited.insert(id.clone()) {
1930 continue;
1931 }
1932 let status = match backend.get_verification_status(&id).await {
1933 Ok(s) => s,
1934 Err(e) if e.is_not_found() => continue,
1935 Err(e) => return Err(e),
1936 };
1937 let is_target = id == *entry_id;
1938 if is_target || status == VerificationStatus::Verified {
1939 backend
1940 .update_verification_status(&id, VerificationStatus::Unverified)
1941 .await?;
1942 }
1943 if let Some(kids) = children.get(&id) {
1944 for kid in kids {
1945 queue.push_back(kid.clone());
1946 }
1947 }
1948 }
1949 Ok(())
1950 }
1951
1952 /// Dispatch callbacks for a write event.
1953 ///
1954 /// Per-database callbacks for `tree_id` get **per-callback events**:
1955 /// each callback's `previous_tips` is read from its own cursor and
1956 /// the cursor advances to `post_tips` synchronously around the
1957 /// fire. The cursor mutex is released before the user callback is
1958 /// awaited, so a slow callback does not stall other callbacks'
1959 /// cursor reads on a concurrent fire.
1960 ///
1961 /// **Per-callback dispatch is concurrent.** Cursor advancement
1962 /// happens synchronously in arrival order under each callback's
1963 /// own mutex, then every callback's closure is spawned on its own
1964 /// tokio task. A slow callback for one subscriber doesn't stall
1965 /// other subscribers' callbacks for the same event. The dispatcher
1966 /// awaits every spawned task before returning, so the per-tree
1967 /// dispatch worker's "this notification is finished" point still
1968 /// serialises against the next event on the same tree — the
1969 /// inter-event ordering contract documented on
1970 /// [`Database::on_write`](crate::Database::on_write) is preserved.
1971 ///
1972 /// Global callbacks fire with a single shared event whose
1973 /// `previous_tips` is the caller-supplied `previous_tips`
1974 /// argument — globals don't track per-tree cursors and continue
1975 /// to receive the pre-write tips view. Globals also dispatch
1976 /// concurrently across subscribers.
1977 ///
1978 /// `pub(crate)` so the service module's reader task can drive this
1979 /// directly when a `Notification::DatabaseWrite` arrives from the
1980 /// daemon — that path is the *sole* publisher on a connected
1981 /// instance.
1982 ///
1983 /// Convenience wrapper over [`Self::spawn_write_callbacks`]: spawns the
1984 /// dispatches and awaits them all. Use this from callers that do **not**
1985 /// hold a [`tree_lock`](Self::tree_lock) across the fire. Callers that
1986 /// hold the lock (the local `put_entry` path, `Database::verify`) must
1987 /// instead call `spawn_write_callbacks` under the lock, drop the guard,
1988 /// then drain the returned `JoinSet` — see that method's contract.
1989 pub(crate) async fn fire_write_callbacks(
1990 &self,
1991 tree_id: &ID,
1992 previous_tips: &Snapshot,
1993 post_tips: &Snapshot,
1994 source: WriteSource,
1995 ) {
1996 let mut joins = self
1997 .spawn_write_callbacks(tree_id, previous_tips, post_tips, source)
1998 .await;
1999 while joins.join_next().await.is_some() {}
2000 }
2001
2002 /// Phase 1 of callback dispatch: advance every callback's cursor and
2003 /// spawn its closure, returning the in-flight [`JoinSet`] **without
2004 /// awaiting it**.
2005 ///
2006 /// Splitting the dispatch in two lets a caller that holds a
2007 /// [`tree_lock`](Self::tree_lock) commit the cursor advances *under*
2008 /// the lock — which is what preserves event ordering against a
2009 /// concurrent writer — and then release the lock *before* awaiting the
2010 /// user closures.
2011 ///
2012 /// **Why the lock must be dropped before draining.** Awaiting a user
2013 /// callback while holding `tree_id`'s lock risks a reentrant deadlock: a
2014 /// callback that reads tips (`Database::snapshot` and friends) can trip
2015 /// the access-time auto-verify hook, which calls `Database::verify`,
2016 /// which acquires the very same `tree_lock`. The awaiting caller still
2017 /// holds it, and `verify` is waiting on the callback it spawned →
2018 /// circular wait.
2019 ///
2020 /// Everything this method does up to (and including) the callback
2021 /// *invocation* is synchronous — the cursor read/advance under each
2022 /// callback's own `std::Mutex`, and calling the callback to obtain its
2023 /// `'static` future, which runs only the closure's synchronous prefix (the
2024 /// service subscription's non-blocking `frame_tx.send`; for a plain
2025 /// `async move { … }` callback the prefix is empty). That prefix must not
2026 /// itself block on the tree lock, which no in-tree callback does. Only the
2027 /// returned future's `.await` can re-enter the lock, and that is what the
2028 /// caller drains lock-free. Running the invocation under the lock — rather
2029 /// than deferring it into the spawned task — is deliberate: it makes each
2030 /// callback's synchronous side effect commit in canonical cursor order, so
2031 /// concurrent same-tree writers can't reorder a subscriber's notification
2032 /// stream.
2033 ///
2034 /// [`JoinSet`]: tokio::task::JoinSet
2035 pub(crate) async fn spawn_write_callbacks(
2036 &self,
2037 tree_id: &ID,
2038 previous_tips: &Snapshot,
2039 post_tips: &Snapshot,
2040 source: WriteSource,
2041 ) -> tokio::task::JoinSet<()> {
2042 let per_db_callbacks = self
2043 .inner
2044 .write_callbacks
2045 .lock()
2046 .unwrap_or_else(|p| p.into_inner())
2047 .get(tree_id)
2048 .cloned();
2049
2050 let global_callbacks = self
2051 .inner
2052 .global_write_callbacks
2053 .lock()
2054 .unwrap_or_else(|p| p.into_inner())
2055 .clone();
2056
2057 let has_callbacks = per_db_callbacks.is_some() || !global_callbacks.is_empty();
2058 if !has_callbacks {
2059 return tokio::task::JoinSet::new();
2060 }
2061
2062 // Create a Database handle for the callbacks. `Database::open` does
2063 // not read tips / trip the auto-verify hook, so it is safe to await
2064 // here even when the caller holds this tree's lock.
2065 let database = match Database::open(self, tree_id).await {
2066 Ok(db) => db,
2067 Err(e) => {
2068 tracing::error!(tree_id = %tree_id, "Failed to open database for callbacks: {}", e);
2069 return tokio::task::JoinSet::new();
2070 }
2071 };
2072
2073 // Single JoinSet across per-db + global callbacks. Two things happen
2074 // synchronously, in arrival order, before any task is spawned — both
2075 // under the caller's tree lock:
2076 //
2077 // 1. Each callback's cursor read+advance (under its own std::Mutex),
2078 // so `previous_tips` brackets commit deterministically.
2079 // 2. The callback *invocation* itself. A callback returns a `'static`
2080 // future, so calling it runs the closure's synchronous prefix now,
2081 // in canonical order — e.g. the service subscription's
2082 // `frame_tx.send` (a non-blocking push whose future is a no-op).
2083 // Same-tree events therefore reach each subscriber's channel in
2084 // order even under concurrent writers; only the returned future's
2085 // async remainder runs concurrently on the drained set.
2086 let mut joins = tokio::task::JoinSet::new();
2087
2088 if let Some(callbacks) = per_db_callbacks {
2089 for entry in callbacks {
2090 let cb_previous = {
2091 let mut guard = entry
2092 .last_tips
2093 .lock()
2094 .unwrap_or_else(|poisoned| poisoned.into_inner());
2095 std::mem::replace(&mut *guard, post_tips.clone())
2096 };
2097 let event = WriteEvent {
2098 previous_tips: cb_previous,
2099 post_tips: post_tips.clone(),
2100 source,
2101 };
2102 // Invoke synchronously in cursor order; spawn only the tail. A
2103 // callback's ordering-critical side effect must live in this
2104 // synchronous prefix (before its future's first await) — that is
2105 // what keeps it under the tree lock and in canonical order. See
2106 // the `frame_tx.send` invariant in `service::server`'s
2107 // `SubscribeWrites` handler.
2108 let fut = (entry.callback)(&event, &database);
2109 let tree_id_for_cb = tree_id.clone();
2110 let cb_id = entry.id;
2111 joins.spawn(async move {
2112 if let Err(e) = fut.await {
2113 tracing::error!(
2114 tree_id = %tree_id_for_cb,
2115 source = ?source,
2116 callback_id = ?cb_id,
2117 "Per-database callback failed: {}", e
2118 );
2119 }
2120 });
2121 }
2122 }
2123
2124 // Globals fire with the shared pre-write tips (no per-callback cursor).
2125 // Invoked synchronously here too, in registration order, for the same
2126 // in-order-side-effect guarantee; only the async remainder is spawned.
2127 for (id, callback) in global_callbacks {
2128 let event = WriteEvent {
2129 previous_tips: previous_tips.clone(),
2130 post_tips: post_tips.clone(),
2131 source,
2132 };
2133 let fut = callback(&event, &database);
2134 let tree_id_for_cb = tree_id.clone();
2135 joins.spawn(async move {
2136 if let Err(e) = fut.await {
2137 tracing::error!(
2138 tree_id = %tree_id_for_cb,
2139 source = ?source,
2140 callback_id = ?id,
2141 "Global callback failed: {}", e
2142 );
2143 }
2144 });
2145 }
2146
2147 joins
2148 }
2149
2150 /// Downgrade to a weak reference.
2151 ///
2152 /// Creates a weak reference that does not prevent the Instance from being dropped.
2153 /// This is useful for preventing circular reference cycles in dependent objects.
2154 ///
2155 /// # Returns
2156 /// A `WeakInstance` that can be upgraded back to a strong reference.
2157 pub fn downgrade(&self) -> WeakInstance {
2158 WeakInstance {
2159 inner: Arc::downgrade(&self.inner),
2160 }
2161 }
2162}
2163
2164impl WeakInstance {
2165 /// Upgrade to a strong reference.
2166 ///
2167 /// Attempts to upgrade this weak reference to a strong `Instance` reference.
2168 /// Returns `None` if the Instance has already been dropped.
2169 ///
2170 /// # Returns
2171 /// `Some(Instance)` if the Instance still exists, `None` otherwise.
2172 ///
2173 /// # Example
2174 /// ```
2175 /// # use eidetica::{Instance, NewUser};
2176 /// # #[tokio::main]
2177 /// # async fn main() -> eidetica::Result<()> {
2178 /// let (instance, maybe_user) = Instance::connect_or_create(
2179 /// "memory://",
2180 /// NewUser::passwordless("alice"),
2181 /// ).await?;
2182 /// let user = maybe_user.expect("memory:// is always fresh");
2183 /// let weak = instance.downgrade();
2184 ///
2185 /// // Upgrade works while instance exists
2186 /// assert!(weak.upgrade().is_some());
2187 ///
2188 /// // User holds its own strong handle to the Instance — drop it too so
2189 /// // the weak upgrade can fail.
2190 /// drop(user);
2191 /// drop(instance);
2192 /// // Upgrade fails after instance is dropped
2193 /// assert!(weak.upgrade().is_none());
2194 /// # Ok(())
2195 /// # }
2196 /// ```
2197 pub fn upgrade(&self) -> Option<Instance> {
2198 self.inner.upgrade().map(|inner| Instance { inner })
2199 }
2200}
2201
2202// ============ URL-dispatch backend constructors ============
2203
2204#[cfg(feature = "sqlite")]
2205async fn open_sqlite_backend(url: &str) -> Result<Box<dyn BackendImpl>> {
2206 let backend = crate::backend::database::Sqlite::connect(url).await?;
2207 Ok(Box::new(backend))
2208}
2209
2210#[cfg(not(feature = "sqlite"))]
2211async fn open_sqlite_backend(_url: &str) -> Result<Box<dyn BackendImpl>> {
2212 Err(InstanceError::BackendUnavailable {
2213 scheme: "sqlite",
2214 missing_feature: "sqlite",
2215 }
2216 .into())
2217}
2218
2219#[cfg(feature = "postgres")]
2220async fn open_postgres_backend(url: &str) -> Result<Box<dyn BackendImpl>> {
2221 let backend = crate::backend::database::Postgres::connect(url).await?;
2222 Ok(Box::new(backend))
2223}
2224
2225#[cfg(not(feature = "postgres"))]
2226async fn open_postgres_backend(_url: &str) -> Result<Box<dyn BackendImpl>> {
2227 Err(InstanceError::BackendUnavailable {
2228 scheme: "postgres",
2229 missing_feature: "postgres",
2230 }
2231 .into())
2232}