@dynoxide/wasm-engine
v1.2.1
Published
The Dynoxide DynamoDB emulator compiled to WebAssembly, with a Web Worker client for running DynamoDB operations in the browser.
Maintainers
Readme
@dynoxide/wasm-engine
The dynoxide DynamoDB-compatible engine, compiled to WebAssembly and packaged to run in the browser. It carries the engine .wasm, the official @sqlite.org/sqlite-wasm SQLite build, the bundled Web Worker, and an EngineClient that drives them, so you can run real DynamoDB operations client-side against a SQLite-backed store persisted to OPFS.
The wasm build is a scored target in the same conformance suite that backs dynoxide's native build; current results are published with every other target. It leaves several operations unimplemented - TransactWriteItems, streams, tags and TTL - so it is not a like-for-like replacement for the native engine.
Install
npm install @dynoxide/wasm-engineThe package is versioned in lockstep with every other installable dynoxide artefact, on the product version stream; the dynoxide-rs crate carries its own number and is not part of that lockstep. See docs/versioning.md for what each number promises and what the conformance suite does and does not exercise. npm install @dynoxide/wasm-engine gets the current release; use ~1.2.0 to hold behaviour still across minors.
Quick start
import { EngineClient } from "@dynoxide/wasm-engine";
const client = new EngineClient();
await client.ready();
await client.execute("CreateTable", {
TableName: "Music",
KeySchema: [
{ AttributeName: "artist", KeyType: "HASH" },
{ AttributeName: "song", KeyType: "RANGE" },
],
AttributeDefinitions: [
{ AttributeName: "artist", AttributeType: "S" },
{ AttributeName: "song", AttributeType: "S" },
],
BillingMode: "PAY_PER_REQUEST",
});
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 the operation name and a plain DynamoDB-JSON request, resolves with the response, and rejects with a typed EngineError. The client runs the engine in a Web Worker (OPFS's synchronous file handles are Worker-only) and owns the message round-trip, so you deal in objects rather than postMessage envelopes.
API
new EngineClient(opts?)- boots the engine eagerly. Calls queue behind boot, so you can issue work beforeready()resolves.await client.ready()- resolves with the boot descriptor ({ contractVersion, capabilities, persistenceMode }).client.execute(op, request)- run one operation.client.supports(op)- whether the engine implements an operation, for capability-gating your UI.client.persistent-truewhen the session persists across reloads (OPFS),falsein the in-memory fallback.client.terminate()- tear the Worker down and reject any in-flight calls.
The package also exports EngineError (the engine's __type is on .type) and CONTRACT_VERSION. TypeScript declarations ship with it, so the client and its boot descriptor are typed out of the box.
Where the assets live
The Worker and the two .wasm travel inside the package. new EngineClient() with no arguments resolves the Worker next to this module, and the Worker resolves its .wasm next to itself, so a bundler that copies the package's files - or a plain static deploy of them - needs no configuration.
Serving the assets from a CDN or a different origin? Two options:
assetBase- the directory the assets sit in, e.g.new EngineClient({ assetBase: "https://cdn.example.com/dynoxide/" }).workerUrl- the exact Worker URL, if it doesn't sit beside its.wasmunder a shared base.
If you'd rather construct the Worker yourself - to let a bundler handle it, say - the package exposes it at the ./worker subpath. Build it and hand it back through createWorker:
new EngineClient({
createWorker: () =>
new Worker(new URL("@dynoxide/wasm-engine/worker", import.meta.url), { type: "module" }),
});Two clients on one page are fine: each gets its own storage pool, keyed on name (default dynoxide.db), so give them distinct names if both should persist.
Hosting
The engine needs a secure context (HTTPS, or localhost for development) for OPFS, but no COOP/COEP cross-origin isolation. A Content-Security-Policy must allow 'wasm-unsafe-eval', or the engine won't instantiate. Serve the .wasm as application/wasm, and serve the assets gzip- or brotli-encoded - the wasm and the Worker both more than halve over the wire.
Versioning
CONTRACT_VERSION stamps the message-envelope shape, not the engine version. Adding an operation leaves it alone; changing a request, response, or error envelope bumps it. The client validates it against the engine on boot and fails loudly on a mismatch, so a pinned consumer fails with a clear error rather than mis-reading a newer engine. The shipped engine and contract versions sit in manifest.json. manifest.engineVersion is the dynoxide product version, and the npm package ships at that same version: every installable artefact shares that number, while the dynoxide-rs crate is on its own. See docs/versioning.md.
Persistence
State persists across reloads via OPFS. Where OPFS synchronous access handles are unavailable (Firefox private windows, older Safari), the client falls back to an in-memory session and reports it through persistent / persistenceMode rather than failing.
Licence
MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
