Skip to main content

nedb_engine/
server.rs

1//! nedbd v2 HTTP server — same /v1/databases/* API surface as v1.
2//! Drop-in replacement: Vision, itsl_mirror, all existing clients work unchanged.
3//!
4//! Built on tokio + axum. Each database is opened once and held in an Arc<RwLock>.
5//! All write paths use the Db's internal atomic operations; the RwLock is only
6//! needed to protect the manager's HashMap (open/close operations), not individual
7//! document writes (which are lock-free at the content-addressed level).
8
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::sync::atomic::AtomicU64;
13
14use axum::{
15    extract::{Path as AxPath, State, Query as AxQuery},
16    http::{HeaderMap, StatusCode},
17    response::{IntoResponse, Response, sse::{Event, KeepAlive, Sse}},
18    routing::{delete, get, post},
19    Json, Router,
20};
21use dashmap::DashMap;
22use serde::Deserialize;
23use serde_json::{json, Value};
24use tokio::sync::{broadcast, RwLock};
25use tokio_stream::wrappers::BroadcastStream;
26use tokio_stream::StreamExt as _;
27
28use crate::db::Db;
29use crate::nql;
30use crate::store::Node;
31
32// ── Log channel — broadcast to all /events SSE subscribers ────────────────────
33
34const LOG_CHANNEL_CAP: usize = 512;
35const SUB_CHANNEL_CAP: usize = 256;
36
37// ── Subscription registry ─────────────────────────────────────────────────────
38// Maps (db_name, sub_id) → (nql_query, result_hash, event_sender)
39// After every write, all registered queries for that db are re-evaluated.
40// Diffs (added/removed/changed rows) are emitted as SSE events.
41
42type SubKey = (String, u64);  // (db_name, sub_id)
43type SubVal = (String, String, broadcast::Sender<String>);  // (nql, last_hash, tx)
44
45/// Send a timestamped log line to both stdout and all /events subscribers.
46macro_rules! nlog {
47    ($tx:expr, $($arg:tt)*) => {{
48        let line = format!($($arg)*);
49        println!("{}", line);
50        let _ = $tx.send(line);
51    }};
52}
53
54// ── Manager ───────────────────────────────────────────────────────────────────
55
56#[derive(Clone)]
57pub struct Manager {
58    inner:     Arc<RwLock<ManagerInner>>,
59    pub token: Option<String>,
60    /// Broadcast channel — every log line goes here; /events streams them.
61    pub log_tx: broadcast::Sender<String>,
62    /// Live query subscriptions: (db_name, sub_id) → (nql, last_hash, event_tx)
63    subs:    Arc<DashMap<SubKey, SubVal>>,
64    sub_ctr: Arc<AtomicU64>,
65    /// Natural-language planner. None unless built with --features cast AND
66    /// enabled at runtime; the whole feature is opt-in so a default nedbd
67    /// carries no model and no extra bytes.
68    #[cfg(feature = "cast")]
69    pub caster: Option<crate::cast::Caster>,
70}
71
72struct ManagerInner {
73    data_dir:    PathBuf,
74    dbs:         HashMap<String, Arc<Db>>,
75    tmk:         Option<[u8; 32]>,
76    memory_mode: bool,
77}
78
79impl Manager {
80    pub fn new(data_dir: &Path, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> Self {
81        let (log_tx, _) = broadcast::channel(LOG_CHANNEL_CAP);
82        Self {
83            inner: Arc::new(RwLock::new(ManagerInner {
84                data_dir: data_dir.to_path_buf(),
85                dbs:      HashMap::new(),
86                tmk,
87                memory_mode,
88            })),
89            token,
90            log_tx,
91            #[cfg(feature = "cast")]
92            caster: None,
93            subs:    Arc::new(DashMap::new()),
94            sub_ctr: Arc::new(AtomicU64::new(1)),
95        }
96    }
97
98    /// Register a live query subscription. Returns (sub_id, receiver).
99    fn subscribe(&self, db: &str, nql: String) -> (u64, broadcast::Receiver<String>) {
100        use std::sync::atomic::Ordering;
101        let id = self.sub_ctr.fetch_add(1, Ordering::Relaxed);
102        let (tx, rx) = broadcast::channel(SUB_CHANNEL_CAP);
103        self.subs.insert((db.to_string(), id), (nql, String::new(), tx));
104        (id, rx)
105    }
106
107    /// Unregister a subscription.
108    fn unsubscribe(&self, db: &str, sub_id: u64) {
109        self.subs.remove(&(db.to_string(), sub_id));
110    }
111
112    /// After a write: re-evaluate all subscriptions for `db`, emit diffs.
113    fn notify_subscribers(&self, db: &str, db_arc: &Arc<crate::db::Db>) {
114        let keys: Vec<SubKey> = self.subs.iter()
115            .filter(|e| e.key().0 == db)
116            .map(|e| e.key().clone())
117            .collect();
118
119        for key in keys {
120            if let Some(mut entry) = self.subs.get_mut(&key) {
121                let (nql, last_hash, tx) = entry.value_mut();
122                // Re-run the query
123                let rows = match crate::nql::query(db_arc, nql) {
124                    Ok((rows, _)) => rows,
125                    Err(_) => continue,
126                };
127                // Hash the result set
128                let new_hash = format!("{:?}", rows.iter().map(|r| r.to_string()).collect::<Vec<_>>());
129                if new_hash == *last_hash { continue; }
130                *last_hash = new_hash;
131                // Send the full current result as a diff event
132                let event = json!({
133                    "sub_id": key.1,
134                    "db":     &key.0,
135                    "nql":    nql.as_str(),
136                    "rows":   rows,
137                    "count":  rows.len(),
138                });
139                let _ = tx.send(event.to_string());
140            }
141        }
142    }
143
144    /// Open all existing databases in the data directory on startup.
145    pub async fn open_all(&self) -> anyhow::Result<()> {
146        let (data_dir, tmk, memory_mode) = {
147            let inner = self.inner.read().await;
148            (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
149        };
150        // In memory mode: nothing to open from disk — all DBs created on first write
151        if memory_mode { return Ok(()); }
152        if !data_dir.exists() {
153            std::fs::create_dir_all(&data_dir)?;
154            return Ok(());
155        }
156        let mut names = vec![];
157        for entry in std::fs::read_dir(&data_dir)? {
158            let entry = entry?;
159            if entry.file_type()?.is_dir() {
160                names.push(entry.file_name().to_string_lossy().to_string());
161            }
162        }
163        let log_tx = self.log_tx.clone();
164        let mut inner = self.inner.write().await;
165        for name in names {
166            let db_path = inner.data_dir.join(&name);
167            let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
168            match Db::open(&db_path, dek) {
169                Ok(db) => {
170                    nlog!(log_tx, "  [nedbd] opened database {:?}", name);
171                    let db_arc = Arc::new(db);
172                    Db::start_cold_scan(Arc::clone(&db_arc));
173                    // Flush MANIFEST every 1s in background — removes I/O from write path
174                    Db::start_manifest_ticker(Arc::clone(&db_arc), 1000);
175                    inner.dbs.insert(name, db_arc);
176                }
177                Err(e) => nlog!(log_tx, "  [nedbd] ERROR opening {:?}: {}", name, e),
178            }
179        }
180        Ok(())
181    }
182
183    async fn get_db(&self, name: &str) -> Option<Arc<Db>> {
184        self.inner.read().await.dbs.get(name).cloned()
185    }
186
187    async fn create_db(&self, name: &str) -> anyhow::Result<Arc<Db>> {
188        let (data_dir, tmk, memory_mode) = {
189            let inner = self.inner.read().await;
190            (inner.data_dir.clone(), inner.tmk, inner.memory_mode)
191        };
192        let db = if memory_mode {
193            // Pure in-memory — instant, no files
194            Arc::new(Db::in_memory())
195        } else {
196            let db_path = data_dir.join(name);
197            let dek = tmk.map(|k| crate::store::Dek::from_tmk(&k, name.as_bytes()));
198            let db = Arc::new(Db::open(&db_path, dek)?);
199            Db::start_cold_scan(Arc::clone(&db));
200            Db::start_manifest_ticker(Arc::clone(&db), 1000);
201            db
202        };
203        self.inner.write().await.dbs.insert(name.to_string(), db.clone());
204        Ok(db)
205    }
206
207    async fn drop_db(&self, name: &str) -> bool {
208        let db = self.inner.write().await.dbs.remove(name);
209        if let Some(db) = db {
210            // Flush manifest before dropping
211            db.flush_manifest_if_dirty();
212            let data_dir = self.inner.read().await.data_dir.clone();
213            let _ = std::fs::remove_dir_all(data_dir.join(name));
214            true
215        } else {
216            false
217        }
218    }
219
220    /// Flush all open databases (id-index WAL + MANIFEST) — call on graceful shutdown.
221    pub async fn flush_all(&self) {
222        let inner = self.inner.read().await;
223        for db in inner.dbs.values() {
224            db.flush_all();  // WAL + manifest
225        }
226    }
227
228    async fn names(&self) -> Vec<String> {
229        self.inner.read().await.dbs.keys().cloned().collect()
230    }
231
232    /// Emit a log line to stdout and all /events SSE subscribers.
233    pub fn log(&self, msg: impl Into<String>) {
234        let line = msg.into();
235        println!("{}", line);
236        let _ = self.log_tx.send(line);
237    }
238
239    fn check_auth(&self, headers: &HeaderMap) -> bool {
240        match &self.token {
241            None => true,
242            Some(required) => {
243                if let Some(auth) = headers.get("authorization") {
244                    if let Ok(s) = auth.to_str() {
245                        return s == format!("Bearer {}", required);
246                    }
247                }
248                false
249            }
250        }
251    }
252}
253
254// ── Error helpers ─────────────────────────────────────────────────────────────
255
256fn err(status: StatusCode, msg: &str) -> Response {
257    (status, Json(json!({"error": msg}))).into_response()
258}
259
260fn ok(body: Value) -> Response {
261    (StatusCode::OK, Json(body)).into_response()
262}
263
264/// Return (seq, head) — both O(1) reads from in-memory atomics/cache.
265/// The head is maintained incrementally by Db::put() and Db::delete()
266/// so we never recompute it from scratch on every response.
267fn db_seq_head(db: &Db) -> (u64, String) {
268    let seq  = db.seq.load(std::sync::atomic::Ordering::SeqCst);
269    let head = db.head();
270    (seq, head)
271}
272
273// ── Route handlers ────────────────────────────────────────────────────────────
274
275async fn health(State(mgr): State<Manager>) -> Response {
276    let names = mgr.names().await;
277    let inner = mgr.inner.read().await;
278    ok(json!({
279        "ok":        true,
280        "service":   "nedbd",
281        "version":   env!("CARGO_PKG_VERSION"),
282        "engine":    "dag",
283        "memory":    inner.memory_mode,
284        "databases": names,
285        "encrypted": inner.tmk.is_some(),
286    }))
287}
288
289async fn list_databases(State(mgr): State<Manager>, headers: HeaderMap) -> Response {
290    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
291    let names = mgr.names().await;
292    let summaries: Vec<Value> = {
293        let inner = mgr.inner.read().await;
294        names.iter().map(|n| {
295            if let Some(db) = inner.dbs.get(n) {
296                let (seq, head) = db_seq_head(db);
297                json!({"name": n, "seq": seq, "head": head, "collections": db.id_index.collections()})
298            } else {
299                json!({"name": n})
300            }
301        }).collect()
302    };
303    ok(json!({"databases": summaries}))
304}
305
306#[derive(Deserialize)]
307struct CreateDbBody { name: String }
308
309async fn create_database(
310    State(mgr): State<Manager>,
311    headers: HeaderMap,
312    Json(body): Json<CreateDbBody>,
313) -> Response {
314    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
315    if body.name.is_empty() { return err(StatusCode::BAD_REQUEST, "name is required"); }
316    match mgr.create_db(&body.name).await {
317        Ok(db) => {
318            let (seq, head) = db_seq_head(&db);
319            (StatusCode::CREATED, Json(json!({"database": {"name": body.name, "seq": seq, "head": head}}))).into_response()
320        }
321        Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
322    }
323}
324
325async fn get_database(
326    State(mgr): State<Manager>,
327    headers: HeaderMap,
328    AxPath(name): AxPath<String>,
329) -> Response {
330    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
331    match mgr.get_db(&name).await {
332        None => err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
333        Some(db) => {
334            let (seq, head) = db_seq_head(&db);
335            ok(json!({"name": name, "seq": seq, "head": head, "collections": db.id_index.collections()}))
336        }
337    }
338}
339
340async fn drop_database(
341    State(mgr): State<Manager>,
342    headers: HeaderMap,
343    AxPath(name): AxPath<String>,
344) -> Response {
345    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
346    let dropped = mgr.drop_db(&name).await;
347    ok(json!({"dropped": dropped}))
348}
349
350#[derive(Deserialize)]
351struct QueryBody { nql: String }
352
353// ── Natural-language planning (feature: cast) ─────────────────────────────────
354
355// Both fields are read only by the `cast`-enabled handler. The
356// `cfg(not(feature = "cast"))` stub still deserializes this body — so that a
357// malformed request is rejected as 400 before the 501, keeping the two builds
358// behaviourally consistent — but never looks at the values, which without this
359// attribute produces a dead_code warning on every default build.
360#[cfg_attr(not(feature = "cast"), allow(dead_code))]
361#[derive(Deserialize)]
362struct CastBody {
363    prompt: String,
364    /// Run the plan immediately. Defaults to FALSE on purpose: the endpoint hands
365    /// back a plan for review rather than executing a guess. A planner that
366    /// silently runs the wrong query is worse than one that admits uncertainty.
367    #[serde(default)]
368    execute: bool,
369}
370
371/// POST /v1/databases/:name/cast — turn a short English prompt into NQL.
372///
373/// The model only ever produces TEXT. Execution goes through the same
374/// `nql::query` path a hand-typed query uses, so there is no second code path
375/// with different validation.
376#[cfg(feature = "cast")]
377async fn cast_prompt(
378    State(mgr): State<Manager>,
379    headers: HeaderMap,
380    AxPath(name): AxPath<String>,
381    Json(body): Json<CastBody>,
382) -> Response {
383    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
384
385    let caster = match &mgr.caster {
386        Some(c) => c,
387        None => return err(
388            StatusCode::SERVICE_UNAVAILABLE,
389            "cast is not enabled; start nedbd with --cast (or NEDBD_CAST=1) \
390             and place model.cast in the data directory",
391        ),
392    };
393
394    let db = match mgr.get_db(&name).await {
395        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
396        Some(db) => db,
397    };
398    if body.prompt.trim().is_empty() {
399        return err(StatusCode::BAD_REQUEST, "prompt is required");
400    }
401
402    // The engine knows the real schema, so constrain against it. This is the
403    // whole reason the planner lives here instead of in a client.
404    let collections = db.id_index.collections();
405    let result = caster.cast_checked(&body.prompt, &collections);
406
407    // Validate by PARSING, not by pattern-matching the text. The parser is the
408    // only authority on whether something is runnable.
409    let parse_err = match nql::parse(&result.nql) {
410        Ok(_)  => None,
411        Err(e) => Some(e.to_string()),
412    };
413
414    let (seq, head) = db_seq_head(&db);
415    let mut out = json!({
416        "prompt":            body.prompt,
417        "nql":               result.nql,
418        "valid":             parse_err.is_none(),
419        "collection":        result.collection,
420        "collection_known":  result.collection_known,
421        "collections":       collections,
422        "executed":          false,
423        "seq":  seq,
424        "head": head,
425    });
426
427    // A literal the model invented rather than copied. Advisory, not fatal —
428    // the plan is well-formed and may be exactly right, so we surface it and
429    // let the caller judge. Warned-about-and-correct is a cost worth paying to
430    // avoid confidently-wrong-and-silent, which for an agent poisons every
431    // subsequent step. Absent from the response when there is nothing to say,
432    // so `"drift" in response` is a usable test.
433    if let Some(d) = &result.drift {
434        out["drift"] = json!(d);
435    }
436
437    if let Some(e) = parse_err {
438        // Report the failure WITH the offending text. Never swallow it into an
439        // empty result set — that reads as "no matching rows", which is a lie.
440        out["error"] = json!(format!("NQL error: {}", e));
441        return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
442    }
443
444    if !result.collection_known {
445        // Parses fine, but names a collection this database does not have. That
446        // is a model miss, not a user error, and it deserves to be said plainly
447        // rather than returning zero rows.
448        out["error"] = json!(format!(
449            "collection {:?} does not exist in {:?}",
450            result.collection.unwrap_or_default(), name
451        ));
452        return (StatusCode::UNPROCESSABLE_ENTITY, Json(out)).into_response();
453    }
454
455    if !body.execute {
456        return ok(out);
457    }
458
459    // Same executor as /query. No special path.
460    let nql_text = out["nql"].as_str().unwrap_or("").to_string();
461    match nql::query(&db, &nql_text) {
462        Ok((rows, count)) => {
463            out["executed"] = json!(true);
464            out["rows"]     = json!(rows);
465            out["count"]    = json!(count);
466            ok(out)
467        }
468        Err(e) => {
469            out["error"] = json!(format!("NQL error: {}", e));
470            (StatusCode::BAD_REQUEST, Json(out)).into_response()
471        }
472    }
473}
474
475/// Stub so the route table compiles identically with the feature off. Callers get
476/// a clear 501 instead of a 404, which would wrongly suggest the URL is wrong.
477#[cfg(not(feature = "cast"))]
478async fn cast_prompt(
479    State(_mgr): State<Manager>,
480    _headers: HeaderMap,
481    AxPath(_name): AxPath<String>,
482    Json(_body): Json<CastBody>,
483) -> Response {
484    err(
485        StatusCode::NOT_IMPLEMENTED,
486        "this nedbd was built without the `cast` feature; \
487         rebuild with --features cast to enable natural-language planning",
488    )
489}
490
491async fn query_database(
492    State(mgr): State<Manager>,
493    headers: HeaderMap,
494    AxPath(name): AxPath<String>,
495    Json(body): Json<QueryBody>,
496) -> Response {
497    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
498    let db = match mgr.get_db(&name).await {
499        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
500        Some(db) => db,
501    };
502    if body.nql.trim().is_empty() {
503        return err(StatusCode::BAD_REQUEST, "nql is required");
504    }
505    match nql::query(&db, &body.nql) {
506        Ok((rows, count)) => {
507            let (seq, head) = db_seq_head(&db);
508            ok(json!({"rows": rows, "count": count, "seq": seq, "head": head}))
509        }
510        Err(e) => err(StatusCode::BAD_REQUEST, &format!("NQL error: {}", e)),
511    }
512}
513
514#[derive(Deserialize)]
515struct PutBody {
516    coll:       String,
517    id:         String,
518    doc:        Value,
519    caused_by:  Option<Vec<serde_json::Value>>,
520    valid_from: Option<String>,
521    valid_to:   Option<String>,
522    #[allow(dead_code)] evidence:   Option<String>,
523    #[allow(dead_code)] confidence: Option<f64>,
524    #[allow(dead_code)] client:     Option<String>,
525    #[allow(dead_code)] nonce:      Option<u64>,
526    #[allow(dead_code)] idem:       Option<String>,
527}
528
529#[derive(Deserialize)]
530struct LinkBody {
531    frm: String,
532    rel: String,
533    to:  String,
534}
535
536async fn put_document(
537    State(mgr): State<Manager>,
538    headers: HeaderMap,
539    AxPath(name): AxPath<String>,
540    Json(body): Json<PutBody>,
541) -> Response {
542    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
543    let db = match mgr.get_db(&name).await {
544        None => {
545            // Auto-create database on first write
546            match mgr.create_db(&name).await {
547                Ok(db) => db,
548                Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
549            }
550        }
551        Some(db) => db,
552    };
553    // Block writes until background startup scan completes (cold start only).
554    // Reads and queries always proceed immediately.
555    if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
556        return err(StatusCode::SERVICE_UNAVAILABLE,
557            "database startup in progress — reads available, writes retry in a moment");
558    }
559    // Resolve caused_by items: accept hash strings (v2 native) OR seq integers (v1 compat).
560    let caused_by: Vec<String> = body.caused_by.unwrap_or_default()
561        .into_iter()
562        .filter_map(|v| match v {
563            serde_json::Value::String(s) => Some(s),
564            serde_json::Value::Number(n) => {
565                n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
566            }
567            _ => None,
568        })
569        .collect();
570    // Run synchronous file I/O (objects.write) on a blocking thread so concurrent
571    // PUTs don't serialize on the tokio async thread pool.
572    let coll = body.coll.clone();
573    let id   = body.id.clone();
574    let doc  = body.doc.clone();
575    let vf   = body.valid_from.clone();
576    let vt   = body.valid_to.clone();
577    let db2  = Arc::clone(&db);
578    let result = tokio::task::spawn_blocking(move || {
579        db2.put(&coll, &id, doc, caused_by, vf, vt)
580    }).await;
581    match result {
582        Err(join_err) => err(StatusCode::INTERNAL_SERVER_ERROR, &join_err.to_string()),
583        Ok(Err(e))    => err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
584        Ok(Ok(node))  => {
585            let (seq, head) = db_seq_head(&db);
586            mgr.notify_subscribers(&name, &db);
587            ok(json!({"ok": true, "doc": node_to_response(&node), "seq": seq, "head": head}))
588        }
589    }
590}
591
592fn node_to_response(node: &Node) -> Value {
593    json!({
594        "_id":   node.id,
595        "_hash": node.hash,
596        "_seq":  node.seq,
597        "_coll": node.coll,
598        "data":  node.data,
599    })
600}
601
602async fn link_document(
603    State(mgr): State<Manager>,
604    headers: HeaderMap,
605    AxPath(name): AxPath<String>,
606    Json(body): Json<LinkBody>,
607) -> Response {
608    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
609    let db = match mgr.get_db(&name).await {
610        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
611        Some(db) => db,
612    };
613    if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
614        return err(StatusCode::SERVICE_UNAVAILABLE, "startup scan in progress");
615    }
616    match db.link(&body.frm, &body.rel, &body.to) {
617        Ok(()) => {
618            let (seq, head) = db_seq_head(&db);
619            ok(json!({"ok": true, "frm": body.frm, "rel": body.rel, "to": body.to, "seq": seq, "head": head}))
620        }
621        Err(e) => err(StatusCode::BAD_REQUEST, &e.to_string()),
622    }
623}
624
625async fn delete_document(
626    State(mgr): State<Manager>,
627    headers: HeaderMap,
628    AxPath((name, coll, id)): AxPath<(String, String, String)>,
629) -> Response {
630    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
631    let db = match mgr.get_db(&name).await {
632        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
633        Some(db) => db,
634    };
635    // v2 DAG: tombstone write + id index removal — doc history is preserved in the DAG,
636    // but the live id pointer is cleared so queries and list() never return the doc.
637    let existed = match db.delete(&coll, &id) {
638        Ok(v)  => v,
639        Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
640    };
641    let (seq, head) = db_seq_head(&db);
642    ok(json!({"ok": existed, "seq": seq, "head": head}))
643}
644
645#[derive(Deserialize)]
646struct BatchOp {
647    op:  String,
648    coll: Option<String>,
649    id:  Option<String>,
650    doc: Option<Value>,
651    caused_by: Option<Vec<serde_json::Value>>,
652}
653#[derive(Deserialize)]
654struct BatchBody { ops: Vec<BatchOp> }
655
656async fn batch_operations(
657    State(mgr): State<Manager>,
658    headers: HeaderMap,
659    AxPath(name): AxPath<String>,
660    Json(body): Json<BatchBody>,
661) -> Response {
662    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
663    let db = match mgr.get_db(&name).await {
664        None => match mgr.create_db(&name).await {
665            Ok(db) => db,
666            Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
667        },
668        Some(db) => db,
669    };
670
671    if !db.startup_ready.load(std::sync::atomic::Ordering::SeqCst) {
672        return err(StatusCode::SERVICE_UNAVAILABLE,
673            "database startup in progress — reads available, writes retry in a moment");
674    }
675
676    // Split ops into puts (parallelisable) and deletes (sequential)
677    // Puts go through put_batch for parallel object + index writes.
678    // Deletes remain sequential (tombstone ordering matters).
679    let mut put_ops = vec![];
680    let mut del_ops: Vec<(String, String)> = vec![];
681    let mut op_order: Vec<(&str, usize)> = vec![];  // ("put"|"del", index into respective vec)
682
683    for op in &body.ops {
684        let t = op.op.to_lowercase();
685        match t.as_str() {
686            "put" => {
687                // Resolve caused_by items: accept hash strings (v2 native) OR seq integers (v1 compat).
688                let caused_by: Vec<String> = op.caused_by.clone().unwrap_or_default()
689                    .into_iter()
690                    .filter_map(|v| match v {
691                        serde_json::Value::String(s) => Some(s),
692                        serde_json::Value::Number(n) => {
693                            n.as_u64().and_then(|seq| db.get_hash_by_seq(seq))
694                        }
695                        _ => None,
696                    })
697                    .collect();
698                op_order.push(("put", put_ops.len()));
699                put_ops.push((
700                    op.coll.clone().unwrap_or_default(),
701                    op.id.clone().unwrap_or_default(),
702                    op.doc.clone().unwrap_or(json!({})),
703                    caused_by,
704                    None::<String>,
705                    None::<String>,
706                ));
707            }
708            "del" | "delete" => {
709                op_order.push(("del", del_ops.len()));
710                del_ops.push((
711                    op.coll.clone().unwrap_or_default(),
712                    op.id.clone().unwrap_or_default(),
713                ));
714            }
715            _ => { op_order.push(("unknown", 0)); }
716        }
717    }
718
719    // Execute all puts in parallel via put_batch
720    let put_results = if put_ops.is_empty() {
721        vec![]
722    } else {
723        match db.put_batch(put_ops) {
724            Ok(nodes) => nodes.into_iter().map(|n| json!({"op":"put","id":n.id,"seq":n.seq,"hash":n.hash})).collect(),
725            Err(e)    => return err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
726        }
727    };
728
729    // Execute deletes sequentially
730    let del_results: Vec<serde_json::Value> = del_ops.iter().map(|(coll, id)| {
731        match db.delete(coll, id) {
732            Ok(existed) => json!({"op":"del","id":id,"ok":existed}),
733            Err(e)      => json!({"op":"del","id":id,"error":e.to_string()}),
734        }
735    }).collect();
736
737    // Reconstruct results in original op order
738    let mut results = vec![];
739    for (kind, idx) in &op_order {
740        let r = match *kind {
741            "put"     => put_results.get(*idx).cloned().unwrap_or(json!({"op":"put","error":"missing"})),
742            "del"     => del_results.get(*idx).cloned().unwrap_or(json!({"op":"del","error":"missing"})),
743            _         => json!({"op": kind, "error": "unknown op"}),
744        };
745        results.push(r);
746    }
747    let (seq, head) = db_seq_head(&db);
748    // Notify live query subscribers after batch completes
749    mgr.notify_subscribers(&name, &db);
750    ok(json!({"results": results, "count": results.len(), "seq": seq, "head": head}))
751}
752
753#[derive(Deserialize)]
754struct IndexBody { coll: String, field: String, kind: Option<String> }
755
756async fn create_index(
757    State(mgr): State<Manager>,
758    headers: HeaderMap,
759    AxPath(name): AxPath<String>,
760    Json(body): Json<IndexBody>,
761) -> Response {
762    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
763    let db = match mgr.get_db(&name).await {
764        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
765        Some(db) => db,
766    };
767    let kind = body.kind.as_deref().unwrap_or("eq");
768    match kind {
769        "sorted" | "eq" => {
770            db.create_sorted_index(&body.coll, &body.field);
771            ok(json!({"ok": true, "coll": body.coll, "field": body.field, "kind": kind}))
772        }
773        _ => err(StatusCode::BAD_REQUEST, &format!("unknown index kind: {}", kind)),
774    }
775}
776
777async fn verify_database(
778    State(mgr): State<Manager>,
779    headers: HeaderMap,
780    AxPath(name): AxPath<String>,
781) -> Response {
782    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
783    let db = match mgr.get_db(&name).await {
784        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
785        Some(db) => db,
786    };
787    let (ok_count, tampered) = db.verify();
788    let (seq, head) = db_seq_head(&db);
789    ok(json!({
790        "ok": tampered.is_empty(),
791        "seq": seq,
792        "head": head,
793        "tamper_evident": true,
794        "objects_checked": ok_count,
795        "tampered": tampered,
796    }))
797}
798
799async fn checkpoint(
800    State(mgr): State<Manager>,
801    headers: HeaderMap,
802    AxPath(name): AxPath<String>,
803) -> Response {
804    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
805    let db = match mgr.get_db(&name).await {
806        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
807        Some(db) => db,
808    };
809    let (seq, head) = db_seq_head(&db);
810    // v2 DAG is always "checkpointed" — content-addressed objects are inherently snapshotted
811    ok(json!({"ok": true, "head": head, "seq": seq}))
812}
813
814#[derive(Deserialize)]
815struct LogQuery { limit: Option<usize> }
816
817async fn get_log(
818    State(mgr): State<Manager>,
819    headers: HeaderMap,
820    AxPath(name): AxPath<String>,
821    AxQuery(q): AxQuery<LogQuery>,
822) -> Response {
823    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
824    let db = match mgr.get_db(&name).await {
825        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
826        Some(db) => db,
827    };
828    let limit = q.limit.unwrap_or(50);
829    // v2: reconstruct log from objects (most recent first)
830    let mut log_entries: Vec<Value> = db.objects.all_hashes()
831        .filter_map(|h| db.objects.read(&h).ok())
832        .take(limit)
833        .map(|n| json!({
834            "seq": n.seq, "coll": n.coll, "id": n.id,
835            "hash": n.hash, "ts": n.ts, "op": "put"
836        }))
837        .collect();
838    log_entries.sort_by(|a, b|
839        b["seq"].as_u64().cmp(&a["seq"].as_u64())
840    );
841    log_entries.truncate(limit);
842    let (seq, head) = db_seq_head(&db);
843    ok(json!({"log": log_entries, "seq": seq, "head": head}))
844}
845
846// ── tip / since — GET /v1/databases/:name/{tip,since} ─────────────────────────
847// tip()   = the most recent write (head of the log), O(1).
848// since() = the changefeed: every write after ?after_seq (exclusive), ascending.
849// Both return full nodes (Node: Serialize), alongside the current seq + head.
850
851async fn tip_database(
852    State(mgr): State<Manager>,
853    headers: HeaderMap,
854    AxPath(name): AxPath<String>,
855) -> Response {
856    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
857    let db = match mgr.get_db(&name).await {
858        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
859        Some(db) => db,
860    };
861    let (seq, head) = db_seq_head(&db);
862    let tip = db.tip().map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
863    ok(json!({"tip": tip, "seq": seq, "head": head}))
864}
865
866// Collection-local tip — GET /v1/databases/:name/collections/:coll/tip.
867async fn tip_collection_database(
868    State(mgr): State<Manager>,
869    headers: HeaderMap,
870    AxPath((name, coll)): AxPath<(String, String)>,
871) -> Response {
872    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
873    let db = match mgr.get_db(&name).await {
874        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
875        Some(db) => db,
876    };
877    let (seq, head) = db_seq_head(&db);
878    let tip = db.tip_collection(&coll).map(|n| serde_json::to_value(&n).unwrap_or(Value::Null));
879    ok(json!({"coll": coll, "tip": tip, "seq": seq, "head": head}))
880}
881
882#[derive(Deserialize)]
883struct SinceQuery { after_seq: Option<u64>, limit: Option<usize> }
884
885async fn since_database(
886    State(mgr): State<Manager>,
887    headers: HeaderMap,
888    AxPath(name): AxPath<String>,
889    AxQuery(q): AxQuery<SinceQuery>,
890) -> Response {
891    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
892    let db = match mgr.get_db(&name).await {
893        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
894        Some(db) => db,
895    };
896    let after = q.after_seq.unwrap_or(0);
897    let b = db.since(after, q.limit.unwrap_or(0));
898    let nodes: Vec<Value> = b.nodes.iter()
899        .map(|n| serde_json::to_value(n).unwrap_or(Value::Null))
900        .collect();
901    let (seq, head) = db_seq_head(&db);
902    ok(json!({
903        "nodes": nodes, "count": nodes.len(),
904        "from_seq": b.from_seq, "to_seq": b.to_seq, "head_seq": b.head_seq, "has_more": b.has_more,
905        "seq": seq, "head": head
906    }))
907}
908
909// Replication readiness — GET /v1/databases/:name/status. scan_complete is the
910// hard gate for correctness-critical catch-up (see Db::scan_status).
911async fn status_database(
912    State(mgr): State<Manager>,
913    headers: HeaderMap,
914    AxPath(name): AxPath<String>,
915) -> Response {
916    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
917    let db = match mgr.get_db(&name).await {
918        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
919        Some(db) => db,
920    };
921    let s = db.scan_status();
922    ok(json!({
923        "ok": true,
924        "scan_complete":   s.scan_complete,
925        "tip_seq":         s.tip_seq,
926        "indexed_seq_min": s.indexed_seq_min,
927        "indexed_seq_max": s.indexed_seq_max,
928        "indexed_count":   s.indexed_count
929    }))
930}
931
932// ── Live query subscriptions — POST /v1/databases/:name/subscribe ─────────────
933
934#[derive(Deserialize)]
935struct SubscribeBody { nql: String }
936
937async fn subscribe_query(
938    State(mgr): State<Manager>,
939    headers: HeaderMap,
940    AxPath(name): AxPath<String>,
941    Json(body): Json<SubscribeBody>,
942) -> Response {
943    if !mgr.check_auth(&headers) {
944        return err(StatusCode::UNAUTHORIZED, "unauthorized");
945    }
946    let db = match mgr.get_db(&name).await {
947        None => return err(StatusCode::NOT_FOUND, &format!("database not found: {}", name)),
948        Some(db) => db,
949    };
950
951    let (sub_id, rx) = mgr.subscribe(&name, body.nql.clone());
952
953    // Send the initial query result immediately as the first SSE event
954    if let Ok((rows, _)) = crate::nql::query(&db, &body.nql) {
955        let init = json!({
956            "sub_id": sub_id,
957            "db":     &name,
958            "nql":    &body.nql,
959            "rows":   rows,
960            "count":  rows.len(),
961            "event":  "initial",
962        });
963        // Update last_hash so we don't re-send this on the next write if unchanged
964        if let Some(mut entry) = mgr.subs.get_mut(&(name.clone(), sub_id)) {
965            let hash = format!("{:?}", rows);
966            entry.value_mut().1 = hash;
967        }
968        // Send the initial result through the channel
969        if let Some(entry) = mgr.subs.get(&(name.clone(), sub_id)) {
970            let _ = entry.value().2.send(init.to_string());
971        }
972    }
973
974    let stream = BroadcastStream::new(rx).filter_map(|msg| {
975        match msg {
976            Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
977            Err(_)   => None,
978        }
979    });
980    Sse::new(stream)
981        .keep_alive(KeepAlive::default())
982        .into_response()
983}
984
985async fn unsubscribe_query(
986    State(mgr): State<Manager>,
987    headers: HeaderMap,
988    AxPath((name, sub_id)): AxPath<(String, u64)>,
989) -> Response {
990    if !mgr.check_auth(&headers) { return err(StatusCode::UNAUTHORIZED, "unauthorized"); }
991    mgr.unsubscribe(&name, sub_id);
992    ok(json!({"ok": true, "sub_id": sub_id}))
993}
994
995// ── SSE log stream — GET /events ──────────────────────────────────────────────
996
997async fn log_events(State(mgr): State<Manager>) -> Sse<impl futures_core::Stream<Item = Result<Event, std::convert::Infallible>>> {
998    let rx = mgr.log_tx.subscribe();
999    let stream = BroadcastStream::new(rx).filter_map(|msg| {
1000        match msg {
1001            Ok(line) => Some(Ok::<Event, std::convert::Infallible>(Event::default().data(line))),
1002            Err(_)   => None,  // lagged — skip
1003        }
1004    });
1005    Sse::new(stream).keep_alive(KeepAlive::default())
1006}
1007
1008// ── Router ────────────────────────────────────────────────────────────────────
1009
1010pub fn router(mgr: Manager) -> Router {
1011    Router::new()
1012        .route("/health",                                        get(health))
1013        .route("/events",                                        get(log_events))
1014        .route("/v1/databases",                                  get(list_databases).post(create_database))
1015        .route("/v1/databases/:name",                            get(get_database).delete(drop_database))
1016        .route("/v1/databases/:name/query",                      post(query_database))
1017        .route("/v1/databases/:name/cast",                       post(cast_prompt))
1018        .route("/v1/databases/:name/put",                        post(put_document))
1019        .route("/v1/databases/:name/link",                       post(link_document))
1020        .route("/v1/databases/:name/rows/:coll/:id",             delete(delete_document))
1021        .route("/v1/databases/:name/batch",                      post(batch_operations))
1022        .route("/v1/databases/:name/index",                      post(create_index))
1023        .route("/v1/databases/:name/verify",                     get(verify_database))
1024        .route("/v1/databases/:name/checkpoint",                 post(checkpoint))
1025        .route("/v1/databases/:name/log",                        get(get_log))
1026        .route("/v1/databases/:name/tip",                        get(tip_database))
1027        .route("/v1/databases/:name/collections/:coll/tip",      get(tip_collection_database))
1028        .route("/v1/databases/:name/since",                      get(since_database))
1029        .route("/v1/databases/:name/status",                     get(status_database))
1030        .route("/v1/databases/:name/subscribe",                  post(subscribe_query))
1031        .route("/v1/databases/:name/subscribe/:sub_id",          delete(unsubscribe_query))
1032        .with_state(mgr)
1033}
1034
1035/// Start the nedbd v2 server.
1036pub async fn run(host: &str, port: u16, data_dir: &str, tmk: Option<[u8; 32]>, token: Option<String>, memory_mode: bool) -> anyhow::Result<()> {
1037    // `mut` is required by the cast block below, which assigns mgr.caster. With
1038    // the feature off nothing mutates it, so an unconditional `mut` warns on
1039    // every default build — and warnings people are used to seeing are warnings
1040    // people stop reading.
1041    #[cfg(feature = "cast")]
1042    let mut mgr = Manager::new(Path::new(data_dir), tmk, token, memory_mode);
1043    #[cfg(not(feature = "cast"))]
1044    let mgr = Manager::new(Path::new(data_dir), tmk, token, memory_mode);
1045
1046    mgr.open_all().await?;
1047
1048    // Load the natural-language planner if this build has the feature AND the
1049    // operator asked for it. Failure to load is reported loudly but is NOT fatal:
1050    // a missing model should not stop a database from serving queries.
1051    #[cfg(feature = "cast")]
1052    {
1053        let want = std::env::var("NEDBD_CAST").map(|v| v == "1").unwrap_or(false);
1054        if want {
1055            match crate::cast::Caster::load(Path::new(data_dir)) {
1056                Ok(c) => {
1057                    println!("  cast     enabled — {:.2}M params, vocab {}, {}",
1058                             c.n_params() as f64 / 1e6, c.vocab_size(), c.source());
1059                    mgr.caster = Some(c);
1060                }
1061                Err(e) => {
1062                    eprintln!("  cast     DISABLED — {}", e);
1063                }
1064            }
1065        }
1066    }
1067    // Freeze it: nothing past this point should mutate the manager. Only
1068    // meaningful in the cast build, where `mgr` was declared `mut` above.
1069    #[cfg(feature = "cast")]
1070    let mgr = mgr;
1071
1072    let has_token = mgr.token.is_some();
1073    let mgr_for_shutdown = mgr.clone();
1074    let app = router(mgr);
1075    let addr = format!("{}:{}", host, port).parse::<std::net::SocketAddr>()?;
1076    let banner = format!(r#"
10771078          ╱ ╲               N E D B  ·  DAG ENGINE  {}
1079         ◆   ◆              ─────────────────────────────────────────────
1080        ╱ ╲ ╱ ╲             content-addressed · tamper-evident · causal
1081       ◆   ◆   ◆            bi-temporal · replay-protected · encrypted
1082      ╱ ╲ ╱ ╲ ╱ ╲
1083     ◆   ◆   ◆   ◆          © INTERCHAINED, LLC  ×  Vex (Claude Sonnet 4.6)
1084    ╱ ╲ ╱ ╲ ╱ ╲ ╱ ╲         interchained.org   ·   hyperagent.com/refer/J2G6TCD7
1085
1086  ─────────────────────────────────────────────────────────────
1087  listen   http://{}
1088  data     {}
1089  enc      {}
1090  token    {}
1091  memory   {}
1092  ─────────────────────────────────────────────────────────────
1093"#,
1094        env!("CARGO_PKG_VERSION"),
1095        addr,
1096        data_dir,
1097        if tmk.is_some() { "AES-256-GCM" } else { "off" },
1098        if has_token { "on" } else { "off (set NEDBD_TOKEN to require auth)" },
1099        if memory_mode { "yes — all data lost on exit (NEDBD_MEMORY=1)" } else { "no — durable DAG on disk" }
1100    );
1101    print!("{}", banner);
1102
1103    let listener = tokio::net::TcpListener::bind(addr).await?;
1104
1105    // ── Scheduled hourly checkpoint ────────────────────────────────────────────
1106    // Flush MANIFEST every hour aligned to the system clock (top of the hour).
1107    // Ensures warm-start data is always fresh even on long-running servers.
1108    let mgr_hourly = mgr_for_shutdown.clone();
1109    tokio::spawn(async move {
1110        loop {
1111            // Sleep until the next top-of-hour boundary
1112            let now_secs = std::time::SystemTime::now()
1113                .duration_since(std::time::UNIX_EPOCH)
1114                .map(|d| d.as_secs()).unwrap_or(0);
1115            let secs_into_hour = now_secs % 3600;
1116            let sleep_secs = 3600 - secs_into_hour;
1117            tokio::time::sleep(tokio::time::Duration::from_secs(sleep_secs)).await;
1118            mgr_hourly.flush_all().await;
1119            println!("  [nedbd] hourly checkpoint — manifests flushed");
1120        }
1121    });
1122
1123    // ── Graceful shutdown: SIGINT (Ctrl+C) + SIGTERM (systemctl stop) ─────────
1124    let shutdown = async {
1125        #[cfg(unix)]
1126        {
1127            use tokio::signal::unix::{signal, SignalKind};
1128            let mut sigterm = signal(SignalKind::terminate()).unwrap();
1129            let mut sigint  = signal(SignalKind::interrupt()).unwrap();
1130            tokio::select! {
1131                _ = sigterm.recv() => println!("  [nedbd] SIGTERM — flushing and exiting..."),
1132                _ = sigint.recv()  => println!("  [nedbd] SIGINT  — flushing and exiting..."),
1133            }
1134        }
1135        #[cfg(not(unix))]
1136        {
1137            tokio::signal::ctrl_c().await.ok();
1138            println!("  [nedbd] shutting down — flushing manifests...");
1139        }
1140    };
1141
1142    axum::serve(listener, app)
1143        .tcp_nodelay(true)
1144        .with_graceful_shutdown(shutdown)
1145        .await?;
1146
1147    // Final flush on exit
1148    mgr_for_shutdown.flush_all().await;
1149    println!("  [nedbd] goodbye");
1150    Ok(())
1151}