nedb-engine 2.8.4 → 2.8.6
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 +78 -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
|
@@ -20,6 +20,83 @@ One Rust core → ships to **PyPI** and **npm** from a single source.
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
+
## ⚠️ New in 2.8.6 — Durability & Recovery (read this if you store anything you care about)
|
|
24
|
+
|
|
25
|
+
Three defects found by killing a real engine at every persistence boundary and by filling a real
|
|
26
|
+
filesystem to zero free blocks. All three are fixed. **If you are on 2.8.5 or earlier, upgrade.**
|
|
27
|
+
|
|
28
|
+
### 1. A failed flush silently discarded acknowledged writes
|
|
29
|
+
|
|
30
|
+
`IdIndex::flush_write_buf` cleared every buffered entry regardless of whether its disk write
|
|
31
|
+
succeeded. So a flush that hit `ENOSPC` threw the entry away, and no later flush retried it.
|
|
32
|
+
|
|
33
|
+
Reproduced on a full 22 MiB filesystem: **30 rows acknowledged by `put() -> Ok`, then `list()`
|
|
34
|
+
returned 0 after reopen — while `verify()` reported all 30 objects healthy.** The content-addressed
|
|
35
|
+
objects were durable; the id-index entries that make them findable were gone.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
before: try_flush_all() -> (no return value) reopen -> 0 rows, verify() = 30 ok
|
|
39
|
+
after: try_flush_all() -> Err("id-index leaf rows/buf_25: No space left on device (os error 28)")
|
|
40
|
+
...free space, retry -> Ok reopen -> 30 rows
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Fixed:** an entry leaves the WAL only when its write actually landed. Failures stay buffered and
|
|
44
|
+
retry on the next flush.
|
|
45
|
+
|
|
46
|
+
### 2. Flush errors were unobservable — new `try_flush_all()`
|
|
47
|
+
|
|
48
|
+
`flush_all()` returns `()` and logged fsync failures to stderr, so a caller could not tell a durable
|
|
49
|
+
flush from a failed one. Anything that takes a destructive or externally-visible action on the
|
|
50
|
+
strength of a persisted record needs to know.
|
|
51
|
+
|
|
52
|
+
```rust
|
|
53
|
+
// Use this when the outcome matters:
|
|
54
|
+
db.try_flush_all()?; // Result<()> — id-index WAL + segment sync + MANIFEST
|
|
55
|
+
|
|
56
|
+
// Still available, still logs, nowhere to propagate (ticker / Drop):
|
|
57
|
+
db.flush_all();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Also new: `Db::try_flush_manifest()` and `IdIndex::try_flush_write_buf()`.
|
|
61
|
+
|
|
62
|
+
### 3. `repair` could not repair, and `since()` claimed "caught up" while behind
|
|
63
|
+
|
|
64
|
+
The cold scan rebuilt `seq_index`, per-collection tips, the Merkle head and `MANIFEST` — but **never
|
|
65
|
+
the id index**. A database whose WAL never reached disk came back with every object verifying and
|
|
66
|
+
`list()` empty, and `nedb-cli repair` printed success without fixing it, because
|
|
67
|
+
`start_cold_scan()` is a deliberate no-op on a warm store.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
nedb-cli repair ./data
|
|
71
|
+
# repaired: 203 id-index entr(ies) rebuilt, 203 node(s) verified, flushed
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```rust
|
|
75
|
+
let restored = db.repair()?; // rebuild id index from objects; highest seq wins
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Every object carries its own `coll`, `id` and `seq`, so the id index is fully derivable — a lost WAL
|
|
79
|
+
is recoverable and nothing is invented. `repair()` also recomputes head and tips, so a repaired
|
|
80
|
+
database reopens **warm** instead of coming back up cold with an empty head.
|
|
81
|
+
|
|
82
|
+
Separately, `since()` set `has_more = hit_limit` alone. On a warm boot the seq index is empty **by
|
|
83
|
+
design** (that is why warm start is O(1)), so every lookup missed and `since()` returned zero nodes
|
|
84
|
+
with `has_more = false` — indistinguishable from genuinely up to date. A consumer following the
|
|
85
|
+
documented drain loop stopped one call in, with every record unread.
|
|
86
|
+
|
|
87
|
+
**Fixed:** `has_more` is true whenever the cursor is behind the log head. `ScanStatus` gains
|
|
88
|
+
**`seq_index_ready`** — replication consumers should gate on that, not on `scan_complete`, which is
|
|
89
|
+
true on a warm boot precisely because the scan was skipped.
|
|
90
|
+
|
|
91
|
+
### Known sharp edge (documented, not changed)
|
|
92
|
+
|
|
93
|
+
`since()`'s cursor is **exclusive** and seqs start at 0, so `since(0, _)` returns `(0, head]` and the
|
|
94
|
+
very first write in a database (seq 0) is unreachable through any cursor value. Ten writes drain as
|
|
95
|
+
nine records. Changing the convention would break existing consumers; a replica seeded from
|
|
96
|
+
`since()` alone starts one record short.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
23
100
|
## NEDB v2.8.0 — Production Stable
|
|
24
101
|
|
|
25
102
|
**Current stable: 2.8.0** — NEDB ships as **three version-aligned distributions** on one tag — `nedb-engine` (flagship), `crypto-database` (verifiable v2/v3 DAG), and `aof-db` (fast append-only) — across npm / PyPI / crates.io with full mac + linux + windows native addons (see [**Releasing**](#releasing) below). All native wheels (Linux + Windows on GitHub Actions; macOS arm64 + x86_64 on Codemagic M2 Mac Minis) **plus** the universal pure-Python wheel ship from a single `v*` tag, with the `nedbd-v2` binary bundled inside `pip install nedb-engine`.
|
|
@@ -47,6 +124,7 @@ Off by default — feature-gated at compile time, flag-gated at runtime, and `ex
|
|
|
47
124
|
**New in 2.5.x:**
|
|
48
125
|
|
|
49
126
|
- **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).
|
|
127
|
+
- **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
128
|
- **`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
129
|
- **Replication contract** — `tip()` (the latest write), a bounded `since()` changefeed, and a `scan_status()` readiness gate. See [**docs/REPLICATION.md**](docs/REPLICATION.md).
|
|
52
130
|
|
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.6",
|
|
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');
|