@libredb/libredb 0.1.2 → 0.1.3
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 +15 -5
- package/dist/browser.d.ts +31 -7
- package/dist/browser.js +16 -6
- package/dist/cli/lock.js +26 -16
- package/dist/cli/run.js +27 -12
- package/dist/core.d.ts +8 -7
- package/dist/core.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
**Multi-model without the magic. One core, three lenses, every line tested.**
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/@libredb/libredb)
|
|
11
|
+
[](https://jsr.io/@libredb/libredb)
|
|
12
|
+
[](https://hub.docker.com/r/libredb/libredb)
|
|
11
13
|
[](https://github.com/libredb/libredb-database/actions/workflows/ci.yml)
|
|
12
14
|
[](https://sonarcloud.io/summary/new_code?id=libredb_libredb-database)
|
|
13
15
|
[](https://sonarcloud.io/summary/new_code?id=libredb_libredb-database)
|
|
@@ -98,7 +100,7 @@ bunx jsr add @libredb/libredb
|
|
|
98
100
|
**CDN** — every release is served from the npm registry by the usual CDNs. Pin a version:
|
|
99
101
|
|
|
100
102
|
```ts
|
|
101
|
-
import { open, kv } from "https://esm.sh/@libredb/libredb@0.1.
|
|
103
|
+
import { open, kv } from "https://esm.sh/@libredb/libredb@0.1.3";
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
**Browser** — a dedicated entry that imports nothing from `node:`, so it bundles for the browser
|
|
@@ -124,8 +126,11 @@ const handle = await file.createSyncAccessHandle();
|
|
|
124
126
|
const db = open({ path: "app.libredb", fs: opfsFileSystem(handle) });
|
|
125
127
|
```
|
|
126
128
|
|
|
127
|
-
A browser-targeting bundler resolves the browser build
|
|
128
|
-
export condition
|
|
129
|
+
A browser-targeting bundler also resolves the browser build from the main `@libredb/libredb` entry
|
|
130
|
+
via the package's `browser` export condition. Note that TypeScript usually still resolves the Node
|
|
131
|
+
types for that entry (where `fs` is optional) unless it is configured for the `browser` condition
|
|
132
|
+
(`customConditions`). To get the browser-specific typing — `fs` required when `path` is given — and
|
|
133
|
+
keep types in step with the runtime, import the explicit `@libredb/libredb/browser` subpath.
|
|
129
134
|
|
|
130
135
|
## Command-line tool
|
|
131
136
|
|
|
@@ -142,7 +147,9 @@ npx libredb import data.libredb seed.json # bulk-set from a JSON object, atomica
|
|
|
142
147
|
```
|
|
143
148
|
|
|
144
149
|
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
|
|
150
|
+
advisory `<path>.lock` to refuse a second concurrent writer. Use `--force` only to clear a stale
|
|
151
|
+
lock left by a crashed writer: the lock is advisory and LibreDB is single-process, so two writers
|
|
152
|
+
that force at the same time can still race and corrupt the file.
|
|
146
153
|
|
|
147
154
|
Prefer a standalone binary with no Node or Bun installed? Each release attaches self-contained
|
|
148
155
|
executables (Linux, macOS, Windows; x64 and arm64) with `.sha256` checksums on its
|
|
@@ -157,7 +164,10 @@ docker run --rm -v "$PWD:/data" ghcr.io/libredb/libredb inspect /data/app.libred
|
|
|
157
164
|
# or from Docker Hub: docker run --rm -v "$PWD:/data" libredb/libredb inspect /data/app.libredb
|
|
158
165
|
```
|
|
159
166
|
|
|
160
|
-
The image is
|
|
167
|
+
The same multi-arch image is published to both registries:
|
|
168
|
+
[Docker Hub](https://hub.docker.com/r/libredb/libredb) and
|
|
169
|
+
[GHCR](https://github.com/libredb/libredb-database/pkgs/container/libredb). It is a CLI shell, not a
|
|
170
|
+
server: LibreDB stays an embedded, in-process database.
|
|
161
171
|
|
|
162
172
|
## How it works: one core, three lenses
|
|
163
173
|
|
package/dist/browser.d.ts
CHANGED
|
@@ -3,14 +3,38 @@
|
|
|
3
3
|
* (`@libredb/libredb/browser`).
|
|
4
4
|
*
|
|
5
5
|
* Same lens surface as the default Node entry ({@link import("./index.ts")}),
|
|
6
|
-
* with one difference: `open`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* with one difference: `open` carries NO default filesystem. An in-memory
|
|
7
|
+
* database (`open()`) works anywhere; a path-backed open requires an injected
|
|
8
|
+
* `fs` (e.g. the bundled {@link opfsFileSystem}). Here that requirement is in
|
|
9
|
+
* the TYPE — {@link BrowserOpenOptions} makes `fs` mandatory when `path` is
|
|
10
|
+
* given, so misuse is a compile error rather than a runtime throw. The point of
|
|
11
|
+
* this entry is the import graph: it reaches nothing in `node:`, so a bundler
|
|
12
|
+
* can ship it to a browser. The node:fs adapter lives behind Node only.
|
|
11
13
|
*/
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
+
import { type Database, type FileSystem } from "./core.ts";
|
|
15
|
+
export { version } from "./core.ts";
|
|
16
|
+
export type { Database, FileSystem, WalFile } from "./core.ts";
|
|
17
|
+
/**
|
|
18
|
+
* Options for the browser {@link open}. Unlike the kernel's permissive
|
|
19
|
+
* `OpenOptions`, `fs` is REQUIRED whenever `path` is present, because the browser
|
|
20
|
+
* entry has no default filesystem — so a path-backed open without an `fs` fails
|
|
21
|
+
* to compile here instead of throwing at runtime. An in-memory open (no `path`)
|
|
22
|
+
* needs no filesystem.
|
|
23
|
+
*/
|
|
24
|
+
export type BrowserOpenOptions = {
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly fs: FileSystem;
|
|
27
|
+
} | {
|
|
28
|
+
readonly path?: never;
|
|
29
|
+
readonly fs?: FileSystem;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Open a database in the browser. The same runtime as the kernel's `open`, typed
|
|
33
|
+
* so a path-backed open requires an injected filesystem (e.g.
|
|
34
|
+
* {@link opfsFileSystem}). Assigning the kernel's wider-typed `open` here is
|
|
35
|
+
* sound by parameter contravariance, so the kernel itself stays unchanged.
|
|
36
|
+
*/
|
|
37
|
+
export declare const open: (options?: BrowserOpenOptions) => Database;
|
|
14
38
|
export { opfsFileSystem } from "./adapter/opfs.ts";
|
|
15
39
|
export type { SyncAccessHandle } from "./adapter/opfs.ts";
|
|
16
40
|
export { kv } from "./lens/kv.ts";
|
package/dist/browser.js
CHANGED
|
@@ -3,13 +3,23 @@
|
|
|
3
3
|
* (`@libredb/libredb/browser`).
|
|
4
4
|
*
|
|
5
5
|
* Same lens surface as the default Node entry ({@link import("./index.ts")}),
|
|
6
|
-
* with one difference: `open`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* with one difference: `open` carries NO default filesystem. An in-memory
|
|
7
|
+
* database (`open()`) works anywhere; a path-backed open requires an injected
|
|
8
|
+
* `fs` (e.g. the bundled {@link opfsFileSystem}). Here that requirement is in
|
|
9
|
+
* the TYPE — {@link BrowserOpenOptions} makes `fs` mandatory when `path` is
|
|
10
|
+
* given, so misuse is a compile error rather than a runtime throw. The point of
|
|
11
|
+
* this entry is the import graph: it reaches nothing in `node:`, so a bundler
|
|
12
|
+
* can ship it to a browser. The node:fs adapter lives behind Node only.
|
|
11
13
|
*/
|
|
12
|
-
|
|
14
|
+
import { open as openKernel } from "./core.js";
|
|
15
|
+
export { version } from "./core.js";
|
|
16
|
+
/**
|
|
17
|
+
* Open a database in the browser. The same runtime as the kernel's `open`, typed
|
|
18
|
+
* so a path-backed open requires an injected filesystem (e.g.
|
|
19
|
+
* {@link opfsFileSystem}). Assigning the kernel's wider-typed `open` here is
|
|
20
|
+
* sound by parameter contravariance, so the kernel itself stays unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export const open = openKernel;
|
|
13
23
|
// OPFS persistence (browser-only): wrap an OPFS sync access handle as the
|
|
14
24
|
// filesystem for a path-backed open. See adapter/opfs.ts for usage in a Worker.
|
|
15
25
|
export { opfsFileSystem } from "./adapter/opfs.js";
|
package/dist/cli/lock.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `--force` drops a stale lock and proceeds. The lock is always released after
|
|
9
9
|
* the write (see withWriteDb in run.ts).
|
|
10
10
|
*/
|
|
11
|
-
import { closeSync, openSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
11
|
+
import { closeSync, existsSync, openSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
12
12
|
// Written into every lock file so a forced acquire can tell a real libredb lock
|
|
13
13
|
// from an unrelated file that merely happens to be named <path>.lock, and refuse
|
|
14
14
|
// to delete the latter.
|
|
@@ -21,11 +21,22 @@ export function acquireLock(path, force) {
|
|
|
21
21
|
dropOwnLock(lockPath);
|
|
22
22
|
try {
|
|
23
23
|
const fd = openSync(lockPath, "wx"); // "wx": exclusive create, fails if it exists
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
try {
|
|
25
|
+
writeSync(fd, SENTINEL);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
closeSync(fd); // always release the descriptor, even if the write throws
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
|
-
catch {
|
|
28
|
-
|
|
31
|
+
catch (error) {
|
|
32
|
+
// A failed sentinel write (e.g. ENOSPC) leaves an EMPTY <path>.lock behind;
|
|
33
|
+
// that stray is recoverable because dropOwnLock treats an empty file as ours.
|
|
34
|
+
// Only an existing lock file (EEXIST) means "locked". Surface any other IO
|
|
35
|
+
// error (missing directory, permissions, ...) unchanged, so a real problem
|
|
36
|
+
// is not misreported as a held lock.
|
|
37
|
+
if (error.code !== "EEXIST")
|
|
38
|
+
throw error;
|
|
39
|
+
throw new Error(`libredb: ${path} is locked (${lockPath}); another writer may be active — use --force to override`, { cause: error });
|
|
29
40
|
}
|
|
30
41
|
return {
|
|
31
42
|
release() {
|
|
@@ -33,18 +44,17 @@ export function acquireLock(path, force) {
|
|
|
33
44
|
},
|
|
34
45
|
};
|
|
35
46
|
}
|
|
36
|
-
/** Remove an existing libredb lock so a forced acquire can proceed.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
47
|
+
/** Remove an existing libredb lock so a forced acquire can proceed. A lock this
|
|
48
|
+
* tool created carries the SENTINEL; an empty file is a stray from a crash or a
|
|
49
|
+
* failed sentinel write (also ours) and is safe to drop. Any other content means
|
|
50
|
+
* the file is not our lock, so `--force` refuses it rather than delete unrelated
|
|
51
|
+
* user data that happens to share the `.lock` name. A read error other than "no
|
|
52
|
+
* such file" (e.g. EACCES) propagates instead of being silently swallowed. */
|
|
39
53
|
function dropOwnLock(lockPath) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
catch {
|
|
45
|
-
return; // no lock file to drop
|
|
46
|
-
}
|
|
47
|
-
if (!contents.startsWith(SENTINEL)) {
|
|
54
|
+
if (!existsSync(lockPath))
|
|
55
|
+
return; // nothing to drop
|
|
56
|
+
const contents = readFileSync(lockPath, "utf8");
|
|
57
|
+
if (contents !== "" && !contents.startsWith(SENTINEL)) {
|
|
48
58
|
throw new Error(`libredb: refusing to remove ${lockPath} with --force: not a libredb lock file`);
|
|
49
59
|
}
|
|
50
60
|
rmSync(lockPath, { force: true });
|
package/dist/cli/run.js
CHANGED
|
@@ -140,6 +140,10 @@ function remove({ path, args, io, force }) {
|
|
|
140
140
|
io.err("missing <key>");
|
|
141
141
|
return 2;
|
|
142
142
|
}
|
|
143
|
+
if (isReservedKey(key)) {
|
|
144
|
+
io.err(`refusing to delete a reserved key: ${key}`);
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
143
147
|
return withWriteDb(path, force, (db) => {
|
|
144
148
|
const { changed } = kv(db).delete(key);
|
|
145
149
|
io.out(`delete ${key} (${changed} removed)`);
|
|
@@ -152,7 +156,17 @@ function importKeys({ path, args, io, force }) {
|
|
|
152
156
|
io.err("missing <file>");
|
|
153
157
|
return 2;
|
|
154
158
|
}
|
|
155
|
-
const
|
|
159
|
+
const raw = readFileSync(file, "utf8");
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(raw);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Malformed JSON is bad input, not a runtime fault — report it like the other
|
|
166
|
+
// usage errors (exit 2) instead of letting it fall through to exit 1.
|
|
167
|
+
io.err("import expects a file containing a JSON object of string values");
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
156
170
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
157
171
|
io.err("import expects a JSON object of string values");
|
|
158
172
|
return 2;
|
|
@@ -180,16 +194,17 @@ function importKeys({ path, args, io, force }) {
|
|
|
180
194
|
return 0;
|
|
181
195
|
});
|
|
182
196
|
}
|
|
183
|
-
/** The commands, keyed by name.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
/** The commands, keyed by name. A Map (not a plain object) so an inherited
|
|
198
|
+
* property name like "toString" or "__proto__" can never resolve to a handler. */
|
|
199
|
+
const commands = new Map([
|
|
200
|
+
["inspect", inspect],
|
|
201
|
+
["stats", stats],
|
|
202
|
+
["get", get],
|
|
203
|
+
["scan", scan],
|
|
204
|
+
["set", set],
|
|
205
|
+
["delete", remove],
|
|
206
|
+
["import", importKeys],
|
|
207
|
+
]);
|
|
193
208
|
export function run(argv, io) {
|
|
194
209
|
let positionals;
|
|
195
210
|
let values;
|
|
@@ -211,7 +226,7 @@ export function run(argv, io) {
|
|
|
211
226
|
return 0;
|
|
212
227
|
}
|
|
213
228
|
const command = positionals[0];
|
|
214
|
-
const handler = commands
|
|
229
|
+
const handler = commands.get(command);
|
|
215
230
|
if (handler === undefined) {
|
|
216
231
|
io.err(`unknown command: ${command}`);
|
|
217
232
|
return 2;
|
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.1.
|
|
23
|
+
export declare const version = "0.1.3";
|
|
24
24
|
/**
|
|
25
25
|
* A key in the kernel: an immutable sequence of bytes.
|
|
26
26
|
*
|
|
@@ -134,12 +134,13 @@ export interface WalFile {
|
|
|
134
134
|
export interface OpenOptions {
|
|
135
135
|
readonly path?: string;
|
|
136
136
|
/**
|
|
137
|
-
* The filesystem the write-ahead log runs on.
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* database never touches a
|
|
137
|
+
* The filesystem the write-ahead log runs on. Optional at the type level, but a
|
|
138
|
+
* path-backed open needs one at runtime: the kernel and the browser entry throw
|
|
139
|
+
* when `path` is given without `fs`, while the Node entry (index.ts) supplies a
|
|
140
|
+
* `node:fs` adapter by default so `open({ path })` works there. Injecting one
|
|
141
|
+
* lets tests simulate crashes and IO faults deterministically (DESIGN.md section
|
|
142
|
+
* 6.4). Ignored when there is no `path` (an in-memory database never touches a
|
|
143
|
+
* filesystem).
|
|
143
144
|
*/
|
|
144
145
|
readonly fs?: FileSystem;
|
|
145
146
|
}
|
package/dist/core.js
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 const version = "0.1.
|
|
23
|
+
export const version = "0.1.3";
|
|
24
24
|
/**
|
|
25
25
|
* Compare two keys by unsigned byte-lexicographic order: the first differing
|
|
26
26
|
* byte decides, and if one key is a prefix of the other the shorter sorts
|