@dreamlake/dreamdb 0.3.0 → 0.4.0

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 ADDED
@@ -0,0 +1,178 @@
1
+ # @dreamlake/dreamdb
2
+
3
+ The DreamDB SDK — read and write, in the browser and on the server, compiled to
4
+ WebAssembly from the same Rust core the CLI and Python SDK use.
5
+
6
+ There is no separate JavaScript implementation of the protocol, and that is the
7
+ point. A hand-written port has to reproduce BLAKE3, canonical CBOR, spatial-key
8
+ encoding, bucket headers and index layout *bit for bit*, forever. The previous
9
+ TypeScript port did not, and its tests were all green anyway, because they were
10
+ its own reader reading its own writer.
11
+
12
+ ```bash
13
+ npm install @dreamlake/dreamdb
14
+ ```
15
+
16
+ ## Reading
17
+
18
+ ```js
19
+ import { Space } from '@dreamlake/dreamdb'
20
+
21
+ const space = await Space.fromUri('https://bucket.s3.amazonaws.com/refs/my-dataset', null)
22
+ const hits = await space.queryVector('visual', queryVec, 24, 8)
23
+ ```
24
+
25
+ Passing `null` for the backend uses direct `fetch` against the URI's base —
26
+ enough for a public bucket. For anything else, supply a Backend.
27
+
28
+ ## Writing
29
+
30
+ ```js
31
+ import { Writer, PresignedBackend } from '@dreamlake/dreamdb'
32
+
33
+ const backend = new PresignedBackend({
34
+ readBase: 'https://bucket.s3.amazonaws.com',
35
+ mintPut: async (paths) => (await postJson('/api/dreamdb/sign', { paths })).urls,
36
+ commitRef: async (path, opts, bytes) =>
37
+ await postJson('/api/dreamdb/ref', {
38
+ path,
39
+ bytesBase64: base64(bytes),
40
+ ifMatch: opts.ifMatch,
41
+ ifNoneMatchStar: opts.ifNoneMatchStar,
42
+ }),
43
+ })
44
+
45
+ const w = await Writer.open('https://bucket.s3.amazonaws.com/refs/my-dataset', backend)
46
+ await w.appendMany([
47
+ {
48
+ anchor: 1735689600000000000n, // nanoseconds — pass a bigint
49
+ visual: { kind: 'embedding', algorithm: 'dreamdb.lsh-cosine', vector: vec },
50
+ caption: { kind: 'categorical', value: 'a red car' },
51
+ },
52
+ ])
53
+ const manifest = await w.commit()
54
+ ```
55
+
56
+ Credentials never enter the browser: your server mints short-lived presigned
57
+ PUT URLs, and performs the ref's compare-and-swap itself.
58
+
59
+ ### Anchors are nanoseconds, and you should pass a `bigint`
60
+
61
+ A `number` is accepted only while it is below `Number.MAX_SAFE_INTEGER`; above
62
+ that the SDK throws rather than round it. This is not pedantry. The previous
63
+ TypeScript SDK wrote anchors in **microseconds** specifically to stay under
64
+ 2^53, and every record it produced reads as 1970 in any conformant reader.
65
+
66
+ ## Server-side authoring
67
+
68
+ `Authoring` adds dataset creation, layers, merge, compaction and history. It is
69
+ **Node only** — the browser build exports a stub whose methods throw with an
70
+ explanation.
71
+
72
+ ```js
73
+ import { Authoring } from '@dreamlake/dreamdb' // resolves to the Node build
74
+
75
+ const a = await Authoring.create(
76
+ 'my-dataset',
77
+ [
78
+ { name: 'visual', kind: 'embedding', dim: 768, algorithm: 'dreamdb.lsh-cosine' },
79
+ { name: 'caption', kind: 'scalar', valueType: 'categorical', required: false },
80
+ ],
81
+ 'https://bucket.s3.amazonaws.com',
82
+ backend,
83
+ )
84
+ await a.writer().appendMany(samples)
85
+ await a.compact(null, 1, 0)
86
+ ```
87
+
88
+ The split is a size boundary with a mechanism behind it. `create` and the
89
+ embedding-layer builders construct a `SpatialDispatcher`, which is an enum — so
90
+ one reachable construction makes *every* index family's build code reachable
91
+ (IVF, IMI, LSH, Vamana, AdaIVF, plus codebook training). Measured: 484 KB
92
+ gzipped for the browser build versus 607 KB with authoring included.
93
+
94
+ Embedding fields declaring `dreamdb.ivf-cosine` or `dreamdb.imi-cosine` cannot
95
+ be created here at all: those index families need training data that does not
96
+ exist at create time. Create with the default `dreamdb.lsh-cosine` and attach a
97
+ trained index as a layer, or build it with the CLI.
98
+
99
+ ## Entry points
100
+
101
+ | Import | Resolves to | Notes |
102
+ | --- | --- | --- |
103
+ | `@dreamlake/dreamdb` in a bundler | `--target bundler` | Needs `vite-plugin-wasm` under Vite |
104
+ | `@dreamlake/dreamdb` in Node | `--target nodejs` | ESM and CJS both work; includes `Authoring` |
105
+ | `@dreamlake/dreamdb/web` | `--target web` | No bundler plugin needed; `await ready()` first |
106
+
107
+ ```html
108
+ <script type="module">
109
+ import ready, { Space } from 'https://esm.sh/@dreamlake/dreamdb/web'
110
+ await ready() // fetches and instantiates the wasm
111
+ </script>
112
+ ```
113
+
114
+ ## Implementing a Backend
115
+
116
+ Only `get` is required; a `get`-only backend is the read-only v1 shape and
117
+ still works for reading. Write entry points check for `put` up front and refuse
118
+ with a message naming what is missing.
119
+
120
+ ```ts
121
+ interface Backend {
122
+ get(path: string, range?: { start: number; end: number })
123
+ : Promise<Uint8Array | { bytes: Uint8Array; etag?: string }>
124
+ head?(path: string): Promise<{ exists?: boolean; etag?: string; size?: number }>
125
+ put?(path: string, bytes: Uint8Array,
126
+ opts?: { ifMatch?: string; ifNoneMatchStar?: boolean })
127
+ : Promise<{ status: 'created' | 'exists' | 'casFailed'; etag?: string }>
128
+ delete?(path: string): Promise<void>
129
+ list?(prefix: string): Promise<string[]>
130
+ }
131
+ ```
132
+
133
+ ### Two things that will bite you
134
+
135
+ **Your bucket's CORS rule must include `ExposeHeaders: ["ETag"]`.**
136
+
137
+ A ref advances by compare-and-swap. The SDK sends `If-Match: <etag>` when it
138
+ knows the ref's current version and `If-None-Match: *` when it does not. Without
139
+ `ExposeHeaders`, the browser can read the response body but not the ETag header,
140
+ so the SDK never learns the current version, falls back to create-only, and the
141
+ **second** commit to any ref fails while the first succeeded. That asymmetry is
142
+ what makes it hard to recognise.
143
+
144
+ ```json
145
+ {
146
+ "AllowedOrigins": ["https://your.app"],
147
+ "AllowedMethods": ["GET", "HEAD", "PUT"],
148
+ "AllowedHeaders": ["*"],
149
+ "ExposeHeaders": ["ETag", "Content-Range", "Content-Length"]
150
+ }
151
+ ```
152
+
153
+ **Any HTTP layer you put in front of the bucket must honour `Range`.**
154
+
155
+ DreamDB reads exact vectors by byte offset. A server that ignores `Range` and
156
+ returns 200 with the whole object makes every ranged read return offset 0, and
157
+ search then returns plausible-looking results with cosine scores of 0.0000 —
158
+ indistinguishable from a genuinely corrupt index. (`python3 -m http.server`
159
+ does exactly this.)
160
+
161
+ ## Conformance
162
+
163
+ `dreamdb-conformance/vectors/` holds 85 language-agnostic JSON vectors. This
164
+ package runs the 48 that map to pure functions on its surface — canonical CBOR,
165
+ BLAKE3 multihash, spatial keys, time anchors and buckets, address round-trips,
166
+ modality parsing, HTTP range translation — and reports the other 37 by name and
167
+ reason rather than omitting them silently.
168
+
169
+ ```bash
170
+ node test/conformance.mjs
171
+ ```
172
+
173
+ The first run of that suite found a real bug in this package's HTTP range
174
+ translation, which is roughly the point.
175
+
176
+ ## Licence
177
+
178
+ MIT OR Apache-2.0
@@ -0,0 +1,326 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * A `fetch`-backed backend rooted at a base URL.
6
+ */
7
+ export class S3Backend {
8
+ free(): void;
9
+ [Symbol.dispose](): void;
10
+ /**
11
+ * Fetch `path` (relative to the base URL) and resolve to its bytes.
12
+ * `opts.noCache` is accepted for API compatibility (currently a no-op:
13
+ * content is hash-addressed; refs are read fresh each page load anyway).
14
+ */
15
+ get(path: string, _opts: any): Promise<Uint8Array>;
16
+ /**
17
+ * List object paths under `prefix`. Stub (returns empty); the current
18
+ * consumer read paths don't require listing.
19
+ */
20
+ list(_prefix: string): Promise<Array<any>>;
21
+ /**
22
+ * Construct rooted at `base_url` (trailing slash trimmed).
23
+ */
24
+ constructor(base_url: string);
25
+ }
26
+
27
+ export class Space {
28
+ private constructor();
29
+ free(): void;
30
+ [Symbol.dispose](): void;
31
+ /**
32
+ * Open a Space from a `.../refs/<name>` or `.../manifests/<hash>` URI,
33
+ * resolving the ref (if any) and loading the manifest. If `backend` is
34
+ * provided it handles IO (relative paths); otherwise a direct-fetch
35
+ * connector rooted at the parsed base URL is used (the `dreamdb-demo`
36
+ * no-backend path).
37
+ */
38
+ static fromUri(uri: string, backend: any): Promise<Space>;
39
+ /**
40
+ * Walk the manifest DAG from HEAD, up to `max_depth` entries. Each entry:
41
+ * `{ manifestHash, ts, writer, tracks: [{modality, address}] }`.
42
+ */
43
+ history(max_depth?: number | null): Promise<Array<any>>;
44
+ /**
45
+ * Hybrid search fusing a lexical (BM25) sub-query and a dense (vector)
46
+ * sub-query per spec/0015 §5/§6. Returns the fused `[{ anchor, score }]`
47
+ * list sorted by fused score descending — the same scored shape as
48
+ * `queryVector`.
49
+ *
50
+ * `vector` is the dense sub-query embedding (a `Float32Array`). `opts` is a
51
+ * plain JS object:
52
+ * ```js
53
+ * {
54
+ * textField, textQuery, // BM25 sub-query (required)
55
+ * vectorField, // dense field the `vector` targets (required)
56
+ * topK = 10, // fused results to return
57
+ * fusion = 'rrf', // 'rrf' | 'linear' | 'max' | 'pareto'
58
+ * rrfK = 60, // RRF rank constant (fusion='rrf')
59
+ * textWeight = 1, vectorWeight = 1, // Linear weights (fusion='linear')
60
+ * textK = topK, vectorK = topK, // per-sub-query candidate depths
61
+ * textRequired = false, vectorRequired = false, // §5.3 boolean-AND gates
62
+ * }
63
+ * ```
64
+ *
65
+ * Delegates to the real (tested) `dreamdb-dataset::Dataset::query_hybrid`.
66
+ * (v0 does not expose the optional scalar pre/post-filter; it runs with no
67
+ * scalar gate.)
68
+ */
69
+ queryHybrid(vector: Float32Array, opts: any): Promise<Array<any>>;
70
+ /**
71
+ * Scalar-index lookup: every anchor whose `field` satisfies `op value`
72
+ * (spec/0011). Returns a `BigUint64Array`-style `Array` of anchors,
73
+ * tombstone-suppressed, sorted ascending and deduped.
74
+ *
75
+ * This is the expansion half of a compact lexical index. A BM25 index
76
+ * built over a scalar field's DISTINCT values returns one representative
77
+ * anchor per matched value; resolving that value back to *every* record
78
+ * carrying it is a scalar-index lookup, not a text-index concern. Keeping
79
+ * the split this way is what lets the text index stay small enough to
80
+ * load in a browser — indexing one document per record instead produced a
81
+ * 406 MB object for ~1,700 distinct strings.
82
+ *
83
+ * `op` is one of `==`, `!=`, `<`, `<=`, `>`, `>=`. `value` may be a
84
+ * string, number, or boolean; strings match both `String` and
85
+ * `Categorical` scalar fields.
86
+ */
87
+ queryScalar(field: string, op: string, value: any): Promise<Array<any>>;
88
+ /**
89
+ * Lexical BM25 text search over a text `field` (spec/0015 §3.6).
90
+ *
91
+ * Tokenizes `query` with the built-in `dreamdb.utf8-words` tokenizer
92
+ * (lowercase + split on non-alphanumeric — pure Rust, compiled into the
93
+ * wasm; no external model needed even though the field's embeddings were
94
+ * produced elsewhere), scores docs with BM25, and suppresses tombstoned
95
+ * anchors. Returns `[{ anchor, score }]` sorted by score descending — the
96
+ * same scored shape as `queryVector`.
97
+ *
98
+ * Delegates to the real (tested) `dreamdb-dataset::Dataset::query_text`.
99
+ */
100
+ queryText(field: string, query: string, top_k: number): Promise<Array<any>>;
101
+ /**
102
+ * Top-K cosine vector search over an embedding `field`. `opts` may be a
103
+ * number (topK) or `{ topK?, probeCount? }`. Returns `[{ anchor, score }]`
104
+ * sorted by score descending — the shape dreamdb-ts's `queryVector` yields.
105
+ *
106
+ * Delegates to the real (tested) `dreamdb-dataset` query path: ADC cull +
107
+ * two-pass rerank + HotShard merge + tombstone suppression.
108
+ */
109
+ queryVector(field: string, query: Float32Array, opts: any): Promise<Array<any>>;
110
+ /**
111
+ * Read a scalar track as a `Map<anchorString, value>`. Single-anchor
112
+ * entries resolve from the track index; multi-anchor entries fetch (and
113
+ * de-dupe) their bucket and roaring-decode the anchor set.
114
+ */
115
+ readScalarColumn(track: any, _opts: any): Promise<Map<any, any>>;
116
+ /**
117
+ * Fetch a track object and return its `object_index` (array of CBOR
118
+ * entries). Inline indexes only — paged indexes are not resolved (matching
119
+ * dreamdb-ts, which also doesn't support them on this path).
120
+ */
121
+ resolveObjectIndex(track: any): Promise<Array<any>>;
122
+ /**
123
+ * Base32 hash of the manifest's first timeline.
124
+ */
125
+ timelineId(): string;
126
+ /**
127
+ * Find a track by its resolved `key` or by `modality`.
128
+ */
129
+ trackFor(key_or_modality: string): any;
130
+ /**
131
+ * List resolved tracks from this manifest.
132
+ */
133
+ tracks(): Array<any>;
134
+ /**
135
+ * The base URL objects are fetched relative to (consumers build their own
136
+ * object URLs from this — e.g. `dreamdb-demo`'s track readers).
137
+ */
138
+ readonly connectorBase: string;
139
+ /**
140
+ * The manifest's base32 multihash.
141
+ */
142
+ readonly manifestHash: string;
143
+ }
144
+
145
+ /**
146
+ * A write handle onto a ref.
147
+ */
148
+ export class Writer {
149
+ private constructor();
150
+ free(): void;
151
+ [Symbol.dispose](): void;
152
+ /**
153
+ * Append records and commit in one call.
154
+ *
155
+ * `samples` is an array of `{ anchor?: bigint|number, <field>: value }`.
156
+ * See `marshal::samples_from_js` for the accepted field shapes.
157
+ *
158
+ * Committing here rather than exposing a separate staged mode is
159
+ * deliberate for the browser surface: a staged write that is never
160
+ * committed leaves uploaded objects unreferenced, and a tab can close at
161
+ * any moment. `appendStaged` exists on the Node surface where the process
162
+ * lifetime is under the caller's control.
163
+ */
164
+ appendMany(samples: Array<any>): Promise<number>;
165
+ /**
166
+ * Flush staged entries and publish a new manifest.
167
+ *
168
+ * Fails with a CAS conflict if the ref moved underneath this writer. That
169
+ * is not retried automatically: an automatic retry would hide a logical
170
+ * conflict, and the caller is the only one who knows whether re-applying
171
+ * its records on top of the new head is correct.
172
+ */
173
+ commit(): Promise<string>;
174
+ /**
175
+ * Tombstone records by anchor. Returns the new manifest hash.
176
+ */
177
+ deleteRecords(anchors: BigUint64Array, reason?: string | null): Promise<string>;
178
+ /**
179
+ * Open a ref for writing.
180
+ *
181
+ * `uri` is the same `.../refs/<name>` form `Space.fromUri` takes; a
182
+ * `.../manifests/<hash>` URI is rejected rather than silently opening
183
+ * something unwritable, because a manifest hash names an immutable
184
+ * snapshot — there is nothing for a commit to advance.
185
+ *
186
+ * `backend` must implement `put` (Backend contract v2). Passing a
187
+ * read-only backend fails at the first write with a clear message rather
188
+ * than here, since the connector cannot know what the JS object omits
189
+ * until it calls it.
190
+ */
191
+ static open(uri: string, backend: any): Promise<Writer>;
192
+ /**
193
+ * Tag the current manifest with an immutable label (`refs/<ref>@<label>`).
194
+ */
195
+ snapshot(label: string): Promise<string>;
196
+ /**
197
+ * Current manifest hash, base32.
198
+ */
199
+ readonly manifestHash: string;
200
+ /**
201
+ * The ref this writer advances.
202
+ */
203
+ readonly refName: string;
204
+ }
205
+
206
+ /**
207
+ * Install a panic hook that logs the panic message AND its `file:line:col`
208
+ * to the console before the wasm trap surfaces to JS.
209
+ *
210
+ * Without this, every Rust panic reaches JavaScript as a bare
211
+ * `RuntimeError: unreachable` with nothing but wasm frame offsets — and it is
212
+ * indistinguishable from an allocation failure, since Rust's alloc-error
213
+ * handler also aborts. That ambiguity cost real debugging time: a browser
214
+ * `unreachable` from exhausting wasm32's 32-bit address space on an oversized
215
+ * index was initially read as an integer-overflow panic.
216
+ *
217
+ * Zero new deps — uses the already-enabled `web-sys` `console` feature.
218
+ * Runs once at module init.
219
+ */
220
+ export function __wasm_init(): void;
221
+
222
+ /**
223
+ * Parse an object path and re-format it. Byte-identity of the result is the
224
+ * actual assertion: a parser that silently drops a component still "parses".
225
+ */
226
+ export function addressRoundTrip(path: string): string;
227
+
228
+ /**
229
+ * The address variant name (`Genesis`, `Manifest`, `Ref`, …) for a path.
230
+ *
231
+ * Derived from the Debug representation rather than a hand-written match.
232
+ * `DreamDbAddress` has seventeen variants and gains one whenever the protocol
233
+ * does; a match arm per variant would be a second list to keep in sync, and
234
+ * the vectors only ever assert the name.
235
+ */
236
+ export function addressVariant(path: string): string;
237
+
238
+ /**
239
+ * Encode arbitrary bytes as lowercase RFC-4648 base32 (no padding).
240
+ *
241
+ * Byte-identical to `dreamdb-ts`'s `bytesToBase32` (alphabet
242
+ * `abcdefghijklmnopqrstuvwxyz234567`) and to `dreamdb_core`'s multihash
243
+ * `to_base32`, which is how object addresses are spelled on disk.
244
+ */
245
+ export function bytesToBase32(bytes: Uint8Array): string;
246
+
247
+ /**
248
+ * Decode a single CBOR value from `bytes` into its JS representation.
249
+ */
250
+ export function decodeCbor(bytes: Uint8Array): any;
251
+
252
+ /**
253
+ * Canonically encode a JS value as CBOR, returning the bytes.
254
+ *
255
+ * Canonical here means what spec/0002 §3.1 means: map keys sorted by their
256
+ * encoded bytes, shortest-form integers, no indefinite lengths. Two writers
257
+ * that disagree on this produce different content hashes for the same logical
258
+ * object, and every address derived from them diverges.
259
+ */
260
+ export function encodeCbor(value: any): Uint8Array;
261
+
262
+ /**
263
+ * Parse a modality tag into `{class, encoding, trackKind, objectKind, params,
264
+ * flags}`, or throw if it is invalid.
265
+ *
266
+ * `params` is a plain object of the `key=value` segments and `flags` an array
267
+ * of the bare ones (`bucketed`, `graph`). They are separate because the
268
+ * grammar treats them differently and merging them would make a flag
269
+ * indistinguishable from a parameter whose value happened to be empty.
270
+ */
271
+ export function modalityParse(tag: string): any;
272
+
273
+ /**
274
+ * BLAKE3-256 multihash of `bytes`, base32 (the form used in object paths).
275
+ */
276
+ export function multihashBase32(bytes: Uint8Array): string;
277
+
278
+ /**
279
+ * BLAKE3-256 multihash of `bytes` as lowercase hex (33 bytes: tag + digest).
280
+ */
281
+ export function multihashHex(bytes: Uint8Array): string;
282
+
283
+ /**
284
+ * DreamDB's half-open `[start, end)` → an HTTP `Range` header value.
285
+ *
286
+ * The off-by-one here is worth a vector of its own: HTTP ranges are
287
+ * *inclusive* of the end byte. Getting it wrong reads one byte too few from
288
+ * every object, which corrupts decode in ways that look like anything but an
289
+ * off-by-one.
290
+ */
291
+ export function rangeHeader(start: bigint, end: bigint): string;
292
+
293
+ /**
294
+ * Round-trip a spatial key through its base-2 form, per spec/0002 §6.
295
+ *
296
+ * Returns the re-encoded string, so a caller can assert byte-identity rather
297
+ * than merely "it parsed".
298
+ */
299
+ export function spatialKeyRoundTrip(bits: string): string;
300
+
301
+ /**
302
+ * The 16-char hex address form → TimeAnchor. Throws on a malformed input
303
+ * (wrong length, uppercase) rather than coercing it.
304
+ */
305
+ export function timeAnchorFromHex(s: string): bigint;
306
+
307
+ /**
308
+ * TimeAnchor → the 16-char hex used in addresses.
309
+ */
310
+ export function timeAnchorHex(value: bigint): string;
311
+
312
+ /**
313
+ * Which time bucket an anchor falls in, given a duration like `"1s"`/`"60s"`.
314
+ */
315
+ export function timeBucket(t_start: bigint, duration: string): bigint;
316
+
317
+ /**
318
+ * Package version — useful for consumers to confirm which build is loaded.
319
+ */
320
+ export function version(): string;
321
+
322
+ /**
323
+ * The all-zero spatial-key path segment used for non-spatial (fragment) object
324
+ * addresses. Matches `dreamdb-ts`'s `ZERO_SPATIAL_KEY` constant exactly.
325
+ */
326
+ export function zeroSpatialKey(): string;
@@ -0,0 +1,9 @@
1
+ /* @ts-self-types="./dreamdb.d.ts" */
2
+ import * as wasm from "./dreamdb_bg.wasm";
3
+ import { __wbg_set_wasm } from "./dreamdb_bg.js";
4
+
5
+ __wbg_set_wasm(wasm);
6
+ wasm.__wbindgen_start();
7
+ export {
8
+ S3Backend, Space, Writer, __wasm_init, addressRoundTrip, addressVariant, bytesToBase32, decodeCbor, encodeCbor, modalityParse, multihashBase32, multihashHex, rangeHeader, spatialKeyRoundTrip, timeAnchorFromHex, timeAnchorHex, timeBucket, version, zeroSpatialKey
9
+ } from "./dreamdb_bg.js";