nedb-engine 2.5.0 → 2.5.34

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/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
@@ -33,4 +38,27 @@ export declare class NedbCore {
33
38
  seq(): bigint
34
39
  /** Flush WAL and MANIFEST — v2 equivalent of v1 flush(). */
35
40
  flush(): void
41
+ /**
42
+ * The tip — the most recent write (latest node) as a JSON string, or null if
43
+ * the database is empty. The cheap "give me the latest write" primitive.
44
+ */
45
+ tip(): string | null
46
+ /**
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.
49
+ */
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
36
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.0",
3
+ "version": "2.5.34",
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": {