@noy-db/to-memory 0.1.0-pre.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vLannaAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @noy-db/to-memory
2
+
3
+ > In-memory adapter for [noy-db](https://github.com/vLannaAi/noy-db) — ideal for testing and ephemeral workloads.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@noy-db/to-memory.svg)](https://www.npmjs.com/package/@noy-db/to-memory)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @noy-db/hub @noy-db/to-memory
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createNoydb } from '@noy-db/hub'
17
+ import { memory } from '@noy-db/to-memory'
18
+
19
+ const db = await createNoydb({
20
+ adapter: memory(),
21
+ userId: 'alice',
22
+ passphrase: 'correct horse battery staple',
23
+ })
24
+ ```
25
+
26
+ Data lives only in the current process — it's gone when the process exits. Perfect for:
27
+
28
+ - Unit and integration tests
29
+ - Short-lived scripts
30
+ - Benchmarks
31
+ - Prototyping before wiring up persistent storage
32
+
33
+ ## License
34
+
35
+ MIT © vLannaAi — see the [noy-db repo](https://github.com/vLannaAi/noy-db) for full documentation.
package/dist/index.cjs ADDED
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ memory: () => memory
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_hub = require("@noy-db/hub");
27
+ function memory() {
28
+ const store = /* @__PURE__ */ new Map();
29
+ function getCollection(vault, collection) {
30
+ let comp = store.get(vault);
31
+ if (!comp) {
32
+ comp = /* @__PURE__ */ new Map();
33
+ store.set(vault, comp);
34
+ }
35
+ let coll = comp.get(collection);
36
+ if (!coll) {
37
+ coll = /* @__PURE__ */ new Map();
38
+ comp.set(collection, coll);
39
+ }
40
+ return coll;
41
+ }
42
+ return {
43
+ name: "memory",
44
+ async get(vault, collection, id) {
45
+ return store.get(vault)?.get(collection)?.get(id) ?? null;
46
+ },
47
+ async put(vault, collection, id, envelope, expectedVersion) {
48
+ const coll = getCollection(vault, collection);
49
+ const existing = coll.get(id);
50
+ if (expectedVersion !== void 0 && existing) {
51
+ if (existing._v !== expectedVersion) {
52
+ throw new import_hub.ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`);
53
+ }
54
+ }
55
+ coll.set(id, envelope);
56
+ },
57
+ async delete(vault, collection, id) {
58
+ store.get(vault)?.get(collection)?.delete(id);
59
+ },
60
+ async list(vault, collection) {
61
+ const coll = store.get(vault)?.get(collection);
62
+ return coll ? [...coll.keys()] : [];
63
+ },
64
+ async loadAll(vault) {
65
+ const comp = store.get(vault);
66
+ const snapshot = {};
67
+ if (comp) {
68
+ for (const [collName, coll] of comp) {
69
+ if (collName.startsWith("_")) continue;
70
+ const records = {};
71
+ for (const [id, envelope] of coll) {
72
+ records[id] = envelope;
73
+ }
74
+ snapshot[collName] = records;
75
+ }
76
+ }
77
+ return snapshot;
78
+ },
79
+ async saveAll(vault, data) {
80
+ const comp = store.get(vault);
81
+ if (comp) {
82
+ for (const key of [...comp.keys()]) {
83
+ if (!key.startsWith("_")) {
84
+ comp.delete(key);
85
+ }
86
+ }
87
+ }
88
+ for (const [collName, records] of Object.entries(data)) {
89
+ const coll = getCollection(vault, collName);
90
+ for (const [id, envelope] of Object.entries(records)) {
91
+ coll.set(id, envelope);
92
+ }
93
+ }
94
+ },
95
+ async ping() {
96
+ return true;
97
+ },
98
+ /**
99
+ * Enumerate every top-level vault held by this in-memory
100
+ * store. Used by `Noydb.listAccessibleVaults()`
101
+ * to get the universe of compartments before filtering down to
102
+ * the ones the calling principal can unwrap.
103
+ *
104
+ * Returns the outer Map's keys directly — O(compartments) and
105
+ * cheap. The result is intentionally unsorted; consumers that
106
+ * want a stable order should sort themselves.
107
+ */
108
+ async listVaults() {
109
+ return [...store.keys()];
110
+ },
111
+ /**
112
+ * Multi-record atomic transaction.
113
+ *
114
+ * Validates every op's `expectedVersion` against the current Map
115
+ * state, throws `ConflictError` on the first mismatch (nothing
116
+ * written), then applies every put/delete in one synchronous burst
117
+ * — the Map mutations are single-threaded in the JS event loop so
118
+ * no concurrent writer can interleave. Truly atomic.
119
+ */
120
+ async tx(ops) {
121
+ for (const op of ops) {
122
+ if (op.expectedVersion === void 0) continue;
123
+ const existing = store.get(op.vault)?.get(op.collection)?.get(op.id);
124
+ const actual = existing?._v ?? 0;
125
+ if (actual !== op.expectedVersion) {
126
+ throw new import_hub.ConflictError(
127
+ actual,
128
+ `tx: ${op.vault}/${op.collection}/${op.id} expected v${op.expectedVersion}, found v${actual}`
129
+ );
130
+ }
131
+ }
132
+ for (const op of ops) {
133
+ if (op.type === "put") {
134
+ if (!op.envelope) {
135
+ throw new Error(`tx: put op for ${op.id} is missing envelope`);
136
+ }
137
+ getCollection(op.vault, op.collection).set(op.id, op.envelope);
138
+ } else {
139
+ store.get(op.vault)?.get(op.collection)?.delete(op.id);
140
+ }
141
+ }
142
+ },
143
+ /**
144
+ * Paginate over a collection. Cursor is a numeric offset (as a string)
145
+ * into the sorted id list — same ordering on every call so pages are
146
+ * stable across runs.
147
+ *
148
+ * The default `limit` is 100. Final page returns `nextCursor: null`.
149
+ */
150
+ async listPage(vault, collection, cursor, limit = 100) {
151
+ const coll = store.get(vault)?.get(collection);
152
+ if (!coll) return { items: [], nextCursor: null };
153
+ const ids = [...coll.keys()].sort();
154
+ const start = cursor ? parseInt(cursor, 10) : 0;
155
+ const end = Math.min(start + limit, ids.length);
156
+ const items = [];
157
+ for (let i = start; i < end; i++) {
158
+ const id = ids[i];
159
+ const envelope = coll.get(id);
160
+ if (envelope) items.push({ id, envelope });
161
+ }
162
+ return {
163
+ items,
164
+ nextCursor: end < ids.length ? String(end) : null
165
+ };
166
+ }
167
+ };
168
+ }
169
+ // Annotate the CommonJS export names for ESM import in node:
170
+ 0 && (module.exports = {
171
+ memory
172
+ });
173
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":[]}
@@ -0,0 +1,39 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-memory** — in-memory store for NOYDB (testing and development).
5
+ *
6
+ * Backed by nested `Map` objects: `vault → collection → id → envelope`.
7
+ * Data is lost when the process exits — this store is intentionally
8
+ * non-persistent.
9
+ *
10
+ * ## When to use
11
+ *
12
+ * - **Unit tests** — fast, zero I/O, works in any environment.
13
+ * - **In-memory caching layer** — pair with `routeStore` to keep a hot
14
+ * copy of frequently-read collections in memory while persisting to
15
+ * a durable backend.
16
+ * - **REPL / prototyping** — explore the NOYDB API without setting up
17
+ * a backend.
18
+ *
19
+ * ## Capabilities
20
+ *
21
+ * | Capability | Value |
22
+ * |---|---|
23
+ * | `casAtomic` | `true` — Map operations are synchronous and inherently atomic |
24
+ * | `txAtomic` | `true` — multi-record `tx()` applies every op in a single synchronous burst |
25
+ * | `listVaults` | ✓ — iterates outer Map keys |
26
+ * | `listPage` | ✓ — cursor-based pagination over sorted id list |
27
+ * | `ping` | ✓ — always returns `true` |
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+
32
+ /**
33
+ * Create an in-memory adapter backed by nested Maps.
34
+ * No persistence — data is lost when the process exits.
35
+ * Intended for testing and development.
36
+ */
37
+ declare function memory(): NoydbStore;
38
+
39
+ export { memory };
@@ -0,0 +1,39 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-memory** — in-memory store for NOYDB (testing and development).
5
+ *
6
+ * Backed by nested `Map` objects: `vault → collection → id → envelope`.
7
+ * Data is lost when the process exits — this store is intentionally
8
+ * non-persistent.
9
+ *
10
+ * ## When to use
11
+ *
12
+ * - **Unit tests** — fast, zero I/O, works in any environment.
13
+ * - **In-memory caching layer** — pair with `routeStore` to keep a hot
14
+ * copy of frequently-read collections in memory while persisting to
15
+ * a durable backend.
16
+ * - **REPL / prototyping** — explore the NOYDB API without setting up
17
+ * a backend.
18
+ *
19
+ * ## Capabilities
20
+ *
21
+ * | Capability | Value |
22
+ * |---|---|
23
+ * | `casAtomic` | `true` — Map operations are synchronous and inherently atomic |
24
+ * | `txAtomic` | `true` — multi-record `tx()` applies every op in a single synchronous burst |
25
+ * | `listVaults` | ✓ — iterates outer Map keys |
26
+ * | `listPage` | ✓ — cursor-based pagination over sorted id list |
27
+ * | `ping` | ✓ — always returns `true` |
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+
32
+ /**
33
+ * Create an in-memory adapter backed by nested Maps.
34
+ * No persistence — data is lost when the process exits.
35
+ * Intended for testing and development.
36
+ */
37
+ declare function memory(): NoydbStore;
38
+
39
+ export { memory };
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ // src/index.ts
2
+ import { ConflictError } from "@noy-db/hub";
3
+ function memory() {
4
+ const store = /* @__PURE__ */ new Map();
5
+ function getCollection(vault, collection) {
6
+ let comp = store.get(vault);
7
+ if (!comp) {
8
+ comp = /* @__PURE__ */ new Map();
9
+ store.set(vault, comp);
10
+ }
11
+ let coll = comp.get(collection);
12
+ if (!coll) {
13
+ coll = /* @__PURE__ */ new Map();
14
+ comp.set(collection, coll);
15
+ }
16
+ return coll;
17
+ }
18
+ return {
19
+ name: "memory",
20
+ async get(vault, collection, id) {
21
+ return store.get(vault)?.get(collection)?.get(id) ?? null;
22
+ },
23
+ async put(vault, collection, id, envelope, expectedVersion) {
24
+ const coll = getCollection(vault, collection);
25
+ const existing = coll.get(id);
26
+ if (expectedVersion !== void 0 && existing) {
27
+ if (existing._v !== expectedVersion) {
28
+ throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`);
29
+ }
30
+ }
31
+ coll.set(id, envelope);
32
+ },
33
+ async delete(vault, collection, id) {
34
+ store.get(vault)?.get(collection)?.delete(id);
35
+ },
36
+ async list(vault, collection) {
37
+ const coll = store.get(vault)?.get(collection);
38
+ return coll ? [...coll.keys()] : [];
39
+ },
40
+ async loadAll(vault) {
41
+ const comp = store.get(vault);
42
+ const snapshot = {};
43
+ if (comp) {
44
+ for (const [collName, coll] of comp) {
45
+ if (collName.startsWith("_")) continue;
46
+ const records = {};
47
+ for (const [id, envelope] of coll) {
48
+ records[id] = envelope;
49
+ }
50
+ snapshot[collName] = records;
51
+ }
52
+ }
53
+ return snapshot;
54
+ },
55
+ async saveAll(vault, data) {
56
+ const comp = store.get(vault);
57
+ if (comp) {
58
+ for (const key of [...comp.keys()]) {
59
+ if (!key.startsWith("_")) {
60
+ comp.delete(key);
61
+ }
62
+ }
63
+ }
64
+ for (const [collName, records] of Object.entries(data)) {
65
+ const coll = getCollection(vault, collName);
66
+ for (const [id, envelope] of Object.entries(records)) {
67
+ coll.set(id, envelope);
68
+ }
69
+ }
70
+ },
71
+ async ping() {
72
+ return true;
73
+ },
74
+ /**
75
+ * Enumerate every top-level vault held by this in-memory
76
+ * store. Used by `Noydb.listAccessibleVaults()`
77
+ * to get the universe of compartments before filtering down to
78
+ * the ones the calling principal can unwrap.
79
+ *
80
+ * Returns the outer Map's keys directly — O(compartments) and
81
+ * cheap. The result is intentionally unsorted; consumers that
82
+ * want a stable order should sort themselves.
83
+ */
84
+ async listVaults() {
85
+ return [...store.keys()];
86
+ },
87
+ /**
88
+ * Multi-record atomic transaction.
89
+ *
90
+ * Validates every op's `expectedVersion` against the current Map
91
+ * state, throws `ConflictError` on the first mismatch (nothing
92
+ * written), then applies every put/delete in one synchronous burst
93
+ * — the Map mutations are single-threaded in the JS event loop so
94
+ * no concurrent writer can interleave. Truly atomic.
95
+ */
96
+ async tx(ops) {
97
+ for (const op of ops) {
98
+ if (op.expectedVersion === void 0) continue;
99
+ const existing = store.get(op.vault)?.get(op.collection)?.get(op.id);
100
+ const actual = existing?._v ?? 0;
101
+ if (actual !== op.expectedVersion) {
102
+ throw new ConflictError(
103
+ actual,
104
+ `tx: ${op.vault}/${op.collection}/${op.id} expected v${op.expectedVersion}, found v${actual}`
105
+ );
106
+ }
107
+ }
108
+ for (const op of ops) {
109
+ if (op.type === "put") {
110
+ if (!op.envelope) {
111
+ throw new Error(`tx: put op for ${op.id} is missing envelope`);
112
+ }
113
+ getCollection(op.vault, op.collection).set(op.id, op.envelope);
114
+ } else {
115
+ store.get(op.vault)?.get(op.collection)?.delete(op.id);
116
+ }
117
+ }
118
+ },
119
+ /**
120
+ * Paginate over a collection. Cursor is a numeric offset (as a string)
121
+ * into the sorted id list — same ordering on every call so pages are
122
+ * stable across runs.
123
+ *
124
+ * The default `limit` is 100. Final page returns `nextCursor: null`.
125
+ */
126
+ async listPage(vault, collection, cursor, limit = 100) {
127
+ const coll = store.get(vault)?.get(collection);
128
+ if (!coll) return { items: [], nextCursor: null };
129
+ const ids = [...coll.keys()].sort();
130
+ const start = cursor ? parseInt(cursor, 10) : 0;
131
+ const end = Math.min(start + limit, ids.length);
132
+ const items = [];
133
+ for (let i = start; i < end; i++) {
134
+ const id = ids[i];
135
+ const envelope = coll.get(id);
136
+ if (envelope) items.push({ id, envelope });
137
+ }
138
+ return {
139
+ items,
140
+ nextCursor: end < ids.length ? String(end) : null
141
+ };
142
+ }
143
+ };
144
+ }
145
+ export {
146
+ memory
147
+ };
148
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@noy-db/to-memory",
3
+ "version": "0.1.0-pre.3",
4
+ "description": "In-memory adapter for noy-db — ideal for testing, development, and ephemeral workloads",
5
+ "license": "MIT",
6
+ "author": "vLannaAi <vicio@lanna.ai>",
7
+ "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/to-memory#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vLannaAi/noy-db.git",
11
+ "directory": "packages/to-memory"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vLannaAi/noy-db/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@noy-db/hub": "0.1.0-pre.3"
43
+ },
44
+ "devDependencies": {
45
+ "@noy-db/hub": "0.1.0-pre.3",
46
+ "@noy-db/test-adapter-conformance": "0.0.0"
47
+ },
48
+ "keywords": [
49
+ "noy-db",
50
+ "adapter",
51
+ "in-memory",
52
+ "memory",
53
+ "storage",
54
+ "testing",
55
+ "encryption",
56
+ "zero-knowledge"
57
+ ],
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "tag": "latest"
61
+ },
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "test": "vitest run",
65
+ "lint": "eslint src/",
66
+ "typecheck": "tsc --noEmit"
67
+ }
68
+ }