Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

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}