Stow S3
Browser storageView source

Binary and worker runtime

The v2 bridge sends JSON metadata and optional Uint8Array bytes separately. Its response is { metadata: string, data?: Uint8Array }. The metadata envelope keeps v1 operation results and structured errors, without object payloads. Go uses CopyBytesToGo and CopyBytesToJS. This avoids base64 encoding but still copies bytes across WASM memory. The v1 JSON/base64 bridge remains available.

EmbeddedStow automatically chooses binary calls when the host provides callBinary. Its methods remain synchronous. AsyncEmbeddedStow uses the same response validation over an asynchronous binary host.

import { AsyncEmbeddedStow } from "@chester-hill-solutions/stow-s3/worker";
import { loadBrowserWasmHost } from "@chester-hill-solutions/stow-s3/browser-wasm";

const host = loadBrowserWasmHost({
  maxInFlight: 4,
  maxInFlightBytes: 64 * 1024 * 1024,
  maxResponseBytes: 64 * 1024 * 1024,
});
const runtime = await AsyncEmbeddedStow.open(host);
try {
  await runtime.createBucket("assets");
  const bytes = new Uint8Array([0, 128, 255]);
  await runtime.putObject("assets", "example.bin", bytes);
  const object = await runtime.getObject("assets", "example.bin");
} finally {
  await runtime.close();
  host.close();
}

The default worker hosts a memory runtime. loadBrowserWasmBridge() instead loads Go in the current realm, allowing a custom worker module to instantiate its own storage adapter. loadBrowserWasmHost({ workerUrl }) connects to such a module. serveEmbeddedWorker(endpoint, ready) accepts a Promise for an async binary host and admits bounded concurrent operations after initialization. The Go runtime and the persistence adapter govern their ordering. Storage functions must run in that worker; functions cannot be passed through postMessage.

The worker and the application share no object buffers by default. Posting an input copies it and leaves the caller's buffer usable. Passing { transfer: true } to putObject deliberately detaches the input's entire ordinary ArrayBuffer; partial views and shared buffers are rejected. Worker response buffers transfer to the caller. They are independent of the runtime's stored bytes.

Admission limits count requests and the UTF-8 metadata plus input bytes. Calls over either limit reject with backpressure; there is no unbounded client queue. Responses include metadata in their byte limit. Server admission and response limits also default to four calls and 64 MiB. A custom worker should configure corresponding limits when the client changes them.

Cancellation rejects the caller immediately. It prevents queued execution when the worker observes it before dispatch. A started operation can still commit: the caller must reconcile uncertain writes. The cancelled call keeps its admission allocation until its response arrives, so repeatedly cancelling does not bypass the memory bound. Both client and server reserve one additional cleanup call with at most 4 KiB of metadata and no input payload for stream abort, reader close, or runtime close. Closing the host rejects pending calls; the browser host also terminates its worker. Closing an individual runtime first permits ordinary cleanup.

The persistent worker opens IndexedDB, the content store, its bounded cache and the Go runtime:

const host = loadBrowserPersistentWasmHost();
const runtime = await AsyncEmbeddedStow.openPersistent(host, {
  namespace: "design-project",
  databaseName: "my-design-tool",
  cacheBytes: 8 * 1024 * 1024,
  physicalQuotaBytes: 1024 * 1024 * 1024,
  maxBytes: 1024 * 1024 * 1024,
});
await runtime.putObjectStream("assets", "image.bin", inputChunks, {
  size: inputByteLength,
  ifNoneMatch: "*",
});
const opened = await runtime.getObjectStream("assets", "image.bin");
try {
  for await (const chunk of opened.chunks) consume(chunk);
} finally {
  await opened.close();
}

Import loadBrowserPersistentWasmHost from the same browser-wasm subpath. Streams use at most 256 KiB per bridge message. Streaming puts require the exact size up front, reserve runtime quotas, and publish only after a successful commit. Their bounded internal copies leave source buffers usable. A streaming source should itself produce bounded chunks. Input chunks must remain unchanged until consumed. Cancellation or source failure aborts the staged publication; cleanup can be unsuccessful after transport failure, so closing the runtime remains the fallback. A commit whose reply is lost can still have published.

A streamed read pins the selected immutable content. Later replacements do not change its bytes, and garbage collection retains its content until close. The consumer must close an opened stream it never iterates. Iteration closes on completion, error, or early exit. Each Go read rechecks authorization.

The persistent worker supports storage maintenance when capabilities().hostMaintenance === true. Memory and custom hosts return unsupported_backend unless they implement that capability. These namespace administration operations are separate from Go object authorization.

const recovery = await runtime.inspectRecovery();
// Show recovery.stages and recovery.protections without loading object payloads.
await runtime.discardAbandonedStage(abandonedStageId);
const usage = await runtime.collectGarbage();

Forced worker termination can leave unpublished staged chunks protected on disk. After reopening the namespace, inspect recovery metadata and explicitly choose an abandoned stage to discard. Discarding an active write's stage fails with stage_active. Releasing an abandoned stage does not publish a partial object. Collection then removes unreachable records and chunks, preserving published heads, retained revisions, active streams, and durable protections. It clears the worker's content cache and returns physical storage accounting. Inspection and collection read metadata; they do not materialize object payloads. Closing the runtime disables maintenance along with its object operations.

getObject remains a complete-object convenience operation and is subject to the response limit. getObjectRange(bucket, key, { offset, length }) reads at most 256 KiB per call. It returns the object's metadata with the requested bytes; size describes the entire object. A range bounds read memory, but separate range calls can read different versions during concurrent replacement. Callers must coordinate their selected revision.

The Node host exposes callBinaryAsync and openHost. Host-backed handles must use asynchronous dispatch because persistence may await JavaScript promises; synchronous dispatch rejects those handles rather than blocking the event loop.