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

@mirk/store

v0.8.1

Published

Code-split key-value, collection, vector, full-text search, graph, and SQL storage ports with in-memory references and a shared-connection SQLite adapter.

Readme

@mirk/store

Code-split storage ports + source adapters under one namespace. Import the whole namespace, or just the specific subpath you need — the interface ports and their in-memory reference implementations are zero-native, and only the SQLite adapter references native bindings (as optional peers).

ESM-only (the package exposes import entry points; there is no CommonJS build).

Install

npm install @mirk/store
# Using the SQLite adapter (@mirk/store/sqlite)? Add its peer:
npm install better-sqlite3
# Optional: vec0 KNN acceleration (graceful exact-JS fallback without it)
npm install sqlite-vec

Subpaths

| Import | What it gives you | Native deps | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | | @mirk/store | the ports + their in-memory references + toAsync / toAsyncSearch, graph contract types, cosine helpers | none | | @mirk/store/kv | SyncStore port (key-value + collections), InMemoryKv, toAsync | none | | @mirk/store/vector | VectorStore port, InMemoryVectorStore, cosine helpers | none | | @mirk/store/search | SearchStore / AsyncSearchStore ports, InMemorySearchStore, toAsyncSearch, BM25-style keyword search | none | | @mirk/store/graph | graph helpers over the collection port (neighbors, traverse, traverseFrontierBatched) plus AsyncGraphTraversal for native graph adapters | none | | @mirk/store/sql | SQL adapter contract types | none | | @mirk/store/coordination | SQLite-backed async keyed coordinator with leases, renewal, fencing generations, and ownership-loss checks | better-sqlite3 (peer) | | @mirk/store/sqlite | the SQLite source adapter — one connection, .kv + .vector + .search facets | better-sqlite3 (peer), sqlite-vec (optional peer) |

Source adapters are reached only through their own subpath (e.g. /sqlite) — the root and the port subpaths never re-export them, so importing @mirk/store, /kv, /vector, /search, or /graph never drags a native binding into a consumer bundle.

Quickstart — zero native deps

The in-memory references implement the same ports as the backends, so you can build against them with nothing installed:

import { InMemoryKv, toAsync } from "@mirk/store/kv";

const kv = new InMemoryKv();
kv.set("user:1", { name: "Ada" });
kv.get<{ name: string }>("user:1"); // { name: "Ada" }
kv.keys("user:"); // ["user:1"]

// Lift any SyncStore to a Promise-returning API (one-way: sync ⊂ async):
const asyncKv = toAsync(kv);
await asyncKv.get("user:1");

Bind logical stores to one backing without exposing physical key or collection prefixes:

import { namespaceStore } from "@mirk/store";

const projects = namespaceStore(kv, "app.projects.v1");
const workspaces = namespaceStore(kv, "app.workspaces.v1");

Namespaces isolate keys and collection names while preserving the ordinary SyncStore contract.

Collections

A SyncStore is also a small document store, keyed by id:

kv.put("posts", { id: "p1", title: "Hello", pinned: true });
kv.getById("posts", "p1"); // { id: "p1", title: "Hello", pinned: true }
kv.list("posts", { where: { pinned: true }, sortBy: "title", limit: 10 });
kv.count("posts"); // 1
kv.remove("posts", "p1");

Full-text search

SearchStore indexes documents by id and returns BM25-ranked keyword matches. Use text for the single-field shorthand or fields for named columns with query-time weighting:

import { InMemorySearchStore, toAsyncSearch } from "@mirk/store/search";

const search = new InMemorySearchStore();
search.index("pages", {
  id: "a",
  fields: { title: "Opal guide", body: "plain body" },
});
search.index("pages", {
  id: "b",
  fields: { title: "plain title", body: "Opal guide" },
});
search.search("pages", "opal", { fieldWeights: { title: 4, body: 1 } }); // [a, b]

const asyncSearch = toAsyncSearch(search);
await asyncSearch.search("pages", "opal");

The first indexed document fixes a collection's field schema; later documents must use the same field names. text and fields: { text } are the same single-field schema for backwards compatibility. Remote search backends should implement AsyncSearchStore directly; local sync backends can be lifted with toAsyncSearch.

Graph helpers

@mirk/store/graph stores edges as ordinary collection records and traverses them through the existing collection port. Policy stays caller-owned through StoreFilter. Remote adapters with a real graph engine can expose AsyncGraphTraversal; traverse() and traverseFrontierBatched() delegate to that native path only for collections where canTraverseGraph(collection) is true. Otherwise traverseFrontierBatched() uses listWhereIn when available, then falls back to the load-once traversal.

import { traverse } from "@mirk/store/graph";

const hits = await traverse(asyncKv, "edges", {
  start: "node:a",
  depth: 2,
  direction: "out",
});

SQLite adapter — one connection, many capabilities

SqliteAdapter opens a single better-sqlite3 database and exposes .kv (SyncStore), .vector (VectorStore), and .search (SearchStore) facets over it:

import { SqliteAdapter } from "@mirk/store/sqlite";

// .kv and .search work immediately; vector dimensions infer on first write.
const db = new SqliteAdapter({ path: "data.db" });

db.kv.set("user:1", { name: "Ada" });

db.search.index("pages", {
  id: "intro",
  fields: { title: "Intro", body: "hello world" },
});
db.search.search("pages", "hello", { fieldWeights: { title: 4, body: 1 } });

const embedding = new Float32Array(768); // your real embedding here
const query = new Float32Array(768);
db.vector.upsert("docs", { id: "a", vector: embedding });
const results = db.vector.search("docs", query, { topK: 10 }); // ranked by cosine

db.transaction(() => {
  db.kv.set("jobs:next", 42);
  db.kv.put("audit", { id: "42", event: "allocated" });
}, "immediate");

db.close();

SqliteAdapter options

| Option | Type | Notes | | --------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | path | string | DB file path, or ":memory:". | | db | Database | Reuse an existing better-sqlite3 connection instead of opening one. | | dimensions | number | Optional embedding dimensionality. If omitted, inferred and persisted from the first vector upsert / upsertMany; search still requires known dimensions. | | forceJsCosine | boolean | Pin the exact JS-cosine path even when sqlite-vec is installed (mainly for tests). | | busyTimeoutMs | number | Bounded wait for another SQLite writer. Defaults to 30 seconds and applies to owned or caller-supplied connections. |

transaction(work, mode?) runs synchronous facet operations atomically on the adapter connection. Modes are deferred (default), immediate, and exclusive. The callback must not perform asynchronous or external work.

Vectors (Vector is a Float32Array) are stored as little-endian float32 BLOBs and ranked by exact cosine. When the optional sqlite-vec peer is installed, search is transparently accelerated by vec0 using the cosine distance metric, so rankings are identical to the JS path; without it, the exact JS-cosine fallback runs. db.vector.meta.accelerated reports which path is live.

Async coordination

@mirk/store/coordination exposes one cooperating-writer critical-section primitive for async external work that cannot live inside a synchronous SQLite transaction:

import { createSqliteCoordinator } from "@mirk/store/coordination";

const coordinator = createSqliteCoordinator({
  path: "roadmap/.locks/coordination.sqlite",
  namespace: "roadmap-specs",
});

await coordinator.runExclusive(
  "specs",
  async (guard) => {
    guard.assertOwned();
    // Read files, write Markdown, update revision state, or run git here.
    guard.assertOwned();
  },
  { waitMs: 5_000, leaseMs: 2_000, renewEveryMs: 500 }
);

Each successful owner receives a unique ownerToken, a monotonic fencingGeneration, an AbortSignal, and assertOwned(). The coordinator renews the lease while the callback runs. If the process stalls past expiry and another owner takes over, the stale guard is aborted and assertOwned() throws CoordinationOwnershipLostError. Release and renewal only affect the current owner token and generation, so a stale owner cannot delete or renew a successor lease.

The SQLite database stores lease metadata only. Acquisition, renewal, stale recovery, and release use short synchronous SQLite statements; no database transaction is held while the callback awaits. Use this to serialize cooperating writers and make ownership loss observable. It is not a transaction over Markdown files, arbitrary filesystem writes, object stores, or git commits, and it cannot roll back partial external side effects after a crash. Consumers must call assertOwned() around each external mutation phase and keep their own revision or idempotency checks where those semantics matter.

Sync by design

Embedded backends are synchronousbetter-sqlite3 is synchronous, and an async-everywhere interface would tax every local call with a Promise it doesn't need. A SyncStore lifts to an async API via toAsync(store), and a SearchStore lifts via toAsyncSearch(search); the reverse is impossible. Pick sync for embedded/local, and reach for async only where a remote backend genuinely requires it.

License

Apache-2.0. See LICENSE.