mostard-qwik-browserdb
v0.1.0
Published
Initialization-only storage selection per data type with XState and Qwik
Readme
mostard-qwik-browserdb
XState orchestration for choosing persistent storage once at initialization. Applications supply ordered adapters for each data type; this package has no Nostr dependency.
Initialization-only selection
import {
createStorageSelection,
openLocalStorageBackend,
} from "mostard-qwik-browserdb";
import { probeLocalStorage } from "mostard-qwik-browserdb/environment";
const storage = createStorageSelection({
optional: true,
candidates: [
{
id: "localstorage",
eligible: probeLocalStorage,
async open({ signal }) {
const value = await openLocalStorageBackend<{
id: string;
text: string;
}>({
namespace: "my-drafts-v1",
signal,
});
return { value, close: () => value.close() };
},
},
],
});
await storage.ready(); // undefined if persistence is disabled
if (storage.getState().persistent) {
await storage.run((db) => db.put({ id: "draft", text: "Hello" }));
}
await storage.close();Use independent selections for event caches and local user state. Mostard uses IndexedDB → durable Turso WASM → disabled for events, and IndexedDB → localStorage → unsaved for drafts/read markers. Configure native SQLite or other platform adapters at the same seam; browserdb does not implement Expo/native storage.
eligible() returns true, false, or undefined. False skips an adapter; unknown attempts the real open. A successful open is the evidence of availability. The environment package supplies probes, not persistence policy. /environment is an optional integration and requires mostard-qwik-environment.
For IndexedDB adapters that already open a real database, use indexedDBEligibility from /environment to check API presence and respect hasIndexedDB overrides without opening a throwaway probe database. Its true result is only an eligibility hint. probeIndexedDB remains available when a caller needs the environment's memoized open/delete capability probe.
Startup failures with codes Unavailable, QuotaExceeded, or Blocked permit trying the next adapter. Adapter codes StorageUnavailable and DatabaseInUse map to Unavailable and Blocked. Validation, schema errors and unexpected failures stop selection. optional: true disables persistence only when eligible choices are exhausted; required selection rejects. No memory fallback is created.
Each candidate has a bounded startup timeout (default 5 seconds), including its probe. Timed-out/cancelled openers receive an abort signal; handles arriving late are closed. Adapter implementations must release partially acquired resources if they throw. Runtime operations are serialized by run(). A failure sets degraded while retaining the selected handle; a successful operation on that same handle clears the error. There is no runtime downgrade, reopen, or backend switching. Close drains accepted operations and rejects new ones, or cancels startup and closes late handles. Observer exceptions cannot interrupt operation notifications.
stateDiagram-v2
[*] --> opening
opening --> opening: startup unavailable / next candidate
opening --> ready: opened
opening --> disabled: optional exhausted
opening --> error: fatal or required exhausted
ready --> degraded: operation failed
degraded --> ready: same backend operation succeeded
opening --> closing: cancel
ready --> closing: drain and close
degraded --> closing: drain and close
disabled --> closing
error --> closing
closing --> closedRecord adapters
openIndexedBackend() provides committed IndexedDB CRUD with application indexes, injected factory and abort signal. openLocalStorageBackend() provides small JSON records, one namespaced key per record, with a real write/read/delete probe. It is unsuitable for Nostr events or large media. It does not provide indexes or atomic predicate deletion; removeWhere() rejects rather than pretending to provide a transaction. Failed parsing and quota/access errors propagate. localStorage read-modify-write is not atomic across tabs; applications should use independent/idempotent records for append-only facts. Concurrent edits to the same draft use the last committed write.
Compatibility API
createBrowserDB() and the existing Qwik hook remain for older generic record consumers. Their explicit memory/recovery policies are legacy behavior, independent of createStorageSelection(). New per-data-type routing should use the selector. Existing inspector diagrams describe that older controller, not the selector above.
Verification
Run bun test __tests__, bun run typecheck, and bun run build. The selection tests exercise startup eligibility, typed failure classification, optional disablement, sticky runtime selection, close draining and late-open cleanup. Existing tests cover the compatibility controller.
