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 +13 -3
- package/index.d.ts +21 -3
- package/nedb-inspector.mjs +377 -0
- package/nedb.darwin-arm64.node +0 -0
- package/nedb.darwin-x64.node +0 -0
- package/nedb.linux-x64-gnu.node +0 -0
- package/nedb.win32-x64-msvc.node +0 -0
- package/nedbd-v2-darwin-arm64 +0 -0
- package/nedbd-v2-darwin-x64 +0 -0
- package/nedbd-v2-linux-x64 +0 -0
- package/nedbd-v2-win-x64.exe +0 -0
- package/package.json +8 -4
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.
|
|
23
|
+
## NEDB v2.5.34 — Production Stable
|
|
24
24
|
|
|
25
|
-
**Current stable: 2.
|
|
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
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
-
|
|
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));
|
package/nedb.darwin-arm64.node
CHANGED
|
Binary file
|
package/nedb.darwin-x64.node
CHANGED
|
Binary file
|
package/nedb.linux-x64-gnu.node
CHANGED
|
Binary file
|
package/nedb.win32-x64-msvc.node
CHANGED
|
Binary file
|
package/nedbd-v2-darwin-arm64
CHANGED
|
Binary file
|
package/nedbd-v2-darwin-x64
CHANGED
|
Binary file
|
package/nedbd-v2-linux-x64
CHANGED
|
Binary file
|
package/nedbd-v2-win-x64.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nedb-engine",
|
|
3
|
-
"version": "2.5.
|
|
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": {
|