@dreamlake/dreamdb 0.3.1 → 0.5.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 +178 -0
- package/{dreamdb.d.ts → browser/dreamdb.d.ts} +142 -0
- package/browser/dreamdb.js +9 -0
- package/{dreamdb_bg.js → browser/dreamdb_bg.js} +555 -2
- package/browser/dreamdb_bg.wasm +0 -0
- package/browser/dreamdb_bg.wasm.d.ts +56 -0
- package/browser-extras.mjs +193 -0
- package/browser.mjs +4 -0
- package/index.d.ts +541 -1
- package/node/dreamdb.cjs +1762 -0
- package/node/dreamdb.d.cts +425 -0
- package/node/dreamdb_bg.wasm +0 -0
- package/node/dreamdb_bg.wasm.d.ts +72 -0
- package/node.cjs +10 -0
- package/node.mjs +19 -0
- package/package.json +40 -16
- package/web/dreamdb.d.ts +407 -0
- package/web/dreamdb.js +1558 -0
- package/web/dreamdb_bg.wasm +0 -0
- package/web/dreamdb_bg.wasm.d.ts +56 -0
- package/web.mjs +17 -0
- package/dreamdb.js +0 -9
- package/dreamdb_bg.wasm +0 -0
- package/index.js +0 -6
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
|
|
@@ -142,6 +142,67 @@ export class Space {
|
|
|
142
142
|
readonly manifestHash: string;
|
|
143
143
|
}
|
|
144
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
|
+
|
|
145
206
|
/**
|
|
146
207
|
* Install a panic hook that logs the panic message AND its `file:line:col`
|
|
147
208
|
* to the console before the wasm trap surfaces to JS.
|
|
@@ -158,6 +219,22 @@ export class Space {
|
|
|
158
219
|
*/
|
|
159
220
|
export function __wasm_init(): void;
|
|
160
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
|
+
|
|
161
238
|
/**
|
|
162
239
|
* Encode arbitrary bytes as lowercase RFC-4648 base32 (no padding).
|
|
163
240
|
*
|
|
@@ -172,6 +249,71 @@ export function bytesToBase32(bytes: Uint8Array): string;
|
|
|
172
249
|
*/
|
|
173
250
|
export function decodeCbor(bytes: Uint8Array): any;
|
|
174
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
|
+
|
|
175
317
|
/**
|
|
176
318
|
* Package version — useful for consumers to confirm which build is loaded.
|
|
177
319
|
*/
|
|
@@ -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";
|