@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 CHANGED
@@ -8,6 +8,8 @@
8
8
  **Multi-model without the magic. One core, three lenses, every line tested.**
9
9
 
10
10
  [![npm version](https://img.shields.io/npm/v/@libredb/libredb.svg)](https://www.npmjs.com/package/@libredb/libredb)
11
+ [![JSR](https://jsr.io/badges/@libredb/libredb)](https://jsr.io/@libredb/libredb)
12
+ [![Docker Hub](https://img.shields.io/docker/v/libredb/libredb?logo=docker&logoColor=white&label=docker%20hub&color=2496ED&sort=semver)](https://hub.docker.com/r/libredb/libredb)
11
13
  [![CI](https://github.com/libredb/libredb-database/actions/workflows/ci.yml/badge.svg)](https://github.com/libredb/libredb-database/actions/workflows/ci.yml)
12
14
  [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=libredb_libredb-database&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=libredb_libredb-database)
13
15
  [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=libredb_libredb-database&metric=coverage)](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.2";
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 automatically via the package's `browser`
128
- export condition, so importing the main `@libredb/libredb` entry works too.
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; pass `--force` to override a stale lock.
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 a CLI shell, not a server: LibreDB stays an embedded, in-process database.
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` 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.
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
- export { open, version } from "./core.ts";
13
- export type { Database, FileSystem, OpenOptions, WalFile } from "./core.ts";
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` 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.
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
- export { open, version } from "./core.js";
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
- writeSync(fd, SENTINEL);
25
- closeSync(fd);
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
- throw new Error(`libredb: ${path} is locked (${lockPath}); another writer may be active — use --force to override`);
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. 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. */
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
- 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)) {
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 parsed = JSON.parse(readFileSync(file, "utf8"));
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. 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
- };
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[command];
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.2";
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. 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).
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.2";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libredb/libredb",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",