# blake3-wasm

BLAKE3 content hashing for talon, written in Rust and compiled to
`wasm32-unknown-unknown`. First brick of the Rust data/indexing plane —
the consumer-facing API is `src/native/blake3.ts` (`blake3Hex`,
`blake3HexFile`), used for media dedupe content hashes.

## Why Rust via WASM

Talon's polyglot policy admits new languages only behind clean boundaries,
and `bun build --compile` single-binary installs must keep working. A wasm
module satisfies both:

- **Clean boundary** — the entire surface is three C-ABI functions over
  linear memory (below). No NAPI, no per-platform prebuilds, no node-gyp.
- **Single-binary safe** — the built `.wasm` is embedded as base64 in
  `src/native/blake3-wasm-bytes.ts` and instantiated from bytes at
  runtime. No fs paths, no `import.meta.url` asset resolution, which
  compiled binaries cannot serve.
- **No wasm-bindgen** — the ABI is hand-rolled, so the build needs only
  `cargo` + the `wasm32-unknown-unknown` target. No extra toolchain, no
  generated JS glue, and the artifact stays ~22KB.

Rust's `blake3` crate is the reference-maintained implementation; even the
portable (non-SIMD) build used under wasm comfortably outruns a JS
implementation, and correctness is pinned to the official upstream test
vectors in `src/__tests__/blake3-wasm.test.ts`.

## ABI contract

The module exports its linear `memory` plus:

| export                          | contract                                                                                                                                                  |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `alloc(len) -> ptr`             | Allocate `len` bytes, return the offset. Returns `0` for `len == 0` or allocator exhaustion. Region is uninitialized.                                       |
| `dealloc(ptr, len)`             | Release a region from `alloc` — must pass the **same `len`**. `ptr == 0` or `len == 0` is a no-op.                                                          |
| `blake3_hash(ptr, len, outPtr)` | Hash `len` bytes at `ptr`, write the 32-byte digest to `outPtr` (caller-allocated, ≥32 bytes). `ptr` may be `0` only when `len == 0`. Regions must not overlap. |
| `hasher_new() -> handle`        | Allocate an incremental hasher; opaque non-zero handle (`0` on exhaustion). Every handle must be consumed by exactly one `hasher_finalize` or `hasher_free`.   |
| `hasher_update(handle, ptr, len)` | Feed `len` bytes at `ptr` into the hasher. Any number of calls. `ptr` may be `0` only when `len == 0`. Never allocates — cannot grow/detach memory.          |
| `hasher_finalize(handle, outPtr)` | Write the 32-byte digest to `outPtr` (caller-allocated, ≥32 bytes) and **consume** the handle.                                                               |
| `hasher_free(handle)`           | Consume a handle without finalizing — host error-path cleanup.                                                                                                 |

One-shot calls are synchronous and stateless between invocations — one
instance is memoized for the process lifetime, and callers allocate, copy
in, hash, copy out, and dealloc with no awaits in between. Incremental
state lives behind its handle, so interleaved hashers are independent;
`src/native/blake3.ts` uses this to stream files chunk-by-chunk
(`blake3HexFile`), bounding peak memory at one 1MiB scratch region per
call with no file-size ceiling (the v1 one-shot file path refused
anything over 256MB).

## Rebuilding

```sh
npm run build:wasm
```

That runs, from this directory:

1. `cargo build --release --target wasm32-unknown-unknown --locked`
2. the shared embed step (`native/shared/build-lib.mjs`) — regenerates
   `src/native/blake3-wasm-bytes.ts`

The generated TS module is the runtime artifact of record and is
committed; the raw `.wasm` is not. The toolchain is pinned in
`rust-toolchain.toml` (rustc codegen changes between stable releases would
change the bytes); the CI `wasm-artifact` job rebuilds and
`git diff --exit-code`s the generated file, so committed bytes always
match the committed Rust source. To bump the toolchain or deps: edit the
pin / `cargo update`, rebuild, and commit the regenerated bytes module
alongside.
