@noy-db/to-memory 0.2.0-pre.2 → 0.2.0-pre.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,8 +24,10 @@ __export(index_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
26
  var import_hub = require("@noy-db/hub");
27
- function memory() {
27
+ function memory(opts = {}) {
28
28
  const store = /* @__PURE__ */ new Map();
29
+ const epsilon = opts.clockUncertainty ?? 0;
30
+ let clock = 0;
29
31
  function getCollection(vault, collection) {
30
32
  let comp = store.get(vault);
31
33
  if (!comp) {
@@ -41,6 +43,21 @@ function memory() {
41
43
  }
42
44
  return {
43
45
  name: "memory",
46
+ // #321 — memory's synchronous Map ops make the expectedVersion check +
47
+ // write atomic, so it can back `vault.sequence()`. Without this the
48
+ // JSDoc's `casAtomic: true` promise was undefined at runtime and
49
+ // `sequence().next()` threw SequenceOfflineError.
50
+ capabilities: {
51
+ casAtomic: true,
52
+ serverWriteTime: true,
53
+ auth: { kind: "none", required: false, flow: "static" }
54
+ },
55
+ // #322-sibling — authoritative store clock for deferred numbering. A
56
+ // single-process counter is perfectly monotonic; ε widens the interval.
57
+ async getStoreTime() {
58
+ const now = ++clock;
59
+ return { earliest: now - epsilon, latest: now + epsilon };
60
+ },
44
61
  async get(vault, collection, id) {
45
62
  return store.get(vault)?.get(collection)?.get(id) ?? null;
46
63
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-memory** — in-memory store for NOYDB (testing and development).\n *\n * Backed by nested `Map` objects: `vault → collection → id → envelope`.\n * Data is lost when the process exits — this store is intentionally\n * non-persistent.\n *\n * ## When to use\n *\n * - **Unit tests** — fast, zero I/O, works in any environment.\n * - **In-memory caching layer** — pair with `routeStore` to keep a hot\n * copy of frequently-read collections in memory while persisting to\n * a durable backend.\n * - **REPL / prototyping** — explore the NOYDB API without setting up\n * a backend.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — Map operations are synchronous and inherently atomic |\n * | `txAtomic` | `true` — multi-record `tx()` applies every op in a single synchronous burst |\n * | `listVaults` | ✓ — iterates outer Map keys |\n * | `listPage` | ✓ — cursor-based pagination over sorted id list |\n * | `ping` | ✓ — always returns `true` |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot, TxOp } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Create an in-memory adapter backed by nested Maps.\n * No persistence — data is lost when the process exits.\n * Intended for testing and development.\n */\nexport function memory(): NoydbStore {\n // vault -> collection -> id -> envelope\n const store = new Map<string, Map<string, Map<string, EncryptedEnvelope>>>()\n\n function getCollection(vault: string, collection: string): Map<string, EncryptedEnvelope> {\n let comp = store.get(vault)\n if (!comp) {\n comp = new Map()\n store.set(vault, comp)\n }\n let coll = comp.get(collection)\n if (!coll) {\n coll = new Map()\n comp.set(collection, coll)\n }\n return coll\n }\n\n return {\n name: 'memory',\n\n async get(vault, collection, id) {\n return store.get(vault)?.get(collection)?.get(id) ?? null\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const coll = getCollection(vault, collection)\n const existing = coll.get(id)\n\n if (expectedVersion !== undefined && existing) {\n if (existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n coll.set(id, envelope)\n },\n\n async delete(vault, collection, id) {\n store.get(vault)?.get(collection)?.delete(id)\n },\n\n async list(vault, collection) {\n const coll = store.get(vault)?.get(collection)\n return coll ? [...coll.keys()] : []\n },\n\n async loadAll(vault) {\n const comp = store.get(vault)\n const snapshot: VaultSnapshot = {}\n if (comp) {\n for (const [collName, coll] of comp) {\n if (collName.startsWith('_')) continue\n const records: Record<string, EncryptedEnvelope> = {}\n for (const [id, envelope] of coll) {\n records[id] = envelope\n }\n snapshot[collName] = records\n }\n }\n return snapshot\n },\n\n async saveAll(vault, data) {\n const comp = store.get(vault)\n if (comp) {\n for (const key of [...comp.keys()]) {\n if (!key.startsWith('_')) {\n comp.delete(key)\n }\n }\n }\n\n for (const [collName, records] of Object.entries(data)) {\n const coll = getCollection(vault, collName)\n for (const [id, envelope] of Object.entries(records)) {\n coll.set(id, envelope)\n }\n }\n },\n\n async ping() {\n return true\n },\n\n /**\n * Enumerate every top-level vault held by this in-memory\n * store. Used by `Noydb.listAccessibleVaults()`\n * to get the universe of compartments before filtering down to\n * the ones the calling principal can unwrap.\n *\n * Returns the outer Map's keys directly — O(compartments) and\n * cheap. The result is intentionally unsorted; consumers that\n * want a stable order should sort themselves.\n */\n async listVaults() {\n return [...store.keys()]\n },\n\n /**\n * Multi-record atomic transaction.\n *\n * Validates every op's `expectedVersion` against the current Map\n * state, throws `ConflictError` on the first mismatch (nothing\n * written), then applies every put/delete in one synchronous burst\n * — the Map mutations are single-threaded in the JS event loop so\n * no concurrent writer can interleave. Truly atomic.\n */\n async tx(ops: readonly TxOp[]) {\n // Phase 1 — validate every expectedVersion against current state.\n // We read the state ONCE up front; subsequent ops that target the\n // same (vault, coll, id) see the same snapshot, matching the\n // atomicity guarantee callers expect from a storage-layer tx.\n for (const op of ops) {\n if (op.expectedVersion === undefined) continue\n const existing = store.get(op.vault)?.get(op.collection)?.get(op.id)\n const actual = existing?._v ?? 0\n if (actual !== op.expectedVersion) {\n throw new ConflictError(\n actual,\n `tx: ${op.vault}/${op.collection}/${op.id} expected v${op.expectedVersion}, found v${actual}`,\n )\n }\n }\n // Phase 2 — apply every op synchronously. No await between ops =\n // no interleave window.\n for (const op of ops) {\n if (op.type === 'put') {\n if (!op.envelope) {\n throw new Error(`tx: put op for ${op.id} is missing envelope`)\n }\n getCollection(op.vault, op.collection).set(op.id, op.envelope)\n } else {\n store.get(op.vault)?.get(op.collection)?.delete(op.id)\n }\n }\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted id list — same ordering on every call so pages are\n * stable across runs.\n *\n * The default `limit` is 100. Final page returns `nextCursor: null`.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const coll = store.get(vault)?.get(collection)\n if (!coll) return { items: [], nextCursor: null }\n\n // Sorted ids for stable pagination — Map preserves insertion order\n // but tests rely on lexicographic order across different inserts.\n const ids = [...coll.keys()].sort()\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, ids.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const id = ids[i]!\n const envelope = coll.get(id)\n if (envelope) items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < ids.length ? String(end) : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-memory** — in-memory store for NOYDB (testing and development).\n *\n * Backed by nested `Map` objects: `vault → collection → id → envelope`.\n * Data is lost when the process exits — this store is intentionally\n * non-persistent.\n *\n * ## When to use\n *\n * - **Unit tests** — fast, zero I/O, works in any environment.\n * - **In-memory caching layer** — pair with `routeStore` to keep a hot\n * copy of frequently-read collections in memory while persisting to\n * a durable backend.\n * - **REPL / prototyping** — explore the NOYDB API without setting up\n * a backend.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — Map operations are synchronous and inherently atomic |\n * | `txAtomic` | `true` — multi-record `tx()` applies every op in a single synchronous burst |\n * | `listVaults` | ✓ — iterates outer Map keys |\n * | `listPage` | ✓ — cursor-based pagination over sorted id list |\n * | `ping` | ✓ — always returns `true` |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot, TxOp } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Create an in-memory adapter backed by nested Maps.\n * No persistence — data is lost when the process exits.\n * Intended for testing and development.\n */\nexport function memory(opts: { clockUncertainty?: number } = {}): NoydbStore {\n // vault -> collection -> id -> envelope\n const store = new Map<string, Map<string, Map<string, EncryptedEnvelope>>>()\n\n // Monotonic store clock — a single-process counter is perfectly ordered.\n // `clockUncertainty` (ε) widens the returned interval to exercise the\n // commit-wait path in deferred numbering; default 0 (exact).\n const epsilon = opts.clockUncertainty ?? 0\n let clock = 0\n\n function getCollection(vault: string, collection: string): Map<string, EncryptedEnvelope> {\n let comp = store.get(vault)\n if (!comp) {\n comp = new Map()\n store.set(vault, comp)\n }\n let coll = comp.get(collection)\n if (!coll) {\n coll = new Map()\n comp.set(collection, coll)\n }\n return coll\n }\n\n return {\n name: 'memory',\n\n // #321 — memory's synchronous Map ops make the expectedVersion check +\n // write atomic, so it can back `vault.sequence()`. Without this the\n // JSDoc's `casAtomic: true` promise was undefined at runtime and\n // `sequence().next()` threw SequenceOfflineError.\n capabilities: {\n casAtomic: true,\n serverWriteTime: true,\n auth: { kind: 'none', required: false, flow: 'static' },\n },\n\n // #322-sibling — authoritative store clock for deferred numbering. A\n // single-process counter is perfectly monotonic; ε widens the interval.\n async getStoreTime() {\n const now = ++clock\n return { earliest: now - epsilon, latest: now + epsilon }\n },\n\n async get(vault, collection, id) {\n return store.get(vault)?.get(collection)?.get(id) ?? null\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const coll = getCollection(vault, collection)\n const existing = coll.get(id)\n\n if (expectedVersion !== undefined && existing) {\n if (existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n coll.set(id, envelope)\n },\n\n async delete(vault, collection, id) {\n store.get(vault)?.get(collection)?.delete(id)\n },\n\n async list(vault, collection) {\n const coll = store.get(vault)?.get(collection)\n return coll ? [...coll.keys()] : []\n },\n\n async loadAll(vault) {\n const comp = store.get(vault)\n const snapshot: VaultSnapshot = {}\n if (comp) {\n for (const [collName, coll] of comp) {\n if (collName.startsWith('_')) continue\n const records: Record<string, EncryptedEnvelope> = {}\n for (const [id, envelope] of coll) {\n records[id] = envelope\n }\n snapshot[collName] = records\n }\n }\n return snapshot\n },\n\n async saveAll(vault, data) {\n const comp = store.get(vault)\n if (comp) {\n for (const key of [...comp.keys()]) {\n if (!key.startsWith('_')) {\n comp.delete(key)\n }\n }\n }\n\n for (const [collName, records] of Object.entries(data)) {\n const coll = getCollection(vault, collName)\n for (const [id, envelope] of Object.entries(records)) {\n coll.set(id, envelope)\n }\n }\n },\n\n async ping() {\n return true\n },\n\n /**\n * Enumerate every top-level vault held by this in-memory\n * store. Used by `Noydb.listAccessibleVaults()`\n * to get the universe of compartments before filtering down to\n * the ones the calling principal can unwrap.\n *\n * Returns the outer Map's keys directly — O(compartments) and\n * cheap. The result is intentionally unsorted; consumers that\n * want a stable order should sort themselves.\n */\n async listVaults() {\n return [...store.keys()]\n },\n\n /**\n * Multi-record atomic transaction.\n *\n * Validates every op's `expectedVersion` against the current Map\n * state, throws `ConflictError` on the first mismatch (nothing\n * written), then applies every put/delete in one synchronous burst\n * — the Map mutations are single-threaded in the JS event loop so\n * no concurrent writer can interleave. Truly atomic.\n */\n async tx(ops: readonly TxOp[]) {\n // Phase 1 — validate every expectedVersion against current state.\n // We read the state ONCE up front; subsequent ops that target the\n // same (vault, coll, id) see the same snapshot, matching the\n // atomicity guarantee callers expect from a storage-layer tx.\n for (const op of ops) {\n if (op.expectedVersion === undefined) continue\n const existing = store.get(op.vault)?.get(op.collection)?.get(op.id)\n const actual = existing?._v ?? 0\n if (actual !== op.expectedVersion) {\n throw new ConflictError(\n actual,\n `tx: ${op.vault}/${op.collection}/${op.id} expected v${op.expectedVersion}, found v${actual}`,\n )\n }\n }\n // Phase 2 — apply every op synchronously. No await between ops =\n // no interleave window.\n for (const op of ops) {\n if (op.type === 'put') {\n if (!op.envelope) {\n throw new Error(`tx: put op for ${op.id} is missing envelope`)\n }\n getCollection(op.vault, op.collection).set(op.id, op.envelope)\n } else {\n store.get(op.vault)?.get(op.collection)?.delete(op.id)\n }\n }\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted id list — same ordering on every call so pages are\n * stable across runs.\n *\n * The default `limit` is 100. Final page returns `nextCursor: null`.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const coll = store.get(vault)?.get(collection)\n if (!coll) return { items: [], nextCursor: null }\n\n // Sorted ids for stable pagination — Map preserves insertion order\n // but tests rely on lexicographic order across different inserts.\n const ids = [...coll.keys()].sort()\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, ids.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const id = ids[i]!\n const envelope = coll.get(id)\n if (envelope) items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < ids.length ? String(end) : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -34,6 +34,8 @@ import { NoydbStore } from '@noy-db/hub';
34
34
  * No persistence — data is lost when the process exits.
35
35
  * Intended for testing and development.
36
36
  */
37
- declare function memory(): NoydbStore;
37
+ declare function memory(opts?: {
38
+ clockUncertainty?: number;
39
+ }): NoydbStore;
38
40
 
39
41
  export { memory };
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ import { NoydbStore } from '@noy-db/hub';
34
34
  * No persistence — data is lost when the process exits.
35
35
  * Intended for testing and development.
36
36
  */
37
- declare function memory(): NoydbStore;
37
+ declare function memory(opts?: {
38
+ clockUncertainty?: number;
39
+ }): NoydbStore;
38
40
 
39
41
  export { memory };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/index.ts
2
2
  import { ConflictError } from "@noy-db/hub";
3
- function memory() {
3
+ function memory(opts = {}) {
4
4
  const store = /* @__PURE__ */ new Map();
5
+ const epsilon = opts.clockUncertainty ?? 0;
6
+ let clock = 0;
5
7
  function getCollection(vault, collection) {
6
8
  let comp = store.get(vault);
7
9
  if (!comp) {
@@ -17,6 +19,21 @@ function memory() {
17
19
  }
18
20
  return {
19
21
  name: "memory",
22
+ // #321 — memory's synchronous Map ops make the expectedVersion check +
23
+ // write atomic, so it can back `vault.sequence()`. Without this the
24
+ // JSDoc's `casAtomic: true` promise was undefined at runtime and
25
+ // `sequence().next()` threw SequenceOfflineError.
26
+ capabilities: {
27
+ casAtomic: true,
28
+ serverWriteTime: true,
29
+ auth: { kind: "none", required: false, flow: "static" }
30
+ },
31
+ // #322-sibling — authoritative store clock for deferred numbering. A
32
+ // single-process counter is perfectly monotonic; ε widens the interval.
33
+ async getStoreTime() {
34
+ const now = ++clock;
35
+ return { earliest: now - epsilon, latest: now + epsilon };
36
+ },
20
37
  async get(vault, collection, id) {
21
38
  return store.get(vault)?.get(collection)?.get(id) ?? null;
22
39
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-memory** — in-memory store for NOYDB (testing and development).\n *\n * Backed by nested `Map` objects: `vault → collection → id → envelope`.\n * Data is lost when the process exits — this store is intentionally\n * non-persistent.\n *\n * ## When to use\n *\n * - **Unit tests** — fast, zero I/O, works in any environment.\n * - **In-memory caching layer** — pair with `routeStore` to keep a hot\n * copy of frequently-read collections in memory while persisting to\n * a durable backend.\n * - **REPL / prototyping** — explore the NOYDB API without setting up\n * a backend.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — Map operations are synchronous and inherently atomic |\n * | `txAtomic` | `true` — multi-record `tx()` applies every op in a single synchronous burst |\n * | `listVaults` | ✓ — iterates outer Map keys |\n * | `listPage` | ✓ — cursor-based pagination over sorted id list |\n * | `ping` | ✓ — always returns `true` |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot, TxOp } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Create an in-memory adapter backed by nested Maps.\n * No persistence — data is lost when the process exits.\n * Intended for testing and development.\n */\nexport function memory(): NoydbStore {\n // vault -> collection -> id -> envelope\n const store = new Map<string, Map<string, Map<string, EncryptedEnvelope>>>()\n\n function getCollection(vault: string, collection: string): Map<string, EncryptedEnvelope> {\n let comp = store.get(vault)\n if (!comp) {\n comp = new Map()\n store.set(vault, comp)\n }\n let coll = comp.get(collection)\n if (!coll) {\n coll = new Map()\n comp.set(collection, coll)\n }\n return coll\n }\n\n return {\n name: 'memory',\n\n async get(vault, collection, id) {\n return store.get(vault)?.get(collection)?.get(id) ?? null\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const coll = getCollection(vault, collection)\n const existing = coll.get(id)\n\n if (expectedVersion !== undefined && existing) {\n if (existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n coll.set(id, envelope)\n },\n\n async delete(vault, collection, id) {\n store.get(vault)?.get(collection)?.delete(id)\n },\n\n async list(vault, collection) {\n const coll = store.get(vault)?.get(collection)\n return coll ? [...coll.keys()] : []\n },\n\n async loadAll(vault) {\n const comp = store.get(vault)\n const snapshot: VaultSnapshot = {}\n if (comp) {\n for (const [collName, coll] of comp) {\n if (collName.startsWith('_')) continue\n const records: Record<string, EncryptedEnvelope> = {}\n for (const [id, envelope] of coll) {\n records[id] = envelope\n }\n snapshot[collName] = records\n }\n }\n return snapshot\n },\n\n async saveAll(vault, data) {\n const comp = store.get(vault)\n if (comp) {\n for (const key of [...comp.keys()]) {\n if (!key.startsWith('_')) {\n comp.delete(key)\n }\n }\n }\n\n for (const [collName, records] of Object.entries(data)) {\n const coll = getCollection(vault, collName)\n for (const [id, envelope] of Object.entries(records)) {\n coll.set(id, envelope)\n }\n }\n },\n\n async ping() {\n return true\n },\n\n /**\n * Enumerate every top-level vault held by this in-memory\n * store. Used by `Noydb.listAccessibleVaults()`\n * to get the universe of compartments before filtering down to\n * the ones the calling principal can unwrap.\n *\n * Returns the outer Map's keys directly — O(compartments) and\n * cheap. The result is intentionally unsorted; consumers that\n * want a stable order should sort themselves.\n */\n async listVaults() {\n return [...store.keys()]\n },\n\n /**\n * Multi-record atomic transaction.\n *\n * Validates every op's `expectedVersion` against the current Map\n * state, throws `ConflictError` on the first mismatch (nothing\n * written), then applies every put/delete in one synchronous burst\n * — the Map mutations are single-threaded in the JS event loop so\n * no concurrent writer can interleave. Truly atomic.\n */\n async tx(ops: readonly TxOp[]) {\n // Phase 1 — validate every expectedVersion against current state.\n // We read the state ONCE up front; subsequent ops that target the\n // same (vault, coll, id) see the same snapshot, matching the\n // atomicity guarantee callers expect from a storage-layer tx.\n for (const op of ops) {\n if (op.expectedVersion === undefined) continue\n const existing = store.get(op.vault)?.get(op.collection)?.get(op.id)\n const actual = existing?._v ?? 0\n if (actual !== op.expectedVersion) {\n throw new ConflictError(\n actual,\n `tx: ${op.vault}/${op.collection}/${op.id} expected v${op.expectedVersion}, found v${actual}`,\n )\n }\n }\n // Phase 2 — apply every op synchronously. No await between ops =\n // no interleave window.\n for (const op of ops) {\n if (op.type === 'put') {\n if (!op.envelope) {\n throw new Error(`tx: put op for ${op.id} is missing envelope`)\n }\n getCollection(op.vault, op.collection).set(op.id, op.envelope)\n } else {\n store.get(op.vault)?.get(op.collection)?.delete(op.id)\n }\n }\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted id list — same ordering on every call so pages are\n * stable across runs.\n *\n * The default `limit` is 100. Final page returns `nextCursor: null`.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const coll = store.get(vault)?.get(collection)\n if (!coll) return { items: [], nextCursor: null }\n\n // Sorted ids for stable pagination — Map preserves insertion order\n // but tests rely on lexicographic order across different inserts.\n const ids = [...coll.keys()].sort()\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, ids.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const id = ids[i]!\n const envelope = coll.get(id)\n if (envelope) items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < ids.length ? String(end) : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-memory** — in-memory store for NOYDB (testing and development).\n *\n * Backed by nested `Map` objects: `vault → collection → id → envelope`.\n * Data is lost when the process exits — this store is intentionally\n * non-persistent.\n *\n * ## When to use\n *\n * - **Unit tests** — fast, zero I/O, works in any environment.\n * - **In-memory caching layer** — pair with `routeStore` to keep a hot\n * copy of frequently-read collections in memory while persisting to\n * a durable backend.\n * - **REPL / prototyping** — explore the NOYDB API without setting up\n * a backend.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — Map operations are synchronous and inherently atomic |\n * | `txAtomic` | `true` — multi-record `tx()` applies every op in a single synchronous burst |\n * | `listVaults` | ✓ — iterates outer Map keys |\n * | `listPage` | ✓ — cursor-based pagination over sorted id list |\n * | `ping` | ✓ — always returns `true` |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot, TxOp } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Create an in-memory adapter backed by nested Maps.\n * No persistence — data is lost when the process exits.\n * Intended for testing and development.\n */\nexport function memory(opts: { clockUncertainty?: number } = {}): NoydbStore {\n // vault -> collection -> id -> envelope\n const store = new Map<string, Map<string, Map<string, EncryptedEnvelope>>>()\n\n // Monotonic store clock — a single-process counter is perfectly ordered.\n // `clockUncertainty` (ε) widens the returned interval to exercise the\n // commit-wait path in deferred numbering; default 0 (exact).\n const epsilon = opts.clockUncertainty ?? 0\n let clock = 0\n\n function getCollection(vault: string, collection: string): Map<string, EncryptedEnvelope> {\n let comp = store.get(vault)\n if (!comp) {\n comp = new Map()\n store.set(vault, comp)\n }\n let coll = comp.get(collection)\n if (!coll) {\n coll = new Map()\n comp.set(collection, coll)\n }\n return coll\n }\n\n return {\n name: 'memory',\n\n // #321 — memory's synchronous Map ops make the expectedVersion check +\n // write atomic, so it can back `vault.sequence()`. Without this the\n // JSDoc's `casAtomic: true` promise was undefined at runtime and\n // `sequence().next()` threw SequenceOfflineError.\n capabilities: {\n casAtomic: true,\n serverWriteTime: true,\n auth: { kind: 'none', required: false, flow: 'static' },\n },\n\n // #322-sibling — authoritative store clock for deferred numbering. A\n // single-process counter is perfectly monotonic; ε widens the interval.\n async getStoreTime() {\n const now = ++clock\n return { earliest: now - epsilon, latest: now + epsilon }\n },\n\n async get(vault, collection, id) {\n return store.get(vault)?.get(collection)?.get(id) ?? null\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const coll = getCollection(vault, collection)\n const existing = coll.get(id)\n\n if (expectedVersion !== undefined && existing) {\n if (existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n coll.set(id, envelope)\n },\n\n async delete(vault, collection, id) {\n store.get(vault)?.get(collection)?.delete(id)\n },\n\n async list(vault, collection) {\n const coll = store.get(vault)?.get(collection)\n return coll ? [...coll.keys()] : []\n },\n\n async loadAll(vault) {\n const comp = store.get(vault)\n const snapshot: VaultSnapshot = {}\n if (comp) {\n for (const [collName, coll] of comp) {\n if (collName.startsWith('_')) continue\n const records: Record<string, EncryptedEnvelope> = {}\n for (const [id, envelope] of coll) {\n records[id] = envelope\n }\n snapshot[collName] = records\n }\n }\n return snapshot\n },\n\n async saveAll(vault, data) {\n const comp = store.get(vault)\n if (comp) {\n for (const key of [...comp.keys()]) {\n if (!key.startsWith('_')) {\n comp.delete(key)\n }\n }\n }\n\n for (const [collName, records] of Object.entries(data)) {\n const coll = getCollection(vault, collName)\n for (const [id, envelope] of Object.entries(records)) {\n coll.set(id, envelope)\n }\n }\n },\n\n async ping() {\n return true\n },\n\n /**\n * Enumerate every top-level vault held by this in-memory\n * store. Used by `Noydb.listAccessibleVaults()`\n * to get the universe of compartments before filtering down to\n * the ones the calling principal can unwrap.\n *\n * Returns the outer Map's keys directly — O(compartments) and\n * cheap. The result is intentionally unsorted; consumers that\n * want a stable order should sort themselves.\n */\n async listVaults() {\n return [...store.keys()]\n },\n\n /**\n * Multi-record atomic transaction.\n *\n * Validates every op's `expectedVersion` against the current Map\n * state, throws `ConflictError` on the first mismatch (nothing\n * written), then applies every put/delete in one synchronous burst\n * — the Map mutations are single-threaded in the JS event loop so\n * no concurrent writer can interleave. Truly atomic.\n */\n async tx(ops: readonly TxOp[]) {\n // Phase 1 — validate every expectedVersion against current state.\n // We read the state ONCE up front; subsequent ops that target the\n // same (vault, coll, id) see the same snapshot, matching the\n // atomicity guarantee callers expect from a storage-layer tx.\n for (const op of ops) {\n if (op.expectedVersion === undefined) continue\n const existing = store.get(op.vault)?.get(op.collection)?.get(op.id)\n const actual = existing?._v ?? 0\n if (actual !== op.expectedVersion) {\n throw new ConflictError(\n actual,\n `tx: ${op.vault}/${op.collection}/${op.id} expected v${op.expectedVersion}, found v${actual}`,\n )\n }\n }\n // Phase 2 — apply every op synchronously. No await between ops =\n // no interleave window.\n for (const op of ops) {\n if (op.type === 'put') {\n if (!op.envelope) {\n throw new Error(`tx: put op for ${op.id} is missing envelope`)\n }\n getCollection(op.vault, op.collection).set(op.id, op.envelope)\n } else {\n store.get(op.vault)?.get(op.collection)?.delete(op.id)\n }\n }\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted id list — same ordering on every call so pages are\n * stable across runs.\n *\n * The default `limit` is 100. Final page returns `nextCursor: null`.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const coll = store.get(vault)?.get(collection)\n if (!coll) return { items: [], nextCursor: null }\n\n // Sorted ids for stable pagination — Map preserves insertion order\n // but tests rely on lexicographic order across different inserts.\n const ids = [...coll.keys()].sort()\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, ids.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const id = ids[i]!\n const envelope = coll.get(id)\n if (envelope) items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < ids.length ? String(end) : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/to-memory",
3
- "version": "0.2.0-pre.2",
3
+ "version": "0.2.0-pre.20",
4
4
  "description": "In-memory adapter for noy-db — ideal for testing, development, and ephemeral workloads",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -39,10 +39,10 @@
39
39
  "node": ">=18.0.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@noy-db/hub": "0.2.0-pre.2"
42
+ "@noy-db/hub": "0.2.0-pre.20"
43
43
  },
44
44
  "devDependencies": {
45
- "@noy-db/hub": "0.2.0-pre.2",
45
+ "@noy-db/hub": "0.2.0-pre.20",
46
46
  "@noy-db/test-adapter-conformance": "0.0.0"
47
47
  },
48
48
  "keywords": [