DreamDB

Browser usage

The browser build reads, queries, appends, and commits. Nothing sits between the page and storage: the SDK fetches byte ranges and does the search locally.

Getting the wasm running

In a bundler, import normally and add vite-plugin-wasm (or the webpack equivalent). Without a bundler, use the /web entry, which fetches and instantiates the WebAssembly itself:

html
<script type="module">
  import ready, { Space } from "https://esm.sh/@dreamlake/dreamdb/web";
  await ready();
</script>

ready() must resolve before you touch any other export.

Rendering stored bytes

There is no blob helper. The SDK gives you an address; you fetch it like any other URL. An object's path is:

<connectorBase>/<track.timeline>/<track.modality>/<spatialKey>/<address>

Blob fields are not spatially bucketed, so the key segment is ZERO_SPATIAL_KEY. The address is the item's hash from resolveObjectIndex, rendered with bytesToBase32:

js
import { Space, ZERO_SPATIAL_KEY, bytesToBase32 } from "@dreamlake/dreamdb";

const space = await Space.fromUri("https://bucket.example/refs/gallery", null);
const track = space.trackFor("image");

for (const [anchor, , size, hash] of await space.resolveObjectIndex(track)) {
  const url = [
    space.connectorBase,
    track.timeline,
    track.modality,
    ZERO_SPATIAL_KEY,
    bytesToBase32(hash),
  ].join("/");

  const img = document.createElement("img");
  img.src = url;                       // no Blob, no object URL, no revoke
  document.body.append(img);
}

Because every object is content-addressed and immutable, the browser cache does the right thing for free — no cache-busting, no revalidation.

Use bytesToBase32 for the address segment, not multihashBase32. Both render the same 33 bytes; only the first produces the path the object is stored at. The other returns a URL that 404s.

Writing without credentials

Writer works in the browser, but credentials must not. PresignedBackend keeps them on your server: it asks the server to mint short-lived presigned PUT URLs, and hands the ref's compare-and-swap back to the server too.

js
import { Writer, PresignedBackend } from "@dreamlake/dreamdb";

const backend = new PresignedBackend({
  readBase: "https://bucket.example",
  mintPut: async (paths) =>
    (await postJson("/api/dreamdb/sign", { paths })).urls,
  commitRef: async (path, opts, bytes) =>
    await postJson("/api/dreamdb/ref", {
      path,
      bytesBase64: base64(bytes),
      ifMatch: opts.ifMatch,
      ifNoneMatchStar: opts.ifNoneMatchStar,
    }),
});

const writer = await Writer.open("https://bucket.example/refs/notes", backend);

Creating a dataset is not available here — Authoring is stubbed out in the browser build, because pulling in every index family's build code costs 123 KB gzipped. Create the dataset server-side or in Python; the browser appends to it.

Three things that will bite you

CORS must expose ETag. A ref advances by compare-and-swap, so the SDK needs to read the ref's current ETag. Without ExposeHeaders, the browser hides that header, the SDK falls back to create-only, and the second commit to any ref fails while the first succeeded — an asymmetry that is hard to recognise for what it is.

json
{
  "AllowedOrigins": ["https://your.app"],
  "AllowedMethods": ["GET", "HEAD", "PUT"],
  "AllowedHeaders": ["*"],
  "ExposeHeaders": ["ETag", "Content-Range", "Content-Length"]
}

Anything in front of the bucket should honour Range. DreamDB reads vectors at byte offsets. A proxy that ignores Range and returns whole objects keeps results correct — the SDK slices locally — at the cost of transferring the whole object per read. If you implement get yourself, note that its range is half-open: bytes=${start}-${end - 1}.

Large historical inline Tracks need getStream for bounded memory. A custom backend with only get remains compatible, but must materialize the complete object. Implement the optional getStream(path) contract with bounded chunks and totalLength to keep that compatibility read bounded in wasm memory.

Anchors are bigint going in, number coming out. appendMany wants BigInt(Date.now()) * 1_000_000n. queryVector returns anchor as a plain number, and readScalarColumn keys its Map by the anchor as a string — map.get(String(hit.anchor)).

Next