nedb-engine 2.5.2 → 2.5.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,15 +20,21 @@ One Rust core → ships to **PyPI** and **npm** from a single source.
20
20
 
21
21
  ---
22
22
 
23
- ## NEDB v2.4.468 — Production Stable
23
+ ## NEDB v2.5.34 — Production Stable
24
24
 
25
- **Current stable: 2.4.468** — NEDB now ships as **three version-aligned distributions** on one tag — `nedb-engine` (flagship), `crypto-database` (verifiable v2/v3 DAG), and `aof-db` (fast append-only) — across npm / PyPI / crates.io with full mac + linux + windows native addons (see [**Releasing**](#releasing) below). On the engine side it remains a polish release on the complete cross-platform line. The `nedbd-v2` daemon now does **real CLI parsing** — `--dag-v3`, `--data`, `--fast-fsync`, `--help`, `--version` are recognized flags instead of being silently swallowed as the positional data dir — and `npm test` ships a **cinematic native smoke test** that tours v1→v2 migration, the v2 DAG, the v3 segment store, and a causal-provenance audit. All native wheels (Linux + Windows on GitHub Actions; macOS arm64 + x86_64 on Codemagic M2 Mac Minis) **plus** the universal pure-Python wheel ship from a single `v*` tag, with the `nedbd-v2` binary bundled inside `pip install nedb-engine`.
25
+ **Current stable: 2.5.34** — NEDB ships as **three version-aligned distributions** on one tag — `nedb-engine` (flagship), `crypto-database` (verifiable v2/v3 DAG), and `aof-db` (fast append-only) — across npm / PyPI / crates.io with full mac + linux + windows native addons (see [**Releasing**](#releasing) below). On the engine side it remains a polish release on the complete cross-platform line. The `nedbd-v2` daemon now does **real CLI parsing** — `--dag-v3`, `--data`, `--fast-fsync`, `--help`, `--version` are recognized flags instead of being silently swallowed as the positional data dir — and `npm test` ships a **cinematic native smoke test** that tours v1→v2 migration, the v2 DAG, the v3 segment store, and a causal-provenance audit. All native wheels (Linux + Windows on GitHub Actions; macOS arm64 + x86_64 on Codemagic M2 Mac Minis) **plus** the universal pure-Python wheel ship from a single `v*` tag, with the `nedbd-v2` binary bundled inside `pip install nedb-engine`.
26
+
27
+ **New in 2.5.x:**
28
+
29
+ - **Durable-mode auto-flush-on-exit** — a durable store flushes buffered writes on `Ctrl+C` / `SIGTERM`, not just on a clean `Drop`. Automatic in the Node and Python bindings; `Db::install_exit_flush(Arc<Db>)` for standalone Rust binaries. See [**docs/DURABILITY.md**](docs/DURABILITY.md).
30
+ - **`nedb-cli`** — operate on a store directory offline (`head` · `status` · `verify` · `get` · `scan` · `flush` · `repair` · `export`), and **`nedb-inspector`** — a deterministic (no-regex, no-LLM) checker that warns when a durable open lacks flush-on-exit wiring. See [**docs/CLI.md**](docs/CLI.md).
31
+ - **Replication contract** — `tip()` (the latest write), a bounded `since()` changefeed, and a `scan_status()` readiness gate. See [**docs/REPLICATION.md**](docs/REPLICATION.md).
26
32
 
27
33
  **The v3 storage line — consolidated, spec'd, and (as of 2.4.3) cleanly published across every platform.** It makes the NEDB **v3 segment/pack object store** a first-class, fully-documented feature:
28
34
 
29
35
  - **`--dag-v3`** (opt-in) — append-only segment store: one `fsync` per group-commit, `.idx` sidecars, compaction, non-destructive dual-read. Took a real itcd chainstate flush from *minutes* to **~1.3 s**. Parsed as a real flag by `nedbd-v2` as of v2.4.3 (or set `NEDB_DAG_V3=1`). (See the v3 section below.)
30
36
  - **`NEDB_FAST_FSYNC`** — macOS fast-fsync: a plain `fsync(2)` instead of `F_FULLFSYNC` (default off; no-op on Linux/Windows).
31
- - Durable **flush-on-close**, a **Windows-safe id-index** (percent-encodes filesystem-unsafe ids), and idempotent object re-writes — shipped across the 2.3.3xxx line.
37
+ - Durable **flush-on-close** — and, as of 2.5.x, **flush-on-exit** on `Ctrl+C` / `SIGTERM` (see [**docs/DURABILITY.md**](docs/DURABILITY.md)) — a **Windows-safe id-index** (percent-encodes filesystem-unsafe ids), and idempotent object re-writes.
32
38
  - **`docs/SPEC.md` §3** now formally specifies the v2 object store, the v3 substrate, and the durability model.
33
39
 
34
40
  NEDB v2 replaces the append-only log (AOF) with a **content-addressed Merkle DAG**. Every document version is an immutable, BLAKE2b-verified object. Nothing is ever overwritten. As of **v2.2.31**, restarts after the first open are **O(1) warm starts** (driven by a `MANIFEST` of `seq` + Merkle head), the **cold scan is deferred** so the daemon accepts connections immediately, and a new **`GET /events` SSE endpoint** streams scan progress + per-write events live.
@@ -235,6 +241,10 @@ nedbd --log-level 2 # 0=errors 1=requests 2=deploy 3=verbo
235
241
  curl http://127.0.0.1:7070/events
236
242
  ```
237
243
 
244
+ ### Companion CLIs
245
+
246
+ Alongside the daemon, `cargo install nedb-engine` ships **`nedb-cli`** — operate on a store directory offline (`head`/`status`/`verify`/`get`/`scan`/`flush`/`repair`/`export`) — and **`nedb-inspector`**, a deterministic checker that warns when a durable open lacks flush-on-exit wiring. Full reference: [**docs/CLI.md**](docs/CLI.md).
247
+
238
248
  ### Startup modes (v2.2.31)
239
249
 
240
250
  - **Warm start** — every restart after the first open reads the `MANIFEST` file and restores `seq` + Merkle `head` in **O(1)**. No scan, no replay, independent of dataset size. Boots in milliseconds.
package/index.d.ts CHANGED
@@ -9,6 +9,11 @@ export declare class NedbCore {
9
9
  /**
10
10
  * Open a durable v2 DAG database at `path`.
11
11
  * Automatically migrates v1 AOF → v2 DAG on first open.
12
+ *
13
+ * Durable-mode auto-flush-on-exit is wired in the JS wrapper via
14
+ * `process.on('SIGTERM'|'SIGINT'|'beforeExit', () => db.flush())` — the
15
+ * libuv-cooperative hook — NOT a C-level signal handler here, which would
16
+ * clobber libuv's own signal machinery.
12
17
  */
13
18
  static open(path: string): NedbCore
14
19
  createIndex(coll: string, field: string, kind: string): void
@@ -39,8 +44,21 @@ export declare class NedbCore {
39
44
  */
40
45
  tip(): string | null
41
46
  /**
42
- * Changefeed: every write AFTER `after_seq` (exclusive), ascending, each as a
43
- * JSON string. Pass the seq you last saw to stream only new writes.
47
+ * Collection-local tip — the most recent write into `coll` as a JSON string,
48
+ * or null if the collection has no writes. Resume one chain without filtering.
44
49
  */
45
- since(afterSeq: bigint): Array<string>
50
+ tipCollection(coll: string): string | null
51
+ /**
52
+ * Changefeed page after `after_seq` (exclusive), up to `limit` nodes (0 = the
53
+ * engine default cap), as a JSON envelope string:
54
+ * `{nodes, from_seq, to_seq, head_seq, has_more}`. Page while `has_more`,
55
+ * advancing your cursor to `to_seq`, then attach to the live subscribe edge.
56
+ */
57
+ since(afterSeq: bigint, limit: number): string
58
+ /**
59
+ * Replication readiness as a JSON string: `{scan_complete, tip_seq,
60
+ * indexed_seq_min, indexed_seq_max, indexed_count}`. Wait for
61
+ * `scan_complete == true` before trusting historical `since()` catch-up.
62
+ */
63
+ scanStatus(): string
46
64
  }
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ // nedb-inspector — deterministic embedder-code checker for NEDB durability.
3
+ //
4
+ // nedb-inspector.mjs <pathToTarget.(rs|js|mjs|ts|py)> [more paths...]
5
+ //
6
+ // It reads how a program EMBEDS NEDB and warns LOUDLY, with the exact correct
7
+ // pattern, when a durable database is opened without flush-on-exit wiring — the
8
+ // "flush on every put? no; lose data on Ctrl+C? also no" mistake. It is the
9
+ // guardrail for the durable-mode auto-flush-on-exit contract.
10
+ //
11
+ // DETERMINISTIC BY DESIGN — no regex, no AI/LLM:
12
+ // 1. A per-language lexer masks comment and string contents (so a `Db::open`
13
+ // inside a comment or a string literal is NEVER matched — the classic
14
+ // regex false-positive), while preserving byte offsets and line numbers.
15
+ // 2. Structural token matching over the masked code finds durable-open call
16
+ // sites and the wiring calls that make them safe.
17
+ // 3. A fixed rule table decides OK / INFO / WARN and prints the exact fix.
18
+ //
19
+ // Exit code: 0 = clean (no warnings), 1 = warnings found, 2 = usage/read error.
20
+ // Set NO_COLOR=1 for plain output. Importable: `import { inspect } from './nedb-inspector.mjs'`.
21
+ //
22
+ // © INTERCHAINED LLC × Claude Opus 4.8
23
+
24
+ import { readFileSync } from 'node:fs';
25
+ import { fileURLToPath } from 'node:url';
26
+ import path from 'node:path';
27
+
28
+ // ── presentation ─────────────────────────────────────────────────────────────
29
+ const COLOR = !process.env.NO_COLOR && (process.stdout.isTTY ?? false);
30
+ const sgr = (c) => (s) => (COLOR ? `\x1b[${c}m${s}\x1b[0m` : String(s));
31
+ const bold = sgr('1'), dim = sgr('2'), red = sgr('31'), green = sgr('32');
32
+ const yellow = sgr('33'), cyan = sgr('36'), magenta = sgr('35');
33
+
34
+ // ── language table ─────────────────────────────────────────────────────────
35
+ const LANGS = {
36
+ rust: { exts: ['.rs'], line: ['//'], block: [['/*', '*/']], nestBlock: true,
37
+ quotes: ['"'], rustRaw: true },
38
+ js: { exts: ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts', '.cts'],
39
+ line: ['//'], block: [['/*', '*/']], nestBlock: false,
40
+ quotes: ['"', "'", '`'] },
41
+ py: { exts: ['.py', '.pyi'], line: ['#'], block: [], nestBlock: false,
42
+ quotes: ['"', "'"], pyTriple: true },
43
+ };
44
+
45
+ function langForFile(file) {
46
+ const ext = path.extname(file).toLowerCase();
47
+ for (const [name, cfg] of Object.entries(LANGS)) if (cfg.exts.includes(ext)) return name;
48
+ return null;
49
+ }
50
+
51
+ const isIdent = (ch) => ch !== undefined && (
52
+ (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
53
+ (ch >= '0' && ch <= '9') || ch === '_');
54
+
55
+ // ── lexer: return { masked, strings } ────────────────────────────────────────
56
+ // `masked` is `src` with every comment and string body replaced by spaces
57
+ // (newlines preserved, so offsets and line numbers are unchanged). `strings` is
58
+ // the list of decoded-ish string-literal bodies with their start line — used to
59
+ // see import specifiers (which the mask would otherwise blank out).
60
+ function maskSource(src, lang) {
61
+ const cfg = LANGS[lang];
62
+ const out = new Array(src.length);
63
+ for (let i = 0; i < src.length; i++) out[i] = src[i] === '\n' ? '\n' : ' ';
64
+ const strings = [];
65
+ let i = 0;
66
+ const n = src.length;
67
+ let line = 1;
68
+ const lineAt = []; // offset -> line (built lazily below is overkill; track inline)
69
+ const bump = (from, to) => { for (let k = from; k < to; k++) if (src[k] === '\n') line++; };
70
+
71
+ const starts = (tok, at) => src.startsWith(tok, at);
72
+
73
+ while (i < n) {
74
+ const ch = src[i];
75
+
76
+ // line comments
77
+ let matchedLine = null;
78
+ for (const lc of cfg.line) if (starts(lc, i)) { matchedLine = lc; break; }
79
+ if (matchedLine) {
80
+ while (i < n && src[i] !== '\n') i++;
81
+ continue;
82
+ }
83
+
84
+ // block comments (with optional nesting for rust)
85
+ let matchedBlock = null;
86
+ for (const [open, close] of cfg.block) if (starts(open, i)) { matchedBlock = [open, close]; break; }
87
+ if (matchedBlock) {
88
+ const [open, close] = matchedBlock;
89
+ let depth = 1;
90
+ bump(i, i + open.length); i += open.length;
91
+ while (i < n && depth > 0) {
92
+ if (cfg.nestBlock && starts(open, i)) { depth++; bump(i, i + open.length); i += open.length; continue; }
93
+ if (starts(close, i)) { depth--; bump(i, i + close.length); i += close.length; continue; }
94
+ if (src[i] === '\n') line++;
95
+ i++;
96
+ }
97
+ continue;
98
+ }
99
+
100
+ // python triple-quoted strings
101
+ if (cfg.pyTriple && (starts('"""', i) || starts("'''", i))) {
102
+ const q = src.substr(i, 3);
103
+ const startLine = line;
104
+ bump(i, i + 3); i += 3;
105
+ let body = '';
106
+ while (i < n && !starts(q, i)) { if (src[i] === '\n') line++; body += src[i]; i++; }
107
+ bump(i, i + 3); i += 3;
108
+ strings.push({ value: body, line: startLine });
109
+ continue;
110
+ }
111
+
112
+ // rust raw strings: r"...", r#"..."#, r##"..."##
113
+ if (cfg.rustRaw && ch === 'r' && !isIdent(src[i - 1])) {
114
+ let j = i + 1, hashes = 0;
115
+ while (src[j] === '#') { hashes++; j++; }
116
+ if (src[j] === '"') {
117
+ const startLine = line;
118
+ const closeTok = '"' + '#'.repeat(hashes);
119
+ bump(i, j + 1); i = j + 1;
120
+ let body = '';
121
+ while (i < n && !starts(closeTok, i)) { if (src[i] === '\n') line++; body += src[i]; i++; }
122
+ bump(i, i + closeTok.length); i += closeTok.length;
123
+ strings.push({ value: body, line: startLine });
124
+ continue;
125
+ }
126
+ }
127
+
128
+ // ordinary quoted strings (with backslash escapes)
129
+ if (cfg.quotes.includes(ch)) {
130
+ const q = ch;
131
+ const startLine = line;
132
+ i++; // opening quote
133
+ let body = '';
134
+ while (i < n && src[i] !== q) {
135
+ if (src[i] === '\\') { if (src[i + 1] === '\n') line++; i += 2; body += ' '; continue; }
136
+ if (src[i] === '\n') { line++; if (q === '`') { body += '\n'; i++; continue; } else break; }
137
+ body += src[i]; i++;
138
+ }
139
+ if (src[i] === q) i++; // closing quote
140
+ strings.push({ value: body, line: startLine });
141
+ continue;
142
+ }
143
+
144
+ if (ch === '\n') line++;
145
+ out[i] = ch; // keep code char
146
+ i++;
147
+ }
148
+ return { masked: out.join(''), strings };
149
+ }
150
+
151
+ // ── offset → line map ────────────────────────────────────────────────────────
152
+ function lineMapper(src) {
153
+ const starts = [0];
154
+ for (let i = 0; i < src.length; i++) if (src[i] === '\n') starts.push(i + 1);
155
+ return (off) => {
156
+ // binary search
157
+ let lo = 0, hi = starts.length - 1;
158
+ while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid] <= off) lo = mid; else hi = mid - 1; }
159
+ return lo + 1;
160
+ };
161
+ }
162
+
163
+ // Find call sites of `pattern` (a literal token sequence like "Db::open",
164
+ // ".open", "new NedbCore") in masked code: the sequence must appear with a
165
+ // non-identifier left boundary (unless it starts with a non-ident char) and be
166
+ // followed — after optional whitespace — by "(". Returns 1-based line numbers.
167
+ function findCallSites(masked, pattern, toLine) {
168
+ const lines = [];
169
+ const first = pattern[0];
170
+ const needLeftBoundary = isIdent(first);
171
+ let from = 0;
172
+ for (;;) {
173
+ const idx = masked.indexOf(pattern, from);
174
+ if (idx === -1) break;
175
+ from = idx + pattern.length;
176
+ if (needLeftBoundary && isIdent(masked[idx - 1])) continue;
177
+ // right boundary of the final identifier char, then optional ws, then "("
178
+ let k = idx + pattern.length;
179
+ const lastCharIsIdent = isIdent(pattern[pattern.length - 1]);
180
+ if (lastCharIsIdent && isIdent(masked[k])) continue; // e.g. "opener(" when seeking ".open"
181
+ while (k < masked.length && (masked[k] === ' ' || masked[k] === '\t' || masked[k] === '\n' || masked[k] === '\r')) k++;
182
+ if (masked[k] === '(') lines.push(toLine(idx));
183
+ }
184
+ return lines;
185
+ }
186
+
187
+ // Presence of a bare identifier token (full-word) anywhere in masked code.
188
+ function hasIdent(masked, name) {
189
+ let from = 0;
190
+ for (;;) {
191
+ const idx = masked.indexOf(name, from);
192
+ if (idx === -1) return false;
193
+ from = idx + name.length;
194
+ if (!isIdent(masked[idx - 1]) && !isIdent(masked[idx + name.length])) return true;
195
+ }
196
+ }
197
+
198
+ // Does `NEDB(` (python pure engine) carry an argument (→ durable path) rather
199
+ // than being empty `NEDB()` (→ in-memory)? Checked on the RAW source, one char
200
+ // past the paren, so a blanked string arg still counts.
201
+ function pyNedbHasArg(src, masked, toLine) {
202
+ const lines = [];
203
+ let from = 0;
204
+ for (;;) {
205
+ const idx = masked.indexOf('NEDB', from);
206
+ if (idx === -1) break;
207
+ from = idx + 4;
208
+ if (isIdent(masked[idx - 1]) || isIdent(masked[idx + 4])) continue;
209
+ let k = idx + 4;
210
+ while (masked[k] === ' ' || masked[k] === '\t') k++;
211
+ if (masked[k] !== '(') continue;
212
+ k++;
213
+ while (k < src.length && (src[k] === ' ' || src[k] === '\t' || src[k] === '\n' || src[k] === '\r')) k++;
214
+ if (src[k] !== ')') lines.push(toLine(idx)); // has an argument
215
+ }
216
+ return lines;
217
+ }
218
+
219
+ // ── the fixes the inspector teaches ──────────────────────────────────────────
220
+ const FIX = {
221
+ rust: [
222
+ 'use std::sync::Arc;',
223
+ 'use nedb_engine::Db;',
224
+ '',
225
+ 'let db = Arc::new(Db::open(path, None)?);',
226
+ 'Db::install_exit_flush(Arc::clone(&db)); // flush buffered writes on SIGINT/SIGTERM',
227
+ ].join('\n'),
228
+ js: [
229
+ "// Import the package entry — it arms process.on(SIGINT/SIGTERM) -> flush for you:",
230
+ "import { NedbCore } from 'nedb-engine';",
231
+ 'const db = NedbCore.open(path);',
232
+ '',
233
+ '// If you must use the raw native binding, wire the exit flush yourself:',
234
+ "process.on('SIGTERM', () => { db.flush(); process.exit(143); });",
235
+ "process.on('SIGINT', () => { db.flush(); process.exit(130); });",
236
+ "process.on('exit', () => db.flush());",
237
+ ].join('\n'),
238
+ py: [
239
+ '# nedb._native.NedbCore.open() arms a Python atexit flush for you (nedb >= 2.5.3).',
240
+ '# But os._exit() SKIPS atexit — flush explicitly before it:',
241
+ 'db = NedbCore.open(path)',
242
+ 'db.flush()',
243
+ 'os._exit(0)',
244
+ ].join('\n'),
245
+ };
246
+
247
+ // ── analysis ─────────────────────────────────────────────────────────────────
248
+ function analyze(src, lang) {
249
+ const { masked, strings } = maskSource(src, lang);
250
+ const toLine = lineMapper(src);
251
+ const findings = [], infos = [], warnings = [];
252
+ const strvals = strings.map((s) => s.value);
253
+
254
+ if (lang === 'rust') {
255
+ const opens = findCallSites(masked, 'Db::open', toLine);
256
+ const mem = findCallSites(masked, 'Db::in_memory', toLine);
257
+ const wired = hasIdent(masked, 'install_exit_flush');
258
+ if (opens.length) {
259
+ findings.push(`durable Db::open() at line ${opens.join(', ')}`);
260
+ if (wired) infos.push('install_exit_flush(...) present — flush-on-exit wired');
261
+ else warnings.push({
262
+ code: 'RUST_NO_EXIT_FLUSH',
263
+ lines: opens,
264
+ msg: 'durable Db::open() with NO install_exit_flush — writes staged since the last flush are LOST on SIGINT/SIGTERM (Drop does not run on a signalled exit).',
265
+ fix: FIX.rust,
266
+ });
267
+ } else if (mem.length) {
268
+ infos.push('only Db::in_memory() — ephemeral, nothing to flush');
269
+ }
270
+ }
271
+
272
+ else if (lang === 'js') {
273
+ const importsPkg = strvals.includes('nedb-engine');
274
+ // Import-specifier shapes only — NOT prose that merely contains "native".
275
+ const importsNative = strvals.some((v) =>
276
+ v.endsWith('.node') || v === './native' || v === './native.js' || v.endsWith('/native.js'));
277
+ const durable = findCallSites(masked, 'NedbCore.open', toLine);
278
+ const mem = findCallSites(masked, 'new NedbCore', toLine);
279
+ const manualWired = findCallSites(masked, 'process.on', toLine).length > 0 && hasIdent(masked, 'flush');
280
+ const optOut = hasIdent(masked, 'NEDB_NO_EXIT_FLUSH');
281
+ if (durable.length) {
282
+ findings.push(`durable NedbCore.open() at line ${durable.join(', ')}`);
283
+ if (importsPkg && !importsNative) infos.push("imports 'nedb-engine' — durable open auto-arms flush-on-exit");
284
+ else if (manualWired || optOut) infos.push('manual process.on(...) flush wiring detected');
285
+ else warnings.push({
286
+ code: 'JS_NO_EXIT_FLUSH',
287
+ lines: durable,
288
+ msg: 'durable open via the raw native binding with NO flush-on-exit wiring. Import from \'nedb-engine\' (its wrapper arms process.on -> flush) or wire it yourself.',
289
+ fix: FIX.js,
290
+ });
291
+ } else if (mem.length) {
292
+ infos.push('only new NedbCore() — in-memory, nothing to flush');
293
+ }
294
+ }
295
+
296
+ else if (lang === 'py') {
297
+ const nativeOpens = findCallSites(masked, 'NedbCore.open', toLine);
298
+ const pureDurable = pyNedbHasArg(src, masked, toLine);
299
+ const usesUnderExit = hasIdent(masked, 'os') && masked.includes('_exit');
300
+ const atexitWired = hasIdent(masked, 'atexit') || (hasIdent(masked, 'signal') && hasIdent(masked, 'flush'));
301
+ if (nativeOpens.length) {
302
+ findings.push(`native NedbCore.open() at line ${nativeOpens.join(', ')}`);
303
+ infos.push('native open arms a Python atexit flush (nedb >= 2.5.3)');
304
+ if (usesUnderExit && !atexitWired) warnings.push({
305
+ code: 'PY_OS_EXIT_BYPASS',
306
+ lines: nativeOpens,
307
+ msg: 'os._exit() bypasses atexit — the native auto-flush will NOT run. Call db.flush() explicitly before os._exit().',
308
+ fix: FIX.py,
309
+ });
310
+ }
311
+ if (pureDurable.length) {
312
+ findings.push(`pure-Python NEDB(path) at line ${pureDurable.join(', ')}`);
313
+ infos.push('pure-Python NEDB(path=...) is per-op fsync durable — no exit flush needed');
314
+ }
315
+ }
316
+
317
+ return { language: lang, findings, infos, warnings, ok: warnings.length === 0 };
318
+ }
319
+
320
+ // Public, testable entry: analyze source text.
321
+ export function inspect(source, filename) {
322
+ const lang = langForFile(filename);
323
+ if (!lang) return { language: null, findings: [], infos: [], warnings: [], ok: true, skipped: true };
324
+ return analyze(source, filename ? lang : lang);
325
+ }
326
+
327
+ // ── report ───────────────────────────────────────────────────────────────────
328
+ function report(file, res) {
329
+ if (res.skipped) { console.log(`${dim('skip')} ${file} ${dim('(unsupported extension)')}`); return; }
330
+ const tag = res.ok ? green(' OK ') : red(' WARN');
331
+ console.log(`\n${tag} ${bold(file)} ${dim(`[${res.language}]`)}`);
332
+ for (const f of res.findings) console.log(` ${cyan('•')} ${f}`);
333
+ for (const inf of res.infos) console.log(` ${dim('· ' + inf)}`);
334
+ for (const w of res.warnings) {
335
+ console.log('');
336
+ console.log(red(bold(' ┌─ NEDB DURABILITY WARNING ─────────────────────────────────')));
337
+ console.log(red(bold(` │ ${w.code}`)) + dim(` (line ${w.lines.join(', ')})`));
338
+ console.log(` ${red('│')} ${yellow(w.msg)}`);
339
+ console.log(red(' │'));
340
+ console.log(` ${red('│')} ${bold('Use this pattern:')}`);
341
+ for (const ln of w.fix.split('\n')) console.log(` ${red('│')} ${green(ln)}`);
342
+ console.log(red(bold(' └────────────────────────────────────────────────────────────')));
343
+ }
344
+ }
345
+
346
+ function usage(code) {
347
+ console.log(`${bold('nedb-inspector')} — deterministic NEDB durability checker
348
+
349
+ ${cyan('nedb-inspector.mjs')} <path.(rs|js|mjs|ts|py)> [more paths...]
350
+
351
+ Warns when a durable NEDB database is opened without flush-on-exit wiring.
352
+ Exit: 0 clean · 1 warnings · 2 usage/read error. NO_COLOR=1 for plain output.`);
353
+ process.exit(code);
354
+ }
355
+
356
+ function main(argv) {
357
+ const files = argv.filter((a) => !a.startsWith('-'));
358
+ if (argv.includes('-h') || argv.includes('--help') || files.length === 0) usage(files.length === 0 ? 2 : 0);
359
+ console.log(bold(magenta('\nnedb-inspector')) + dim(' · durable-mode flush-on-exit guardrail'));
360
+ let warned = 0, read = 0;
361
+ for (const file of files) {
362
+ let src;
363
+ try { src = readFileSync(file, 'utf8'); }
364
+ catch (e) { console.log(`${red('ERR ')} ${file} ${dim('(' + (e.code || e.message) + ')')}`); process.exitCode = 2; continue; }
365
+ read++;
366
+ const res = inspect(src, file);
367
+ report(file, res);
368
+ if (!res.ok) warned += res.warnings.length;
369
+ }
370
+ console.log('');
371
+ if (warned > 0) { console.log(red(bold(`✗ ${warned} warning(s) across ${read} file(s)`))); process.exit(1); }
372
+ else { console.log(green(bold(`✓ clean — ${read} file(s) inspected`))); process.exit(process.exitCode === 2 ? 2 : 0); }
373
+ }
374
+
375
+ // Run as CLI when invoked directly (not when imported by the test suite).
376
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
377
+ if (invokedDirectly) main(process.argv.slice(2));
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "nedb-engine",
3
- "version": "2.5.2",
3
+ "version": "2.5.43",
4
4
  "description": "NEDB — hash-chained, time-traveling, bi-temporal embedded database with Rust native core. SQL, Redis, MongoDB adapters. Causal Write Provenance. RESP2 wire protocol.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "bin": {
8
8
  "nedbd-v2": "./nedbd-v2.js",
9
- "nedbdv2": "./nedbd-v2.js"
9
+ "nedbdv2": "./nedbd-v2.js",
10
+ "nedb-inspector": "./nedb-inspector.mjs"
10
11
  },
11
12
  "files": [
12
13
  "index.js",
13
14
  "index.d.ts",
15
+ "native.js",
16
+ "native.d.ts",
14
17
  "nedbd-v2.js",
18
+ "nedb-inspector.mjs",
15
19
  "*.node",
16
20
  "nedbd-v2*",
17
21
  "test/smoke.mjs",
@@ -53,8 +57,8 @@
53
57
  }
54
58
  },
55
59
  "scripts": {
56
- "build": "napi build --release --platform --cargo-cwd rust/crates/nedb-node",
57
- "build:debug": "napi build --platform --cargo-cwd rust/crates/nedb-node",
60
+ "build": "napi build --release --platform --cargo-cwd rust/crates/nedb-node --js native.js --dts native.d.ts",
61
+ "build:debug": "napi build --platform --cargo-cwd rust/crates/nedb-node --js native.js --dts native.d.ts",
58
62
  "test": "node test/smoke.mjs"
59
63
  },
60
64
  "devDependencies": {