Skip to main content

nedb_engine/
server.rs

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