WebAssembly runtime¶
@holographic-memory/wasm runs the document store locally in browsers, Node, Deno and Bun. It is the same engine code as the native and Node bindings: ingest, stemmed BM25 passage search with metadata filters, titleWeight, bm25K1 / bm25B, optional caller-supplied dense embeddings with blend or RRF fusion, delete, stats, and a serialized store you can export, import and persist. No service calls.
Package documentation (entry points, usage, storage, encryption): bindings/wasm/npm/README.md. Encryption decision and measurements: ENCRYPTION.md. Browser demo: examples/wasm (see its README section below).
What is and is not the engine¶
The wasm module runs MemoryDocumentStore (src/core/documents/memory.rs): the engine's document chunking, term statistics, postings, filters and dense index, without the journal, shards, file locks or mmap that the native HmsCore adds for durability and multi-process use. A test (matches_the_engine_document_path) asserts its search results are byte-identical to HmsCore's, and the benchmark compares top-10 digests across wasm, the native store and HmsCore. Not in the wasm build: the vector/graph/knowledge-graph engine, local model stages, the HTTP server, signing and provenance.
The core crate compiles for wasm32 by gating the file- and thread-bound modules (engine, storage, admin, store_lock, ...) with cfg(not(target_arch = "wasm32")) in src/core/mod.rs; fs2 is a non-wasm dependency. A new module on the document path must stay wasm-clean: check with cargo check --manifest-path bindings/wasm/Cargo.toml --target wasm32-unknown-unknown.
Building¶
cd bindings/wasm/npm
npm install
node scripts/build.mjs # both variants, all targets, TypeScript layer
Needs the wasm32-unknown-unknown target and a wasm-bindgen CLI that matches Cargo.lock (the script uses wasm-pack's cache; cargo install wasm-bindgen-cli --version <v> also works). The script runs one cargo build --release per variant, then wasm-bindgen for each target (web, bundler, nodejs, deno), then wasm-opt -Oz with an explicit feature list (SIMD only on the SIMD variant, so a scalar file containing a SIMD instruction fails the optimizer's validation). Output is dist/wasm/<target>/<scalar|simd>/ plus dist/wasm-sizes.json. Bun uses the nodejs target.
Release profile: opt-level = "s", fat LTO, panic = "abort". A trap (including a panic) leaves that module instance unusable; the store validates every input and imported snapshot so that hostile bytes return errors instead of reaching an assertion.
SIMD and threads¶
Two builds ship: scalar and +simd128. openStore validates a probe module with WebAssembly.validate and loads the SIMD build when the runtime accepts it; simd: false forces scalar. Both builds return identical results and identical snapshots (tested). The scalar build assumes the features rustc enables by default for wasm32-unknown-unknown (bulk memory, reference types, multivalue, sign extension, non-trapping float-to-int, mutable globals).
There is no threaded build. The document path is single-threaded (ingest of a batch is sequential), so threads would need SharedArrayBuffer, cross-origin isolation headers and a nightly build-std rebuild for no gain. Run the store in a Worker to keep the page responsive during large ingests.
Persistence¶
A store saves as one snapshot (magic HMSD, checksum, LZ4-compressed JSON) under a key. A StorageAdapter is get, set (atomic replace), delete and optional keys:
| Adapter | Where | Atomicity |
|---|---|---|
IndexedDbStorage | browsers | one put is one transaction |
OpfsStorage | browsers (origin private file system) | createWritable() swap file; Worker fallback uses a sync access handle |
FileStorage | Node, Deno, Bun | write temp file, fsync, rename |
MemoryStorage | anywhere | n/a |
Autosave (default on when a storage adapter is given, 500 ms debounce) re-exports the whole store, so its cost grows with store size; set autosave: false and call save() at your own cadence for large stores. exportSnapshot() / openStore({ snapshot }) move a store between hosts.
Tests¶
| Surface | Command (from bindings/wasm/npm) |
|---|---|
| Rust unit tests incl. forged-snapshot rejection | cargo test --lib documents::memory (repo root) |
| wasm-bindgen-test, headless Chrome and Firefox | node scripts/wasm-test.mjs |
| Node | npm run test:node |
| Deno | npm run test:deno |
| Bun | npm run test:bun |
| Browser pages and the demo, offline, Chrome and Firefox | HMS_FIREFOX=... node test/browser/run.mjs |
webpack 5 (bundler and web entries) in Chrome | node test/bundler/run.mjs |
| Packed tarball installed and imported from Node, Deno, Bun, plus type-check | node scripts/pack-check.mjs |
The Node, Deno and Bun runners execute one shared suite (test/shared/suite.mjs).
Demo¶
examples/wasm is a static page: add notes or text files, search with BM25 knobs, persist to OPFS or IndexedDB with an optional passphrase, download or restore a snapshot. A service worker caches the app so it reloads with the server stopped. node examples/wasm/serve.mjs serves it on localhost (a secure context, which OPFS and service workers need) with the built package at /pkg/.
Benchmarks¶
benchmarks/results/wasm_v1.json: synthetic Zipf-like corpus (100 words per document, 20 000-word vocabulary, 3-term queries sampled from the corpus), wasm under Node and headless Chrome/Firefox against the native in-memory store and the native HmsCore, with wasm module size (raw, gzip, brotli), module load time, ingest rate, search latency percentiles, memory, snapshot size, export and import time, and top-10 agreement. Regenerate with node bench/run.mjs in bindings/wasm/npm after a build.