@libredb/libredb 0.0.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/dist/core.js ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * core.ts — the LibreDB kernel.
3
+ *
4
+ * This file is the trust boundary, the comprehension boundary, and the
5
+ * architecture boundary all at once (see DESIGN.md section 5). Everything that
6
+ * touches durability lives here and nowhere else:
7
+ *
8
+ * - storage an ordered key-value substrate
9
+ * - transactions atomic apply with the isolation scoped in DESIGN.md
10
+ * - recovery survive a crash and reopen with consistent state
11
+ *
12
+ * The kernel is small because it is genuinely minimal, never because
13
+ * complexity was swept into sibling files. Model lenses (lens/kv.ts,
14
+ * lens/document.ts, lens/relational.ts) sit on top of this; they never
15
+ * reach around it.
16
+ *
17
+ * The boundaries are declared first as types — the contract every lens builds
18
+ * on — followed by the implementation: an ordered key-value store, serializable
19
+ * transactions, and a write-ahead log for durability. Recovery replays that log
20
+ * and discards any record a crash left half-written.
21
+ */
22
+ import { closeSync, existsSync, fsyncSync, openSync, readFileSync, truncateSync, writeSync, } from "node:fs";
23
+ /** The LibreDB package version. Kept in sync with package.json. */
24
+ export const version = "0.0.1";
25
+ /**
26
+ * Compare two keys by unsigned byte-lexicographic order: the first differing
27
+ * byte decides, and if one key is a prefix of the other the shorter sorts
28
+ * first. Returns <0, 0, or >0 like every comparator.
29
+ *
30
+ * Bytes are unsigned (0..255) because they come from a Uint8Array, so this is
31
+ * the byte order a sorted file would use — not JavaScript's UTF-16 string
32
+ * order, where "10" would sort before "2".
33
+ */
34
+ function compareKeys(a, b) {
35
+ const shared = Math.min(a.length, b.length);
36
+ for (let i = 0; i < shared; i++) {
37
+ const delta = a[i] - b[i];
38
+ if (delta !== 0)
39
+ return delta;
40
+ }
41
+ return a.length - b.length;
42
+ }
43
+ /**
44
+ * Binary-search a sorted entry array for `key`. Returns whether it is present
45
+ * and the index: the entry's own index if found, otherwise the index at which
46
+ * inserting would keep the array sorted.
47
+ */
48
+ function locate(entries, key) {
49
+ let low = 0;
50
+ let high = entries.length;
51
+ while (low < high) {
52
+ const mid = (low + high) >>> 1;
53
+ const order = compareKeys(entries[mid].key, key);
54
+ if (order === 0)
55
+ return { found: true, index: mid };
56
+ if (order < 0)
57
+ low = mid + 1;
58
+ else
59
+ high = mid;
60
+ }
61
+ return { found: false, index: low };
62
+ }
63
+ /** Set `key` to `value` in a sorted entry array, keeping it sorted. Shared by
64
+ * live transaction writes and by log replay during recovery, so "what a set
65
+ * means" has exactly one definition. */
66
+ function applySet(entries, key, value) {
67
+ const { found, index } = locate(entries, key);
68
+ if (found)
69
+ entries[index] = { key, value };
70
+ else
71
+ entries.splice(index, 0, { key, value });
72
+ }
73
+ /** Remove `key` from a sorted entry array. A no-op if it is absent. Shared by
74
+ * live writes and log replay (see {@link applySet}). */
75
+ function applyDelete(entries, key) {
76
+ const { found, index } = locate(entries, key);
77
+ if (found)
78
+ entries.splice(index, 1);
79
+ }
80
+ /**
81
+ * A transaction backed by `working`, a mutable snapshot of the committed store.
82
+ * Reads and writes hit the snapshot directly, which is what gives
83
+ * read-your-writes; the caller commits or discards the snapshot as a whole.
84
+ * Every mutation is also appended to `journal`, the redo record a durable
85
+ * database writes to its log on commit (and ignores when purely in-memory).
86
+ */
87
+ function makeTransaction(working, journal) {
88
+ return {
89
+ get(key) {
90
+ const { found, index } = locate(working, key);
91
+ return found ? working[index].value : undefined;
92
+ },
93
+ set(key, value) {
94
+ applySet(working, key, value);
95
+ journal.push({ kind: "set", key, value });
96
+ },
97
+ delete(key) {
98
+ applyDelete(working, key);
99
+ journal.push({ kind: "delete", key });
100
+ },
101
+ *getRange(start, end) {
102
+ // locate() returns the first index whose key is >= start (the insertion
103
+ // point), so the scan is naturally inclusive of start. It stops at the
104
+ // first key that is not < end, making the range half-open [start, end).
105
+ for (let i = locate(working, start).index; i < working.length; i++) {
106
+ const entry = working[i];
107
+ if (compareKeys(entry.key, end) >= 0)
108
+ break;
109
+ yield entry;
110
+ }
111
+ },
112
+ };
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Durability: a write-ahead log
116
+ //
117
+ // Without a path the kernel is purely in-memory. With one, durability is a
118
+ // write-ahead log (a redo log): every committed transaction is appended to the
119
+ // file as a single record and fsync'd BEFORE the commit becomes visible, so a
120
+ // returned transact() is durable. Recovery replays the records in order to
121
+ // rebuild the store. This is the same mechanism real databases use, written
122
+ // plainly so the file still teaches how durability works.
123
+ //
124
+ // On-disk record = an 8-byte header followed by a payload:
125
+ // [u32 payloadLength][u32 crc32(payload)][payload]
126
+ // The payload is the transaction's mutations back to back. Each op is:
127
+ // set: [u8 1][u32 keyLength][key][u32 valueLength][value]
128
+ // delete: [u8 0][u32 keyLength][key]
129
+ // Integers are big-endian. Because the log is append-only and fsync'd, a crash
130
+ // can only ever damage the LAST record, so recovery trusts every record up to
131
+ // the first one that is incomplete or fails its checksum, and truncates the
132
+ // rest away.
133
+ // ---------------------------------------------------------------------------
134
+ const OP_SET = 1;
135
+ const OP_DELETE = 0;
136
+ /** Bytes in a record header: the payload length and its checksum, both u32. */
137
+ const RECORD_HEADER = 8;
138
+ /** Write `value` as a big-endian u32 at `offset`. */
139
+ function writeU32(out, offset, value) {
140
+ out[offset] = (value >>> 24) & 0xff;
141
+ out[offset + 1] = (value >>> 16) & 0xff;
142
+ out[offset + 2] = (value >>> 8) & 0xff;
143
+ out[offset + 3] = value & 0xff;
144
+ }
145
+ /** Read a big-endian u32 from `offset`. The top byte is multiplied, not
146
+ * shifted: `<< 24` would land on the sign bit and yield a negative int. */
147
+ function readU32(buffer, offset) {
148
+ return ((buffer[offset] * 0x1000000 +
149
+ (buffer[offset + 1] << 16) +
150
+ (buffer[offset + 2] << 8) +
151
+ buffer[offset + 3]) >>>
152
+ 0);
153
+ }
154
+ /**
155
+ * CRC-32 (IEEE 802.3 polynomial), computed without a lookup table to keep the
156
+ * mechanism in plain sight. It lets recovery tell a fully-written record from
157
+ * one a crash left half-flushed.
158
+ */
159
+ function crc32(data) {
160
+ let crc = 0xffffffff;
161
+ for (let i = 0; i < data.length; i++) {
162
+ crc ^= data[i];
163
+ for (let bit = 0; bit < 8; bit++) {
164
+ crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
165
+ }
166
+ }
167
+ return (crc ^ 0xffffffff) >>> 0;
168
+ }
169
+ /** Encode one committed transaction's ops as a length-framed, checksummed
170
+ * record ready to append to the log. */
171
+ function encodeRecord(ops) {
172
+ let size = 0;
173
+ for (const op of ops) {
174
+ size += 1 + 4 + op.key.length;
175
+ if (op.kind === "set")
176
+ size += 4 + op.value.length;
177
+ }
178
+ const payload = new Uint8Array(size);
179
+ let off = 0;
180
+ for (const op of ops) {
181
+ payload[off++] = op.kind === "set" ? OP_SET : OP_DELETE;
182
+ writeU32(payload, off, op.key.length);
183
+ off += 4;
184
+ payload.set(op.key, off);
185
+ off += op.key.length;
186
+ if (op.kind === "set") {
187
+ writeU32(payload, off, op.value.length);
188
+ off += 4;
189
+ payload.set(op.value, off);
190
+ off += op.value.length;
191
+ }
192
+ }
193
+ const record = new Uint8Array(RECORD_HEADER + size);
194
+ writeU32(record, 0, size);
195
+ writeU32(record, 4, crc32(payload));
196
+ record.set(payload, RECORD_HEADER);
197
+ return record;
198
+ }
199
+ /** Replay one record's payload onto `entries`, in order, reconstructing the
200
+ * committed state the transaction produced. */
201
+ function replayPayload(entries, payload) {
202
+ let off = 0;
203
+ while (off < payload.length) {
204
+ const tag = payload[off++];
205
+ const keyLength = readU32(payload, off);
206
+ off += 4;
207
+ const key = payload.slice(off, off + keyLength);
208
+ off += keyLength;
209
+ if (tag === OP_SET) {
210
+ const valueLength = readU32(payload, off);
211
+ off += 4;
212
+ const value = payload.slice(off, off + valueLength);
213
+ off += valueLength;
214
+ applySet(entries, key, value);
215
+ }
216
+ else {
217
+ applyDelete(entries, key);
218
+ }
219
+ }
220
+ }
221
+ /**
222
+ * Rebuild the committed store from the log at `path`, returning its entries.
223
+ * Replays every intact record and stops at the first record a crash left torn
224
+ * (header promises bytes that are not there) or corrupt (checksum mismatch),
225
+ * truncating that tail away so the next append starts from a clean boundary.
226
+ */
227
+ function recover(path) {
228
+ const entries = [];
229
+ if (!existsSync(path))
230
+ return entries;
231
+ // Copy into a plain Uint8Array so slices are independent copies, not views
232
+ // aliasing a shared Buffer pool.
233
+ const log = new Uint8Array(readFileSync(path));
234
+ let offset = 0;
235
+ while (offset + RECORD_HEADER <= log.length) {
236
+ const size = readU32(log, offset);
237
+ const checksum = readU32(log, offset + 4);
238
+ const start = offset + RECORD_HEADER;
239
+ const end = start + size;
240
+ if (end > log.length)
241
+ break; // torn: fewer bytes than the header promised
242
+ const payload = log.subarray(start, end);
243
+ if (crc32(payload) !== checksum)
244
+ break; // corrupt: record damaged mid-write
245
+ replayPayload(entries, payload);
246
+ offset = end;
247
+ }
248
+ if (offset < log.length)
249
+ truncateSync(path, offset);
250
+ return entries;
251
+ }
252
+ /** Recover the store at `path` and return it together with a {@link Log} that
253
+ * appends future commits to the same file. */
254
+ function openLog(path) {
255
+ const entries = recover(path);
256
+ const fd = openSync(path, "a"); // append-only; creates the file if missing
257
+ return {
258
+ entries,
259
+ log: {
260
+ append(ops) {
261
+ const record = encodeRecord(ops);
262
+ for (let written = 0; written < record.length;) {
263
+ written += writeSync(fd, record, written);
264
+ }
265
+ fsyncSync(fd); // the durability point: bytes are on disk before we return
266
+ },
267
+ close() {
268
+ closeSync(fd);
269
+ },
270
+ },
271
+ };
272
+ }
273
+ /**
274
+ * Open a kernel instance. See {@link Open} and {@link OpenOptions}.
275
+ *
276
+ * With a `path` the store is recovered from its write-ahead log and future
277
+ * commits are appended to it durably. Without one the store is purely
278
+ * in-memory.
279
+ */
280
+ export const open = (options) => {
281
+ // The committed store. Each transaction works on a copy and replaces this
282
+ // reference on success, so a commit is one atomic assignment and an abort is
283
+ // simply never reaching that assignment. With a path, the initial contents
284
+ // are recovered from the log and `log` persists every later commit.
285
+ let committed;
286
+ let log;
287
+ if (options?.path !== undefined) {
288
+ const opened = openLog(options.path);
289
+ committed = opened.entries;
290
+ log = opened.log;
291
+ }
292
+ else {
293
+ committed = [];
294
+ log = null;
295
+ }
296
+ let closed = false;
297
+ // Guards against re-entrancy. The API is synchronous and single-threaded, so
298
+ // a transaction body runs to completion before the next one begins — making
299
+ // the schedule serial (hence serializable) by construction. The only way to
300
+ // overlap two transactions is to call transact() from inside another, which
301
+ // we forbid: the inner snapshot would miss the outer's pending writes and the
302
+ // outer's later commit would clobber the inner's, a silent lost update. With
303
+ // this guard the serializable guarantee is a fact, not an assumption.
304
+ let inTransaction = false;
305
+ return {
306
+ transact(run) {
307
+ if (closed)
308
+ throw new Error("libredb: database is closed");
309
+ if (inTransaction) {
310
+ throw new Error("libredb: nested transactions are not supported");
311
+ }
312
+ inTransaction = true;
313
+ try {
314
+ const working = committed.slice();
315
+ const journal = [];
316
+ const result = run(makeTransaction(working, journal));
317
+ // Reached only if run() did not throw. Make the commit DURABLE before
318
+ // exposing it in memory: if the append fails, we throw with memory and
319
+ // disk still agreeing on the prior state. A read-only transaction has
320
+ // nothing to persist, so it skips the log entirely. The in-memory
321
+ // commit is then one atomic reference swap.
322
+ if (log !== null && journal.length > 0)
323
+ log.append(journal);
324
+ committed = working;
325
+ return result;
326
+ }
327
+ finally {
328
+ // Always clear the guard, even on abort, so a thrown body does not wedge
329
+ // the database against all future transactions.
330
+ inTransaction = false;
331
+ }
332
+ },
333
+ close() {
334
+ if (closed)
335
+ return; // idempotent: never double-close the underlying file
336
+ closed = true;
337
+ if (log !== null)
338
+ log.close();
339
+ committed = [];
340
+ },
341
+ };
342
+ };
343
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * index.ts — the public entry point of the LibreDB npm package.
3
+ *
4
+ * The public surface is the kv lens (DESIGN.md section 6): you `open` a kernel
5
+ * database and put a lens over it — {@link kv} (strings) or {@link doc} (JSON
6
+ * documents). The kernel's byte-level internals (and each lens's private codec)
7
+ * stay unexported on purpose — the lens is the usable face. The relational lens
8
+ * ({@link table}) completes the trio.
9
+ */
10
+ export { version, open } from "./core.ts";
11
+ export type { Database, OpenOptions } from "./core.ts";
12
+ export { kv } from "./lens/kv.ts";
13
+ export type { Kv, KvEntry } from "./lens/kv.ts";
14
+ export { doc } from "./lens/document.ts";
15
+ export type { DocCollection, Doc, DocEntry, JsonValue } from "./lens/document.ts";
16
+ export { table } from "./lens/relational.ts";
17
+ export type { Table, TableSchema, Row, ColumnType, Query } from "./lens/relational.ts";
18
+ export type { Result, WriteResult } from "./lens/types.ts";
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAElF,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC7C,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAEvF,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * index.ts — the public entry point of the LibreDB npm package.
3
+ *
4
+ * The public surface is the kv lens (DESIGN.md section 6): you `open` a kernel
5
+ * database and put a lens over it — {@link kv} (strings) or {@link doc} (JSON
6
+ * documents). The kernel's byte-level internals (and each lens's private codec)
7
+ * stay unexported on purpose — the lens is the usable face. The relational lens
8
+ * ({@link table}) completes the trio.
9
+ */
10
+ export { version, open } from "./core.js";
11
+ export { kv } from "./lens/kv.js";
12
+ export { doc } from "./lens/document.js";
13
+ export { table } from "./lens/relational.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAGlC,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGzC,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * lens/document.ts — the document lens, LibreDB's differentiator lens.
3
+ *
4
+ * Where kv (lens/kv.ts) is the *string* face over the byte kernel, document is
5
+ * the *JSON* face: a document is a JSON-serializable object stored as UTF-8 JSON
6
+ * bytes (the value) under a `<collection>:<id>` kernel key. The lens adds JSON
7
+ * ergonomics and nothing else — no storage, transactions, or recovery live here;
8
+ * those stay in core.ts (DESIGN.md section 6.1).
9
+ *
10
+ * The collection handle from {@link doc} adds by-id CRUD on top of that codec;
11
+ * collection scans (`all`) and field matching (`find`) build on it in the
12
+ * phases that follow.
13
+ */
14
+ import { type Result, type WriteResult } from "./types.ts";
15
+ import type { Store } from "../adapter/store.ts";
16
+ /**
17
+ * Any value JSON can represent: the closure of the primitives under arrays and
18
+ * objects. This is the honest type of "what survives a JSON round-trip" —
19
+ * `undefined`, functions, and symbols are deliberately absent because JSON drops
20
+ * them.
21
+ */
22
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
23
+ [key: string]: JsonValue;
24
+ };
25
+ /**
26
+ * A document: a JSON object. Stored as one value in the kernel, keyed by its id
27
+ * within a collection. The top level is always an object (the unit a collection
28
+ * holds), while field values range over all of {@link JsonValue}.
29
+ */
30
+ export type Doc = {
31
+ [key: string]: JsonValue;
32
+ };
33
+ /**
34
+ * One document from a collection scan: its `id` paired with the decoded `doc`.
35
+ * This is the document-lens Row type — collection reads return
36
+ * `Result<DocEntry>` (lens/types.ts), the same envelope the kv lens uses.
37
+ */
38
+ export interface DocEntry {
39
+ readonly id: string;
40
+ readonly doc: Doc;
41
+ }
42
+ /** Serialize a document to UTF-8 JSON bytes — the form the kernel stores. */
43
+ export declare function encodeDoc(doc: Doc): Uint8Array;
44
+ /**
45
+ * Parse UTF-8 JSON bytes back into a document. The inverse of {@link encodeDoc}:
46
+ * bytes produced by it round-trip with full fidelity (numbers stay numbers,
47
+ * nested objects and unicode survive), which the document lens relies on for
48
+ * type-sensitive field matching.
49
+ */
50
+ export declare function decodeDoc(bytes: Uint8Array): Doc;
51
+ /**
52
+ * Whether `document` satisfies `predicate`: every top-level field named in the
53
+ * predicate is present on the document and deeply equal to the predicate's value
54
+ * (DESIGN.md section 6.1). Multiple fields are an implicit AND. An empty
55
+ * predicate names no fields and so matches every document.
56
+ *
57
+ * Exported so the relational lens's `where` can reuse the exact same matching
58
+ * semantics over rows (a row is a Doc), instead of re-deriving a second matcher.
59
+ */
60
+ export declare function matches(document: Doc, predicate: Doc): boolean;
61
+ /**
62
+ * A handle on one collection: by-id CRUD over the kernel.
63
+ *
64
+ * Each operation auto-commits in its own kernel transaction, so the collection
65
+ * behaves like a durable map of documents — a write is atomic and, on a
66
+ * file-backed database, fsync'd before it returns. Multi-document atomicity is a
67
+ * conscious omission (DESIGN.md section 6.1): a caller that needs it drops to the
68
+ * kernel's `transact`. The handle is a view — it depends only on the
69
+ * {@link Store} seam and never touches the store's lifecycle.
70
+ */
71
+ export interface DocCollection {
72
+ /** Store `document` under `id`, overwriting any existing document at that id.
73
+ * Always reports one changed entry. */
74
+ put(id: string, document: Doc): WriteResult;
75
+ /** The document stored at `id`, decoded, or `undefined` if none is set. */
76
+ get(id: string): Doc | undefined;
77
+ /** Remove the document at `id`. Reports one changed entry if it existed, zero
78
+ * if it did not. */
79
+ delete(id: string): WriteResult;
80
+ /** Every document in the collection, as {@link DocEntry} rows in ascending id
81
+ * order (the kernel's byte order over the `<collection>:<id>` keys). The
82
+ * {@link Result} is lazy and re-iterable: each pass re-runs the scan against
83
+ * the current state. A scan sees only this collection's documents — the
84
+ * `<collection>:` prefix is a byte boundary, so a sibling whose name shares a
85
+ * prefix (`users` vs `users2`) is never included. */
86
+ all(): Result<DocEntry>;
87
+ /** Every document whose top-level fields match `predicate` by deep
88
+ * (structural) equality; multiple fields are an implicit AND, and an empty
89
+ * predicate matches everything (so `find({})` equals `all()`). Returns the
90
+ * same lazy, re-iterable {@link Result} as `all`, scoped to this collection.
91
+ *
92
+ * Cost: O(n) in the collection size. `find` is a full collection scan with an
93
+ * in-engine predicate — there are no secondary indexes (DESIGN.md section 6.1,
94
+ * a deliberate v1 omission). Every document is decoded and tested on each
95
+ * pass. */
96
+ find(predicate: Doc): Result<DocEntry>;
97
+ }
98
+ /**
99
+ * Build a {@link DocCollection} handle scoped to `collection` over a
100
+ * {@link Store} (the kernel's `Database` satisfies it, as does any object that
101
+ * can run a transaction).
102
+ */
103
+ export declare function doc(store: Store, collection: string): DocCollection;
104
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/lens/document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAU,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAEnE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;GAIG;AACH,MAAM,MAAM,GAAG,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAE/C;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;CACnB;AAKD,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,UAAU,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,CAEhD;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG,OAAO,CAE9D;AAaD;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B;2CACuC;IACvC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,WAAW,CAAC;IAC5C,2EAA2E;IAC3E,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC;IACjC;wBACoB;IACpB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,CAAC;IAChC;;;;;yDAKqD;IACrD,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxB;;;;;;;;eAQW;IACX,IAAI,CAAC,SAAS,EAAE,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;CACxC;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,CA2DnE"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * lens/document.ts — the document lens, LibreDB's differentiator lens.
3
+ *
4
+ * Where kv (lens/kv.ts) is the *string* face over the byte kernel, document is
5
+ * the *JSON* face: a document is a JSON-serializable object stored as UTF-8 JSON
6
+ * bytes (the value) under a `<collection>:<id>` kernel key. The lens adds JSON
7
+ * ergonomics and nothing else — no storage, transactions, or recovery live here;
8
+ * those stay in core.ts (DESIGN.md section 6.1).
9
+ *
10
+ * The collection handle from {@link doc} adds by-id CRUD on top of that codec;
11
+ * collection scans (`all`) and field matching (`find`) build on it in the
12
+ * phases that follow.
13
+ */
14
+ import { result } from "./types.js";
15
+ import { prefixRange } from "../query/range.js";
16
+ const utf8 = new TextEncoder();
17
+ const fromUtf8 = new TextDecoder();
18
+ /** Serialize a document to UTF-8 JSON bytes — the form the kernel stores. */
19
+ export function encodeDoc(doc) {
20
+ return utf8.encode(JSON.stringify(doc));
21
+ }
22
+ /**
23
+ * Parse UTF-8 JSON bytes back into a document. The inverse of {@link encodeDoc}:
24
+ * bytes produced by it round-trip with full fidelity (numbers stay numbers,
25
+ * nested objects and unicode survive), which the document lens relies on for
26
+ * type-sensitive field matching.
27
+ */
28
+ export function decodeDoc(bytes) {
29
+ return JSON.parse(fromUtf8.decode(bytes));
30
+ }
31
+ /**
32
+ * Deep structural equality over {@link JsonValue}. This is what document
33
+ * matching compares with, so it must be the JSON notion of "same value", not
34
+ * reference identity and not `JSON.stringify` (which is sensitive to object key
35
+ * order and so would call two equal objects unequal).
36
+ *
37
+ * - Primitives (string/number/boolean/null) compare by `===`, which is
38
+ * type-sensitive on purpose: `1` is not `"1"` and `true` is not `"true"`.
39
+ * - Arrays are equal element-wise, in order (order is part of an array's value).
40
+ * - Objects are equal when they have the same set of keys and every value is
41
+ * deeply equal; key *order* is irrelevant.
42
+ *
43
+ * The arguments are `JsonValue | undefined` so a missing field (`doc[key]` with
44
+ * no such key) is handled directly: it is `undefined`, which equals nothing a
45
+ * predicate can hold (predicate values are always defined {@link JsonValue}).
46
+ */
47
+ function deepEqual(a, b) {
48
+ if (a === b)
49
+ return true; // same primitive, same reference, or both undefined
50
+ if (a === null || b === null)
51
+ return false; // one is null but not the other (=== caught both)
52
+ if (typeof a !== "object" || typeof b !== "object")
53
+ return false; // unequal primitives/undefined
54
+ const aIsArray = Array.isArray(a);
55
+ if (aIsArray !== Array.isArray(b))
56
+ return false; // an array is never equal to a plain object
57
+ if (aIsArray) {
58
+ const bArr = b;
59
+ if (a.length !== bArr.length)
60
+ return false;
61
+ return a.every((item, i) => deepEqual(item, bArr[i]));
62
+ }
63
+ const aObj = a;
64
+ const bObj = b;
65
+ const aKeys = Object.keys(aObj);
66
+ if (aKeys.length !== Object.keys(bObj).length)
67
+ return false;
68
+ return aKeys.every((key) => Object.prototype.hasOwnProperty.call(bObj, key) && deepEqual(aObj[key], bObj[key]));
69
+ }
70
+ /**
71
+ * Whether `document` satisfies `predicate`: every top-level field named in the
72
+ * predicate is present on the document and deeply equal to the predicate's value
73
+ * (DESIGN.md section 6.1). Multiple fields are an implicit AND. An empty
74
+ * predicate names no fields and so matches every document.
75
+ *
76
+ * Exported so the relational lens's `where` can reuse the exact same matching
77
+ * semantics over rows (a row is a Doc), instead of re-deriving a second matcher.
78
+ */
79
+ export function matches(document, predicate) {
80
+ return Object.keys(predicate).every((key) => deepEqual(document[key], predicate[key]));
81
+ }
82
+ /**
83
+ * The kernel key for one document: `<collection>:<id>`, UTF-8 encoded (DESIGN.md
84
+ * section 6.1). Prefixing every id with the collection name is what scopes a
85
+ * collection to a contiguous byte range, so a later `<collection>:` prefix scan
86
+ * (D3) selects exactly that collection. For a point operation it just makes the
87
+ * key deterministic: the same `(collection, id)` always maps to the same bytes.
88
+ */
89
+ function keyOf(collection, id) {
90
+ return utf8.encode(`${collection}:${id}`);
91
+ }
92
+ /**
93
+ * Build a {@link DocCollection} handle scoped to `collection` over a
94
+ * {@link Store} (the kernel's `Database` satisfies it, as does any object that
95
+ * can run a transaction).
96
+ */
97
+ export function doc(store, collection) {
98
+ // The byte range covering every `<collection>:` key. prefixRange computes the
99
+ // [start, end) bound on raw bytes so it agrees with the kernel's order, which
100
+ // is what makes the colon a sound collection boundary (a sibling like "users2"
101
+ // sorts outside ["users:", "users;") and is never seen). The prefix string
102
+ // length is also where every key's id begins, so stripping it recovers the id.
103
+ const prefix = `${collection}:`;
104
+ const { start, end } = prefixRange(utf8.encode(prefix));
105
+ // The shared collection scan behind both all() and find(): walk the prefix
106
+ // range in id (byte) order and return the documents `keep` accepts. The thunk
107
+ // re-runs per iteration, so the Result stays lazy and re-iterable; rows are
108
+ // materialized inside the transaction because the kernel's getRange is only
109
+ // valid in the tx body.
110
+ const scan = (keep) => result(() => store.transact((tx) => {
111
+ const rows = [];
112
+ for (const entry of tx.getRange(start, end)) {
113
+ const document = decodeDoc(entry.value);
114
+ if (keep(document)) {
115
+ rows.push({ id: fromUtf8.decode(entry.key).slice(prefix.length), doc: document });
116
+ }
117
+ }
118
+ return rows;
119
+ }));
120
+ return {
121
+ put(id, document) {
122
+ store.transact((tx) => {
123
+ tx.set(keyOf(collection, id), encodeDoc(document));
124
+ });
125
+ return { changed: 1 };
126
+ },
127
+ get(id) {
128
+ return store.transact((tx) => {
129
+ const bytes = tx.get(keyOf(collection, id));
130
+ return bytes === undefined ? undefined : decodeDoc(bytes);
131
+ });
132
+ },
133
+ delete(id) {
134
+ // Read-before-delete in one transaction: the kernel's delete is a silent
135
+ // no-op on a missing key, so this is how the lens tells 1 from 0 changes.
136
+ const changed = store.transact((tx) => {
137
+ const k = keyOf(collection, id);
138
+ const existed = tx.get(k) !== undefined;
139
+ tx.delete(k);
140
+ return existed ? 1 : 0;
141
+ });
142
+ return { changed };
143
+ },
144
+ all() {
145
+ return scan(() => true);
146
+ },
147
+ find(predicate) {
148
+ return scan((document) => matches(document, predicate));
149
+ },
150
+ };
151
+ }
152
+ //# sourceMappingURL=document.js.map