@libredb/libredb 0.0.4 → 0.1.1
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 +98 -0
- package/dist/adapter/node-fs.d.ts +3 -0
- package/dist/adapter/node-fs.js +49 -0
- package/dist/adapter/opfs.d.ts +49 -0
- package/dist/adapter/opfs.js +36 -0
- package/dist/browser.d.ts +24 -0
- package/dist/browser.js +19 -0
- package/dist/cli/lock.d.ts +7 -0
- package/dist/cli/lock.js +51 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +15 -0
- package/dist/cli/readonly-fs.d.ts +3 -0
- package/dist/cli/readonly-fs.js +41 -0
- package/dist/cli/run.d.ts +7 -0
- package/dist/cli/run.js +231 -0
- package/dist/core.d.ts +11 -8
- package/dist/core.js +15 -41
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# LibreDB
|
|
2
2
|
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/img/01-hero-dark.png">
|
|
5
|
+
<img alt="LibreDB - Multi-model without the magic. One core, three lenses, every line tested." src="docs/img/01-hero-light.png" width="100%">
|
|
6
|
+
</picture>
|
|
7
|
+
|
|
3
8
|
**Multi-model without the magic. One core, three lenses, every line tested.**
|
|
4
9
|
|
|
5
10
|
[](https://www.npmjs.com/package/@libredb/libredb)
|
|
@@ -79,8 +84,86 @@ Each lens has its own guide: [key-value](./docs/guides/key-value.md) ·
|
|
|
79
84
|
[document](./docs/guides/document.md) · [relational](./docs/guides/relational.md) ·
|
|
80
85
|
[catalog](./docs/guides/catalog.md).
|
|
81
86
|
|
|
87
|
+
## Install elsewhere: JSR, CDN, and the browser
|
|
88
|
+
|
|
89
|
+
LibreDB is the same ESM-only package everywhere; only how you reach it changes.
|
|
90
|
+
|
|
91
|
+
**JSR** — published to [jsr.io](https://jsr.io/@libredb/libredb) alongside npm:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
bunx jsr add @libredb/libredb
|
|
95
|
+
# or: npx jsr add @libredb/libredb / deno add jsr:@libredb/libredb
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**CDN** — every release is served from the npm registry by the usual CDNs. Pin a version:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { open, kv } from "https://esm.sh/@libredb/libredb@0.1.1";
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Browser** — a dedicated entry that imports nothing from `node:`, so it bundles for the browser
|
|
105
|
+
cleanly. Its `open` carries no default filesystem: an in-memory database works anywhere, and a
|
|
106
|
+
path-backed open takes a filesystem you inject (e.g. the OPFS adapter shown below).
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { open, kv } from "@libredb/libredb/browser";
|
|
110
|
+
|
|
111
|
+
const db = open(); // in-memory
|
|
112
|
+
kv(db).set("greeting", "hello");
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For durable storage in the browser, run inside a Web Worker and back the database with an OPFS sync
|
|
116
|
+
access handle (the kernel stays synchronous — no async core):
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { open, opfsFileSystem } from "@libredb/libredb/browser";
|
|
120
|
+
|
|
121
|
+
const root = await navigator.storage.getDirectory();
|
|
122
|
+
const file = await root.getFileHandle("app.libredb", { create: true });
|
|
123
|
+
const handle = await file.createSyncAccessHandle();
|
|
124
|
+
const db = open({ path: "app.libredb", fs: opfsFileSystem(handle) });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A browser-targeting bundler resolves the browser build automatically via the package's `browser`
|
|
128
|
+
export condition, so importing the main `@libredb/libredb` entry works too.
|
|
129
|
+
|
|
130
|
+
## Command-line tool
|
|
131
|
+
|
|
132
|
+
The package ships a `libredb` bin for inspecting and editing `.libredb` files — no code required:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
npx libredb inspect data.libredb # namespaces, kinds, and table schemas
|
|
136
|
+
npx libredb stats data.libredb # file size and namespace counts
|
|
137
|
+
npx libredb get data.libredb user:1 # print one value
|
|
138
|
+
npx libredb scan data.libredb user: # print key=value under a prefix
|
|
139
|
+
npx libredb set data.libredb user:1 Ada # set a key
|
|
140
|
+
npx libredb delete data.libredb user:1 # remove a key
|
|
141
|
+
npx libredb import data.libredb seed.json # bulk-set from a JSON object, atomically
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Read commands open the file read-only, so inspection never mutates it. Write commands take an
|
|
145
|
+
advisory `<path>.lock` to refuse a second concurrent writer; pass `--force` to override a stale lock.
|
|
146
|
+
|
|
147
|
+
Prefer a standalone binary with no Node or Bun installed? Each release attaches self-contained
|
|
148
|
+
executables (Linux, macOS, Windows; x64 and arm64) with `.sha256` checksums on its
|
|
149
|
+
[GitHub Release](https://github.com/libredb/libredb-database/releases). Or build one locally with
|
|
150
|
+
`bun run compile`.
|
|
151
|
+
|
|
152
|
+
Or run the CLI from a container (multi-arch, published to GHCR) — mount your data and pass a command:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
docker run --rm -v "$PWD:/data" ghcr.io/libredb/libredb inspect /data/app.libredb
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The image is a CLI shell, not a server: LibreDB stays an embedded, in-process database.
|
|
159
|
+
|
|
82
160
|
## How it works: one core, three lenses
|
|
83
161
|
|
|
162
|
+
<picture>
|
|
163
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/img/02-lenses-dark.png">
|
|
164
|
+
<img alt="One core, three lenses: kv, document, and relational APIs over a single ordered key-value core (core.ts), reaching disk through one FileSystem seam." src="docs/img/02-lenses-light.png" width="100%">
|
|
165
|
+
</picture>
|
|
166
|
+
|
|
84
167
|
LibreDB has a single ordered byte key-value kernel (`src/core.ts`). Key-value, document, and relational
|
|
85
168
|
are thin typed *lenses* over it — three faces of the same store. A relational table is physically a
|
|
86
169
|
JSON document collection, which is physically ordered key-value entries built from composite keys like
|
|
@@ -122,6 +205,11 @@ flowchart TB
|
|
|
122
205
|
CORE --> FS
|
|
123
206
|
```
|
|
124
207
|
|
|
208
|
+
<picture>
|
|
209
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/img/03-trust-dark.png">
|
|
210
|
+
<img alt="Open at the edges, guarded at the core: lenses, query surface, catalog, adapters, Studio, and docs are open to contribute; core.ts is guarded with 100% line coverage, deterministic crash tests, and heavy review - everything reaches the store through one narrow transact() port." src="docs/img/03-trust-light.png" width="100%">
|
|
211
|
+
</picture>
|
|
212
|
+
|
|
125
213
|
**The file boundary is the trust boundary.** Below the line (`core.ts`) is guarded: heavy review and
|
|
126
214
|
deterministic crash tests, because a bug there corrupts data. Above the line (lenses, query, catalog)
|
|
127
215
|
is open and fast to contribute to, because the worst a bug can do is present a bad *view* — it reaches
|
|
@@ -150,6 +238,11 @@ See the [Manifesto](./MANIFESTO.md).
|
|
|
150
238
|
|
|
151
239
|
## Reliability
|
|
152
240
|
|
|
241
|
+
<picture>
|
|
242
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/img/04-reliability-dark.png">
|
|
243
|
+
<img alt="Crash recovery you can trust: a length-framed, CRC-32-checksummed write-ahead log fsync'd before commit; recovery truncates the last un-fsync'd record so what remains is always a valid committed prefix - proven by deterministic simulation testing." src="docs/img/04-reliability-light.png" width="100%">
|
|
244
|
+
</picture>
|
|
245
|
+
|
|
153
246
|
A transaction that returns has been written to a length-framed, CRC-32-checksummed write-ahead log and
|
|
154
247
|
`fsync`'d *before* the commit becomes visible — so a committed write survives a crash, and a crash can
|
|
155
248
|
only ever damage the last, un-fsync'd record (which recovery detects and truncates). This is not just
|
|
@@ -187,6 +280,11 @@ but the API may still change and it is not yet meant for production data.
|
|
|
187
280
|
|
|
188
281
|
## The LibreDB family
|
|
189
282
|
|
|
283
|
+
<picture>
|
|
284
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/img/05-family-dark.png">
|
|
285
|
+
<img alt="The LibreDB family: LibreDB (the database, this repo), LibreDB Studio (the open-source IDE for every database), and LibreDB Platform (the managed, team-oriented form of data) - three products, one access-model spine." src="docs/img/05-family-light.png" width="100%">
|
|
286
|
+
</picture>
|
|
287
|
+
|
|
190
288
|
LibreDB is the database in a three-product family that shares one access-model spine:
|
|
191
289
|
|
|
192
290
|
- **LibreDB** — the database (this repository).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter/node-fs.ts — the default {@link FileSystem} for Node and Bun.
|
|
3
|
+
*
|
|
4
|
+
* This is an edge, not the kernel (DESIGN.md section 5): it holds the one place
|
|
5
|
+
* LibreDB touches `node:fs`. The kernel (`core.ts`) is runtime-agnostic and
|
|
6
|
+
* imports nothing from `node:`; it reaches the disk only through the injected
|
|
7
|
+
* {@link FileSystem} seam. Keeping the node dependency HERE — and out of the
|
|
8
|
+
* kernel — is what lets the browser entry (`browser.ts`) ship without dragging
|
|
9
|
+
* `node:fs` into its import graph. The default Node entry (`index.ts`) wires this
|
|
10
|
+
* adapter in as the default `fs`, so production behaviour is unchanged.
|
|
11
|
+
*
|
|
12
|
+
* Each method is the obvious synchronous syscall, so the adapter adds an
|
|
13
|
+
* interface boundary, not behaviour. Appends go through one append-mode
|
|
14
|
+
* descriptor (creating the file if missing); reads, size and truncate work by
|
|
15
|
+
* path, matching how the WAL has always reached the disk.
|
|
16
|
+
*/
|
|
17
|
+
import { closeSync, fsyncSync, openSync, readFileSync, statSync, truncateSync, writeSync } from "node:fs";
|
|
18
|
+
/** Build the default node:fs-backed {@link FileSystem}. */
|
|
19
|
+
export function nodeFileSystem() {
|
|
20
|
+
return {
|
|
21
|
+
open(path) {
|
|
22
|
+
const fd = openSync(path, "a"); // append-only; creates the file if missing
|
|
23
|
+
return {
|
|
24
|
+
size() {
|
|
25
|
+
return statSync(path).size;
|
|
26
|
+
},
|
|
27
|
+
read(offset, length) {
|
|
28
|
+
// A fresh Uint8Array so the returned slice is an independent copy, not
|
|
29
|
+
// a view aliasing a shared Buffer pool.
|
|
30
|
+
return new Uint8Array(readFileSync(path)).subarray(offset, offset + length);
|
|
31
|
+
},
|
|
32
|
+
append(bytes) {
|
|
33
|
+
for (let written = 0; written < bytes.length;) {
|
|
34
|
+
written += writeSync(fd, bytes, written);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
fsync() {
|
|
38
|
+
fsyncSync(fd);
|
|
39
|
+
},
|
|
40
|
+
truncate(length) {
|
|
41
|
+
truncateSync(path, length);
|
|
42
|
+
},
|
|
43
|
+
close() {
|
|
44
|
+
closeSync(fd);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter/opfs.ts — an OPFS-backed {@link FileSystem} for the browser.
|
|
3
|
+
*
|
|
4
|
+
* An edge, not the kernel: it carries no durability logic, only the mapping from
|
|
5
|
+
* the kernel's synchronous {@link FileSystem} seam onto an OPFS sync access
|
|
6
|
+
* handle. The point is that a `FileSystemSyncAccessHandle` exposes SYNCHRONOUS
|
|
7
|
+
* read/write/getSize/truncate/flush/close — the exact shape the kernel's WAL
|
|
8
|
+
* needs — so LibreDB can be durable in a browser with no async core.
|
|
9
|
+
*
|
|
10
|
+
* Sync access handles only exist inside a Web Worker, and obtaining one is async
|
|
11
|
+
* (navigator.storage.getDirectory -> getFileHandle -> createSyncAccessHandle).
|
|
12
|
+
* That async acquisition is the caller's, done once before {@link
|
|
13
|
+
* import("../core.ts").open}; this adapter takes the already-open handle and
|
|
14
|
+
* wraps it synchronously, keeping `open` synchronous. Typical use in a Worker:
|
|
15
|
+
*
|
|
16
|
+
* const root = await navigator.storage.getDirectory();
|
|
17
|
+
* const file = await root.getFileHandle("app.libredb", { create: true });
|
|
18
|
+
* const handle = await file.createSyncAccessHandle();
|
|
19
|
+
* const db = open({ path: "app.libredb", fs: opfsFileSystem(handle) });
|
|
20
|
+
*
|
|
21
|
+
* The handle is bound to one file, so the kernel's `path` is unused here — the
|
|
22
|
+
* database opened with this adapter is the file the handle was created for.
|
|
23
|
+
*/
|
|
24
|
+
import type { FileSystem } from "../core.ts";
|
|
25
|
+
/**
|
|
26
|
+
* The synchronous subset of a browser `FileSystemSyncAccessHandle` the WAL uses.
|
|
27
|
+
* Declared locally so the package needs no DOM lib types; a real sync access
|
|
28
|
+
* handle satisfies it structurally.
|
|
29
|
+
*/
|
|
30
|
+
export interface SyncAccessHandle {
|
|
31
|
+
/** Read into `buffer` starting at `options.at` (default 0); returns bytes read. */
|
|
32
|
+
read(buffer: Uint8Array, options?: {
|
|
33
|
+
at?: number;
|
|
34
|
+
}): number;
|
|
35
|
+
/** Write `buffer` starting at `options.at` (default 0); returns bytes written. */
|
|
36
|
+
write(buffer: Uint8Array, options?: {
|
|
37
|
+
at?: number;
|
|
38
|
+
}): number;
|
|
39
|
+
/** The file's current size in bytes. */
|
|
40
|
+
getSize(): number;
|
|
41
|
+
/** Resize the file to `newSize`, dropping anything beyond it. */
|
|
42
|
+
truncate(newSize: number): void;
|
|
43
|
+
/** Persist buffered writes to storage. */
|
|
44
|
+
flush(): void;
|
|
45
|
+
/** Release the handle. */
|
|
46
|
+
close(): void;
|
|
47
|
+
}
|
|
48
|
+
/** Build a {@link FileSystem} backed by an open OPFS sync access `handle`. */
|
|
49
|
+
export declare function opfsFileSystem(handle: SyncAccessHandle): FileSystem;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Build a {@link FileSystem} backed by an open OPFS sync access `handle`. */
|
|
2
|
+
export function opfsFileSystem(handle) {
|
|
3
|
+
return {
|
|
4
|
+
open() {
|
|
5
|
+
return {
|
|
6
|
+
size() {
|
|
7
|
+
return handle.getSize();
|
|
8
|
+
},
|
|
9
|
+
read(offset, length) {
|
|
10
|
+
const buffer = new Uint8Array(length);
|
|
11
|
+
const read = handle.read(buffer, { at: offset });
|
|
12
|
+
return buffer.subarray(0, read);
|
|
13
|
+
},
|
|
14
|
+
append(data) {
|
|
15
|
+
// write() may write fewer bytes than asked (hence the returned count),
|
|
16
|
+
// so loop until the whole record is on the handle — otherwise a short
|
|
17
|
+
// write would persist a torn record while fsync reports success. This
|
|
18
|
+
// mirrors the node-fs adapter's writeSync loop.
|
|
19
|
+
const base = handle.getSize();
|
|
20
|
+
for (let written = 0; written < data.length;) {
|
|
21
|
+
written += handle.write(data.subarray(written), { at: base + written });
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
fsync() {
|
|
25
|
+
handle.flush();
|
|
26
|
+
},
|
|
27
|
+
truncate(length) {
|
|
28
|
+
handle.truncate(length);
|
|
29
|
+
},
|
|
30
|
+
close() {
|
|
31
|
+
handle.close();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser.ts — the browser entry point of the LibreDB npm package
|
|
3
|
+
* (`@libredb/libredb/browser`).
|
|
4
|
+
*
|
|
5
|
+
* Same lens surface as the default Node entry ({@link import("./index.ts")}),
|
|
6
|
+
* with one difference: `open` is the kernel's own, carrying NO default
|
|
7
|
+
* filesystem. An in-memory database (`open()`) works anywhere; a path-backed
|
|
8
|
+
* open requires an injected `fs` (e.g. the bundled {@link opfsFileSystem}). The
|
|
9
|
+
* point of this entry is the import graph: it reaches nothing in `node:`, so a
|
|
10
|
+
* bundler can ship it to a browser. The node:fs adapter lives behind Node only.
|
|
11
|
+
*/
|
|
12
|
+
export { open, version } from "./core.ts";
|
|
13
|
+
export type { Database, FileSystem, OpenOptions, WalFile } from "./core.ts";
|
|
14
|
+
export { opfsFileSystem } from "./adapter/opfs.ts";
|
|
15
|
+
export type { SyncAccessHandle } from "./adapter/opfs.ts";
|
|
16
|
+
export { kv } from "./lens/kv.ts";
|
|
17
|
+
export type { Kv, KvEntry } from "./lens/kv.ts";
|
|
18
|
+
export { doc } from "./lens/document.ts";
|
|
19
|
+
export type { DocCollection, Doc, DocEntry, JsonValue } from "./lens/document.ts";
|
|
20
|
+
export { table } from "./lens/relational.ts";
|
|
21
|
+
export type { Table, TableSchema, Row, ColumnType, Query } from "./lens/relational.ts";
|
|
22
|
+
export { catalog, isReservedKey, CATALOG_PREFIX, RESERVED_MARKER } from "./lens/catalog.ts";
|
|
23
|
+
export type { CatalogEntry, CatalogRegistry } from "./lens/catalog.ts";
|
|
24
|
+
export type { Result, WriteResult } from "./lens/types.ts";
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser.ts — the browser entry point of the LibreDB npm package
|
|
3
|
+
* (`@libredb/libredb/browser`).
|
|
4
|
+
*
|
|
5
|
+
* Same lens surface as the default Node entry ({@link import("./index.ts")}),
|
|
6
|
+
* with one difference: `open` is the kernel's own, carrying NO default
|
|
7
|
+
* filesystem. An in-memory database (`open()`) works anywhere; a path-backed
|
|
8
|
+
* open requires an injected `fs` (e.g. the bundled {@link opfsFileSystem}). The
|
|
9
|
+
* point of this entry is the import graph: it reaches nothing in `node:`, so a
|
|
10
|
+
* bundler can ship it to a browser. The node:fs adapter lives behind Node only.
|
|
11
|
+
*/
|
|
12
|
+
export { open, version } from "./core.js";
|
|
13
|
+
// OPFS persistence (browser-only): wrap an OPFS sync access handle as the
|
|
14
|
+
// filesystem for a path-backed open. See adapter/opfs.ts for usage in a Worker.
|
|
15
|
+
export { opfsFileSystem } from "./adapter/opfs.js";
|
|
16
|
+
export { kv } from "./lens/kv.js";
|
|
17
|
+
export { doc } from "./lens/document.js";
|
|
18
|
+
export { table } from "./lens/relational.js";
|
|
19
|
+
export { catalog, isReservedKey, CATALOG_PREFIX, RESERVED_MARKER } from "./lens/catalog.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A held lock. Call {@link Lock.release} once the write is done. */
|
|
2
|
+
export interface Lock {
|
|
3
|
+
release(): void;
|
|
4
|
+
}
|
|
5
|
+
/** Acquire the advisory lock for `path`. With `force`, an existing libredb lock
|
|
6
|
+
* is dropped first; otherwise an existing lock makes this throw. */
|
|
7
|
+
export declare function acquireLock(path: string, force: boolean): Lock;
|
package/dist/cli/lock.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/lock.ts — an advisory write lock for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* LibreDB is single-process and does no file locking itself, so two writers to
|
|
5
|
+
* one file would corrupt it. Write commands take a `<path>.lock` first: an
|
|
6
|
+
* exclusive create that fails if the file already exists, turning a concurrent
|
|
7
|
+
* writer into a loud error instead of silent corruption. It is advisory only —
|
|
8
|
+
* `--force` drops a stale lock and proceeds. The lock is always released after
|
|
9
|
+
* the write (see withWriteDb in run.ts).
|
|
10
|
+
*/
|
|
11
|
+
import { closeSync, openSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
12
|
+
// Written into every lock file so a forced acquire can tell a real libredb lock
|
|
13
|
+
// from an unrelated file that merely happens to be named <path>.lock, and refuse
|
|
14
|
+
// to delete the latter.
|
|
15
|
+
const SENTINEL = "libredb-lock\n";
|
|
16
|
+
/** Acquire the advisory lock for `path`. With `force`, an existing libredb lock
|
|
17
|
+
* is dropped first; otherwise an existing lock makes this throw. */
|
|
18
|
+
export function acquireLock(path, force) {
|
|
19
|
+
const lockPath = `${path}.lock`;
|
|
20
|
+
if (force)
|
|
21
|
+
dropOwnLock(lockPath);
|
|
22
|
+
try {
|
|
23
|
+
const fd = openSync(lockPath, "wx"); // "wx": exclusive create, fails if it exists
|
|
24
|
+
writeSync(fd, SENTINEL);
|
|
25
|
+
closeSync(fd);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`libredb: ${path} is locked (${lockPath}); another writer may be active — use --force to override`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
release() {
|
|
32
|
+
rmSync(lockPath, { force: true });
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Remove an existing libredb lock so a forced acquire can proceed. Refuses to
|
|
37
|
+
* touch a file that is not a libredb lock, so `--force` can never delete
|
|
38
|
+
* unrelated user data that happens to share the `.lock` name. */
|
|
39
|
+
function dropOwnLock(lockPath) {
|
|
40
|
+
let contents;
|
|
41
|
+
try {
|
|
42
|
+
contents = readFileSync(lockPath, "utf8");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return; // no lock file to drop
|
|
46
|
+
}
|
|
47
|
+
if (!contents.startsWith(SENTINEL)) {
|
|
48
|
+
throw new Error(`libredb: refusing to remove ${lockPath} with --force: not a libredb lock file`);
|
|
49
|
+
}
|
|
50
|
+
rmSync(lockPath, { force: true });
|
|
51
|
+
}
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli/main.ts — the `libredb` bin shim.
|
|
4
|
+
*
|
|
5
|
+
* The only file that touches the real process: it wires process argv/stdout/
|
|
6
|
+
* stderr/exit code to the pure {@link run}. All behaviour lives in run.ts and is
|
|
7
|
+
* tested there; this glue is excluded from coverage (see bunfig.toml) because it
|
|
8
|
+
* cannot be exercised without spawning a process, and a behavioural smoke test
|
|
9
|
+
* (main.test.ts) runs the bin end-to-end instead.
|
|
10
|
+
*/
|
|
11
|
+
import { run } from "./run.js";
|
|
12
|
+
process.exitCode = run(process.argv.slice(2), {
|
|
13
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
14
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
15
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/readonly-fs.ts — a read-only {@link FileSystem} for the inspection CLI.
|
|
3
|
+
*
|
|
4
|
+
* An edge, not the kernel: it carries no durability logic, only the node:fs
|
|
5
|
+
* calls a read needs. Inspection commands open a database purely to read it, but
|
|
6
|
+
* open() runs recovery, which truncates a torn tail — a write. This adapter makes
|
|
7
|
+
* that impossible: `append` and `fsync` refuse, and `truncate` is a no-op, so a
|
|
8
|
+
* crash-interrupted file is recovered correctly IN MEMORY (the torn tail is
|
|
9
|
+
* dropped from the returned entries) while the bytes on disk are left exactly as
|
|
10
|
+
* found. `size` and `read` are the obvious read syscalls.
|
|
11
|
+
*/
|
|
12
|
+
import { closeSync, openSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
/** Build a read-only {@link FileSystem} over `node:fs`. */
|
|
14
|
+
export function readonlyFileSystem() {
|
|
15
|
+
return {
|
|
16
|
+
open(path) {
|
|
17
|
+
const fd = openSync(path, "r"); // read-only; throws if the file is absent
|
|
18
|
+
return {
|
|
19
|
+
size() {
|
|
20
|
+
return statSync(path).size;
|
|
21
|
+
},
|
|
22
|
+
read(offset, length) {
|
|
23
|
+
return new Uint8Array(readFileSync(path)).subarray(offset, offset + length);
|
|
24
|
+
},
|
|
25
|
+
append() {
|
|
26
|
+
throw new Error("libredb: read-only database; refusing to write");
|
|
27
|
+
},
|
|
28
|
+
fsync() {
|
|
29
|
+
throw new Error("libredb: read-only database; refusing to write");
|
|
30
|
+
},
|
|
31
|
+
truncate() {
|
|
32
|
+
// Deliberate no-op: a read must not alter the file. Recovery still
|
|
33
|
+
// drops a torn tail from the in-memory state; the disk is untouched.
|
|
34
|
+
},
|
|
35
|
+
close() {
|
|
36
|
+
closeSync(fd);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/run.ts — the LibreDB CLI as a pure function.
|
|
3
|
+
*
|
|
4
|
+
* `run(argv, io)` takes an argument vector and an IO sink and returns a process
|
|
5
|
+
* exit code. Keeping the whole CLI behind this seam — no direct stdout, no
|
|
6
|
+
* process.exit — is what makes every command and error path unit-testable; the
|
|
7
|
+
* bin shim (main.ts) is the only place that touches the real process.
|
|
8
|
+
*
|
|
9
|
+
* This is open-edge tooling over the public API, not kernel code: it adds no
|
|
10
|
+
* durability logic. Read commands (inspect/stats/get/scan) open through the
|
|
11
|
+
* read-only filesystem adapter so inspecting a file never mutates it. Write
|
|
12
|
+
* commands (set/delete/import) take an advisory lock first, and import commits
|
|
13
|
+
* all keys in one transaction so a bulk load is atomic.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, statSync } from "node:fs";
|
|
16
|
+
import { parseArgs } from "node:util";
|
|
17
|
+
import { open } from "../index.js";
|
|
18
|
+
import { catalog, isReservedKey } from "../lens/catalog.js";
|
|
19
|
+
import { kv } from "../lens/kv.js";
|
|
20
|
+
import { acquireLock } from "./lock.js";
|
|
21
|
+
import { readonlyFileSystem } from "./readonly-fs.js";
|
|
22
|
+
const encoder = new TextEncoder();
|
|
23
|
+
const utf8 = (s) => encoder.encode(s);
|
|
24
|
+
const USAGE = [
|
|
25
|
+
"libredb - inspect and edit .libredb files",
|
|
26
|
+
"",
|
|
27
|
+
"Usage:",
|
|
28
|
+
" libredb inspect <path> List each namespace, its kind, and table schemas",
|
|
29
|
+
" libredb stats <path> Summarize the file: size and namespace counts",
|
|
30
|
+
" libredb get <path> <key> Print the value stored at a key",
|
|
31
|
+
" libredb scan <path> <prefix> Print key=value for every key under a prefix",
|
|
32
|
+
" libredb set <path> <key> <value> Set a key to a value",
|
|
33
|
+
" libredb delete <path> <key> Remove a key",
|
|
34
|
+
" libredb import <path> <file.json> Bulk-set keys from a JSON object (one atomic commit)",
|
|
35
|
+
"",
|
|
36
|
+
"Options:",
|
|
37
|
+
" --force Override an existing write lock",
|
|
38
|
+
].join("\n");
|
|
39
|
+
/** Open `path` read-only, run `fn`, and always close — so a read leaves the file
|
|
40
|
+
* exactly as it was (recovery cannot truncate a torn tail through this adapter). */
|
|
41
|
+
const withReadDb = (path, fn) => {
|
|
42
|
+
const db = open({ path, fs: readonlyFileSystem() });
|
|
43
|
+
try {
|
|
44
|
+
return fn(db);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
db.close();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** Take the advisory lock, open `path` for writing, run `fn`, then always close
|
|
51
|
+
* and release — so a crash mid-write cannot leave the lock stranded. */
|
|
52
|
+
const withWriteDb = (path, force, fn) => {
|
|
53
|
+
const lock = acquireLock(path, force);
|
|
54
|
+
try {
|
|
55
|
+
const db = open({ path });
|
|
56
|
+
try {
|
|
57
|
+
return fn(db);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
db.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
lock.release();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function inspect({ path, io }) {
|
|
68
|
+
return withReadDb(path, (db) => {
|
|
69
|
+
const registry = catalog(db);
|
|
70
|
+
io.out(`${path} ${statSync(path).size} bytes`);
|
|
71
|
+
if (registry.size === 0) {
|
|
72
|
+
io.out(" (no catalogued namespaces)");
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
for (const [name, entry] of registry) {
|
|
76
|
+
const schema = entry.schema === undefined ? "" : ` ${JSON.stringify(entry.schema)}`;
|
|
77
|
+
io.out(` ${name} ${entry.kind}${schema}`);
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function stats({ path, io }) {
|
|
83
|
+
return withReadDb(path, (db) => {
|
|
84
|
+
const registry = catalog(db);
|
|
85
|
+
const counts = { kv: 0, document: 0, relational: 0 };
|
|
86
|
+
for (const entry of registry.values())
|
|
87
|
+
counts[entry.kind]++;
|
|
88
|
+
io.out(`${path} ${statSync(path).size} bytes ${registry.size} namespaces`);
|
|
89
|
+
io.out(` kv: ${counts.kv} document: ${counts.document} relational: ${counts.relational}`);
|
|
90
|
+
return 0;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function get({ path, args, io }) {
|
|
94
|
+
const [key] = args;
|
|
95
|
+
if (key === undefined) {
|
|
96
|
+
io.err("missing <key>");
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
return withReadDb(path, (db) => {
|
|
100
|
+
const value = kv(db).get(key);
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
io.err(`key not found: ${key}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
io.out(value);
|
|
106
|
+
return 0;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function scan({ path, args, io }) {
|
|
110
|
+
const [prefix] = args;
|
|
111
|
+
if (prefix === undefined) {
|
|
112
|
+
io.err("missing <prefix>");
|
|
113
|
+
return 2;
|
|
114
|
+
}
|
|
115
|
+
return withReadDb(path, (db) => {
|
|
116
|
+
for (const entry of kv(db).prefix(prefix))
|
|
117
|
+
io.out(`${entry.key}=${entry.value}`);
|
|
118
|
+
return 0;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function set({ path, args, io, force }) {
|
|
122
|
+
const [key, value] = args;
|
|
123
|
+
if (key === undefined || value === undefined) {
|
|
124
|
+
io.err("missing <key> <value>");
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
if (isReservedKey(key)) {
|
|
128
|
+
io.err(`refusing to write a reserved key: ${key}`);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
return withWriteDb(path, force, (db) => {
|
|
132
|
+
const { changed } = kv(db).set(key, value);
|
|
133
|
+
io.out(`set ${key} (${changed} changed)`);
|
|
134
|
+
return 0;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function remove({ path, args, io, force }) {
|
|
138
|
+
const [key] = args;
|
|
139
|
+
if (key === undefined) {
|
|
140
|
+
io.err("missing <key>");
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
return withWriteDb(path, force, (db) => {
|
|
144
|
+
const { changed } = kv(db).delete(key);
|
|
145
|
+
io.out(`delete ${key} (${changed} removed)`);
|
|
146
|
+
return 0;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function importKeys({ path, args, io, force }) {
|
|
150
|
+
const [file] = args;
|
|
151
|
+
if (file === undefined) {
|
|
152
|
+
io.err("missing <file>");
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
156
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
157
|
+
io.err("import expects a JSON object of string values");
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
const pairs = [];
|
|
161
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
162
|
+
if (typeof value !== "string") {
|
|
163
|
+
io.err("import expects a JSON object of string values");
|
|
164
|
+
return 2;
|
|
165
|
+
}
|
|
166
|
+
if (isReservedKey(key)) {
|
|
167
|
+
io.err(`import: refusing to write a reserved key: ${key}`);
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
pairs.push([key, value]);
|
|
171
|
+
}
|
|
172
|
+
return withWriteDb(path, force, (db) => {
|
|
173
|
+
// One transaction for the whole load: a bulk import either lands entirely or,
|
|
174
|
+
// on a crash mid-write, not at all (recovery discards the torn record).
|
|
175
|
+
db.transact((tx) => {
|
|
176
|
+
for (const [key, value] of pairs)
|
|
177
|
+
tx.set(utf8(key), utf8(value));
|
|
178
|
+
});
|
|
179
|
+
io.out(`import ${pairs.length} keys`);
|
|
180
|
+
return 0;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** The commands, keyed by name. Each takes the parsed {@link Ctx}. */
|
|
184
|
+
const commands = {
|
|
185
|
+
inspect,
|
|
186
|
+
stats,
|
|
187
|
+
get,
|
|
188
|
+
scan,
|
|
189
|
+
set,
|
|
190
|
+
delete: remove,
|
|
191
|
+
import: importKeys,
|
|
192
|
+
};
|
|
193
|
+
export function run(argv, io) {
|
|
194
|
+
let positionals;
|
|
195
|
+
let values;
|
|
196
|
+
try {
|
|
197
|
+
const parsed = parseArgs({
|
|
198
|
+
args: argv,
|
|
199
|
+
allowPositionals: true,
|
|
200
|
+
options: { help: { type: "boolean", short: "h" }, force: { type: "boolean" } },
|
|
201
|
+
});
|
|
202
|
+
positionals = parsed.positionals;
|
|
203
|
+
values = parsed.values;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
io.err(error instanceof Error ? error.message : String(error));
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
if (values.help === true || positionals.length === 0) {
|
|
210
|
+
io.out(USAGE);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
const command = positionals[0];
|
|
214
|
+
const handler = commands[command];
|
|
215
|
+
if (handler === undefined) {
|
|
216
|
+
io.err(`unknown command: ${command}`);
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
const path = positionals[1];
|
|
220
|
+
if (path === undefined) {
|
|
221
|
+
io.err("missing <path>");
|
|
222
|
+
return 2;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return handler({ path, args: positionals.slice(2), io, force: values.force === true });
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
io.err(error instanceof Error ? error.message : String(error));
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
package/dist/core.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* and discards any record a crash left half-written.
|
|
21
21
|
*/
|
|
22
22
|
/** The LibreDB package version. Kept in sync with package.json. */
|
|
23
|
-
export declare const version = "0.
|
|
23
|
+
export declare const version = "0.1.1";
|
|
24
24
|
/**
|
|
25
25
|
* A key in the kernel: an immutable sequence of bytes.
|
|
26
26
|
*
|
|
@@ -94,9 +94,10 @@ export interface Database {
|
|
|
94
94
|
* goes through this interface. That keeps the IO boundary explicit and in one
|
|
95
95
|
* place (a readability gain), and it lets a test inject a simulated filesystem
|
|
96
96
|
* to torture crash recovery without a real disk (DESIGN.md section 6.4). The
|
|
97
|
-
* default
|
|
98
|
-
*
|
|
99
|
-
*
|
|
97
|
+
* kernel carries NO default filesystem: a path-backed open must be given one.
|
|
98
|
+
* The Node entry (index.ts) supplies a `node:fs`-backed adapter by default; the
|
|
99
|
+
* browser entry supplies none. The interface is deliberately the SMALLEST set
|
|
100
|
+
* of operations the WAL performs and nothing more.
|
|
100
101
|
*/
|
|
101
102
|
export interface FileSystem {
|
|
102
103
|
/** Open the log file at `path` for reading and appending, creating it if it
|
|
@@ -133,10 +134,12 @@ export interface WalFile {
|
|
|
133
134
|
export interface OpenOptions {
|
|
134
135
|
readonly path?: string;
|
|
135
136
|
/**
|
|
136
|
-
* The filesystem the write-ahead log runs on.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
137
|
+
* The filesystem the write-ahead log runs on. Required for a path-backed open:
|
|
138
|
+
* the kernel carries no default, so omitting it with a `path` throws. (The
|
|
139
|
+
* Node entry defaults this to a `node:fs` adapter; the browser entry has none.)
|
|
140
|
+
* Injecting one lets tests simulate crashes and IO faults deterministically
|
|
141
|
+
* (DESIGN.md section 6.4). Ignored when there is no `path` (an in-memory
|
|
142
|
+
* database never touches a filesystem).
|
|
140
143
|
*/
|
|
141
144
|
readonly fs?: FileSystem;
|
|
142
145
|
}
|
package/dist/core.js
CHANGED
|
@@ -19,9 +19,8 @@
|
|
|
19
19
|
* transactions, and a write-ahead log for durability. Recovery replays that log
|
|
20
20
|
* and discards any record a crash left half-written.
|
|
21
21
|
*/
|
|
22
|
-
import { closeSync, fsyncSync, openSync, readFileSync, statSync, truncateSync, writeSync } from "node:fs";
|
|
23
22
|
/** The LibreDB package version. Kept in sync with package.json. */
|
|
24
|
-
export const version = "0.
|
|
23
|
+
export const version = "0.1.1";
|
|
25
24
|
/**
|
|
26
25
|
* Compare two keys by unsigned byte-lexicographic order: the first differing
|
|
27
26
|
* byte decides, and if one key is a prefix of the other the shorter sorts
|
|
@@ -245,44 +244,6 @@ function recover(file) {
|
|
|
245
244
|
file.truncate(offset);
|
|
246
245
|
return entries;
|
|
247
246
|
}
|
|
248
|
-
/**
|
|
249
|
-
* The default {@link FileSystem}: a thin adapter over `node:fs`. Each method is
|
|
250
|
-
* the obvious synchronous syscall, so the seam adds an interface, not behaviour.
|
|
251
|
-
* Appends go through one append-mode descriptor (creating the file if missing);
|
|
252
|
-
* reads, size and truncate work by path, matching how the WAL has always
|
|
253
|
-
* reached the disk.
|
|
254
|
-
*/
|
|
255
|
-
function nodeFileSystem() {
|
|
256
|
-
return {
|
|
257
|
-
open(path) {
|
|
258
|
-
const fd = openSync(path, "a"); // append-only; creates the file if missing
|
|
259
|
-
return {
|
|
260
|
-
size() {
|
|
261
|
-
return statSync(path).size;
|
|
262
|
-
},
|
|
263
|
-
read(offset, length) {
|
|
264
|
-
// A fresh Uint8Array so the returned slice is an independent copy, not
|
|
265
|
-
// a view aliasing a shared Buffer pool.
|
|
266
|
-
return new Uint8Array(readFileSync(path)).subarray(offset, offset + length);
|
|
267
|
-
},
|
|
268
|
-
append(bytes) {
|
|
269
|
-
for (let written = 0; written < bytes.length;) {
|
|
270
|
-
written += writeSync(fd, bytes, written);
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
fsync() {
|
|
274
|
-
fsyncSync(fd);
|
|
275
|
-
},
|
|
276
|
-
truncate(length) {
|
|
277
|
-
truncateSync(path, length);
|
|
278
|
-
},
|
|
279
|
-
close() {
|
|
280
|
-
closeSync(fd);
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
247
|
/** Open the log file at `path` on `fs`, recover the store from it, and return
|
|
287
248
|
* the recovered entries together with a {@link Log} that appends future commits
|
|
288
249
|
* to the same file. */
|
|
@@ -317,7 +278,20 @@ export const open = (options) => {
|
|
|
317
278
|
let committed;
|
|
318
279
|
let log;
|
|
319
280
|
if (options?.path !== undefined) {
|
|
320
|
-
|
|
281
|
+
// A path must actually name a file: reject the degenerate empty string here
|
|
282
|
+
// with a clear error, rather than letting it reach the filesystem and
|
|
283
|
+
// surface a raw, adapter-specific failure (e.g. node's ENOENT for "").
|
|
284
|
+
if (options.path === "") {
|
|
285
|
+
throw new Error("libredb: open({ path }) requires a non-empty path");
|
|
286
|
+
}
|
|
287
|
+
// The kernel is runtime-agnostic: it carries no default filesystem, so a
|
|
288
|
+
// path-backed open MUST be given one. The default node:fs adapter lives at
|
|
289
|
+
// the package edge (index.ts wires it in); the browser entry has none. A
|
|
290
|
+
// pathless, in-memory open never reaches here and needs no filesystem.
|
|
291
|
+
if (options.fs === undefined) {
|
|
292
|
+
throw new Error("libredb: open({ path }) requires a filesystem; none was provided");
|
|
293
|
+
}
|
|
294
|
+
const opened = openLog(options.path, options.fs);
|
|
321
295
|
committed = opened.entries;
|
|
322
296
|
log = opened.log;
|
|
323
297
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,18 @@
|
|
|
10
10
|
* {@link isReservedKey} (with {@link RESERVED_MARKER} / {@link CATALOG_PREFIX})
|
|
11
11
|
* lets a raw-KV tool hide engine-internal keys instead of hardcoding the layout.
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
export
|
|
13
|
+
import { type Open } from "./core.ts";
|
|
14
|
+
export { version } from "./core.ts";
|
|
15
|
+
export type { Database, FileSystem, OpenOptions, WalFile } from "./core.ts";
|
|
16
|
+
/**
|
|
17
|
+
* Open a LibreDB database on Node or Bun. Identical to the kernel's
|
|
18
|
+
* {@link import("./core.ts").open}, except a path-backed open with no `fs`
|
|
19
|
+
* defaults to the real `node:fs` adapter — so `open({ path })` is durable out of
|
|
20
|
+
* the box. A pathless open stays in-memory; an explicit `fs` is passed through.
|
|
21
|
+
* The browser entry (`@libredb/libredb/browser`) omits this default so it never
|
|
22
|
+
* imports `node:fs`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const open: Open;
|
|
15
25
|
export { kv } from "./lens/kv.ts";
|
|
16
26
|
export type { Kv, KvEntry } from "./lens/kv.ts";
|
|
17
27
|
export { doc } from "./lens/document.ts";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,20 @@
|
|
|
10
10
|
* {@link isReservedKey} (with {@link RESERVED_MARKER} / {@link CATALOG_PREFIX})
|
|
11
11
|
* lets a raw-KV tool hide engine-internal keys instead of hardcoding the layout.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
import { open as openKernel } from "./core.js";
|
|
14
|
+
import { nodeFileSystem } from "./adapter/node-fs.js";
|
|
15
|
+
export { version } from "./core.js";
|
|
16
|
+
/**
|
|
17
|
+
* Open a LibreDB database on Node or Bun. Identical to the kernel's
|
|
18
|
+
* {@link import("./core.ts").open}, except a path-backed open with no `fs`
|
|
19
|
+
* defaults to the real `node:fs` adapter — so `open({ path })` is durable out of
|
|
20
|
+
* the box. A pathless open stays in-memory; an explicit `fs` is passed through.
|
|
21
|
+
* The browser entry (`@libredb/libredb/browser`) omits this default so it never
|
|
22
|
+
* imports `node:fs`.
|
|
23
|
+
*/
|
|
24
|
+
export const open = (options) => options?.path !== undefined && options.fs === undefined
|
|
25
|
+
? openKernel({ ...options, fs: nodeFileSystem() })
|
|
26
|
+
: openKernel(options);
|
|
14
27
|
export { kv } from "./lens/kv.js";
|
|
15
28
|
export { doc } from "./lens/document.js";
|
|
16
29
|
export { table } from "./lens/relational.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libredb/libredb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A small, readable, embeddable, multi-model database. One ordered key-value core, thin model lenses on top.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"database",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
25
|
"license": "MIT",
|
|
26
|
+
"bin": {
|
|
27
|
+
"libredb": "./dist/cli/main.js"
|
|
28
|
+
},
|
|
26
29
|
"files": [
|
|
27
30
|
"dist"
|
|
28
31
|
],
|
|
@@ -34,8 +37,16 @@
|
|
|
34
37
|
"types": "./dist/index.d.ts",
|
|
35
38
|
"exports": {
|
|
36
39
|
".": {
|
|
40
|
+
"browser": {
|
|
41
|
+
"types": "./dist/browser.d.ts",
|
|
42
|
+
"import": "./dist/browser.js"
|
|
43
|
+
},
|
|
37
44
|
"types": "./dist/index.d.ts",
|
|
38
45
|
"import": "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./browser": {
|
|
48
|
+
"types": "./dist/browser.d.ts",
|
|
49
|
+
"import": "./dist/browser.js"
|
|
39
50
|
}
|
|
40
51
|
},
|
|
41
52
|
"sideEffects": false,
|
|
@@ -50,6 +61,7 @@
|
|
|
50
61
|
"lint": "oxlint && eslint .",
|
|
51
62
|
"test": "bun test --coverage",
|
|
52
63
|
"build": "tsc --project tsconfig.build.json",
|
|
64
|
+
"compile": "bun build --compile src/cli/main.ts --outfile libredb",
|
|
53
65
|
"size": "size-limit",
|
|
54
66
|
"attw": "rm -rf .attw && bun pm pack --quiet --destination .attw && attw .attw/*.tgz --profile esm-only",
|
|
55
67
|
"publint": "publint",
|