A PouchDB storage adapter backed by Deno KV.
Cushion implements the PouchDB adapter interface directly against Deno KV. It gets you a real PouchDB: rev trees, conflict resolution, map/reduce views, and replication, backed by Deno's built-in database.
Quick start
import { PouchDB } from "jsr:@mikehall314/cushion"; const db = new PouchDB("my-app", { adapter: "cushion" }); await db.put({ _id: "hello", msg: "world" }); const doc = await db.get("hello");
Documents
Revisions are tracked automatically, giving you optimistic concurrency for free: if two writers update the same document concurrently, the loser gets a 409 and should re-read and retry.
async function increment(id: string) { const doc = await db.get<{ count: number }>(id); try { return await db.put({ ...doc, count: doc.count + 1 }); } catch (err) { // someone else wrote first -- reread and retry } } }
Views
Two helpers to make the map-reduce API a little more ergonomic to work with.
withEmithelps making a map function with type safetyViewQueryis a query builder fordb.query()
import { PouchDB, ViewQuery, withEmit } from "jsr:@mikehall314/cushion"; type Product = { _id: string; category: string; price: number; qty: number }; const db = new PouchDB("shop", { adapter: "cushion" }); await db.put({ _id: "_design/stock", views: { "value-by-category": { map: withEmit<Product, number>((emit) => (doc) => { emit(doc.category, doc.price * doc.qty); }), reduce: "_sum", }, }, }); // Total stock value per category const query = ViewQuery.for("stock", "value-by-category").group(true); const result = await db.query(...query.getParams()); for (const row of result.rows) { console.log(row.key, row.value); }
ViewQuery covers the usual PouchDB query shapes:
ViewQuery.for("stock", "value-by-category"); // full scan ViewQuery.for("stock", "value-by-category").key("produce"); // single key ViewQuery.for("stock", "value-by-category").keys(["produce", "dairy"]); // specific keys ViewQuery.for("stock", "value-by-category").range("bakery", "dairy", ViewQuery.INCLUDE_END); // key range ViewQuery.for("stock", "value-by-category").order(ViewQuery.DESCENDING); // sort order ViewQuery.for("stock", "value-by-category").limit(10).skip(20); // pagination ViewQuery.for("stock", "value-by-category").includeDocs(); // inline full documents ViewQuery.for("stock", "value-by-category").reduce().group(true); // reduce, grouped by full key ViewQuery.for("stock", "value-by-category").reduce().group(1); // reduce, grouped by first key part
getParams() returns a [viewName, params] tuple matching db.query()'s
signature, so use spread: db.query(...query.getParams()) or if you really want
to you can use const [viewName, params] = query.getParams();
Custom configuration
The default PouchDB export is bound to Deno's default KV, opened lazily.
For a specific Deno.Kv instance or file path (for tests, multiple independent
stores, an in-memory database, whatever) build can your own chain with init()
instead:
import PouchDBCore from "npm:pouchdb-core"; import mapreduce from "npm:pouchdb-mapreduce"; import replication from "npm:pouchdb-replication"; import { init } from "jsr:@mikehall314/cushion/plugin"; // Use an existing Deno.Kv instance const kv = await Deno.openKv(":memory:"); const PouchDB = PouchDBCore .plugin(mapreduce) .plugin(replication) .plugin(init({ kv })); const db = new PouchDB("mydb", { adapter: "cushion" });
init() only registers the adapter -- add mapreduce/replication yourself
if you want views or sync, same as any other PouchDB plugin.
Limits and caveats
Deno KV imposes a 64 KiB limit per value, so a single document revision (or attachment) larger than that will fail to write. Deno KV atomic operations are also capped at 1000 mutations and roughly 800 KiB total; cushion chunks large bulkDocs batches across multiple atomic commits, which means a very large batch is not atomic as a whole. Batches under ~100 document writes commit in one atomic operation.
Writes within a process are serialised, and cross-process writers are handled with an optimistic versionstamp check and retry (3 attempts). Under sustained contention from many processes a write can fail with a 409 after retries are exhausted.
Compaction removes old revision bodies but does not garbage-collect attachment
data, since attachments are stored by digest and may be shared across revisions.
db.close() does not close the underlying KV handle because it may be shared by
other databases; close the handle yourself if you opened it, or let the process
exit.
Development
deno task test
License
MIT