aof-db 2.4.68
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/LICENSE +65 -0
- package/README.md +45 -0
- package/aof-db.linux-x64-gnu.node +0 -0
- package/aof-db.win32-x64-msvc.node +0 -0
- package/index.d.ts +36 -0
- package/index.js +315 -0
- package/nedbd-v2-linux-x64 +0 -0
- package/nedbd-v2-win-x64.exe +0 -0
- package/nedbd-v2.js +91 -0
- package/package.json +65 -0
- package/test/smoke.mjs +299 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Licensor: Interchained LLC, VibeCode 101, and/or the applicable copyright holders of NEDB
|
|
4
|
+
|
|
5
|
+
Licensed Work: NEDB, including all source code, documentation, examples, tests, build scripts, specifications, APIs, SDKs, packages, binaries, and derivative works distributed from or based on the NEDB repository.
|
|
6
|
+
|
|
7
|
+
Copyright: Copyright (c) 2026 Interchained LLC, VibeCode 101, and contributors. All rights reserved.
|
|
8
|
+
|
|
9
|
+
Change Date: Four years after the first publicly available distribution of each specific version of the Licensed Work under this License.
|
|
10
|
+
|
|
11
|
+
Change License: GNU Affero General Public License v3.0 or later.
|
|
12
|
+
|
|
13
|
+
Additional Use Grant:
|
|
14
|
+
|
|
15
|
+
You may use the Licensed Work only for non-production purposes, including evaluation, development, testing, research, local experimentation, security review, benchmarking, and personal learning.
|
|
16
|
+
|
|
17
|
+
No production use is granted automatically.
|
|
18
|
+
|
|
19
|
+
For clarity, the following uses are not permitted unless you have prior written permission from the Licensor:
|
|
20
|
+
|
|
21
|
+
1. Using the Licensed Work in production.
|
|
22
|
+
2. Using the Licensed Work to store, process, serve, index, replicate, query, or manage production data.
|
|
23
|
+
3. Offering the Licensed Work, or any derivative work, as a hosted, managed, embedded, bundled, white-labeled, commercial, paid, revenue-generating, or customer-facing product or service.
|
|
24
|
+
4. Offering database-as-a-service, storage-as-a-service, cache-as-a-service, search-as-a-service, analytics-as-a-service, AI-memory-as-a-service, agent-memory-as-a-service, blockchain-indexing-as-a-service, or any substantially similar service using the Licensed Work.
|
|
25
|
+
5. Using the Licensed Work to compete with NEDB, Interchained LLC, VibeCode 101, AiAssist Secure, or any affiliated product, service, infrastructure platform, database engine, AI runtime, agent platform, or hosted developer infrastructure.
|
|
26
|
+
6. Embedding the Licensed Work into commercial software, SaaS products, internal business systems, enterprise systems, hosted infrastructure, blockchain infrastructure, AI-agent infrastructure, or customer deliverables.
|
|
27
|
+
7. Removing, hiding, modifying, or misrepresenting this License, copyright notices, attribution notices, authorship notices, repository references, or licensing notices.
|
|
28
|
+
8. Circumventing license checks, access controls, paid licensing requirements, commercial-use restrictions, attribution requirements, or technical protections included with the Licensed Work.
|
|
29
|
+
9. Using the Licensed Work in a way that implies endorsement, partnership, sponsorship, certification, or approval by the Licensor without written permission.
|
|
30
|
+
10. Reselling, relicensing, sublicensing, renting, leasing, or otherwise commercially exploiting the Licensed Work except as expressly authorized in writing.
|
|
31
|
+
|
|
32
|
+
Additional production, commercial, hosted, enterprise, embedded, OEM, resale, white-label, managed-service, competitive, or otherwise restricted use grants are available only by separate written permission from the Licensor.
|
|
33
|
+
|
|
34
|
+
Written permission must be obtained from at least one of the following authorized licensing contacts:
|
|
35
|
+
|
|
36
|
+
[founders@vibecode-101.com](mailto:founders@vibecode-101.com)
|
|
37
|
+
[dev@interchained.org](mailto:dev@interchained.org)
|
|
38
|
+
|
|
39
|
+
Permission is valid only if it is expressly granted in writing by an authorized representative of the Licensor and specifically identifies the permitted use, scope, duration, parties, and any applicable commercial terms.
|
|
40
|
+
|
|
41
|
+
A general email conversation, informal message, pull request, issue comment, social media message, verbal discussion, repository access, package download, or contribution acceptance does not create a production, commercial, hosted, enterprise, resale, or competitive use license.
|
|
42
|
+
|
|
43
|
+
Terms:
|
|
44
|
+
|
|
45
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
46
|
+
|
|
47
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate.
|
|
48
|
+
|
|
49
|
+
If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase or obtain a separate commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work.
|
|
50
|
+
|
|
51
|
+
All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work, and the Change Date may vary for each version of the Licensed Work released by Licensor.
|
|
52
|
+
|
|
53
|
+
You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work.
|
|
54
|
+
|
|
55
|
+
Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work.
|
|
56
|
+
|
|
57
|
+
This License does not grant you any right in any trademark, service mark, trade name, logo, domain name, brand identity, product name, project name, or other identifier of Licensor or its affiliates, except as expressly required to preserve legally required notices.
|
|
58
|
+
|
|
59
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN βAS ISβ BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE, SECURITY, ACCURACY, AVAILABILITY, DATA INTEGRITY, PERFORMANCE, AND FITNESS FOR PRODUCTION USE.
|
|
60
|
+
|
|
61
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR SHALL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, LOSSES, COSTS, EXPENSES, BUSINESS INTERRUPTION, LOST PROFITS, LOST REVENUE, LOST DATA, SECURITY INCIDENTS, SERVICE OUTAGES, OR OTHER LIABILITY ARISING FROM OR RELATED TO THE LICENSED WORK OR YOUR USE OF THE LICENSED WORK.
|
|
62
|
+
|
|
63
|
+
Notice:
|
|
64
|
+
|
|
65
|
+
The Business Source License is not an Open Source license. However, each version of the Licensed Work will become available under the Change License on the applicable Change Date, or on the fourth anniversary of the first publicly available distribution of that version under this License, whichever comes first.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<h1 align="center">β‘ aof-db</h1>
|
|
2
|
+
<p align="center"><b>The embedded database that goes brrr.</b></p>
|
|
3
|
+
<p align="center"><i>Stupid-fast, append-only, zero-server storage you drop into your app and forget about.</i></p>
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
63,400 writes/sec 1,340,000 point reads/sec one fsync per batch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
aof-db is a **tiny, blazing-fast, in-process** datastore. No daemon. No schema migrations. No 400 MB of dependencies. Just `import` it and start flooding it with data β it keeps up.
|
|
12
|
+
|
|
13
|
+
## Why people can't shut up about it
|
|
14
|
+
|
|
15
|
+
- ποΈ **Fast where it counts.** Append-only + group-commit: one `fsync` per *batch*, not per record. Bigger batches get **cheaper per item**. Throughput goes *up* under load, not down.
|
|
16
|
+
- πͺΆ **Featherweight.** In-process, near-zero footprint. Boots instantly, runs in memory, persists when you say so.
|
|
17
|
+
- π§· **Durable, not fragile.** Every write is hash-chained and the log replays clean on open β crash-safe without the ceremony.
|
|
18
|
+
- π **Scales with your ambition.** Same engine family as **crypto-database** β flip on the content-addressed DAG the day you need time-travel and tamper-evidence. Start fast, grow powerful.
|
|
19
|
+
|
|
20
|
+
## Install (10 seconds, tops)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install aof-db # pip install aof-db # cargo add aof-db
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { NedbCore } from "aof-db";
|
|
28
|
+
const db = new NedbCore();
|
|
29
|
+
console.time("100k");
|
|
30
|
+
for (let i = 0; i < 100_000; i++) db.put("events", String(i), JSON.stringify({ t: Date.now(), i }));
|
|
31
|
+
db.flush(); // one durable group-commit for the whole burst
|
|
32
|
+
console.timeEnd("100k"); // β¦go on, time it.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Built for
|
|
36
|
+
|
|
37
|
+
Edge & embedded Β· high-throughput event logging Β· local-first apps Β· game state Β· CLIs Β· **anywhere "just make it fast" beats "stand up a server."**
|
|
38
|
+
|
|
39
|
+
> **Note:** aof-db was previously published as `nitrodb`. The rename aligns the public package name with what it is β an append-only-file (AOF) fast-path distribution of NEDB. Same engine, same speed, new name.
|
|
40
|
+
|
|
41
|
+
<p align="center"><b>If aof-db just saved your afternoon, β it and tell a friend.</b></p>
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<sub>aof-db is a distribution of the <b>NEDB</b> engine, tuned for speed + simplicity (benchmarks measured on commodity hardware β yours will vary). Engine development: <a href="https://github.com/Eth-Interchained/nedb">Eth-Interchained/nedb</a>. Β© Interchained LLC.</sub>
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
static open(path: string): NedbCore
|
|
14
|
+
createIndex(coll: string, field: string, kind: string): void
|
|
15
|
+
/** Put a document. Returns the stored doc as a JSON string. */
|
|
16
|
+
put(coll: string, id: string, docJson: string): string
|
|
17
|
+
/** Full put with optional client / nonce β API compat, v2 ignores these. */
|
|
18
|
+
putEx(coll: string, id: string, docJson: string, client?: string | undefined | null, nonce?: bigint | undefined | null, idem?: string | undefined | null): string
|
|
19
|
+
delete(coll: string, id: string): void
|
|
20
|
+
deleteEx(coll: string, id: string, client?: string | undefined | null, nonce?: bigint | undefined | null, idem?: string | undefined | null): void
|
|
21
|
+
/** Link: stored as a doc in __links__ collection for NQL traversal. */
|
|
22
|
+
link(frm: string, rel: string, to: string): void
|
|
23
|
+
unlink(frm: string, rel: string, to: string): void
|
|
24
|
+
get(coll: string, id: string): string | null
|
|
25
|
+
getAsOf(coll: string, id: string, asOf: bigint): string | null
|
|
26
|
+
query(nqlStr: string): Array<string>
|
|
27
|
+
neighbors(frm: string, rel: string): Array<string>
|
|
28
|
+
neighborsAsOf(frm: string, rel: string, asOf: bigint): Array<string>
|
|
29
|
+
inbound(to: string, rel: string): Array<string>
|
|
30
|
+
inboundAsOf(to: string, rel: string, asOf: bigint): Array<string>
|
|
31
|
+
verify(): boolean
|
|
32
|
+
head(): string
|
|
33
|
+
seq(): bigint
|
|
34
|
+
/** Flush WAL and MANIFEST β v2 equivalent of v1 flush(). */
|
|
35
|
+
flush(): void
|
|
36
|
+
}
|
package/index.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, 'aof-db.android-arm64.node'))
|
|
36
|
+
try {
|
|
37
|
+
if (localFileExisted) {
|
|
38
|
+
nativeBinding = require('./aof-db.android-arm64.node')
|
|
39
|
+
} else {
|
|
40
|
+
nativeBinding = require('aof-db-android-arm64')
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
loadError = e
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
case 'arm':
|
|
47
|
+
localFileExisted = existsSync(join(__dirname, 'aof-db.android-arm-eabi.node'))
|
|
48
|
+
try {
|
|
49
|
+
if (localFileExisted) {
|
|
50
|
+
nativeBinding = require('./aof-db.android-arm-eabi.node')
|
|
51
|
+
} else {
|
|
52
|
+
nativeBinding = require('aof-db-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, 'aof-db.win32-x64-msvc.node')
|
|
67
|
+
)
|
|
68
|
+
try {
|
|
69
|
+
if (localFileExisted) {
|
|
70
|
+
nativeBinding = require('./aof-db.win32-x64-msvc.node')
|
|
71
|
+
} else {
|
|
72
|
+
nativeBinding = require('aof-db-win32-x64-msvc')
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
loadError = e
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
case 'ia32':
|
|
79
|
+
localFileExisted = existsSync(
|
|
80
|
+
join(__dirname, 'aof-db.win32-ia32-msvc.node')
|
|
81
|
+
)
|
|
82
|
+
try {
|
|
83
|
+
if (localFileExisted) {
|
|
84
|
+
nativeBinding = require('./aof-db.win32-ia32-msvc.node')
|
|
85
|
+
} else {
|
|
86
|
+
nativeBinding = require('aof-db-win32-ia32-msvc')
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
loadError = e
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
case 'arm64':
|
|
93
|
+
localFileExisted = existsSync(
|
|
94
|
+
join(__dirname, 'aof-db.win32-arm64-msvc.node')
|
|
95
|
+
)
|
|
96
|
+
try {
|
|
97
|
+
if (localFileExisted) {
|
|
98
|
+
nativeBinding = require('./aof-db.win32-arm64-msvc.node')
|
|
99
|
+
} else {
|
|
100
|
+
nativeBinding = require('aof-db-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, 'aof-db.darwin-universal.node'))
|
|
112
|
+
try {
|
|
113
|
+
if (localFileExisted) {
|
|
114
|
+
nativeBinding = require('./aof-db.darwin-universal.node')
|
|
115
|
+
} else {
|
|
116
|
+
nativeBinding = require('aof-db-darwin-universal')
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
} catch {}
|
|
120
|
+
switch (arch) {
|
|
121
|
+
case 'x64':
|
|
122
|
+
localFileExisted = existsSync(join(__dirname, 'aof-db.darwin-x64.node'))
|
|
123
|
+
try {
|
|
124
|
+
if (localFileExisted) {
|
|
125
|
+
nativeBinding = require('./aof-db.darwin-x64.node')
|
|
126
|
+
} else {
|
|
127
|
+
nativeBinding = require('aof-db-darwin-x64')
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
loadError = e
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
case 'arm64':
|
|
134
|
+
localFileExisted = existsSync(
|
|
135
|
+
join(__dirname, 'aof-db.darwin-arm64.node')
|
|
136
|
+
)
|
|
137
|
+
try {
|
|
138
|
+
if (localFileExisted) {
|
|
139
|
+
nativeBinding = require('./aof-db.darwin-arm64.node')
|
|
140
|
+
} else {
|
|
141
|
+
nativeBinding = require('aof-db-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, 'aof-db.freebsd-x64.node'))
|
|
156
|
+
try {
|
|
157
|
+
if (localFileExisted) {
|
|
158
|
+
nativeBinding = require('./aof-db.freebsd-x64.node')
|
|
159
|
+
} else {
|
|
160
|
+
nativeBinding = require('aof-db-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, 'aof-db.linux-x64-musl.node')
|
|
172
|
+
)
|
|
173
|
+
try {
|
|
174
|
+
if (localFileExisted) {
|
|
175
|
+
nativeBinding = require('./aof-db.linux-x64-musl.node')
|
|
176
|
+
} else {
|
|
177
|
+
nativeBinding = require('aof-db-linux-x64-musl')
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
loadError = e
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
localFileExisted = existsSync(
|
|
184
|
+
join(__dirname, 'aof-db.linux-x64-gnu.node')
|
|
185
|
+
)
|
|
186
|
+
try {
|
|
187
|
+
if (localFileExisted) {
|
|
188
|
+
nativeBinding = require('./aof-db.linux-x64-gnu.node')
|
|
189
|
+
} else {
|
|
190
|
+
nativeBinding = require('aof-db-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, 'aof-db.linux-arm64-musl.node')
|
|
201
|
+
)
|
|
202
|
+
try {
|
|
203
|
+
if (localFileExisted) {
|
|
204
|
+
nativeBinding = require('./aof-db.linux-arm64-musl.node')
|
|
205
|
+
} else {
|
|
206
|
+
nativeBinding = require('aof-db-linux-arm64-musl')
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
loadError = e
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
localFileExisted = existsSync(
|
|
213
|
+
join(__dirname, 'aof-db.linux-arm64-gnu.node')
|
|
214
|
+
)
|
|
215
|
+
try {
|
|
216
|
+
if (localFileExisted) {
|
|
217
|
+
nativeBinding = require('./aof-db.linux-arm64-gnu.node')
|
|
218
|
+
} else {
|
|
219
|
+
nativeBinding = require('aof-db-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, 'aof-db.linux-arm-musleabihf.node')
|
|
230
|
+
)
|
|
231
|
+
try {
|
|
232
|
+
if (localFileExisted) {
|
|
233
|
+
nativeBinding = require('./aof-db.linux-arm-musleabihf.node')
|
|
234
|
+
} else {
|
|
235
|
+
nativeBinding = require('aof-db-linux-arm-musleabihf')
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
loadError = e
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
localFileExisted = existsSync(
|
|
242
|
+
join(__dirname, 'aof-db.linux-arm-gnueabihf.node')
|
|
243
|
+
)
|
|
244
|
+
try {
|
|
245
|
+
if (localFileExisted) {
|
|
246
|
+
nativeBinding = require('./aof-db.linux-arm-gnueabihf.node')
|
|
247
|
+
} else {
|
|
248
|
+
nativeBinding = require('aof-db-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, 'aof-db.linux-riscv64-musl.node')
|
|
259
|
+
)
|
|
260
|
+
try {
|
|
261
|
+
if (localFileExisted) {
|
|
262
|
+
nativeBinding = require('./aof-db.linux-riscv64-musl.node')
|
|
263
|
+
} else {
|
|
264
|
+
nativeBinding = require('aof-db-linux-riscv64-musl')
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
loadError = e
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
localFileExisted = existsSync(
|
|
271
|
+
join(__dirname, 'aof-db.linux-riscv64-gnu.node')
|
|
272
|
+
)
|
|
273
|
+
try {
|
|
274
|
+
if (localFileExisted) {
|
|
275
|
+
nativeBinding = require('./aof-db.linux-riscv64-gnu.node')
|
|
276
|
+
} else {
|
|
277
|
+
nativeBinding = require('aof-db-linux-riscv64-gnu')
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
loadError = e
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
case 's390x':
|
|
285
|
+
localFileExisted = existsSync(
|
|
286
|
+
join(__dirname, 'aof-db.linux-s390x-gnu.node')
|
|
287
|
+
)
|
|
288
|
+
try {
|
|
289
|
+
if (localFileExisted) {
|
|
290
|
+
nativeBinding = require('./aof-db.linux-s390x-gnu.node')
|
|
291
|
+
} else {
|
|
292
|
+
nativeBinding = require('aof-db-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
|
package/nedbd-v2.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// nedbd-v2 β thin platform shim that locates and spawns the right
|
|
3
|
+
// pre-built native binary for the current platform/arch.
|
|
4
|
+
//
|
|
5
|
+
// The actual native binaries are shipped alongside this file in the
|
|
6
|
+
// npm package root (same directory as package.json). Naming convention
|
|
7
|
+
// matches the .node files produced by napi-rs:
|
|
8
|
+
//
|
|
9
|
+
// Linux x64 -> nedbd-v2-linux-x64
|
|
10
|
+
// Windows x64 -> nedbd-v2-win-x64.exe
|
|
11
|
+
// macOS arm64 -> nedbd-v2-darwin-arm64
|
|
12
|
+
// macOS x64 -> nedbd-v2-darwin-x64
|
|
13
|
+
//
|
|
14
|
+
// We pass through stdin/stdout/stderr and exit with the child's exit code so
|
|
15
|
+
// `npx nedbd-v2 β¦` is indistinguishable from invoking the binary directly.
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const { spawn } = require("child_process");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
|
|
23
|
+
function resolveBinaryName() {
|
|
24
|
+
const platform = process.platform; // 'linux' | 'darwin' | 'win32' | ...
|
|
25
|
+
const arch = process.arch; // 'x64' | 'arm64' | ...
|
|
26
|
+
|
|
27
|
+
if (platform === "linux" && arch === "x64") {
|
|
28
|
+
return "nedbd-v2-linux-x64";
|
|
29
|
+
}
|
|
30
|
+
if (platform === "win32" && arch === "x64") {
|
|
31
|
+
return "nedbd-v2-win-x64.exe";
|
|
32
|
+
}
|
|
33
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
34
|
+
return "nedbd-v2-darwin-arm64";
|
|
35
|
+
}
|
|
36
|
+
if (platform === "darwin" && arch === "x64") {
|
|
37
|
+
return "nedbd-v2-darwin-x64";
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function main() {
|
|
43
|
+
const name = resolveBinaryName();
|
|
44
|
+
if (!name) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`nedbd-v2: unsupported platform/arch: ${process.platform}/${process.arch}\n` +
|
|
47
|
+
`Supported: linux-x64, win32-x64, darwin-arm64, darwin-x64\n`
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const binPath = path.join(__dirname, name);
|
|
53
|
+
if (!fs.existsSync(binPath)) {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`nedbd-v2: binary not found for this platform: ${binPath}\n` +
|
|
56
|
+
`The nedb-engine npm package may be missing the prebuilt binary for ` +
|
|
57
|
+
`${process.platform}/${process.arch}.\n`
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Ensure executable bit on POSIX (npm sometimes strips it from tarballs).
|
|
63
|
+
if (process.platform !== "win32") {
|
|
64
|
+
try {
|
|
65
|
+
fs.chmodSync(binPath, 0o755);
|
|
66
|
+
} catch (_err) {
|
|
67
|
+
// best-effort; ignore β spawn will surface a clearer error if needed.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const child = spawn(binPath, process.argv.slice(2), {
|
|
72
|
+
stdio: "inherit",
|
|
73
|
+
windowsHide: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
child.on("error", (err) => {
|
|
77
|
+
process.stderr.write(`nedbd-v2: failed to spawn ${binPath}: ${err.message}\n`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
child.on("exit", (code, signal) => {
|
|
82
|
+
if (signal) {
|
|
83
|
+
// Re-raise the signal so shells see the correct termination cause.
|
|
84
|
+
process.kill(process.pid, signal);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
process.exit(code === null ? 1 : code);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aof-db",
|
|
3
|
+
"version": "2.4.68",
|
|
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
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nedbd-v2": "./nedbd-v2.js",
|
|
9
|
+
"nedbdv2": "./nedbd-v2.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"index.d.ts",
|
|
14
|
+
"nedbd-v2.js",
|
|
15
|
+
"*.node",
|
|
16
|
+
"nedbd-v2*",
|
|
17
|
+
"test/smoke.mjs",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"license": "GPL-3.0-or-later",
|
|
22
|
+
"homepage": "https://github.com/aiassistsecure/nedb#readme",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/aiassistsecure/nedb.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"database",
|
|
29
|
+
"embedded",
|
|
30
|
+
"mvcc",
|
|
31
|
+
"time-travel",
|
|
32
|
+
"bi-temporal",
|
|
33
|
+
"causal-provenance",
|
|
34
|
+
"tamper-evident",
|
|
35
|
+
"hash-chain",
|
|
36
|
+
"versioning",
|
|
37
|
+
"napi-rs",
|
|
38
|
+
"rust",
|
|
39
|
+
"nedb",
|
|
40
|
+
"aof",
|
|
41
|
+
"aof-db"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">= 16"
|
|
45
|
+
},
|
|
46
|
+
"napi": {
|
|
47
|
+
"name": "aof-db",
|
|
48
|
+
"triples": {
|
|
49
|
+
"defaults": true,
|
|
50
|
+
"additional": [
|
|
51
|
+
"aarch64-apple-darwin",
|
|
52
|
+
"x86_64-unknown-linux-gnu",
|
|
53
|
+
"x86_64-pc-windows-msvc"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "napi build --release --platform --cargo-cwd rust/crates/nedb-node",
|
|
59
|
+
"build:debug": "napi build --platform --cargo-cwd rust/crates/nedb-node",
|
|
60
|
+
"test": "node test/smoke.mjs"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@napi-rs/cli": "^2.18.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/test/smoke.mjs
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// nedb-engine β cinematic smoke test (`npm test`)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// A five-act tour of the engine, driven entirely by the real native addon
|
|
5
|
+
// (NedbCore β the same prebuilt .node binary the npm package ships). No external
|
|
6
|
+
// deps; Node built-ins only. Exits 0 on success.
|
|
7
|
+
//
|
|
8
|
+
// Act I β v1: the legacy append-only op-log (log.aof)
|
|
9
|
+
// Act II β automatic v1 -> v2 DAG migration (zero user action, lossless)
|
|
10
|
+
// Act III β v2: the content-addressed, hash-chained, time-traveling DAG
|
|
11
|
+
// Act IV β v3: the segment/pack object store (one fsync per group-commit)
|
|
12
|
+
// Act V β The Honest Dispatch: a causal rideshare decision you can audit
|
|
13
|
+
//
|
|
14
|
+
// Β© INTERCHAINED LLC Γ Claude Opus 4.8
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
// ββ tiny presentation toolkit βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
20
|
+
const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
21
|
+
const sgr = (code) => (s) => (COLOR ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
22
|
+
const dim = sgr('2'), bold = sgr('1'), ul = sgr('4');
|
|
23
|
+
const cyan = sgr('36'), green = sgr('32'), yellow = sgr('33');
|
|
24
|
+
const magenta = sgr('35'), blue = sgr('34'), red = sgr('31');
|
|
25
|
+
|
|
26
|
+
const log = (...a) => console.log(...a);
|
|
27
|
+
const rule = (ch = 'β') => log(dim(ch.repeat(74)));
|
|
28
|
+
function act(n, title, subtitle) {
|
|
29
|
+
log('');
|
|
30
|
+
rule('β');
|
|
31
|
+
log(`${bold(magenta(` ACT ${n}`))} ${bold(title)}`);
|
|
32
|
+
if (subtitle) log(` ${dim(subtitle)}`);
|
|
33
|
+
rule('β');
|
|
34
|
+
}
|
|
35
|
+
const step = (s) => log(` ${cyan('β')} ${s}`);
|
|
36
|
+
const tick = (s) => log(` ${green('β')} ${s}`);
|
|
37
|
+
const note = (s) => log(` ${dim(s)}`);
|
|
38
|
+
const kv = (k, v) => log(` ${dim(k.padEnd(16))} ${v}`);
|
|
39
|
+
|
|
40
|
+
// Positive sanity guard β confirms an invariant; only speaks up if reality
|
|
41
|
+
// disagrees (which, on an intact build, it won't).
|
|
42
|
+
function expect(cond, msg) {
|
|
43
|
+
if (!cond) {
|
|
44
|
+
log(` ${red('β')} ${bold('unexpected:')} ${msg}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
throw new Error(msg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const short = (h) => (h ? `${h.slice(0, 12)}β¦` : 'β
');
|
|
51
|
+
const tmp = (label) => fs.mkdtempSync(path.join(os.tmpdir(), `nedb-smoke-${label}-`));
|
|
52
|
+
const rm = (d) => { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} };
|
|
53
|
+
|
|
54
|
+
// ββ resolve the native addon (installed package, or in-repo after a build) βββ
|
|
55
|
+
let NedbCore;
|
|
56
|
+
try {
|
|
57
|
+
({ NedbCore } = await import('nedb-engine'));
|
|
58
|
+
} catch {
|
|
59
|
+
try {
|
|
60
|
+
({ NedbCore } = await import(new URL('../index.js', import.meta.url)));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log(red(bold('\n nedb-engine native addon not found.')));
|
|
63
|
+
note('This smoke test drives the prebuilt NedbCore binding.');
|
|
64
|
+
note('From a source checkout, build it first: npm run build');
|
|
65
|
+
note(`(${err && err.message ? err.message : err})`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ββ banner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
71
|
+
log('');
|
|
72
|
+
log(bold(cyan(' N E D B Β· native smoke test')));
|
|
73
|
+
log(dim(' hash-chained Β· bi-temporal Β· causal-provenance Β· v1βv2βv3'));
|
|
74
|
+
|
|
75
|
+
const cleanup = [];
|
|
76
|
+
const t0 = Date.now();
|
|
77
|
+
try {
|
|
78
|
+
|
|
79
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
80
|
+
act('I', 'v1 β the legacy append-only op-log', 'where NEDB began: one JSON op per line, in log.aof');
|
|
81
|
+
|
|
82
|
+
const v1dir = tmp('v1'); cleanup.push(v1dir);
|
|
83
|
+
// The v1 wire format the migrator understands: {seq, op, ts, payload:{coll,id,doc}}.
|
|
84
|
+
// A tiny legacy "rides ledger" left behind by an older NEDB.
|
|
85
|
+
const v1ops = [
|
|
86
|
+
{ seq: 0, op: 'put', ts: 1719400000.0, payload: { coll: 'trips', id: 't-1001', doc: { rider: 'maya', driver: 'sam', fare: 18.5, city: 'metropolis' } } },
|
|
87
|
+
{ seq: 1, op: 'put', ts: 1719400100.0, payload: { coll: 'trips', id: 't-1002', doc: { rider: 'omar', driver: 'ana', fare: 24.0, city: 'metropolis' } } },
|
|
88
|
+
{ seq: 2, op: 'put', ts: 1719400200.0, payload: { coll: 'drivers', id: 'sam', doc: { name: 'Sam', rating: 4.97, joined: 2024 } } },
|
|
89
|
+
];
|
|
90
|
+
fs.writeFileSync(path.join(v1dir, 'log.aof'), v1ops.map((o) => JSON.stringify(o)).join('\n') + '\n');
|
|
91
|
+
step(`wrote a legacy ${bold('log.aof')} β ${v1ops.length} ops, append-only, plain JSON`);
|
|
92
|
+
for (const o of v1ops) note(`${o.op.toUpperCase()} ${o.payload.coll}/${o.payload.id} ${JSON.stringify(o.payload.doc)}`);
|
|
93
|
+
note('No hashes. No chain. No time-travel. Just an op log β that\'s v1.');
|
|
94
|
+
|
|
95
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
96
|
+
act('II', 'v1 β v2 β automatic migration', 'open() detects log.aof and rebuilds it as a content-addressed DAG');
|
|
97
|
+
|
|
98
|
+
step(`${bold('NedbCore.open(dir)')} β the engine speaks for itself:`);
|
|
99
|
+
const migrated = NedbCore.open(v1dir); // <- triggers migrate_if_needed()
|
|
100
|
+
const t1001 = JSON.parse(migrated.get('trips', 't-1001'));
|
|
101
|
+
tick(`legacy data is live in v2: trips/t-1001 β fare ${bold(t1001.fare)}, rider ${bold(t1001.rider)}`);
|
|
102
|
+
kv('now content-addressed', `_hash ${cyan(short(t1001._hash))} (BLAKE2b-256, ${t1001._hash.length} hex chars)`);
|
|
103
|
+
expect(t1001._hash && t1001._hash.length === 64, 'migrated node is content-addressed');
|
|
104
|
+
|
|
105
|
+
const bakKept = fs.existsSync(path.join(v1dir, 'log.aof.v1.bak'));
|
|
106
|
+
const aofGone = !fs.existsSync(path.join(v1dir, 'log.aof'));
|
|
107
|
+
tick(`non-destructive: original preserved as ${bold('log.aof.v1.bak')} (${bakKept ? 'kept' : 'MISSING'})`);
|
|
108
|
+
expect(bakKept && aofGone, 'log.aof renamed to .v1.bak after migration');
|
|
109
|
+
tick(`integrity after migration: verify() = ${green(String(migrated.verify()))}`);
|
|
110
|
+
expect(migrated.verify() === true, 'migrated store verifies clean');
|
|
111
|
+
note('Zero user action. Lossless. Reversible. The op-log became a DAG.');
|
|
112
|
+
|
|
113
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
114
|
+
act('III', 'v2 β the content-addressed DAG', 'hash chain Β· MVCC time-travel Β· causal graph Β· tamper-evident');
|
|
115
|
+
|
|
116
|
+
const v2 = new NedbCore(); // pure in-memory v2 β zero disk I/O
|
|
117
|
+
step('a fresh in-memory v2 DAG (no disk) β watch the Merkle head advance');
|
|
118
|
+
const h0 = v2.head(), s0 = v2.seq();
|
|
119
|
+
for (let i = 0; i < 5; i++) v2.put('blocks', String(i), JSON.stringify({ height: i, note: `block ${i}` }));
|
|
120
|
+
kv('seq', `${dim(String(s0))} β ${bold(String(v2.seq()))} (every write extends the chain)`);
|
|
121
|
+
kv('head', `${dim(short(h0))} β ${bold(short(v2.head()))}`);
|
|
122
|
+
expect(v2.seq() > s0 && v2.head() !== h0, 'chain advanced');
|
|
123
|
+
|
|
124
|
+
step('NQL β a real query language over the DAG');
|
|
125
|
+
kv('FROM blocks', `${v2.query('FROM blocks').length} rows`);
|
|
126
|
+
const q = v2.query('FROM blocks WHERE height = 3').map(JSON.parse);
|
|
127
|
+
kv('β¦ WHERE height = 3', `${q.length} row β ${JSON.stringify({ height: q[0].height, note: q[0].note })}`);
|
|
128
|
+
|
|
129
|
+
step('MVCC time-travel β AS OF a past sequence');
|
|
130
|
+
const v1n = JSON.parse(v2.put('account', 'alice', JSON.stringify({ balance: 100 })));
|
|
131
|
+
const asOf = BigInt(v1n._seq);
|
|
132
|
+
v2.put('account', 'alice', JSON.stringify({ balance: 250 })); // new version, same id
|
|
133
|
+
const nowBal = JSON.parse(v2.get('account', 'alice')).balance;
|
|
134
|
+
const thenBal = JSON.parse(v2.getAsOf('account', 'alice', asOf)).balance;
|
|
135
|
+
kv('alice now', bold(`$${nowBal}`));
|
|
136
|
+
kv(`alice AS OF #${asOf}`, bold(`$${thenBal}`) + dim(' β the past is still there, exactly'));
|
|
137
|
+
expect(nowBal === 250 && thenBal === 100, 'AS OF returns the historical version');
|
|
138
|
+
|
|
139
|
+
step('causal graph β typed edges you can traverse');
|
|
140
|
+
v2.link('blocks:4', 'prev', 'blocks:3');
|
|
141
|
+
v2.link('blocks:3', 'prev', 'blocks:2');
|
|
142
|
+
kv('neighbors(4,prev)', JSON.stringify(v2.neighbors('blocks:4', 'prev')));
|
|
143
|
+
kv('inbound(3,prev)', JSON.stringify(v2.inbound('blocks:3', 'prev')) + dim(' β who points at me?'));
|
|
144
|
+
|
|
145
|
+
step('tamper-evidence β the whole store self-verifies');
|
|
146
|
+
tick(`verify() = ${green(String(v2.verify()))} ${dim('β every node\'s hash checked against its content')}`);
|
|
147
|
+
expect(v2.verify() === true, 'intact store verifies clean');
|
|
148
|
+
|
|
149
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
150
|
+
act('IV', 'v3 β the segment/pack object store', 'same API, denser substrate: one fsync per group-commit, not per object');
|
|
151
|
+
|
|
152
|
+
const docs = 64;
|
|
153
|
+
const sample = (i) => JSON.stringify({ i, payload: `coin-${i}`, ts: 1719400000 + i });
|
|
154
|
+
|
|
155
|
+
// v2 substrate (env unset): loose objects β one file per write.
|
|
156
|
+
delete process.env.NEDB_DAG_V3;
|
|
157
|
+
const looseDir = tmp('v2loose'); cleanup.push(looseDir);
|
|
158
|
+
const looseDb = NedbCore.open(looseDir);
|
|
159
|
+
for (let i = 0; i < docs; i++) looseDb.put('utxo', String(i), sample(i));
|
|
160
|
+
looseDb.flush();
|
|
161
|
+
const looseObjs = countFiles(path.join(looseDir, 'objects'));
|
|
162
|
+
step(`v2 default substrate β ${bold(docs)} writes`);
|
|
163
|
+
kv('objects/ layout', `${bold(looseObjs)} loose object files ${dim('(content-addressed, one per object)')}`);
|
|
164
|
+
|
|
165
|
+
// v3 substrate (env set BEFORE open β the engine reads it per-open).
|
|
166
|
+
process.env.NEDB_DAG_V3 = '1';
|
|
167
|
+
const segDir = tmp('v3seg'); cleanup.push(segDir);
|
|
168
|
+
const segDb = NedbCore.open(segDir);
|
|
169
|
+
for (let i = 0; i < docs; i++) segDb.put('utxo', String(i), sample(i));
|
|
170
|
+
segDb.flush();
|
|
171
|
+
const segPath = path.join(segDir, 'objects', 'segments');
|
|
172
|
+
const segFiles = fs.existsSync(segPath) ? fs.readdirSync(segPath).filter((f) => f.endsWith('.dat')) : [];
|
|
173
|
+
step(`v3 segment substrate β ${bold(docs)} writes ${dim('(NEDB_DAG_V3=1)')}`);
|
|
174
|
+
if (segFiles.length) {
|
|
175
|
+
const seg0 = path.join(segPath, segFiles[0]);
|
|
176
|
+
const sz = fs.statSync(seg0).size;
|
|
177
|
+
kv('objects/segments/', `${bold(segFiles.length)} segment file: ${cyan(segFiles[0])} (${sz} bytes)`);
|
|
178
|
+
note(`${docs} objects packed into ${segFiles.length} append-only segment β the metadata-write ceiling is gone.`);
|
|
179
|
+
} else {
|
|
180
|
+
note('segment file not present yet (objects buffered) β engine still serving from memory+WAL.');
|
|
181
|
+
}
|
|
182
|
+
step('v3 round-trips and verifies after reopen');
|
|
183
|
+
const segReopen = NedbCore.open(segDir); // env still set β reopen as v3
|
|
184
|
+
const rt = JSON.parse(segReopen.get('utxo', '7'));
|
|
185
|
+
kv('reopen β utxo/7', JSON.stringify({ i: rt.i, payload: rt.payload }));
|
|
186
|
+
tick(`verify() = ${green(String(segReopen.verify()))} ${dim('β segment store + dual-read of any v2 loose objects')}`);
|
|
187
|
+
expect(rt.i === 7 && segReopen.verify() === true, 'v3 persists and verifies across reopen');
|
|
188
|
+
delete process.env.NEDB_DAG_V3; // leave the environment as we found it
|
|
189
|
+
|
|
190
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
191
|
+
act('V', 'The Honest Dispatch', 'a rideshare match that ends in a good choice β because the data says so');
|
|
192
|
+
|
|
193
|
+
const rs = new NedbCore();
|
|
194
|
+
step('the world at request time β a rider and three real candidates');
|
|
195
|
+
// Facts. Each put() returns the stored node; we keep its _hash as an immutable
|
|
196
|
+
// citation we can later prove the decision was built from.
|
|
197
|
+
const req = JSON.parse(rs.put('request', 'trip-9001', JSON.stringify({
|
|
198
|
+
rider: 'maya', from: 'zone:downtown', to: 'airport', when: '18:10', wants: 'fast pickup',
|
|
199
|
+
})));
|
|
200
|
+
const surge = JSON.parse(rs.put('surge', 'zone:downtown', JSON.stringify({ multiplier: 1.2, at: '18:10' })));
|
|
201
|
+
|
|
202
|
+
const drivers = {
|
|
203
|
+
sam: { name: 'Sam', rating: 4.97, etaMin: 4, distMi: 0.7, recentCancels: 0, acceptsPool: true },
|
|
204
|
+
lee: { name: 'Lee', rating: 4.61, etaMin: 2, distMi: 0.3, recentCancels: 2, acceptsPool: false },
|
|
205
|
+
ana: { name: 'Ana', rating: 4.99, etaMin: 9, distMi: 1.2, recentCancels: 0, acceptsPool: true },
|
|
206
|
+
};
|
|
207
|
+
const driverHash = {};
|
|
208
|
+
for (const [id, d] of Object.entries(drivers)) {
|
|
209
|
+
driverHash[id] = JSON.parse(rs.put('driver', id, JSON.stringify({ ...d, available: true })))._hash;
|
|
210
|
+
}
|
|
211
|
+
for (const [id, d] of Object.entries(drivers)) {
|
|
212
|
+
kv(`driver:${id}`, `${d.name} β
${d.rating} eta ${d.etaMin}m ${d.distMi}mi cancels ${d.recentCancels} ${d.acceptsPool ? 'pool' : 'solo'}`);
|
|
213
|
+
}
|
|
214
|
+
note(`surge in downtown right now: ${bold(surge.multiplier + 'Γ')}`);
|
|
215
|
+
|
|
216
|
+
step('score every candidate from the stored facts β not a hunch, a calculation');
|
|
217
|
+
// Transparent scoring: reward rating + ETA + pool; penalize recent cancels.
|
|
218
|
+
const score = (d) => +(
|
|
219
|
+
d.rating * 2
|
|
220
|
+
- d.etaMin * 0.25
|
|
221
|
+
- d.recentCancels * 1.5
|
|
222
|
+
+ (d.acceptsPool ? 0.4 : 0)
|
|
223
|
+
).toFixed(3);
|
|
224
|
+
const ranked = Object.entries(drivers)
|
|
225
|
+
.map(([id, d]) => ({ id, d, s: score(d) }))
|
|
226
|
+
.sort((a, b) => b.s - a.s);
|
|
227
|
+
for (const r of ranked) {
|
|
228
|
+
const why = r.id === 'lee' ? dim('(closest β but 2 recent cancels drag it down)')
|
|
229
|
+
: r.id === 'ana' ? dim('(top-rated β but a 9-min ETA for a "fast pickup")')
|
|
230
|
+
: dim('(4.97β
, 4-min ETA, no cancels, takes pool)');
|
|
231
|
+
kv(`score driver:${r.id}`, `${bold(r.s.toFixed(2))} ${why}`);
|
|
232
|
+
}
|
|
233
|
+
const winner = ranked[0];
|
|
234
|
+
note(`naive "closest" would pick ${bold('Lee')} (0.3mi). The data picks ${bold(drivers[winner.id].name)}.`);
|
|
235
|
+
|
|
236
|
+
step('record the decision β with its causes wired in, permanently');
|
|
237
|
+
const decision = JSON.parse(rs.put('decision', 'trip-9001', JSON.stringify({
|
|
238
|
+
chosen: `driver:${winner.id}`,
|
|
239
|
+
score: winner.s,
|
|
240
|
+
policy: 'rating+eta+reliability+pool',
|
|
241
|
+
// caused_by: the exact immutable facts this choice was built from.
|
|
242
|
+
caused_by: [req._hash, surge._hash, driverHash[winner.id]],
|
|
243
|
+
})));
|
|
244
|
+
const decisionSeq = BigInt(decision._seq);
|
|
245
|
+
rs.link(`decision:trip-9001`, 'chose', `driver:${winner.id}`);
|
|
246
|
+
for (const r of ranked.slice(1)) rs.link(`decision:trip-9001`, 'considered', `driver:${r.id}`);
|
|
247
|
+
tick(`chose ${bold('driver:' + winner.id)} (${drivers[winner.id].name}) Β· decision is now a node in the DAG`);
|
|
248
|
+
|
|
249
|
+
step('AUDIT β "why did dispatch pick this driver?" Follow the causal trail.');
|
|
250
|
+
kv('decision.caused_by', `[ ${decision.caused_by.map(short).join(', ')} ]`);
|
|
251
|
+
const causeLabel = { [req._hash]: 'the rider\'s request', [surge._hash]: 'the surge snapshot', [driverHash[winner.id]]: `${drivers[winner.id].name}'s live state` };
|
|
252
|
+
for (const h of decision.caused_by) note(`${cyan(short(h))} β ${causeLabel[h] || 'a fact'}`);
|
|
253
|
+
kv('chose', JSON.stringify(rs.neighbors('decision:trip-9001', 'chose')));
|
|
254
|
+
kv('considered', JSON.stringify(rs.neighbors('decision:trip-9001', 'considered')) + dim(' β the alternatives, on the record'));
|
|
255
|
+
|
|
256
|
+
step('REPRODUCE β surge spikes after the fact; the decision is unmoved');
|
|
257
|
+
rs.put('surge', 'zone:downtown', JSON.stringify({ multiplier: 2.5, at: '18:25' })); // later reality
|
|
258
|
+
const surgeNow = JSON.parse(rs.get('surge', 'zone:downtown')).multiplier;
|
|
259
|
+
const surgeThen = JSON.parse(rs.getAsOf('surge', 'zone:downtown', decisionSeq)).multiplier;
|
|
260
|
+
kv('surge now', bold(surgeNow + 'Γ'));
|
|
261
|
+
kv('surge AS OF decision', bold(surgeThen + 'Γ') + dim(' β what the dispatcher actually saw; the audit is reproducible'));
|
|
262
|
+
expect(surgeThen === 1.2, 'AS OF reconstructs the world at decision time');
|
|
263
|
+
|
|
264
|
+
step('the good ending');
|
|
265
|
+
rs.put('trip', 'trip-9001', JSON.stringify({ status: 'completed', driver: `driver:${winner.id}`, riderRating: 5 }));
|
|
266
|
+
tick(`${bold('Maya')} matched with ${bold(drivers[winner.id].name)} β trip completed β ${yellow('β
β
β
β
β
')}`);
|
|
267
|
+
tick(`fully auditable, content-addressed, reproducible β ${bold('a good choice, grounded in causal data')}`);
|
|
268
|
+
expect(rs.verify() === true, 'rideshare store verifies clean');
|
|
269
|
+
|
|
270
|
+
// ββ curtain ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
271
|
+
log('');
|
|
272
|
+
rule('β');
|
|
273
|
+
log(` ${green(bold('β all five acts passed'))} ${dim(`in ${Date.now() - t0} ms`)}`);
|
|
274
|
+
log(` ${dim('v1 β v2 migration Β· v2 DAG Β· v3 segments Β· causal audit β all on the native engine')}`);
|
|
275
|
+
rule('β');
|
|
276
|
+
log('');
|
|
277
|
+
} catch (err) {
|
|
278
|
+
log('');
|
|
279
|
+
log(red(bold(' smoke test failed:')) + ' ' + (err && err.message ? err.message : String(err)));
|
|
280
|
+
if (err && err.stack) log(dim(err.stack.split('\n').slice(1, 4).join('\n')));
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
} finally {
|
|
283
|
+
for (const d of cleanup) rm(d);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ββ helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
287
|
+
function countFiles(root) {
|
|
288
|
+
let n = 0;
|
|
289
|
+
const walk = (d) => {
|
|
290
|
+
let ents;
|
|
291
|
+
try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
292
|
+
for (const e of ents) {
|
|
293
|
+
const p = path.join(d, e.name);
|
|
294
|
+
if (e.isDirectory()) walk(p); else n++;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
walk(root);
|
|
298
|
+
return n;
|
|
299
|
+
}
|