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 +1 -0
- package/index.d.ts +5 -64
- package/index.js +72 -306
- package/native.d.ts +64 -0
- package/native.js +315 -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 +3 -2
- package/test/durability.test.mjs +87 -0
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
20
|
+
const native = require('./native.js');
|
|
6
21
|
|
|
7
|
-
const
|
|
8
|
-
const { join } = require('path')
|
|
22
|
+
const Native = native.NedbCore;
|
|
9
23
|
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
|
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
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nedb-engine",
|
|
3
|
-
"version": "2.8.
|
|
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');
|