DreamDB

React example

A grid of images and their labels, read straight from object storage. No API route, no server component, no state library.

The hook

Open the Space once, then resolve the image track into URLs and the label track into a lookup:

jsx
import { useEffect, useState } from "react";
import { Space, ZERO_SPATIAL_KEY, bytesToBase32 } from "@dreamlake/dreamdb";

export function useGallery(refUri) {
  const [items, setItems] = useState([]);

  useEffect(() => {
    let cancelled = false;

    (async () => {
      const space = await Space.fromUri(refUri, null);
      const imageTrack = space.trackFor("image");

      const [entries, labels] = await Promise.all([
        space.resolveObjectIndex(imageTrack),
        space.readScalarColumn(space.trackFor("label"), null),
      ]);

      const next = entries.map(([anchor, , size, hash]) => ({
        anchor: String(anchor),
        size,
        label: labels.get(String(anchor)),
        url: [
          space.connectorBase,
          imageTrack.timeline,
          imageTrack.modality,
          ZERO_SPATIAL_KEY,
          bytesToBase32(hash),
        ].join("/"),
      }));

      if (!cancelled) setItems(next);
    })();

    return () => { cancelled = true; };
  }, [refUri]);

  return items;
}

The cancelled flag matters in StrictMode, where the effect runs twice in development.

The component

jsx
import { useGallery } from "./useGallery";

export function Gallery({ refUri }) {
  const items = useGallery(refUri);

  if (!items.length) return <p>Loading…</p>;

  return (
    <ul className="grid">
      {items.map((item) => (
        <li key={item.anchor}>
          <img src={item.url} alt={item.label ?? ""} loading="lazy" />
          <figcaption>{item.label}</figcaption>
        </li>
      ))}
    </ul>
  );
}
jsx
<Gallery refUri="https://bucket.example/refs/gallery" />

Every URL is content-addressed and immutable, so loading="lazy" plus the browser cache is the whole image-loading strategy. Nothing needs revoking, because nothing allocated a Blob.

Adding search

useGallery returns every record. To narrow it, run a query first and keep the anchors:

jsx
const hits = await space.queryVector("embedding", queryVector, { topK: 24 });
const keep = new Set(hits.map((h) => String(h.anchor)));
const shown = items.filter((item) => keep.has(item.anchor));

Hits come back ordered by score, so sort shown by the hit order rather than the index order if ranking matters to you. Semantic search shows where queryVector's input comes from.

Bundler setup

Vite needs vite-plugin-wasm for the default import. Next.js needs experiments.asyncWebAssembly, or keep DreamDB on the server. Details in Install.