npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openmaic/storage

v0.2.2

Published

The MAIC pluggable persistence layer: document / runtime / KV / asset primitives with browser and HTTP backends, depending only on @openmaic/dsl.

Readme

@openmaic/storage

The MAIC pluggable persistence layer: small, swappable-backend primitives for persisting app state, depending only on @openmaic/dsl.

The DSL owns what persists (document / runtime shape + validation + migration + the asset StorageProvider interface). This package owns where / how it persists — the primitives and their backends. The pluggable seam is the backend, not the database driver: browser backends (the zero-server clone-and-run default), HTTP clients plus a reference server, and PostgreSQL server backends.

Dependency arrow (acyclic)

@openmaic/storage -> @openmaic/dsl

No dependency on React, zustand, or any host app. Backends take their Storage / IDBFactory by injection, so the package is app-agnostic and testable without a browser.

What's in here

| Export | Role | Browser backend | | --- | --- | --- | | KVStore | small device / account-scoped values not owned by the DSL | BrowserKVStore over localStorage | | StorageProvider (from @openmaic/dsl) | the asset seam: put(blob) → ref, resolve(ref) → url, remove(ref) | BrowserAssetStore over IndexedDB (assets registry + blobs) + object URLs | | kvPersistStorage | adapt a KVStore into a zustand persist storage | — | | DocumentStore | persist the DSL document aggregate (stage + scenes + embedded agents / quiz / actions + an outline snapshot) | BrowserDocumentStore over IndexedDB (normalized stages / scenes / outlines) | | RuntimeStore | persist what a learner produces while taking a course — sessions + append-only records (chat, quiz attempts, playback facts) | BrowserRuntimeStore over IndexedDB (sessions / records) |

  • Scopes. account values are user data a server-backed deployment syncs across devices; device values (theme, locale, layout) never leave the device — every backend honours that, so the scope is part of the primitive, not the backend choice. The KV HTTP contract is account-only and carries no scope on the wire at all, so HttpKVStore routes device to a LocalKVStore it requires at construction — a branded local backend, because a networked store satisfies plain KVStore structurally and would otherwise be accepted as the place device values live.
  • The asset pool. BrowserAssetStore is a global asset pool (#1007): an allocated AssetId (ast_ + 128 random bits) names a registry entry (contentHash, mime, meta), and the registry names content-addressed bytes. A document embeds only the id and the store resolves it to a URL at render time (a raw URL would bake in a provider + expiry and break portability). Two levels of indirection buy three things at once: an id survives the bytes behind it being regenerated through BrowserAssetStore.replace; identical bytes are stored once however many ids name them; and the content hash never leaves the package, so the "whoever knows the hash can reach the bytes" threat that pure content-addressing must defend against does not arise. Images, audio and video share one id space — the medium is a mime column, not a partition. put always allocates a new id, so its successful return values and branches do not reveal whether the bytes were already present. The browser registry embeds its blobs table in the same database because reference counting, byte writes, and reclamation must share one transaction. This is not a replaceable browser-side blob backend; a replaceable blob interface is a server-backend concern (delivery plan part 4), where consistency is enforced server-side. Resource-accounting channels remain: quota errors, storage estimates, and server billing or metering can disclose existence, so server deployments must budget them per principal. Object URLs are minted per id, not shared per contentHash: sharing would let a holder of two ids learn that their bytes match by comparing URL strings. Each replace(id, ...) followed by resolve(id) adds one retired snapshot that only release(id) or close() reclaims; each ref retains at most one current snapshot plus that retired history. A returned URL is an immutable snapshot: mutations affect future resolutions but never revoke a URL already issued by this or another store instance. Application code that constructs a concrete BrowserAssetStore owns that lifecycle (the narrower DSL StorageProvider seam exposes neither method), and media-heavy applications should reclaim snapshots explicitly. release is an owner-level escape hatch for a caller that owns every use of every URL returned for that id in the instance; close reclaims the whole instance. Cross-instance correctness comes from comparing the registry identity on every resolve, so a remove yields null and a replacement yields a fresh URL on the next call without reclaiming older snapshots. The id domain is opaque and unvalidated (the KV key-domain lesson, applied forward): an unrecognized id is a miss, never an error. The server backend is still to come.
  • Document normalization. The DSL document is a portable embedded aggregate; DocumentStore normalizes it into per-entity rows so scene-level writes (putScene) stay cheap, and reassembles it on read. Each document is stamped with a dslVersion; reads run the DSL migration ladder forward, and writes are validated against the DSL gate (validateStage / validateScene) so schema drift fails loud. The outline is an opaque, app-owned snapshot carried alongside — persisted verbatim, neither validated nor migrated.
  • Generic over scene type. DocumentStore<TScene> defaults to the DSL Scene (universal slide / quiz). An app that widens Scene with its own kinds (interactive / pbl, content the DSL does not own) parameterizes the store over its scene union and injects a matching validateScene, so those scenes persist and the gate stays fail-loud for the app's shapes.
  • Runtime layer. RuntimeStore is partitioned by (stageId, learnerKey): a stage has many sessions — one or more per learner — so every listing is partition-scoped (there is deliberately no global listing; single-session operations are id-keyed, and mergeLearner is the one deliberate cross-stage sweep). Sessions are born stamped: the store writes runtimeDslVersion itself at createSession, and the runtime line has no unversioned epoch, so an unstamped row fails loud instead of being lifted like a legacy document. Records are append-only ordered facts under an active session; the store assigns the per-session monotonic seq on append — the sole replay ordering key, never timestamps. Record payloads are gated per kind by injectable validators, defaulting to the DSL skeleton guards for chat / quizAttempt (playback and app-defined kinds carry app-owned payloads). mergeLearner re-keys an anonymous learner's sessions to a signed-in key across all stages; deleteLearnerRuntime cascades one learner's sessions + records on one stage, and deleteStageRuntime clears a whole stage — the hook a document deletion cascades through.
  • deleteAllRuntime clears every runtime session and record for explicit whole-cache reset flows.

Upgrading from 0.1.x

Version 0.2.0 removes BrowserAssetProvider outright; it no longer ships. The asset API is now BrowserAssetStore, whose refs are allocated ids and whose data lives in the new maic-asset-pool database.

Reusing a custom dbName created by a 0.1.x provider raises an explicit legacy-schema error rather than corrupting data or operating only partially.

  • BrowserAssetStore deliberately does not read data written by the 0.1.x provider in maic-assets. Its content-addressed sha256- refs are no longer outward references, and the contract suite pins sha256-shaped refs as misses; a silent read-through would restore the reference model this release removes.
  • If persisted 0.1.x data must be carried forward, open an issue. The supported shape is an explicit one-time import helper that enumerates old rows, allocates an id per blob, and returns an old-ref-to-new-id mapping for the caller to apply to its documents.

Backend equivalence

Each primitive has one implementation-agnostic contract suite (test/kv-contract.ts, test/asset-contract.ts, test/document-contract.ts, test/runtime-contract.ts). Every backend is proven by running the same suite against it, so browser, HTTP, and PostgreSQL implementations cannot silently diverge from a primitive's semantics. Assets use the single test/asset-contract.ts suite for the allocated-id store: identical bytes never share a caller-visible id. Asset backends must let the suite temporarily instrument the production allocation source while constructing the store through the same factory used by every other contract test. This proves that every successful put consumes exactly one allocator output, independent of whether the bytes already existed, without adding a caller-configurable allocation path.

Roadmap

  • [x] KVStore + browser backend; zustand persist adapter
  • [x] StorageProvider (in @openmaic/dsl) + browser asset registry: allocated AssetId over an embedded byte table in BrowserAssetStore (#1007)
  • [x] implementation-agnostic contract suites
  • [x] DocumentStore (aggregate ↔ normalized adapter, migrate-on-read via the DSL migration registry, validation gate) + browser backend
  • [x] RuntimeStore (sessions + append-only records, runtime version line, per-kind payload gate) + browser backend
  • [x] wire the app's settings + user-profile persist stores through KVStore (both account scope). No automatic migration of pre-cutover data: new data persists through KVStore, legacy localStorage keys are ignored (not migrated) and best-effort purged, and a user reconfigures once on upgrade
  • [ ] wire the app's third persist store (agent-registry-storage), still on zustand's default localStorage
  • [ ] wire the app's remaining ad-hoc localStorage keys through KVStore
  • [ ] a hydration gate the app actually consumes — required before an account scope can be served remotely. With the browser backend, hydration resolves within microtasks of module evaluation and nothing observes it; a network round trip makes the gap visible, and the one-shot decisions taken against a not-yet-hydrated store (classroom agent-selection restore, media orchestration, scene-generator retry, server-provider reconcile) decide wrongly and then have their corrective writes refused
  • [x] RuntimeStore HTTP backend + reference server + HTTP contract
  • [x] RuntimeStore PostgreSQL backend
  • [x] DocumentStore HTTP backend + reference-server routes + HTTP contract
  • [x] DocumentStore PostgreSQL backend
  • [x] KVStore (account) HTTP backend + HTTP contract
  • [ ] KVStore server-side reference backend and reference-server route
  • [ ] asset server backend — registry (principal column, server-derived) + replaceable blob storage, over the global resource pool model (#1007), with consistency enforced server-side. It must validate or allowlist content types before serving bytes rather than reflecting cross-principal metadata into response content types
  • [ ] asset manifest: the one enumeration of "which AssetIds does this course reference?" the export paths converge on (#1007)

License

MIT