Dynoxide 0.11.0: wasm on npm, plus a stack of fixes
Dynoxide 0.11.0 packages the browser engine as an npm module and fixes a stack of correctness bugs. Last release got the engine running in the browser, but you had to wire up the Web Worker yourself to use it.
Now there's a package that does that for you. The fixes are mostly small things the conformance suite turned up - nothing dramatic, but the sort of drift you want closed if you're going to claim the thing behaves like DynamoDB.
npm install @dynoxide/wasm-engine
In 0.10.0 the browser build was a dist/ of a worker and two .wasm files, and the postMessage plumbing between your page and the engine was your problem. The package handles that:
npm install @dynoxide/wasm-engine
It bundles the engine .wasm, the SQLite build, the Web Worker, and an EngineClient that drives them. You import the client and run operations against a store that persists to the browser's origin private file system:
import { EngineClient } from "@dynoxide/wasm-engine";
const client = new EngineClient();
await client.ready();
await client.execute("PutItem", {
TableName: "Music",
Item: { artist: { S: "Pixies" }, song: { S: "Debaser" } },
});
const { Items } = await client.execute("Query", {
TableName: "Music",
KeyConditionExpression: "artist = :a",
ExpressionAttributeValues: { ":a": { S: "Pixies" } },
});
execute takes an operation name and a DynamoDB-JSON request, resolves with the response, and rejects with a typed EngineError. It runs in a Web Worker because OPFS's synchronous file handles are Worker-only, and the client handles the round trip, so you work in objects rather than message envelopes. It needs no server and no cross-origin isolation headers, so it'll sit on plain static hosting. Types are included.
The client also checks a contract version against the engine when it boots, and fails loudly if they don't match, so a pinned consumer gets a clear error rather than quietly mis-reading a newer engine.
The SQLite underneath is now the official @sqlite.org/sqlite-wasm build. The open/exec/query/close contract didn't change, so nothing using the package needs touching.
It's a preview, and the version says so (0.11.0-preview). The reason for the label is that the browser build doesn't run against the conformance suite the native build does, so I trust it less than the native one, and you should too.
The fixes
Most of these came out of the conformance suite, which runs dynoxide against real DynamoDB and flags where they differ. Two were actual bugs; the rest is error-message fidelity.
The first bug: a write whose secondary-index key was the wrong type, a non-scalar, or an empty string was accepted when it shouldn't have been. The base row went in, but the bad value was kept out of the index, so you'd be left with a row the table had and its index didn't, and nothing told you. It's now rejected on every write path - put, update, batch, transactional, PartiQL, import - with the message DynamoDB uses.
The second is related. A Scan or Query on a composite GSI was returning items that were missing the index sort key. Index membership was keyed on the partition key alone, so an item with the partition key but no sort key got written into the index at an empty sort-key position. DynamoDB leaves those out; that's how sparse indexes are meant to work. It's now one membership rule shared by global and local indexes, applied on every write.
The rest you'd only notice if you assert on the exact error string. Empty-string and empty-binary key values now come back as a top-level ValidationException everywhere, including inside TransactWriteItems, where a few paths were burying them in a cancellation reason instead. I checked the wording against four regions to be sure it matched, since AWS had just reworded a chunk of these. BatchWriteItem returns DynamoDB's generic The provided key element does not match the schema for a wrong-type key now, rather than PutItem's more specific message, and Query and Scan reject a couple of Select/ProjectionExpression combinations that DynamoDB rejects before it reads anything.
One change went the other way and accepted something it used to reject: { NULL: false }, now read back as { NULL: true }. That's the change I saw AWS make a couple of weeks ago, when it dropped the old true-only rule. The field was a plain boolean in the model anyway, so false was always valid input.
UpdateTable in the browser
The browser engine also got UpdateTable: add or drop a global secondary index (a new one backfills the existing rows), or change the simple settings like billing mode, table class, throughput and deletion protection. One difference from native: a new GSI comes back ACTIVE straight away instead of going through CREATING, because there's no background index build happening in a single tab. A stream-spec change is still rejected - streams aren't done in the browser yet.
What's next
This one took longer than I'd hoped - a month between releases, most of it found in odd hours, and some weeks I couldn't find any.
The next job is to run the wasm build against the conformance suite, so the browser engine is held to the same standard as the native one and I can drop the preview label. Streams in the browser come after that, once I've worked out how delivery should work with no server to push from.
The package is on npm and the source's on GitHub. If you try it and something doesn't match DynamoDB, that's worth a bug report - it's the sort of thing the suite is there to catch.