nedb-engine 2.8.2 → 2.8.5

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
@@ -47,6 +47,7 @@ Off by default — feature-gated at compile time, flag-gated at runtime, and `ex
47
47
  **New in 2.5.x:**
48
48
 
49
49
  - **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).
50
+ - **2.8.5 — embedded bindings flush on a cadence.** `NedbCore.open()` (Node + Python) now runs the 1 s manifest ticker exactly as `nedbd` does, so a `SIGKILL` / OOM / power cut loses at most one tick of acknowledged writes instead of everything since open. `NEDB_FLUSH_MS` tunes or disables it. See [docs/DURABILITY.md](docs/DURABILITY.md).
50
51
  - **`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).
51
52
  - **Replication contract** — `tip()` (the latest write), a bounded `since()` changefeed, and a `scan_status()` readiness gate. See [**docs/REPLICATION.md**](docs/REPLICATION.md).
52
53
 
package/index.d.ts CHANGED
@@ -1,64 +1,5 @@
1
- /* tslint:disable */
2
- /* eslint-disable */
3
-
4
- /* auto-generated by NAPI-RS */
5
-
6
- export declare class NedbCore {
7
- /** Create an in-memory v2 DAG database — zero disk I/O. */
8
- constructor()
9
- /**
10
- * Open a durable v2 DAG database at `path`.
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.
17
- */
18
- static open(path: string): NedbCore
19
- createIndex(coll: string, field: string, kind: string): void
20
- /** Put a document. Returns the stored doc as a JSON string. */
21
- put(coll: string, id: string, docJson: string): string
22
- /** Full put with optional client / nonce — API compat, v2 ignores these. */
23
- putEx(coll: string, id: string, docJson: string, client?: string | undefined | null, nonce?: bigint | undefined | null, idem?: string | undefined | null): string
24
- delete(coll: string, id: string): void
25
- deleteEx(coll: string, id: string, client?: string | undefined | null, nonce?: bigint | undefined | null, idem?: string | undefined | null): void
26
- /** Link: stored as a doc in __links__ collection for NQL traversal. */
27
- link(frm: string, rel: string, to: string): void
28
- unlink(frm: string, rel: string, to: string): void
29
- get(coll: string, id: string): string | null
30
- getAsOf(coll: string, id: string, asOf: bigint): string | null
31
- query(nqlStr: string): Array<string>
32
- neighbors(frm: string, rel: string): Array<string>
33
- neighborsAsOf(frm: string, rel: string, asOf: bigint): Array<string>
34
- inbound(to: string, rel: string): Array<string>
35
- inboundAsOf(to: string, rel: string, asOf: bigint): Array<string>
36
- verify(): boolean
37
- head(): string
38
- seq(): bigint
39
- /** Flush WAL and MANIFEST — v2 equivalent of v1 flush(). */
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
64
- }
1
+ // nedb-engine — public type surface.
2
+ //
3
+ // Runtime behavior (durable-mode auto-flush-on-exit) is added by the wrapper in
4
+ // index.js; the type surface is exactly the generated native binding's.
5
+ export * from './native';
package/index.js CHANGED
@@ -1,315 +1,81 @@
1
- /* tslint:disable */
2
- /* eslint-disable */
3
- /* prettier-ignore */
1
+ 'use strict';
2
+ // nedb-engine — durable-mode auto-flush-on-exit wrapper.
3
+ //
4
+ // The native addon (generated napi binding in ./native.js) exposes `NedbCore`.
5
+ // A durable `NedbCore.open(path)` buffers writes in the engine's id-index WAL and
6
+ // only makes them durable on `flush()`; a hard exit (Ctrl+C, `SIGTERM` from an
7
+ // orchestrator, `pm2 stop`) that never runs an explicit flush would lose writes
8
+ // staged since the last flush.
9
+ //
10
+ // We close that gap the libuv-cooperative way — `process.on('SIGINT'|'SIGTERM'
11
+ // |'exit', () => db.flush())` — NOT a C-level signal handler inside the addon,
12
+ // which would clobber libuv's own signal machinery. In-memory databases
13
+ // (`new NedbCore()`) are never armed; there is nothing to flush.
14
+ //
15
+ // Escape hatch: set NEDB_NO_EXIT_FLUSH=1 to leave signal handling entirely to
16
+ // the host app (it can still call `db.flush()` itself).
17
+ //
18
+ // © INTERCHAINED LLC × Claude Opus 4.8
4
19
 
5
- /* auto-generated by NAPI-RS */
20
+ const native = require('./native.js');
6
21
 
7
- const { existsSync, readFileSync } = require('fs')
8
- const { join } = require('path')
22
+ const Native = native.NedbCore;
9
23
 
10
- const { platform, arch } = process
24
+ // The napi class defines `open` as a NON-writable, NON-configurable static, so the
25
+ // 2.5.x wrapper's `NedbCore.open = …` threw ("Cannot assign to read only property")
26
+ // — and CI's `napi build` was overwriting this file with the generated loader
27
+ // anyway, so the published package never carried the wrapper at all. Both fixed
28
+ // in 2.8.5: wrap by SUBCLASS (an own static on the subclass shadows the parent's),
29
+ // build with `--js native.js` so this file survives, and gate the publish on
30
+ // `NedbCore.__exitFlushWrapped` (see test/durability.test.mjs).
31
+ let NedbCore = Native;
32
+ if (Native && typeof Native.open === 'function' && !Native.__exitFlushWrapped) {
33
+ // Durable handles opened in this process. Strong refs: a durable DB is meant to
34
+ // live for the process, and we must be able to flush it on the way out.
35
+ const live = new Set();
36
+ let armed = false;
11
37
 
12
- let nativeBinding = null
13
- let localFileExisted = false
14
- let loadError = null
15
-
16
- function isMusl() {
17
- // For Node 10
18
- if (!process.report || typeof process.report.getReport !== 'function') {
19
- try {
20
- const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
- return readFileSync(lddPath, 'utf8').includes('musl')
22
- } catch (e) {
23
- return true
24
- }
25
- } else {
26
- const { glibcVersionRuntime } = process.report.getReport().header
27
- return !glibcVersionRuntime
28
- }
29
- }
30
-
31
- switch (platform) {
32
- case 'android':
33
- switch (arch) {
34
- case 'arm64':
35
- localFileExisted = existsSync(join(__dirname, 'nedb.android-arm64.node'))
36
- try {
37
- if (localFileExisted) {
38
- nativeBinding = require('./nedb.android-arm64.node')
39
- } else {
40
- nativeBinding = require('nedb-engine-android-arm64')
41
- }
42
- } catch (e) {
43
- loadError = e
44
- }
45
- break
46
- case 'arm':
47
- localFileExisted = existsSync(join(__dirname, 'nedb.android-arm-eabi.node'))
48
- try {
49
- if (localFileExisted) {
50
- nativeBinding = require('./nedb.android-arm-eabi.node')
51
- } else {
52
- nativeBinding = require('nedb-engine-android-arm-eabi')
53
- }
54
- } catch (e) {
55
- loadError = e
56
- }
57
- break
58
- default:
59
- throw new Error(`Unsupported architecture on Android ${arch}`)
60
- }
61
- break
62
- case 'win32':
63
- switch (arch) {
64
- case 'x64':
65
- localFileExisted = existsSync(
66
- join(__dirname, 'nedb.win32-x64-msvc.node')
67
- )
68
- try {
69
- if (localFileExisted) {
70
- nativeBinding = require('./nedb.win32-x64-msvc.node')
71
- } else {
72
- nativeBinding = require('nedb-engine-win32-x64-msvc')
73
- }
74
- } catch (e) {
75
- loadError = e
76
- }
77
- break
78
- case 'ia32':
79
- localFileExisted = existsSync(
80
- join(__dirname, 'nedb.win32-ia32-msvc.node')
81
- )
82
- try {
83
- if (localFileExisted) {
84
- nativeBinding = require('./nedb.win32-ia32-msvc.node')
85
- } else {
86
- nativeBinding = require('nedb-engine-win32-ia32-msvc')
87
- }
88
- } catch (e) {
89
- loadError = e
90
- }
91
- break
92
- case 'arm64':
93
- localFileExisted = existsSync(
94
- join(__dirname, 'nedb.win32-arm64-msvc.node')
95
- )
96
- try {
97
- if (localFileExisted) {
98
- nativeBinding = require('./nedb.win32-arm64-msvc.node')
99
- } else {
100
- nativeBinding = require('nedb-engine-win32-arm64-msvc')
101
- }
102
- } catch (e) {
103
- loadError = e
104
- }
105
- break
106
- default:
107
- throw new Error(`Unsupported architecture on Windows: ${arch}`)
108
- }
109
- break
110
- case 'darwin':
111
- localFileExisted = existsSync(join(__dirname, 'nedb.darwin-universal.node'))
112
- try {
113
- if (localFileExisted) {
114
- nativeBinding = require('./nedb.darwin-universal.node')
115
- } else {
116
- nativeBinding = require('nedb-engine-darwin-universal')
117
- }
118
- break
119
- } catch {}
120
- switch (arch) {
121
- case 'x64':
122
- localFileExisted = existsSync(join(__dirname, 'nedb.darwin-x64.node'))
123
- try {
124
- if (localFileExisted) {
125
- nativeBinding = require('./nedb.darwin-x64.node')
126
- } else {
127
- nativeBinding = require('nedb-engine-darwin-x64')
128
- }
129
- } catch (e) {
130
- loadError = e
131
- }
132
- break
133
- case 'arm64':
134
- localFileExisted = existsSync(
135
- join(__dirname, 'nedb.darwin-arm64.node')
136
- )
137
- try {
138
- if (localFileExisted) {
139
- nativeBinding = require('./nedb.darwin-arm64.node')
140
- } else {
141
- nativeBinding = require('nedb-engine-darwin-arm64')
142
- }
143
- } catch (e) {
144
- loadError = e
145
- }
146
- break
147
- default:
148
- throw new Error(`Unsupported architecture on macOS: ${arch}`)
149
- }
150
- break
151
- case 'freebsd':
152
- if (arch !== 'x64') {
153
- throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
- }
155
- localFileExisted = existsSync(join(__dirname, 'nedb.freebsd-x64.node'))
156
- try {
157
- if (localFileExisted) {
158
- nativeBinding = require('./nedb.freebsd-x64.node')
159
- } else {
160
- nativeBinding = require('nedb-engine-freebsd-x64')
38
+ const flushAll = () => {
39
+ for (const db of live) {
40
+ try {
41
+ db.flush();
42
+ } catch (_) {
43
+ // Best-effort on shutdown — never throw out of an exit handler.
161
44
  }
162
- } catch (e) {
163
- loadError = e
164
45
  }
165
- break
166
- case 'linux':
167
- switch (arch) {
168
- case 'x64':
169
- if (isMusl()) {
170
- localFileExisted = existsSync(
171
- join(__dirname, 'nedb.linux-x64-musl.node')
172
- )
173
- try {
174
- if (localFileExisted) {
175
- nativeBinding = require('./nedb.linux-x64-musl.node')
176
- } else {
177
- nativeBinding = require('nedb-engine-linux-x64-musl')
178
- }
179
- } catch (e) {
180
- loadError = e
181
- }
182
- } else {
183
- localFileExisted = existsSync(
184
- join(__dirname, 'nedb.linux-x64-gnu.node')
185
- )
186
- try {
187
- if (localFileExisted) {
188
- nativeBinding = require('./nedb.linux-x64-gnu.node')
189
- } else {
190
- nativeBinding = require('nedb-engine-linux-x64-gnu')
191
- }
192
- } catch (e) {
193
- loadError = e
194
- }
195
- }
196
- break
197
- case 'arm64':
198
- if (isMusl()) {
199
- localFileExisted = existsSync(
200
- join(__dirname, 'nedb.linux-arm64-musl.node')
201
- )
202
- try {
203
- if (localFileExisted) {
204
- nativeBinding = require('./nedb.linux-arm64-musl.node')
205
- } else {
206
- nativeBinding = require('nedb-engine-linux-arm64-musl')
207
- }
208
- } catch (e) {
209
- loadError = e
210
- }
211
- } else {
212
- localFileExisted = existsSync(
213
- join(__dirname, 'nedb.linux-arm64-gnu.node')
214
- )
215
- try {
216
- if (localFileExisted) {
217
- nativeBinding = require('./nedb.linux-arm64-gnu.node')
218
- } else {
219
- nativeBinding = require('nedb-engine-linux-arm64-gnu')
220
- }
221
- } catch (e) {
222
- loadError = e
223
- }
224
- }
225
- break
226
- case 'arm':
227
- if (isMusl()) {
228
- localFileExisted = existsSync(
229
- join(__dirname, 'nedb.linux-arm-musleabihf.node')
230
- )
231
- try {
232
- if (localFileExisted) {
233
- nativeBinding = require('./nedb.linux-arm-musleabihf.node')
234
- } else {
235
- nativeBinding = require('nedb-engine-linux-arm-musleabihf')
236
- }
237
- } catch (e) {
238
- loadError = e
239
- }
240
- } else {
241
- localFileExisted = existsSync(
242
- join(__dirname, 'nedb.linux-arm-gnueabihf.node')
243
- )
244
- try {
245
- if (localFileExisted) {
246
- nativeBinding = require('./nedb.linux-arm-gnueabihf.node')
247
- } else {
248
- nativeBinding = require('nedb-engine-linux-arm-gnueabihf')
249
- }
250
- } catch (e) {
251
- loadError = e
252
- }
253
- }
254
- break
255
- case 'riscv64':
256
- if (isMusl()) {
257
- localFileExisted = existsSync(
258
- join(__dirname, 'nedb.linux-riscv64-musl.node')
259
- )
260
- try {
261
- if (localFileExisted) {
262
- nativeBinding = require('./nedb.linux-riscv64-musl.node')
263
- } else {
264
- nativeBinding = require('nedb-engine-linux-riscv64-musl')
265
- }
266
- } catch (e) {
267
- loadError = e
268
- }
269
- } else {
270
- localFileExisted = existsSync(
271
- join(__dirname, 'nedb.linux-riscv64-gnu.node')
272
- )
273
- try {
274
- if (localFileExisted) {
275
- nativeBinding = require('./nedb.linux-riscv64-gnu.node')
276
- } else {
277
- nativeBinding = require('nedb-engine-linux-riscv64-gnu')
278
- }
279
- } catch (e) {
280
- loadError = e
281
- }
282
- }
283
- break
284
- case 's390x':
285
- localFileExisted = existsSync(
286
- join(__dirname, 'nedb.linux-s390x-gnu.node')
287
- )
288
- try {
289
- if (localFileExisted) {
290
- nativeBinding = require('./nedb.linux-s390x-gnu.node')
291
- } else {
292
- nativeBinding = require('nedb-engine-linux-s390x-gnu')
293
- }
294
- } catch (e) {
295
- loadError = e
296
- }
297
- break
298
- default:
299
- throw new Error(`Unsupported architecture on Linux: ${arch}`)
300
- }
301
- break
302
- default:
303
- throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
304
- }
46
+ };
305
47
 
306
- if (!nativeBinding) {
307
- if (loadError) {
308
- throw loadError
309
- }
310
- throw new Error(`Failed to load native binding`)
311
- }
48
+ const arm = () => {
49
+ if (armed || process.env.NEDB_NO_EXIT_FLUSH) return;
50
+ armed = true;
51
+ // 'exit' fires on normal termination; handlers must be synchronous, and
52
+ // db.flush() is a synchronous native call — so this is safe and sufficient
53
+ // for clean exits and uncaught-exception exits.
54
+ process.on('exit', flushAll);
55
+ // Registering a SIGINT/SIGTERM listener SUPPRESSES Node's default
56
+ // termination, so once we listen we own the exit: flush, then terminate with
57
+ // the conventional 128+signum status.
58
+ const onSignal = (signum) => () => {
59
+ flushAll();
60
+ process.exit(128 + signum);
61
+ };
62
+ process.on('SIGINT', onSignal(2));
63
+ process.on('SIGTERM', onSignal(15));
64
+ };
312
65
 
313
- const { NedbCore } = nativeBinding
66
+ NedbCore = class NedbCore extends Native {
67
+ static open(path) {
68
+ const db = Native.open(path);
69
+ live.add(db);
70
+ arm();
71
+ return db;
72
+ }
73
+ };
74
+ // Mark so a re-require (or a wrapped re-export) never double-wraps.
75
+ Object.defineProperty(NedbCore, '__exitFlushWrapped', { value: true, enumerable: false });
76
+ }
314
77
 
315
- module.exports.NedbCore = NedbCore
78
+ module.exports = { ...native, NedbCore };
79
+ // Explicit named re-export so ESM `import { NedbCore } from 'nedb-engine'` (used
80
+ // by the test suite) resolves the class through cjs-module-lexer.
81
+ module.exports.NedbCore = NedbCore;
package/native.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ export declare class NedbCore {
7
+ /** Create an in-memory v2 DAG database — zero disk I/O. */
8
+ constructor()
9
+ /**
10
+ * Open a durable v2 DAG database at `path`.
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.
17
+ */
18
+ static open(path: string): NedbCore
19
+ createIndex(coll: string, field: string, kind: string): void
20
+ /** Put a document. Returns the stored doc as a JSON string. */
21
+ put(coll: string, id: string, docJson: string): string
22
+ /** Full put with optional client / nonce — API compat, v2 ignores these. */
23
+ putEx(coll: string, id: string, docJson: string, client?: string | undefined | null, nonce?: bigint | undefined | null, idem?: string | undefined | null): string
24
+ delete(coll: string, id: string): void
25
+ deleteEx(coll: string, id: string, client?: string | undefined | null, nonce?: bigint | undefined | null, idem?: string | undefined | null): void
26
+ /** Link: stored as a doc in __links__ collection for NQL traversal. */
27
+ link(frm: string, rel: string, to: string): void
28
+ unlink(frm: string, rel: string, to: string): void
29
+ get(coll: string, id: string): string | null
30
+ getAsOf(coll: string, id: string, asOf: bigint): string | null
31
+ query(nqlStr: string): Array<string>
32
+ neighbors(frm: string, rel: string): Array<string>
33
+ neighborsAsOf(frm: string, rel: string, asOf: bigint): Array<string>
34
+ inbound(to: string, rel: string): Array<string>
35
+ inboundAsOf(to: string, rel: string, asOf: bigint): Array<string>
36
+ verify(): boolean
37
+ head(): string
38
+ seq(): bigint
39
+ /** Flush WAL and MANIFEST — v2 equivalent of v1 flush(). */
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
64
+ }
package/native.js ADDED
@@ -0,0 +1,315 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /* prettier-ignore */
4
+
5
+ /* auto-generated by NAPI-RS */
6
+
7
+ const { existsSync, readFileSync } = require('fs')
8
+ const { join } = require('path')
9
+
10
+ const { platform, arch } = process
11
+
12
+ let nativeBinding = null
13
+ let localFileExisted = false
14
+ let loadError = null
15
+
16
+ function isMusl() {
17
+ // For Node 10
18
+ if (!process.report || typeof process.report.getReport !== 'function') {
19
+ try {
20
+ const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
+ return readFileSync(lddPath, 'utf8').includes('musl')
22
+ } catch (e) {
23
+ return true
24
+ }
25
+ } else {
26
+ const { glibcVersionRuntime } = process.report.getReport().header
27
+ return !glibcVersionRuntime
28
+ }
29
+ }
30
+
31
+ switch (platform) {
32
+ case 'android':
33
+ switch (arch) {
34
+ case 'arm64':
35
+ localFileExisted = existsSync(join(__dirname, 'nedb.android-arm64.node'))
36
+ try {
37
+ if (localFileExisted) {
38
+ nativeBinding = require('./nedb.android-arm64.node')
39
+ } else {
40
+ nativeBinding = require('nedb-engine-android-arm64')
41
+ }
42
+ } catch (e) {
43
+ loadError = e
44
+ }
45
+ break
46
+ case 'arm':
47
+ localFileExisted = existsSync(join(__dirname, 'nedb.android-arm-eabi.node'))
48
+ try {
49
+ if (localFileExisted) {
50
+ nativeBinding = require('./nedb.android-arm-eabi.node')
51
+ } else {
52
+ nativeBinding = require('nedb-engine-android-arm-eabi')
53
+ }
54
+ } catch (e) {
55
+ loadError = e
56
+ }
57
+ break
58
+ default:
59
+ throw new Error(`Unsupported architecture on Android ${arch}`)
60
+ }
61
+ break
62
+ case 'win32':
63
+ switch (arch) {
64
+ case 'x64':
65
+ localFileExisted = existsSync(
66
+ join(__dirname, 'nedb.win32-x64-msvc.node')
67
+ )
68
+ try {
69
+ if (localFileExisted) {
70
+ nativeBinding = require('./nedb.win32-x64-msvc.node')
71
+ } else {
72
+ nativeBinding = require('nedb-engine-win32-x64-msvc')
73
+ }
74
+ } catch (e) {
75
+ loadError = e
76
+ }
77
+ break
78
+ case 'ia32':
79
+ localFileExisted = existsSync(
80
+ join(__dirname, 'nedb.win32-ia32-msvc.node')
81
+ )
82
+ try {
83
+ if (localFileExisted) {
84
+ nativeBinding = require('./nedb.win32-ia32-msvc.node')
85
+ } else {
86
+ nativeBinding = require('nedb-engine-win32-ia32-msvc')
87
+ }
88
+ } catch (e) {
89
+ loadError = e
90
+ }
91
+ break
92
+ case 'arm64':
93
+ localFileExisted = existsSync(
94
+ join(__dirname, 'nedb.win32-arm64-msvc.node')
95
+ )
96
+ try {
97
+ if (localFileExisted) {
98
+ nativeBinding = require('./nedb.win32-arm64-msvc.node')
99
+ } else {
100
+ nativeBinding = require('nedb-engine-win32-arm64-msvc')
101
+ }
102
+ } catch (e) {
103
+ loadError = e
104
+ }
105
+ break
106
+ default:
107
+ throw new Error(`Unsupported architecture on Windows: ${arch}`)
108
+ }
109
+ break
110
+ case 'darwin':
111
+ localFileExisted = existsSync(join(__dirname, 'nedb.darwin-universal.node'))
112
+ try {
113
+ if (localFileExisted) {
114
+ nativeBinding = require('./nedb.darwin-universal.node')
115
+ } else {
116
+ nativeBinding = require('nedb-engine-darwin-universal')
117
+ }
118
+ break
119
+ } catch {}
120
+ switch (arch) {
121
+ case 'x64':
122
+ localFileExisted = existsSync(join(__dirname, 'nedb.darwin-x64.node'))
123
+ try {
124
+ if (localFileExisted) {
125
+ nativeBinding = require('./nedb.darwin-x64.node')
126
+ } else {
127
+ nativeBinding = require('nedb-engine-darwin-x64')
128
+ }
129
+ } catch (e) {
130
+ loadError = e
131
+ }
132
+ break
133
+ case 'arm64':
134
+ localFileExisted = existsSync(
135
+ join(__dirname, 'nedb.darwin-arm64.node')
136
+ )
137
+ try {
138
+ if (localFileExisted) {
139
+ nativeBinding = require('./nedb.darwin-arm64.node')
140
+ } else {
141
+ nativeBinding = require('nedb-engine-darwin-arm64')
142
+ }
143
+ } catch (e) {
144
+ loadError = e
145
+ }
146
+ break
147
+ default:
148
+ throw new Error(`Unsupported architecture on macOS: ${arch}`)
149
+ }
150
+ break
151
+ case 'freebsd':
152
+ if (arch !== 'x64') {
153
+ throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
+ }
155
+ localFileExisted = existsSync(join(__dirname, 'nedb.freebsd-x64.node'))
156
+ try {
157
+ if (localFileExisted) {
158
+ nativeBinding = require('./nedb.freebsd-x64.node')
159
+ } else {
160
+ nativeBinding = require('nedb-engine-freebsd-x64')
161
+ }
162
+ } catch (e) {
163
+ loadError = e
164
+ }
165
+ break
166
+ case 'linux':
167
+ switch (arch) {
168
+ case 'x64':
169
+ if (isMusl()) {
170
+ localFileExisted = existsSync(
171
+ join(__dirname, 'nedb.linux-x64-musl.node')
172
+ )
173
+ try {
174
+ if (localFileExisted) {
175
+ nativeBinding = require('./nedb.linux-x64-musl.node')
176
+ } else {
177
+ nativeBinding = require('nedb-engine-linux-x64-musl')
178
+ }
179
+ } catch (e) {
180
+ loadError = e
181
+ }
182
+ } else {
183
+ localFileExisted = existsSync(
184
+ join(__dirname, 'nedb.linux-x64-gnu.node')
185
+ )
186
+ try {
187
+ if (localFileExisted) {
188
+ nativeBinding = require('./nedb.linux-x64-gnu.node')
189
+ } else {
190
+ nativeBinding = require('nedb-engine-linux-x64-gnu')
191
+ }
192
+ } catch (e) {
193
+ loadError = e
194
+ }
195
+ }
196
+ break
197
+ case 'arm64':
198
+ if (isMusl()) {
199
+ localFileExisted = existsSync(
200
+ join(__dirname, 'nedb.linux-arm64-musl.node')
201
+ )
202
+ try {
203
+ if (localFileExisted) {
204
+ nativeBinding = require('./nedb.linux-arm64-musl.node')
205
+ } else {
206
+ nativeBinding = require('nedb-engine-linux-arm64-musl')
207
+ }
208
+ } catch (e) {
209
+ loadError = e
210
+ }
211
+ } else {
212
+ localFileExisted = existsSync(
213
+ join(__dirname, 'nedb.linux-arm64-gnu.node')
214
+ )
215
+ try {
216
+ if (localFileExisted) {
217
+ nativeBinding = require('./nedb.linux-arm64-gnu.node')
218
+ } else {
219
+ nativeBinding = require('nedb-engine-linux-arm64-gnu')
220
+ }
221
+ } catch (e) {
222
+ loadError = e
223
+ }
224
+ }
225
+ break
226
+ case 'arm':
227
+ if (isMusl()) {
228
+ localFileExisted = existsSync(
229
+ join(__dirname, 'nedb.linux-arm-musleabihf.node')
230
+ )
231
+ try {
232
+ if (localFileExisted) {
233
+ nativeBinding = require('./nedb.linux-arm-musleabihf.node')
234
+ } else {
235
+ nativeBinding = require('nedb-engine-linux-arm-musleabihf')
236
+ }
237
+ } catch (e) {
238
+ loadError = e
239
+ }
240
+ } else {
241
+ localFileExisted = existsSync(
242
+ join(__dirname, 'nedb.linux-arm-gnueabihf.node')
243
+ )
244
+ try {
245
+ if (localFileExisted) {
246
+ nativeBinding = require('./nedb.linux-arm-gnueabihf.node')
247
+ } else {
248
+ nativeBinding = require('nedb-engine-linux-arm-gnueabihf')
249
+ }
250
+ } catch (e) {
251
+ loadError = e
252
+ }
253
+ }
254
+ break
255
+ case 'riscv64':
256
+ if (isMusl()) {
257
+ localFileExisted = existsSync(
258
+ join(__dirname, 'nedb.linux-riscv64-musl.node')
259
+ )
260
+ try {
261
+ if (localFileExisted) {
262
+ nativeBinding = require('./nedb.linux-riscv64-musl.node')
263
+ } else {
264
+ nativeBinding = require('nedb-engine-linux-riscv64-musl')
265
+ }
266
+ } catch (e) {
267
+ loadError = e
268
+ }
269
+ } else {
270
+ localFileExisted = existsSync(
271
+ join(__dirname, 'nedb.linux-riscv64-gnu.node')
272
+ )
273
+ try {
274
+ if (localFileExisted) {
275
+ nativeBinding = require('./nedb.linux-riscv64-gnu.node')
276
+ } else {
277
+ nativeBinding = require('nedb-engine-linux-riscv64-gnu')
278
+ }
279
+ } catch (e) {
280
+ loadError = e
281
+ }
282
+ }
283
+ break
284
+ case 's390x':
285
+ localFileExisted = existsSync(
286
+ join(__dirname, 'nedb.linux-s390x-gnu.node')
287
+ )
288
+ try {
289
+ if (localFileExisted) {
290
+ nativeBinding = require('./nedb.linux-s390x-gnu.node')
291
+ } else {
292
+ nativeBinding = require('nedb-engine-linux-s390x-gnu')
293
+ }
294
+ } catch (e) {
295
+ loadError = e
296
+ }
297
+ break
298
+ default:
299
+ throw new Error(`Unsupported architecture on Linux: ${arch}`)
300
+ }
301
+ break
302
+ default:
303
+ throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
304
+ }
305
+
306
+ if (!nativeBinding) {
307
+ if (loadError) {
308
+ throw loadError
309
+ }
310
+ throw new Error(`Failed to load native binding`)
311
+ }
312
+
313
+ const { NedbCore } = nativeBinding
314
+
315
+ module.exports.NedbCore = NedbCore
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nedb-engine",
3
- "version": "2.8.2",
3
+ "version": "2.8.5",
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",
@@ -19,6 +19,7 @@
19
19
  "*.node",
20
20
  "nedbd-v2*",
21
21
  "test/smoke.mjs",
22
+ "test/durability.test.mjs",
22
23
  "README.md",
23
24
  "LICENSE"
24
25
  ],
@@ -59,7 +60,7 @@
59
60
  "scripts": {
60
61
  "build": "napi build --release --platform --cargo-cwd rust/crates/nedb-node --js native.js --dts native.d.ts",
61
62
  "build:debug": "napi build --platform --cargo-cwd rust/crates/nedb-node --js native.js --dts native.d.ts",
62
- "test": "node test/smoke.mjs"
63
+ "test": "node test/smoke.mjs && node test/durability.test.mjs"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@napi-rs/cli": "^2.18.0"
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ // nedb-engine — embedded durability under SIGKILL (2.8.5)
3
+ // ---------------------------------------------------------------------------
4
+ // The contract: an acknowledged write is on disk within one manifest tick (default 1 s) or at
5
+ // exit, whichever comes first. This test proves the tick half the only way that means anything:
6
+ // a child process opens a durable db, puts a document, gets the hash back, waits past one tick,
7
+ // and is killed with SIGKILL — no exit hook can run. The parent then opens the same directory
8
+ // and must find the document.
9
+ //
10
+ // Negative control: the same sequence with NEDB_FLUSH_MS=0 (ticker disabled) must LOSE the
11
+ // document — otherwise this test would pass for reasons unrelated to the ticker.
12
+ // Wrapper case: ticker off + graceful SIGTERM must KEEP the document — the exit-flush wrapper's job.
13
+ //
14
+ // © INTERCHAINED LLC
15
+ import os from 'node:os';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { spawn } from 'node:child_process';
19
+
20
+ let NedbCore, addonSpecifier;
21
+ try { ({ NedbCore } = await import('nedb-engine')); addonSpecifier = 'nedb-engine'; }
22
+ catch { addonSpecifier = new URL('../index.js', import.meta.url).href; ({ NedbCore } = await import(addonSpecifier)); }
23
+
24
+ const CHILD = `
25
+ const { NedbCore } = await import(process.argv[1]); // node -e: argv[1] is the first user arg
26
+ const db = NedbCore.open(process.argv[2]);
27
+ const out = db.put('durability', 'doc-1', JSON.stringify({ written_at: Date.now(), note: 'must survive SIGKILL' }));
28
+ process.stdout.write('PUT ' + JSON.parse(out)._hash + '\\n');
29
+ setInterval(() => {}, 1 << 30); // stay alive until killed
30
+ `;
31
+
32
+ function runChild(dir, env) {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(process.execPath, ['--input-type=module', '-e', CHILD, addonSpecifier, dir], { env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'inherit'] });
35
+ let buf = '';
36
+ child.stdout.on('data', (d) => { buf += d; const m = buf.match(/PUT ([0-9a-f]+)/); if (m) resolve({ child, hash: m[1] }); });
37
+ child.on('exit', (code, sig) => reject(new Error(`child exited early (${code ?? sig}) — ${buf}`)));
38
+ setTimeout(() => reject(new Error('child never acknowledged the put')), 15000).unref();
39
+ });
40
+ }
41
+
42
+ const tmp = (l) => fs.mkdtempSync(path.join(os.tmpdir(), `nedb-dur-${l}-`));
43
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
44
+ const readBack = (dir) => { const db = NedbCore.open(dir); const raw = db.get('durability', 'doc-1'); return raw ? JSON.parse(raw) : null; };
45
+ let failed = 0;
46
+ const check = (ok, msg) => { console.log(` ${ok ? '✓' : '✗'} ${msg}`); if (!ok) failed++; };
47
+
48
+ console.log('\n N E D B · embedded durability under SIGKILL');
49
+
50
+ // ── positive: default ticker (1000 ms) ─────────────────────────────────────
51
+ {
52
+ const dir = tmp('tick');
53
+ const { child, hash } = await runChild(dir, { NEDB_FLUSH_MS: '1000' });
54
+ console.log(` → child acknowledged put ${hash.slice(0, 12)}…; waiting 2.5 s (two ticks) then SIGKILL`);
55
+ await sleep(2500);
56
+ child.kill('SIGKILL'); await new Promise((r) => child.on('exit', r));
57
+ const doc = readBack(dir);
58
+ check(doc !== null && doc._hash === hash, `after SIGKILL + reopen the document is present with the acknowledged hash (${doc ? doc._hash.slice(0, 12) + '…' : 'GONE'})`);
59
+ fs.rmSync(dir, { recursive: true, force: true });
60
+ }
61
+
62
+ // ── negative control: ticker disabled → the write must be lost ─────────────
63
+ {
64
+ const dir = tmp('notick');
65
+ const { child, hash } = await runChild(dir, { NEDB_FLUSH_MS: '0' });
66
+ console.log(` → control: NEDB_FLUSH_MS=0, put ${hash.slice(0, 12)}…, 2.5 s, SIGKILL`);
67
+ await sleep(2500);
68
+ child.kill('SIGKILL'); await new Promise((r) => child.on('exit', r));
69
+ const doc = readBack(dir);
70
+ check(doc === null, `with the ticker disabled the same sequence loses the write (${doc ? 'unexpectedly present' : 'gone, as the control requires'})`);
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ }
73
+
74
+ // ── wrapper: graceful SIGTERM with the ticker OFF → the exit flush must save it ──
75
+ {
76
+ check(NedbCore.__exitFlushWrapped === true, 'index.js is the durable-mode wrapper (not the generated loader)');
77
+ const dir = tmp('term');
78
+ const { child, hash } = await runChild(dir, { NEDB_FLUSH_MS: '0' });
79
+ console.log(` → wrapper: NEDB_FLUSH_MS=0, put ${hash.slice(0, 12)}…, SIGTERM (graceful)`);
80
+ child.kill('SIGTERM'); await new Promise((r) => child.on('exit', r));
81
+ const doc = readBack(dir);
82
+ check(doc !== null && doc._hash === hash, `after SIGTERM the exit-flush wrapper made the write durable (${doc ? 'present' : 'GONE'})`);
83
+ fs.rmSync(dir, { recursive: true, force: true });
84
+ }
85
+
86
+ if (failed) { console.log(`\n ${failed} check(s) failed`); process.exit(1); }
87
+ console.log('\n durability contract holds: on disk within one tick, or at exit.\n');