@kyneta/leveldb-store 1.3.1 → 1.5.0

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.
@@ -0,0 +1,36 @@
1
+ import { DocId, Store, StoreMeta, StoreRecord } from "@kyneta/exchange";
2
+
3
+ //#region src/index.d.ts
4
+ declare function encodeStoreRecord(record: StoreRecord): Uint8Array;
5
+ declare function decodeStoreRecord(bytes: Uint8Array): StoreRecord;
6
+ declare class LevelDBStore implements Store {
7
+ #private;
8
+ constructor(dbPath: string);
9
+ currentMeta(docId: DocId): Promise<StoreMeta | null>;
10
+ append(docId: DocId, record: StoreRecord): Promise<void>;
11
+ loadAll(docId: DocId): AsyncIterable<StoreRecord>;
12
+ replace(docId: DocId, records: StoreRecord[]): Promise<void>;
13
+ delete(docId: DocId): Promise<void>;
14
+ listDocIds(prefix?: string): AsyncIterable<DocId>;
15
+ close(): Promise<void>;
16
+ }
17
+ /**
18
+ * Create a LevelDB storage backend for server-side persistence.
19
+ *
20
+ * Returns a `Store` — pass directly to `Exchange({ stores: [...] })`.
21
+ *
22
+ * @param dbPath - Directory path where LevelDB stores its files
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { createLevelDBStore } from "@kyneta/leveldb-store"
27
+ *
28
+ * const exchange = new Exchange({
29
+ * stores: [createLevelDBStore("./data/exchange-db")],
30
+ * })
31
+ * ```
32
+ */
33
+ declare function createLevelDBStore(dbPath: string): Store;
34
+ //#endregion
35
+ export { LevelDBStore, createLevelDBStore, decodeStoreRecord, encodeStoreRecord };
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;iBAuDgB,iBAAA,CAAkB,MAAA,EAAQ,WAAA,GAAc,UAAA;AAAA,iBAmCxC,iBAAA,CAAkB,KAAA,EAAO,UAAA,GAAa,WAAA;AAAA,cAsEzC,YAAA,YAAwB,KAAA;EAAA;cAIvB,MAAA;EAUN,WAAA,CAAY,KAAA,EAAO,KAAA,GAAQ,OAAA,CAAQ,SAAA;EAUnC,MAAA,CAAO,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,WAAA,GAAc,OAAA;EA0B1C,OAAA,CAAQ,KAAA,EAAO,KAAA,GAAQ,aAAA,CAAc,WAAA;EAUtC,OAAA,CAAQ,KAAA,EAAO,KAAA,EAAO,OAAA,EAAS,WAAA,KAAgB,OAAA;EA2C/C,MAAA,CAAO,KAAA,EAAO,KAAA,GAAQ,OAAA;EAoBrB,UAAA,CAAW,MAAA,YAAkB,aAAA,CAAc,KAAA;EAW5C,KAAA,CAAA,GAAS,OAAA;AAAA;;;;;;;;AAtIjB;;;;;;;;;iBA+JgB,kBAAA,CAAmB,MAAA,WAAiB,KAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ import { SeqNoTracker, resolveMetaFromBatch, validateAppend } from "@kyneta/exchange";
2
+ import { ClassicLevel } from "classic-level";
3
+ //#region src/index.ts
4
+ const SEP = "\0";
5
+ const META_PREFIX = `meta${SEP}`;
6
+ const RECORD_PREFIX = `record${SEP}`;
7
+ const SEQ_PAD = 16;
8
+ const encoder = new TextEncoder();
9
+ const decoder = new TextDecoder();
10
+ function encodeStoreRecord(record) {
11
+ if (record.kind === "meta") {
12
+ const metaBytes = encoder.encode(JSON.stringify(record.meta));
13
+ const buf = new Uint8Array(1 + metaBytes.length);
14
+ buf[0] = 8;
15
+ buf.set(metaBytes, 1);
16
+ return buf;
17
+ }
18
+ const { payload, version } = record;
19
+ let flags = 0;
20
+ if (payload.kind === "since") flags |= 1;
21
+ if (payload.encoding === "binary") flags |= 2;
22
+ const isDataBinary = payload.data instanceof Uint8Array;
23
+ if (isDataBinary) flags |= 4;
24
+ const versionBytes = encoder.encode(version);
25
+ const dataBytes = isDataBinary ? payload.data : encoder.encode(payload.data);
26
+ const buf = new Uint8Array(5 + versionBytes.length + dataBytes.length);
27
+ const view = new DataView(buf.buffer);
28
+ buf[0] = flags;
29
+ view.setUint32(1, versionBytes.length, false);
30
+ buf.set(versionBytes, 5);
31
+ buf.set(dataBytes, 5 + versionBytes.length);
32
+ return buf;
33
+ }
34
+ function decodeStoreRecord(bytes) {
35
+ const flagByte = bytes[0];
36
+ if (flagByte === void 0) throw new Error("empty store record bytes");
37
+ const flags = flagByte;
38
+ if ((flags & 128) !== 0) throw new Error("unknown store record format (future-format bit set)");
39
+ if ((flags & 8) !== 0) {
40
+ const metaJson = decoder.decode(bytes.subarray(1));
41
+ return {
42
+ kind: "meta",
43
+ meta: JSON.parse(metaJson)
44
+ };
45
+ }
46
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
47
+ const kind = (flags & 1) !== 0 ? "since" : "entirety";
48
+ const encoding = (flags & 2) !== 0 ? "binary" : "json";
49
+ const isDataBinary = (flags & 4) !== 0;
50
+ const versionLen = view.getUint32(1, false);
51
+ const version = decoder.decode(bytes.subarray(5, 5 + versionLen));
52
+ const dataStart = 5 + versionLen;
53
+ const rawData = bytes.subarray(dataStart);
54
+ return {
55
+ kind: "entry",
56
+ payload: {
57
+ kind,
58
+ encoding,
59
+ data: isDataBinary ? new Uint8Array(rawData) : decoder.decode(rawData)
60
+ },
61
+ version
62
+ };
63
+ }
64
+ function metaKey(docId) {
65
+ return `${META_PREFIX}${docId}`;
66
+ }
67
+ function recordPrefix(docId) {
68
+ return `${RECORD_PREFIX}${docId}${SEP}`;
69
+ }
70
+ function recordKey(docId, seqNo) {
71
+ return `${recordPrefix(docId)}${String(seqNo).padStart(SEQ_PAD, "0")}`;
72
+ }
73
+ function parseDocIdFromMetaKey(key) {
74
+ return key.slice(META_PREFIX.length);
75
+ }
76
+ function parseSeqNoFromRecordKey(key, docId) {
77
+ const prefix = recordPrefix(docId);
78
+ return Number.parseInt(key.slice(prefix.length), 10);
79
+ }
80
+ var LevelDBStore = class {
81
+ #db;
82
+ #seqNos = new SeqNoTracker();
83
+ constructor(dbPath) {
84
+ this.#db = new ClassicLevel(dbPath, { valueEncoding: "binary" });
85
+ }
86
+ async currentMeta(docId) {
87
+ try {
88
+ const raw = await this.#db.get(metaKey(docId));
89
+ return JSON.parse(decoder.decode(raw));
90
+ } catch (error) {
91
+ if (error.code === "LEVEL_NOT_FOUND") return null;
92
+ throw error;
93
+ }
94
+ }
95
+ async append(docId, record) {
96
+ const resolved = validateAppend(docId, record, await this.currentMeta(docId));
97
+ if (resolved !== null) await this.#db.put(metaKey(docId), encoder.encode(JSON.stringify(resolved)));
98
+ const seq = await this.#seqNos.next(docId, async () => {
99
+ const prefix = recordPrefix(docId);
100
+ for await (const key of this.#db.keys({
101
+ gte: prefix,
102
+ lt: `${prefix}\xff`,
103
+ reverse: true,
104
+ limit: 1
105
+ })) return parseSeqNoFromRecordKey(key, docId);
106
+ return null;
107
+ });
108
+ await this.#db.put(recordKey(docId, seq), encodeStoreRecord(record));
109
+ }
110
+ async *loadAll(docId) {
111
+ const prefix = recordPrefix(docId);
112
+ for await (const value of this.#db.values({
113
+ gte: prefix,
114
+ lt: `${prefix}\xff`
115
+ })) yield decodeStoreRecord(value);
116
+ }
117
+ async replace(docId, records) {
118
+ const resolved = resolveMetaFromBatch(records, await this.currentMeta(docId));
119
+ const prefix = recordPrefix(docId);
120
+ const keysToDelete = [];
121
+ for await (const key of this.#db.keys({
122
+ gte: prefix,
123
+ lt: `${prefix}\xff`
124
+ })) keysToDelete.push(key);
125
+ const ops = keysToDelete.map((key) => ({
126
+ type: "del",
127
+ key
128
+ }));
129
+ for (let i = 0; i < records.length; i++) ops.push({
130
+ type: "put",
131
+ key: recordKey(docId, i),
132
+ value: encodeStoreRecord(records[i])
133
+ });
134
+ ops.push({
135
+ type: "put",
136
+ key: metaKey(docId),
137
+ value: encoder.encode(JSON.stringify(resolved))
138
+ });
139
+ await this.#db.batch(ops);
140
+ this.#seqNos.reset(docId, records.length - 1);
141
+ }
142
+ async delete(docId) {
143
+ const prefix = recordPrefix(docId);
144
+ const keysToDelete = [metaKey(docId)];
145
+ for await (const key of this.#db.keys({
146
+ gte: prefix,
147
+ lt: `${prefix}\xff`
148
+ })) keysToDelete.push(key);
149
+ await this.#db.batch(keysToDelete.map((key) => ({
150
+ type: "del",
151
+ key
152
+ })));
153
+ this.#seqNos.remove(docId);
154
+ }
155
+ async *listDocIds(prefix) {
156
+ const rangePrefix = prefix !== void 0 ? `${META_PREFIX}${prefix}` : META_PREFIX;
157
+ for await (const key of this.#db.keys({
158
+ gte: rangePrefix,
159
+ lt: `${rangePrefix}\xff`
160
+ })) yield parseDocIdFromMetaKey(key);
161
+ }
162
+ async close() {
163
+ await this.#db.close();
164
+ }
165
+ };
166
+ /**
167
+ * Create a LevelDB storage backend for server-side persistence.
168
+ *
169
+ * Returns a `Store` — pass directly to `Exchange({ stores: [...] })`.
170
+ *
171
+ * @param dbPath - Directory path where LevelDB stores its files
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * import { createLevelDBStore } from "@kyneta/leveldb-store"
176
+ *
177
+ * const exchange = new Exchange({
178
+ * stores: [createLevelDBStore("./data/exchange-db")],
179
+ * })
180
+ * ```
181
+ */
182
+ function createLevelDBStore(dbPath) {
183
+ return new LevelDBStore(dbPath);
184
+ }
185
+ //#endregion
186
+ export { LevelDBStore, createLevelDBStore, decodeStoreRecord, encodeStoreRecord };
187
+
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["#db","#seqNos"],"sources":["../src/index.ts"],"sourcesContent":["// server — LevelDB storage backend for @kyneta/exchange.\n//\n// Implements the Store interface using classic-level.\n//\n// Key-space design (FoundationDB convention — \\x00 null-byte separator):\n// meta\\x00{docId} → JSON-encoded StoreMeta (materialized index)\n// record\\x00{docId}\\x00{seqNo} → binary-encoded StoreRecord (unified stream)\n//\n// The \\x00 separator cannot appear in valid UTF-8 strings, so no docId\n// validation is needed — the key-space imposes zero constraints on callers.\n//\n// SeqNo is a zero-padded 16-digit monotonic counter per doc, tracked in\n// memory. On reboot, the max seqNo for a doc is lazily discovered via a\n// single reverse-iterator seek on first append.\n\nimport {\n type DocId,\n resolveMetaFromBatch,\n SeqNoTracker,\n type Store,\n type StoreMeta,\n type StoreRecord,\n validateAppend,\n} from \"@kyneta/exchange\"\nimport { ClassicLevel } from \"classic-level\"\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SEP = \"\\x00\"\nconst META_PREFIX = `meta${SEP}`\nconst RECORD_PREFIX = `record${SEP}`\nconst SEQ_PAD = 16\n\n// ---------------------------------------------------------------------------\n// Binary envelope v2 — pure encode/decode for StoreRecord\n// ---------------------------------------------------------------------------\n\n// Flags byte layout:\n// bit 0: payload kind (0 = entirety, 1 = since) — entry records only\n// bit 1: encoding (0 = json, 1 = binary) — entry records only\n// bit 2: data type (0 = string, 1 = Uint8Array) — entry records only\n// bit 3: record kind (0 = entry, 1 = meta)\n// bit 7: future-format (0 = current format, reserved)\n//\n// Meta records (bit 3 = 1):\n// [1 byte flags] [remaining: JSON-encoded StoreMeta]\n//\n// Entry records (bit 3 = 0):\n// [1 byte flags] [4 bytes version length BE] [N bytes version UTF-8] [remaining: payload data]\n\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder()\n\nexport function encodeStoreRecord(record: StoreRecord): Uint8Array {\n if (record.kind === \"meta\") {\n const metaBytes = encoder.encode(JSON.stringify(record.meta))\n const buf = new Uint8Array(1 + metaBytes.length)\n buf[0] = 0x08 // bit 3 set (meta)\n buf.set(metaBytes, 1)\n return buf\n }\n\n // Entry record\n const { payload, version } = record\n\n let flags = 0\n if (payload.kind === \"since\") flags |= 0x01\n if (payload.encoding === \"binary\") flags |= 0x02\n\n const isDataBinary = payload.data instanceof Uint8Array\n if (isDataBinary) flags |= 0x04\n\n const versionBytes = encoder.encode(version)\n const dataBytes = isDataBinary\n ? (payload.data as Uint8Array)\n : encoder.encode(payload.data as string)\n\n const buf = new Uint8Array(1 + 4 + versionBytes.length + dataBytes.length)\n const view = new DataView(buf.buffer)\n\n buf[0] = flags\n view.setUint32(1, versionBytes.length, false) // big-endian\n buf.set(versionBytes, 5)\n buf.set(dataBytes, 5 + versionBytes.length)\n\n return buf\n}\n\nexport function decodeStoreRecord(bytes: Uint8Array): StoreRecord {\n const flagByte = bytes[0]\n if (flagByte === undefined) throw new Error(\"empty store record bytes\")\n const flags = flagByte\n\n // Check future-format flag (bit 7)\n if ((flags & 0x80) !== 0) {\n throw new Error(\"unknown store record format (future-format bit set)\")\n }\n\n const isMeta = (flags & 0x08) !== 0\n\n if (isMeta) {\n const metaJson = decoder.decode(bytes.subarray(1))\n return { kind: \"meta\", meta: JSON.parse(metaJson) as StoreMeta }\n }\n\n // Entry record\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n\n const kind = (flags & 0x01) !== 0 ? \"since\" : \"entirety\"\n const encoding = (flags & 0x02) !== 0 ? \"binary\" : \"json\"\n const isDataBinary = (flags & 0x04) !== 0\n\n const versionLen = view.getUint32(1, false)\n const version = decoder.decode(bytes.subarray(5, 5 + versionLen))\n\n const dataStart = 5 + versionLen\n const rawData = bytes.subarray(dataStart)\n\n const data: string | Uint8Array = isDataBinary\n ? new Uint8Array(rawData)\n : decoder.decode(rawData)\n\n return {\n kind: \"entry\",\n payload: { kind, encoding, data },\n version,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Key helpers\n// ---------------------------------------------------------------------------\n\nfunction metaKey(docId: DocId): string {\n return `${META_PREFIX}${docId}`\n}\n\nfunction recordPrefix(docId: DocId): string {\n return `${RECORD_PREFIX}${docId}${SEP}`\n}\n\nfunction recordKey(docId: DocId, seqNo: number): string {\n return `${recordPrefix(docId)}${String(seqNo).padStart(SEQ_PAD, \"0\")}`\n}\n\nfunction parseDocIdFromMetaKey(key: string): DocId {\n return key.slice(META_PREFIX.length)\n}\n\nfunction parseSeqNoFromRecordKey(key: string, docId: DocId): number {\n const prefix = recordPrefix(docId)\n return Number.parseInt(key.slice(prefix.length), 10)\n}\n\n// ---------------------------------------------------------------------------\n// LevelDBStore\n// ---------------------------------------------------------------------------\n\nexport class LevelDBStore implements Store {\n readonly #db: ClassicLevel<string, Uint8Array>\n readonly #seqNos = new SeqNoTracker()\n\n constructor(dbPath: string) {\n this.#db = new ClassicLevel(dbPath, {\n valueEncoding: \"binary\",\n })\n }\n\n // -------------------------------------------------------------------------\n // Store interface\n // -------------------------------------------------------------------------\n\n async currentMeta(docId: DocId): Promise<StoreMeta | null> {\n try {\n const raw = await this.#db.get(metaKey(docId))\n return JSON.parse(decoder.decode(raw)) as StoreMeta\n } catch (error: any) {\n if (error.code === \"LEVEL_NOT_FOUND\") return null\n throw error\n }\n }\n\n async append(docId: DocId, record: StoreRecord): Promise<void> {\n const existingMeta = await this.currentMeta(docId)\n const resolved = validateAppend(docId, record, existingMeta)\n\n if (resolved !== null) {\n await this.#db.put(\n metaKey(docId),\n encoder.encode(JSON.stringify(resolved)),\n )\n }\n\n const seq = await this.#seqNos.next(docId, async () => {\n const prefix = recordPrefix(docId)\n for await (const key of this.#db.keys({\n gte: prefix,\n lt: `${prefix}\\xff`,\n reverse: true,\n limit: 1,\n })) {\n return parseSeqNoFromRecordKey(key, docId)\n }\n return null\n })\n await this.#db.put(recordKey(docId, seq), encodeStoreRecord(record))\n }\n\n async *loadAll(docId: DocId): AsyncIterable<StoreRecord> {\n const prefix = recordPrefix(docId)\n for await (const value of this.#db.values({\n gte: prefix,\n lt: `${prefix}\\xff`,\n })) {\n yield decodeStoreRecord(value)\n }\n }\n\n async replace(docId: DocId, records: StoreRecord[]): Promise<void> {\n const existingMeta = await this.currentMeta(docId)\n\n // Resolve validates: at least one meta present, immutable fields match.\n const resolved = resolveMetaFromBatch(records, existingMeta)\n\n const prefix = recordPrefix(docId)\n\n // Collect existing record keys to delete\n const keysToDelete: string[] = []\n for await (const key of this.#db.keys({\n gte: prefix,\n lt: `${prefix}\\xff`,\n })) {\n keysToDelete.push(key)\n }\n\n // Atomic batch: delete all existing records, write replacements, upsert meta\n const ops: Array<\n | { type: \"del\"; key: string }\n | { type: \"put\"; key: string; value: Uint8Array }\n > = keysToDelete.map(key => ({ type: \"del\" as const, key }))\n\n for (let i = 0; i < records.length; i++) {\n ops.push({\n type: \"put\",\n key: recordKey(docId, i),\n value: encodeStoreRecord(records[i]!),\n })\n }\n\n ops.push({\n type: \"put\",\n key: metaKey(docId),\n value: encoder.encode(JSON.stringify(resolved)),\n })\n\n await this.#db.batch(ops)\n\n // Reset seqNo counter to the last written index\n this.#seqNos.reset(docId, records.length - 1)\n }\n\n async delete(docId: DocId): Promise<void> {\n const prefix = recordPrefix(docId)\n\n // Collect record keys to delete\n const keysToDelete: string[] = [metaKey(docId)]\n for await (const key of this.#db.keys({\n gte: prefix,\n lt: `${prefix}\\xff`,\n })) {\n keysToDelete.push(key)\n }\n\n await this.#db.batch(\n keysToDelete.map(key => ({ type: \"del\" as const, key })),\n )\n\n // Remove from in-memory seqNo tracker\n this.#seqNos.remove(docId)\n }\n\n async *listDocIds(prefix?: string): AsyncIterable<DocId> {\n const rangePrefix =\n prefix !== undefined ? `${META_PREFIX}${prefix}` : META_PREFIX\n for await (const key of this.#db.keys({\n gte: rangePrefix,\n lt: `${rangePrefix}\\xff`,\n })) {\n yield parseDocIdFromMetaKey(key)\n }\n }\n\n async close(): Promise<void> {\n await this.#db.close()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Create a LevelDB storage backend for server-side persistence.\n *\n * Returns a `Store` — pass directly to `Exchange({ stores: [...] })`.\n *\n * @param dbPath - Directory path where LevelDB stores its files\n *\n * @example\n * ```typescript\n * import { createLevelDBStore } from \"@kyneta/leveldb-store\"\n *\n * const exchange = new Exchange({\n * stores: [createLevelDBStore(\"./data/exchange-db\")],\n * })\n * ```\n */\nexport function createLevelDBStore(dbPath: string): Store {\n return new LevelDBStore(dbPath)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyneta/leveldb-store",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "LevelDB storage backend for @kyneta/exchange — server-side persistent storage",
5
5
  "author": "Duane Johnson",
6
6
  "license": "MIT",
@@ -13,34 +13,39 @@
13
13
  "access": "public"
14
14
  },
15
15
  "type": "module",
16
+ "main": "./dist/index.js",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
16
19
  "files": [
17
20
  "dist",
18
21
  "src"
19
22
  ],
20
23
  "exports": {
21
- "./server": {
22
- "types": "./dist/server.d.ts",
23
- "import": "./dist/server.js"
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
24
28
  },
29
+ "./src": "./src/index.ts",
25
30
  "./src/*": "./src/*"
26
31
  },
27
32
  "peerDependencies": {
28
- "@kyneta/exchange": "^1.3.1",
29
- "@kyneta/schema": "^1.3.1"
33
+ "@kyneta/exchange": "^1.5.0",
34
+ "@kyneta/schema": "^1.5.0"
30
35
  },
31
36
  "dependencies": {
32
37
  "classic-level": "^1.4.1"
33
38
  },
34
39
  "devDependencies": {
35
40
  "@types/node": "^22",
36
- "tsup": "^8.5.0",
41
+ "tsdown": "^0.21.9",
37
42
  "typescript": "^5.9.2",
38
43
  "vitest": "^4.0.17",
39
- "@kyneta/exchange": "^1.3.1",
40
- "@kyneta/schema": "^1.3.1"
44
+ "@kyneta/exchange": "^1.5.0",
45
+ "@kyneta/schema": "^1.5.0"
41
46
  },
42
47
  "scripts": {
43
- "build": "tsup",
48
+ "build": "tsdown",
44
49
  "test": "verify logic",
45
50
  "verify": "verify"
46
51
  }
@@ -7,15 +7,17 @@
7
7
  import * as fs from "node:fs"
8
8
  import * as os from "node:os"
9
9
  import * as path from "node:path"
10
- import type { StoreEntry } from "@kyneta/exchange"
10
+ import type { StoreRecord } from "@kyneta/exchange"
11
11
  import {
12
12
  collectAll,
13
13
  describeStore,
14
- makeEntry,
15
- plainMetadata,
14
+ makeBinaryEntryRecord,
15
+ makeEntryRecord,
16
+ makeMetaRecord,
17
+ plainMeta,
16
18
  } from "@kyneta/exchange/testing"
17
19
  import { afterAll, describe, expect, it } from "vitest"
18
- import { decodeStoreEntry, encodeStoreEntry, LevelDBStore } from "../server.js"
20
+ import { decodeStoreRecord, encodeStoreRecord, LevelDBStore } from "../index.js"
19
21
 
20
22
  // ---------------------------------------------------------------------------
21
23
  // Temp directory management
@@ -43,13 +45,11 @@ afterAll(() => {
43
45
  // Conformance suite — validates the full Store contract
44
46
  // ---------------------------------------------------------------------------
45
47
 
46
- describeStore(
47
- "LevelDBStore",
48
- () => new LevelDBStore(makeTmpDir()),
49
- async backend => {
50
- if (backend.close) await backend.close()
48
+ describeStore("LevelDBStore", () => new LevelDBStore(makeTmpDir()), {
49
+ cleanup: async backend => {
50
+ await backend.close()
51
51
  },
52
- )
52
+ })
53
53
 
54
54
  // ---------------------------------------------------------------------------
55
55
  // LevelDB-specific tests
@@ -61,19 +61,26 @@ describe("LevelDBStore — close + reopen", () => {
61
61
 
62
62
  // Phase 1: write data then close
63
63
  const backend1 = new LevelDBStore(dir)
64
- await backend1.ensureDoc("doc-1", plainMetadata)
65
- await backend1.append("doc-1", makeEntry("entirety", "v1"))
66
- await backend1.append("doc-1", makeEntry("since", "v2"))
64
+ await backend1.append("doc-1", makeMetaRecord())
65
+ await backend1.append("doc-1", makeEntryRecord("entirety", "v1"))
66
+ await backend1.append("doc-1", makeEntryRecord("since", "v2"))
67
67
  await backend1.close()
68
68
 
69
69
  // Phase 2: reopen and verify
70
70
  const backend2 = new LevelDBStore(dir)
71
- expect(await backend2.lookup("doc-1")).toEqual(plainMetadata)
71
+ expect(await backend2.currentMeta("doc-1")).toEqual(plainMeta)
72
72
 
73
- const entries = await collectAll(backend2.loadAll("doc-1"))
73
+ const records = await collectAll(backend2.loadAll("doc-1"))
74
+ expect(records).toHaveLength(3)
75
+ expect(records[0]).toEqual(makeMetaRecord())
76
+ const entries = records.filter(r => r.kind === "entry")
74
77
  expect(entries).toHaveLength(2)
75
- expect(entries[0]?.version).toBe("v1")
76
- expect(entries[1]?.version).toBe("v2")
78
+ expect((entries[0] as { kind: "entry"; version: string }).version).toBe(
79
+ "v1",
80
+ )
81
+ expect((entries[1] as { kind: "entry"; version: string }).version).toBe(
82
+ "v2",
83
+ )
77
84
  await backend2.close()
78
85
  })
79
86
 
@@ -81,37 +88,51 @@ describe("LevelDBStore — close + reopen", () => {
81
88
  const dir = makeTmpDir()
82
89
 
83
90
  const backend1 = new LevelDBStore(dir)
84
- await backend1.ensureDoc("doc-1", plainMetadata)
85
- await backend1.append("doc-1", makeEntry("entirety", "v1"))
86
- await backend1.append("doc-1", makeEntry("since", "v2"))
91
+ await backend1.append("doc-1", makeMetaRecord())
92
+ await backend1.append("doc-1", makeEntryRecord("entirety", "v1"))
93
+ await backend1.append("doc-1", makeEntryRecord("since", "v2"))
87
94
  await backend1.close()
88
95
 
89
96
  // Reopen and append more
90
97
  const backend2 = new LevelDBStore(dir)
91
- await backend2.append("doc-1", makeEntry("since", "v3"))
98
+ await backend2.append("doc-1", makeEntryRecord("since", "v3"))
92
99
 
93
- const entries = await collectAll(backend2.loadAll("doc-1"))
100
+ const records = await collectAll(backend2.loadAll("doc-1"))
101
+ const entries = records.filter(r => r.kind === "entry")
94
102
  expect(entries).toHaveLength(3)
95
- expect(entries[0]?.version).toBe("v1")
96
- expect(entries[1]?.version).toBe("v2")
97
- expect(entries[2]?.version).toBe("v3")
103
+ expect((entries[0] as { kind: "entry"; version: string }).version).toBe(
104
+ "v1",
105
+ )
106
+ expect((entries[1] as { kind: "entry"; version: string }).version).toBe(
107
+ "v2",
108
+ )
109
+ expect((entries[2] as { kind: "entry"; version: string }).version).toBe(
110
+ "v3",
111
+ )
98
112
  await backend2.close()
99
113
  })
100
114
 
101
- it("replace then reopen preserves the single entry", async () => {
115
+ it("replace then reopen preserves the replacement records", async () => {
102
116
  const dir = makeTmpDir()
103
117
 
104
118
  const backend1 = new LevelDBStore(dir)
105
- await backend1.ensureDoc("doc-1", plainMetadata)
106
- await backend1.append("doc-1", makeEntry("since", "v1"))
107
- await backend1.append("doc-1", makeEntry("since", "v2"))
108
- await backend1.replace("doc-1", makeEntry("entirety", "v3"))
119
+ await backend1.append("doc-1", makeMetaRecord())
120
+ await backend1.append("doc-1", makeEntryRecord("since", "v1"))
121
+ await backend1.append("doc-1", makeEntryRecord("since", "v2"))
122
+ await backend1.replace("doc-1", [
123
+ makeMetaRecord(),
124
+ makeEntryRecord("entirety", "v3"),
125
+ ])
109
126
  await backend1.close()
110
127
 
111
128
  const backend2 = new LevelDBStore(dir)
112
- const entries = await collectAll(backend2.loadAll("doc-1"))
113
- expect(entries).toHaveLength(1)
114
- expect(entries[0]?.version).toBe("v3")
129
+ const records = await collectAll(backend2.loadAll("doc-1"))
130
+ expect(records).toHaveLength(2)
131
+ expect(records[0]?.kind).toBe("meta")
132
+ expect(records[1]?.kind).toBe("entry")
133
+ expect((records[1] as { kind: "entry"; version: string }).version).toBe(
134
+ "v3",
135
+ )
115
136
  await backend2.close()
116
137
  })
117
138
 
@@ -119,9 +140,9 @@ describe("LevelDBStore — close + reopen", () => {
119
140
  const dir = makeTmpDir()
120
141
 
121
142
  const backend1 = new LevelDBStore(dir)
122
- await backend1.ensureDoc("alpha", plainMetadata)
123
- await backend1.ensureDoc("beta", plainMetadata)
124
- await backend1.ensureDoc("gamma", plainMetadata)
143
+ await backend1.append("alpha", makeMetaRecord())
144
+ await backend1.append("beta", makeMetaRecord())
145
+ await backend1.append("gamma", makeMetaRecord())
125
146
  await backend1.close()
126
147
 
127
148
  const backend2 = new LevelDBStore(dir)
@@ -135,9 +156,26 @@ describe("LevelDBStore — close + reopen", () => {
135
156
  // encode/decode round-trip — pure function unit tests
136
157
  // ---------------------------------------------------------------------------
137
158
 
138
- describe("encodeStoreEntry / decodeStoreEntry", () => {
139
- it("round-trips a JSON string payload (entirety)", () => {
140
- const entry: StoreEntry = {
159
+ describe("encodeStoreRecord / decodeStoreRecord", () => {
160
+ it("round-trips a meta record", () => {
161
+ const record: StoreRecord = makeMetaRecord()
162
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
163
+ expect(decoded).toEqual(record)
164
+ })
165
+
166
+ it("round-trips a meta record with custom schemaHash", () => {
167
+ const record: StoreRecord = makeMetaRecord({ schemaHash: "00custom" })
168
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
169
+ expect(decoded).toEqual(record)
170
+ expect(decoded.kind).toBe("meta")
171
+ if (decoded.kind === "meta") {
172
+ expect(decoded.meta.schemaHash).toBe("00custom")
173
+ }
174
+ })
175
+
176
+ it("round-trips a JSON string payload entry (entirety)", () => {
177
+ const record: StoreRecord = {
178
+ kind: "entry",
141
179
  payload: {
142
180
  kind: "entirety",
143
181
  encoding: "json",
@@ -145,51 +183,75 @@ describe("encodeStoreEntry / decodeStoreEntry", () => {
145
183
  },
146
184
  version: "42",
147
185
  }
148
- const decoded = decodeStoreEntry(encodeStoreEntry(entry))
149
- expect(decoded).toEqual(entry)
186
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
187
+ expect(decoded).toEqual(record)
150
188
  })
151
189
 
152
- it("round-trips a binary Uint8Array payload (since)", () => {
190
+ it("round-trips a binary Uint8Array payload entry (since)", () => {
153
191
  const bytes = new Uint8Array([0x00, 0x01, 0x02, 0xff, 0xfe])
154
- const entry: StoreEntry = {
192
+ const record: StoreRecord = {
193
+ kind: "entry",
155
194
  payload: { kind: "since", encoding: "binary", data: bytes },
156
195
  version: "v7",
157
196
  }
158
- const decoded = decodeStoreEntry(encodeStoreEntry(entry))
159
- expect(decoded.version).toBe("v7")
160
- expect(decoded.payload.kind).toBe("since")
161
- expect(decoded.payload.encoding).toBe("binary")
162
- expect(decoded.payload.data).toBeInstanceOf(Uint8Array)
163
- expect(decoded.payload.data).toEqual(bytes)
197
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
198
+ expect(decoded.kind).toBe("entry")
199
+ if (decoded.kind === "entry") {
200
+ expect(decoded.version).toBe("v7")
201
+ expect(decoded.payload.kind).toBe("since")
202
+ expect(decoded.payload.encoding).toBe("binary")
203
+ expect(decoded.payload.data).toBeInstanceOf(Uint8Array)
204
+ expect(decoded.payload.data).toEqual(bytes)
205
+ }
206
+ })
207
+
208
+ it("round-trips a binary entry record via makeBinaryEntryRecord", () => {
209
+ const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef])
210
+ const record: StoreRecord = makeBinaryEntryRecord(
211
+ "entirety",
212
+ "bin-v1",
213
+ bytes,
214
+ )
215
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
216
+ expect(decoded).toEqual(record)
164
217
  })
165
218
 
166
219
  it("handles empty string data", () => {
167
- const entry: StoreEntry = {
220
+ const record: StoreRecord = {
221
+ kind: "entry",
168
222
  payload: { kind: "entirety", encoding: "json", data: "" },
169
223
  version: "v0",
170
224
  }
171
- const decoded = decodeStoreEntry(encodeStoreEntry(entry))
172
- expect(decoded).toEqual(entry)
225
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
226
+ expect(decoded).toEqual(record)
173
227
  })
174
228
 
175
229
  it("handles empty Uint8Array data", () => {
176
- const entry: StoreEntry = {
230
+ const record: StoreRecord = {
231
+ kind: "entry",
177
232
  payload: { kind: "since", encoding: "binary", data: new Uint8Array(0) },
178
233
  version: "v0",
179
234
  }
180
- const decoded = decodeStoreEntry(encodeStoreEntry(entry))
181
- expect(decoded.payload.data).toBeInstanceOf(Uint8Array)
182
- expect((decoded.payload.data as Uint8Array).length).toBe(0)
235
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
236
+ expect(decoded.kind).toBe("entry")
237
+ if (decoded.kind === "entry") {
238
+ expect(decoded.payload.data).toBeInstanceOf(Uint8Array)
239
+ expect((decoded.payload.data as Uint8Array).length).toBe(0)
240
+ }
183
241
  })
184
242
 
185
243
  it("handles empty version string", () => {
186
- const entry: StoreEntry = {
244
+ const record: StoreRecord = {
245
+ kind: "entry",
187
246
  payload: { kind: "entirety", encoding: "json", data: "{}" },
188
247
  version: "",
189
248
  }
190
- const decoded = decodeStoreEntry(encodeStoreEntry(entry))
191
- expect(decoded.version).toBe("")
192
- expect(decoded.payload.data).toBe("{}")
249
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
250
+ expect(decoded.kind).toBe("entry")
251
+ if (decoded.kind === "entry") {
252
+ expect(decoded.version).toBe("")
253
+ expect(decoded.payload.data).toBe("{}")
254
+ }
193
255
  })
194
256
 
195
257
  it("handles large binary payload", () => {
@@ -197,11 +259,40 @@ describe("encodeStoreEntry / decodeStoreEntry", () => {
197
259
  for (let i = 0; i < largeData.length; i++) {
198
260
  largeData[i] = i % 256
199
261
  }
200
- const entry: StoreEntry = {
262
+ const record: StoreRecord = {
263
+ kind: "entry",
201
264
  payload: { kind: "entirety", encoding: "binary", data: largeData },
202
265
  version: "large-v1",
203
266
  }
204
- const decoded = decodeStoreEntry(encodeStoreEntry(entry))
205
- expect(decoded.payload.data).toEqual(largeData)
267
+ const decoded = decodeStoreRecord(encodeStoreRecord(record))
268
+ expect(decoded.kind).toBe("entry")
269
+ if (decoded.kind === "entry") {
270
+ expect(decoded.payload.data).toEqual(largeData)
271
+ }
272
+ })
273
+
274
+ it("meta record flags byte has bit 3 set", () => {
275
+ const record: StoreRecord = makeMetaRecord()
276
+ const encoded = encodeStoreRecord(record)
277
+ expect(encoded[0]! & 0x08).toBe(0x08) // bit 3 set
278
+ expect(encoded[0]! & 0x80).toBe(0x00) // bit 7 clear (current format)
279
+ })
280
+
281
+ it("entry record flags byte has bit 3 clear", () => {
282
+ const record: StoreRecord = makeEntryRecord("entirety", "v1")
283
+ const encoded = encodeStoreRecord(record)
284
+ expect(encoded[0]! & 0x08).toBe(0x00) // bit 3 clear
285
+ expect(encoded[0]! & 0x80).toBe(0x00) // bit 7 clear (current format)
286
+ })
287
+
288
+ it("rejects bytes with future-format bit set", () => {
289
+ const record: StoreRecord = makeEntryRecord("entirety", "v1")
290
+ const encoded = encodeStoreRecord(record)
291
+ encoded[0] = encoded[0]! | 0x80 // set bit 7
292
+ expect(() => decodeStoreRecord(encoded)).toThrow(/future-format/)
293
+ })
294
+
295
+ it("throws on empty bytes", () => {
296
+ expect(() => decodeStoreRecord(new Uint8Array(0))).toThrow()
206
297
  })
207
298
  })
@@ -3,18 +3,25 @@
3
3
  // Implements the Store interface using classic-level.
4
4
  //
5
5
  // Key-space design (FoundationDB convention — \x00 null-byte separator):
6
- // meta\x00{docId} → JSON-encoded DocMetadata
7
- // entry\x00{docId}\x00{seqNo} → binary-encoded StoreEntry
6
+ // meta\x00{docId} → JSON-encoded StoreMeta (materialized index)
7
+ // record\x00{docId}\x00{seqNo} → binary-encoded StoreRecord (unified stream)
8
8
  //
9
9
  // The \x00 separator cannot appear in valid UTF-8 strings, so no docId
10
10
  // validation is needed — the key-space imposes zero constraints on callers.
11
11
  //
12
- // SeqNo is a zero-padded 10-digit monotonic counter per doc, tracked in
12
+ // SeqNo is a zero-padded 16-digit monotonic counter per doc, tracked in
13
13
  // memory. On reboot, the max seqNo for a doc is lazily discovered via a
14
14
  // single reverse-iterator seek on first append.
15
15
 
16
- import type { DocId, Store, StoreEntry } from "@kyneta/exchange"
17
- import type { DocMetadata } from "@kyneta/schema"
16
+ import {
17
+ type DocId,
18
+ resolveMetaFromBatch,
19
+ SeqNoTracker,
20
+ type Store,
21
+ type StoreMeta,
22
+ type StoreRecord,
23
+ validateAppend,
24
+ } from "@kyneta/exchange"
18
25
  import { ClassicLevel } from "classic-level"
19
26
 
20
27
  // ---------------------------------------------------------------------------
@@ -23,25 +30,40 @@ import { ClassicLevel } from "classic-level"
23
30
 
24
31
  const SEP = "\x00"
25
32
  const META_PREFIX = `meta${SEP}`
26
- const ENTRY_PREFIX = `entry${SEP}`
27
- const SEQ_PAD = 10
33
+ const RECORD_PREFIX = `record${SEP}`
34
+ const SEQ_PAD = 16
28
35
 
29
36
  // ---------------------------------------------------------------------------
30
- // Binary envelope — pure encode/decode for StoreEntry
37
+ // Binary envelope v2 — pure encode/decode for StoreRecord
31
38
  // ---------------------------------------------------------------------------
32
39
 
33
40
  // Flags byte layout:
34
- // bit 0: kind (0 = entirety, 1 = since)
35
- // bit 1: encoding (0 = json, 1 = binary)
36
- // bit 2: data type (0 = string, 1 = Uint8Array)
41
+ // bit 0: payload kind (0 = entirety, 1 = since) — entry records only
42
+ // bit 1: encoding (0 = json, 1 = binary) — entry records only
43
+ // bit 2: data type (0 = string, 1 = Uint8Array) — entry records only
44
+ // bit 3: record kind (0 = entry, 1 = meta)
45
+ // bit 7: future-format (0 = current format, reserved)
37
46
  //
38
- // Layout: [1 byte flags] [4 bytes version length BE] [N bytes version UTF-8] [remaining: payload data]
47
+ // Meta records (bit 3 = 1):
48
+ // [1 byte flags] [remaining: JSON-encoded StoreMeta]
49
+ //
50
+ // Entry records (bit 3 = 0):
51
+ // [1 byte flags] [4 bytes version length BE] [N bytes version UTF-8] [remaining: payload data]
39
52
 
40
53
  const encoder = new TextEncoder()
41
54
  const decoder = new TextDecoder()
42
55
 
43
- export function encodeStoreEntry(entry: StoreEntry): Uint8Array {
44
- const { payload, version } = entry
56
+ export function encodeStoreRecord(record: StoreRecord): Uint8Array {
57
+ if (record.kind === "meta") {
58
+ const metaBytes = encoder.encode(JSON.stringify(record.meta))
59
+ const buf = new Uint8Array(1 + metaBytes.length)
60
+ buf[0] = 0x08 // bit 3 set (meta)
61
+ buf.set(metaBytes, 1)
62
+ return buf
63
+ }
64
+
65
+ // Entry record
66
+ const { payload, version } = record
45
67
 
46
68
  let flags = 0
47
69
  if (payload.kind === "since") flags |= 0x01
@@ -66,12 +88,26 @@ export function encodeStoreEntry(entry: StoreEntry): Uint8Array {
66
88
  return buf
67
89
  }
68
90
 
69
- export function decodeStoreEntry(bytes: Uint8Array): StoreEntry {
70
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
71
-
91
+ export function decodeStoreRecord(bytes: Uint8Array): StoreRecord {
72
92
  const flagByte = bytes[0]
73
- if (flagByte === undefined) throw new Error("empty store entry bytes")
93
+ if (flagByte === undefined) throw new Error("empty store record bytes")
74
94
  const flags = flagByte
95
+
96
+ // Check future-format flag (bit 7)
97
+ if ((flags & 0x80) !== 0) {
98
+ throw new Error("unknown store record format (future-format bit set)")
99
+ }
100
+
101
+ const isMeta = (flags & 0x08) !== 0
102
+
103
+ if (isMeta) {
104
+ const metaJson = decoder.decode(bytes.subarray(1))
105
+ return { kind: "meta", meta: JSON.parse(metaJson) as StoreMeta }
106
+ }
107
+
108
+ // Entry record
109
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
110
+
75
111
  const kind = (flags & 0x01) !== 0 ? "since" : "entirety"
76
112
  const encoding = (flags & 0x02) !== 0 ? "binary" : "json"
77
113
  const isDataBinary = (flags & 0x04) !== 0
@@ -87,6 +123,7 @@ export function decodeStoreEntry(bytes: Uint8Array): StoreEntry {
87
123
  : decoder.decode(rawData)
88
124
 
89
125
  return {
126
+ kind: "entry",
90
127
  payload: { kind, encoding, data },
91
128
  version,
92
129
  }
@@ -100,20 +137,20 @@ function metaKey(docId: DocId): string {
100
137
  return `${META_PREFIX}${docId}`
101
138
  }
102
139
 
103
- function entryPrefix(docId: DocId): string {
104
- return `${ENTRY_PREFIX}${docId}${SEP}`
140
+ function recordPrefix(docId: DocId): string {
141
+ return `${RECORD_PREFIX}${docId}${SEP}`
105
142
  }
106
143
 
107
- function entryKey(docId: DocId, seqNo: number): string {
108
- return `${entryPrefix(docId)}${String(seqNo).padStart(SEQ_PAD, "0")}`
144
+ function recordKey(docId: DocId, seqNo: number): string {
145
+ return `${recordPrefix(docId)}${String(seqNo).padStart(SEQ_PAD, "0")}`
109
146
  }
110
147
 
111
148
  function parseDocIdFromMetaKey(key: string): DocId {
112
149
  return key.slice(META_PREFIX.length)
113
150
  }
114
151
 
115
- function parseSeqNoFromEntryKey(key: string, docId: DocId): number {
116
- const prefix = entryPrefix(docId)
152
+ function parseSeqNoFromRecordKey(key: string, docId: DocId): number {
153
+ const prefix = recordPrefix(docId)
117
154
  return Number.parseInt(key.slice(prefix.length), 10)
118
155
  }
119
156
 
@@ -123,7 +160,7 @@ function parseSeqNoFromEntryKey(key: string, docId: DocId): number {
123
160
 
124
161
  export class LevelDBStore implements Store {
125
162
  readonly #db: ClassicLevel<string, Uint8Array>
126
- readonly #seqNos: Map<DocId, number> = new Map()
163
+ readonly #seqNos = new SeqNoTracker()
127
164
 
128
165
  constructor(dbPath: string) {
129
166
  this.#db = new ClassicLevel(dbPath, {
@@ -131,88 +168,65 @@ export class LevelDBStore implements Store {
131
168
  })
132
169
  }
133
170
 
134
- // -------------------------------------------------------------------------
135
- // Private — seqNo management
136
- // -------------------------------------------------------------------------
137
-
138
- /**
139
- * Get the next seqNo for a doc. On first call after reboot, discovers
140
- * the max existing seqNo via a single reverse-iterator seek.
141
- */
142
- async #nextSeqNo(docId: DocId): Promise<number> {
143
- const cached = this.#seqNos.get(docId)
144
- if (cached !== undefined) {
145
- const next = cached + 1
146
- this.#seqNos.set(docId, next)
147
- return next
148
- }
149
-
150
- // Lazy discovery: reverse iterate to find the highest existing seqNo
151
- const prefix = entryPrefix(docId)
152
- let maxSeq = -1
153
- for await (const key of this.#db.keys({
154
- gte: prefix,
155
- lt: `${prefix}\xff`,
156
- reverse: true,
157
- limit: 1,
158
- })) {
159
- maxSeq = parseSeqNoFromEntryKey(key, docId)
160
- }
161
-
162
- const next = maxSeq + 1
163
- this.#seqNos.set(docId, next)
164
- return next
165
- }
166
-
167
171
  // -------------------------------------------------------------------------
168
172
  // Store interface
169
173
  // -------------------------------------------------------------------------
170
174
 
171
- async lookup(docId: DocId): Promise<DocMetadata | null> {
175
+ async currentMeta(docId: DocId): Promise<StoreMeta | null> {
172
176
  try {
173
177
  const raw = await this.#db.get(metaKey(docId))
174
- return JSON.parse(decoder.decode(raw)) as DocMetadata
178
+ return JSON.parse(decoder.decode(raw)) as StoreMeta
175
179
  } catch (error: any) {
176
180
  if (error.code === "LEVEL_NOT_FOUND") return null
177
181
  throw error
178
182
  }
179
183
  }
180
184
 
181
- async ensureDoc(docId: DocId, metadata: DocMetadata): Promise<void> {
182
- try {
183
- await this.#db.get(metaKey(docId))
184
- // Already exists — no-op (first call wins)
185
- } catch (error: any) {
186
- if (error.code === "LEVEL_NOT_FOUND") {
187
- await this.#db.put(
188
- metaKey(docId),
189
- encoder.encode(JSON.stringify(metadata)),
190
- )
191
- return
192
- }
193
- throw error
185
+ async append(docId: DocId, record: StoreRecord): Promise<void> {
186
+ const existingMeta = await this.currentMeta(docId)
187
+ const resolved = validateAppend(docId, record, existingMeta)
188
+
189
+ if (resolved !== null) {
190
+ await this.#db.put(
191
+ metaKey(docId),
192
+ encoder.encode(JSON.stringify(resolved)),
193
+ )
194
194
  }
195
- }
196
195
 
197
- async append(docId: DocId, entry: StoreEntry): Promise<void> {
198
- const seq = await this.#nextSeqNo(docId)
199
- await this.#db.put(entryKey(docId, seq), encodeStoreEntry(entry))
196
+ const seq = await this.#seqNos.next(docId, async () => {
197
+ const prefix = recordPrefix(docId)
198
+ for await (const key of this.#db.keys({
199
+ gte: prefix,
200
+ lt: `${prefix}\xff`,
201
+ reverse: true,
202
+ limit: 1,
203
+ })) {
204
+ return parseSeqNoFromRecordKey(key, docId)
205
+ }
206
+ return null
207
+ })
208
+ await this.#db.put(recordKey(docId, seq), encodeStoreRecord(record))
200
209
  }
201
210
 
202
- async *loadAll(docId: DocId): AsyncIterable<StoreEntry> {
203
- const prefix = entryPrefix(docId)
211
+ async *loadAll(docId: DocId): AsyncIterable<StoreRecord> {
212
+ const prefix = recordPrefix(docId)
204
213
  for await (const value of this.#db.values({
205
214
  gte: prefix,
206
215
  lt: `${prefix}\xff`,
207
216
  })) {
208
- yield decodeStoreEntry(value)
217
+ yield decodeStoreRecord(value)
209
218
  }
210
219
  }
211
220
 
212
- async replace(docId: DocId, entry: StoreEntry): Promise<void> {
213
- const prefix = entryPrefix(docId)
221
+ async replace(docId: DocId, records: StoreRecord[]): Promise<void> {
222
+ const existingMeta = await this.currentMeta(docId)
223
+
224
+ // Resolve validates: at least one meta present, immutable fields match.
225
+ const resolved = resolveMetaFromBatch(records, existingMeta)
214
226
 
215
- // Collect keys to delete
227
+ const prefix = recordPrefix(docId)
228
+
229
+ // Collect existing record keys to delete
216
230
  const keysToDelete: string[] = []
217
231
  for await (const key of this.#db.keys({
218
232
  gte: prefix,
@@ -221,26 +235,36 @@ export class LevelDBStore implements Store {
221
235
  keysToDelete.push(key)
222
236
  }
223
237
 
224
- // Atomic batch: delete all existing entries + write the single replacement
238
+ // Atomic batch: delete all existing records, write replacements, upsert meta
225
239
  const ops: Array<
226
240
  | { type: "del"; key: string }
227
241
  | { type: "put"; key: string; value: Uint8Array }
228
242
  > = keysToDelete.map(key => ({ type: "del" as const, key }))
243
+
244
+ for (let i = 0; i < records.length; i++) {
245
+ ops.push({
246
+ type: "put",
247
+ key: recordKey(docId, i),
248
+ value: encodeStoreRecord(records[i]!),
249
+ })
250
+ }
251
+
229
252
  ops.push({
230
253
  type: "put",
231
- key: entryKey(docId, 0),
232
- value: encodeStoreEntry(entry),
254
+ key: metaKey(docId),
255
+ value: encoder.encode(JSON.stringify(resolved)),
233
256
  })
257
+
234
258
  await this.#db.batch(ops)
235
259
 
236
- // Reset seqNo counter
237
- this.#seqNos.set(docId, 0)
260
+ // Reset seqNo counter to the last written index
261
+ this.#seqNos.reset(docId, records.length - 1)
238
262
  }
239
263
 
240
264
  async delete(docId: DocId): Promise<void> {
241
- const prefix = entryPrefix(docId)
265
+ const prefix = recordPrefix(docId)
242
266
 
243
- // Collect entry keys to delete
267
+ // Collect record keys to delete
244
268
  const keysToDelete: string[] = [metaKey(docId)]
245
269
  for await (const key of this.#db.keys({
246
270
  gte: prefix,
@@ -254,13 +278,15 @@ export class LevelDBStore implements Store {
254
278
  )
255
279
 
256
280
  // Remove from in-memory seqNo tracker
257
- this.#seqNos.delete(docId)
281
+ this.#seqNos.remove(docId)
258
282
  }
259
283
 
260
- async *listDocIds(): AsyncIterable<DocId> {
284
+ async *listDocIds(prefix?: string): AsyncIterable<DocId> {
285
+ const rangePrefix =
286
+ prefix !== undefined ? `${META_PREFIX}${prefix}` : META_PREFIX
261
287
  for await (const key of this.#db.keys({
262
- gte: META_PREFIX,
263
- lt: `${META_PREFIX}\xff`,
288
+ gte: rangePrefix,
289
+ lt: `${rangePrefix}\xff`,
264
290
  })) {
265
291
  yield parseDocIdFromMetaKey(key)
266
292
  }
@@ -284,7 +310,7 @@ export class LevelDBStore implements Store {
284
310
  *
285
311
  * @example
286
312
  * ```typescript
287
- * import { createLevelDBStore } from "@kyneta/leveldb-store/server"
313
+ * import { createLevelDBStore } from "@kyneta/leveldb-store"
288
314
  *
289
315
  * const exchange = new Exchange({
290
316
  * stores: [createLevelDBStore("./data/exchange-db")],
package/dist/server.d.ts DELETED
@@ -1,36 +0,0 @@
1
- import { Store, DocId, StoreEntry } from '@kyneta/exchange';
2
- import { DocMetadata } from '@kyneta/schema';
3
-
4
- declare function encodeStoreEntry(entry: StoreEntry): Uint8Array;
5
- declare function decodeStoreEntry(bytes: Uint8Array): StoreEntry;
6
- declare class LevelDBStore implements Store {
7
- #private;
8
- constructor(dbPath: string);
9
- lookup(docId: DocId): Promise<DocMetadata | null>;
10
- ensureDoc(docId: DocId, metadata: DocMetadata): Promise<void>;
11
- append(docId: DocId, entry: StoreEntry): Promise<void>;
12
- loadAll(docId: DocId): AsyncIterable<StoreEntry>;
13
- replace(docId: DocId, entry: StoreEntry): Promise<void>;
14
- delete(docId: DocId): Promise<void>;
15
- listDocIds(): AsyncIterable<DocId>;
16
- close(): Promise<void>;
17
- }
18
- /**
19
- * Create a LevelDB storage backend for server-side persistence.
20
- *
21
- * Returns a `Store` — pass directly to `Exchange({ stores: [...] })`.
22
- *
23
- * @param dbPath - Directory path where LevelDB stores its files
24
- *
25
- * @example
26
- * ```typescript
27
- * import { createLevelDBStore } from "@kyneta/leveldb-store/server"
28
- *
29
- * const exchange = new Exchange({
30
- * stores: [createLevelDBStore("./data/exchange-db")],
31
- * })
32
- * ```
33
- */
34
- declare function createLevelDBStore(dbPath: string): Store;
35
-
36
- export { LevelDBStore, createLevelDBStore, decodeStoreEntry, encodeStoreEntry };
package/dist/server.js DELETED
@@ -1,188 +0,0 @@
1
- // src/server.ts
2
- import { ClassicLevel } from "classic-level";
3
- var SEP = "\0";
4
- var META_PREFIX = `meta${SEP}`;
5
- var ENTRY_PREFIX = `entry${SEP}`;
6
- var SEQ_PAD = 10;
7
- var encoder = new TextEncoder();
8
- var decoder = new TextDecoder();
9
- function encodeStoreEntry(entry) {
10
- const { payload, version } = entry;
11
- let flags = 0;
12
- if (payload.kind === "since") flags |= 1;
13
- if (payload.encoding === "binary") flags |= 2;
14
- const isDataBinary = payload.data instanceof Uint8Array;
15
- if (isDataBinary) flags |= 4;
16
- const versionBytes = encoder.encode(version);
17
- const dataBytes = isDataBinary ? payload.data : encoder.encode(payload.data);
18
- const buf = new Uint8Array(1 + 4 + versionBytes.length + dataBytes.length);
19
- const view = new DataView(buf.buffer);
20
- buf[0] = flags;
21
- view.setUint32(1, versionBytes.length, false);
22
- buf.set(versionBytes, 5);
23
- buf.set(dataBytes, 5 + versionBytes.length);
24
- return buf;
25
- }
26
- function decodeStoreEntry(bytes) {
27
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
28
- const flagByte = bytes[0];
29
- if (flagByte === void 0) throw new Error("empty store entry bytes");
30
- const flags = flagByte;
31
- const kind = (flags & 1) !== 0 ? "since" : "entirety";
32
- const encoding = (flags & 2) !== 0 ? "binary" : "json";
33
- const isDataBinary = (flags & 4) !== 0;
34
- const versionLen = view.getUint32(1, false);
35
- const version = decoder.decode(bytes.subarray(5, 5 + versionLen));
36
- const dataStart = 5 + versionLen;
37
- const rawData = bytes.subarray(dataStart);
38
- const data = isDataBinary ? new Uint8Array(rawData) : decoder.decode(rawData);
39
- return {
40
- payload: { kind, encoding, data },
41
- version
42
- };
43
- }
44
- function metaKey(docId) {
45
- return `${META_PREFIX}${docId}`;
46
- }
47
- function entryPrefix(docId) {
48
- return `${ENTRY_PREFIX}${docId}${SEP}`;
49
- }
50
- function entryKey(docId, seqNo) {
51
- return `${entryPrefix(docId)}${String(seqNo).padStart(SEQ_PAD, "0")}`;
52
- }
53
- function parseDocIdFromMetaKey(key) {
54
- return key.slice(META_PREFIX.length);
55
- }
56
- function parseSeqNoFromEntryKey(key, docId) {
57
- const prefix = entryPrefix(docId);
58
- return Number.parseInt(key.slice(prefix.length), 10);
59
- }
60
- var LevelDBStore = class {
61
- #db;
62
- #seqNos = /* @__PURE__ */ new Map();
63
- constructor(dbPath) {
64
- this.#db = new ClassicLevel(dbPath, {
65
- valueEncoding: "binary"
66
- });
67
- }
68
- // -------------------------------------------------------------------------
69
- // Private — seqNo management
70
- // -------------------------------------------------------------------------
71
- /**
72
- * Get the next seqNo for a doc. On first call after reboot, discovers
73
- * the max existing seqNo via a single reverse-iterator seek.
74
- */
75
- async #nextSeqNo(docId) {
76
- const cached = this.#seqNos.get(docId);
77
- if (cached !== void 0) {
78
- const next2 = cached + 1;
79
- this.#seqNos.set(docId, next2);
80
- return next2;
81
- }
82
- const prefix = entryPrefix(docId);
83
- let maxSeq = -1;
84
- for await (const key of this.#db.keys({
85
- gte: prefix,
86
- lt: `${prefix}\xFF`,
87
- reverse: true,
88
- limit: 1
89
- })) {
90
- maxSeq = parseSeqNoFromEntryKey(key, docId);
91
- }
92
- const next = maxSeq + 1;
93
- this.#seqNos.set(docId, next);
94
- return next;
95
- }
96
- // -------------------------------------------------------------------------
97
- // Store interface
98
- // -------------------------------------------------------------------------
99
- async lookup(docId) {
100
- try {
101
- const raw = await this.#db.get(metaKey(docId));
102
- return JSON.parse(decoder.decode(raw));
103
- } catch (error) {
104
- if (error.code === "LEVEL_NOT_FOUND") return null;
105
- throw error;
106
- }
107
- }
108
- async ensureDoc(docId, metadata) {
109
- try {
110
- await this.#db.get(metaKey(docId));
111
- } catch (error) {
112
- if (error.code === "LEVEL_NOT_FOUND") {
113
- await this.#db.put(
114
- metaKey(docId),
115
- encoder.encode(JSON.stringify(metadata))
116
- );
117
- return;
118
- }
119
- throw error;
120
- }
121
- }
122
- async append(docId, entry) {
123
- const seq = await this.#nextSeqNo(docId);
124
- await this.#db.put(entryKey(docId, seq), encodeStoreEntry(entry));
125
- }
126
- async *loadAll(docId) {
127
- const prefix = entryPrefix(docId);
128
- for await (const value of this.#db.values({
129
- gte: prefix,
130
- lt: `${prefix}\xFF`
131
- })) {
132
- yield decodeStoreEntry(value);
133
- }
134
- }
135
- async replace(docId, entry) {
136
- const prefix = entryPrefix(docId);
137
- const keysToDelete = [];
138
- for await (const key of this.#db.keys({
139
- gte: prefix,
140
- lt: `${prefix}\xFF`
141
- })) {
142
- keysToDelete.push(key);
143
- }
144
- const ops = keysToDelete.map((key) => ({ type: "del", key }));
145
- ops.push({
146
- type: "put",
147
- key: entryKey(docId, 0),
148
- value: encodeStoreEntry(entry)
149
- });
150
- await this.#db.batch(ops);
151
- this.#seqNos.set(docId, 0);
152
- }
153
- async delete(docId) {
154
- const prefix = entryPrefix(docId);
155
- const keysToDelete = [metaKey(docId)];
156
- for await (const key of this.#db.keys({
157
- gte: prefix,
158
- lt: `${prefix}\xFF`
159
- })) {
160
- keysToDelete.push(key);
161
- }
162
- await this.#db.batch(
163
- keysToDelete.map((key) => ({ type: "del", key }))
164
- );
165
- this.#seqNos.delete(docId);
166
- }
167
- async *listDocIds() {
168
- for await (const key of this.#db.keys({
169
- gte: META_PREFIX,
170
- lt: `${META_PREFIX}\xFF`
171
- })) {
172
- yield parseDocIdFromMetaKey(key);
173
- }
174
- }
175
- async close() {
176
- await this.#db.close();
177
- }
178
- };
179
- function createLevelDBStore(dbPath) {
180
- return new LevelDBStore(dbPath);
181
- }
182
- export {
183
- LevelDBStore,
184
- createLevelDBStore,
185
- decodeStoreEntry,
186
- encodeStoreEntry
187
- };
188
- //# sourceMappingURL=server.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["// server — LevelDB storage backend for @kyneta/exchange.\n//\n// Implements the Store interface using classic-level.\n//\n// Key-space design (FoundationDB convention — \\x00 null-byte separator):\n// meta\\x00{docId} → JSON-encoded DocMetadata\n// entry\\x00{docId}\\x00{seqNo} → binary-encoded StoreEntry\n//\n// The \\x00 separator cannot appear in valid UTF-8 strings, so no docId\n// validation is needed — the key-space imposes zero constraints on callers.\n//\n// SeqNo is a zero-padded 10-digit monotonic counter per doc, tracked in\n// memory. On reboot, the max seqNo for a doc is lazily discovered via a\n// single reverse-iterator seek on first append.\n\nimport type { DocId, Store, StoreEntry } from \"@kyneta/exchange\"\nimport type { DocMetadata } from \"@kyneta/schema\"\nimport { ClassicLevel } from \"classic-level\"\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SEP = \"\\x00\"\nconst META_PREFIX = `meta${SEP}`\nconst ENTRY_PREFIX = `entry${SEP}`\nconst SEQ_PAD = 10\n\n// ---------------------------------------------------------------------------\n// Binary envelope — pure encode/decode for StoreEntry\n// ---------------------------------------------------------------------------\n\n// Flags byte layout:\n// bit 0: kind (0 = entirety, 1 = since)\n// bit 1: encoding (0 = json, 1 = binary)\n// bit 2: data type (0 = string, 1 = Uint8Array)\n//\n// Layout: [1 byte flags] [4 bytes version length BE] [N bytes version UTF-8] [remaining: payload data]\n\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder()\n\nexport function encodeStoreEntry(entry: StoreEntry): Uint8Array {\n const { payload, version } = entry\n\n let flags = 0\n if (payload.kind === \"since\") flags |= 0x01\n if (payload.encoding === \"binary\") flags |= 0x02\n\n const isDataBinary = payload.data instanceof Uint8Array\n if (isDataBinary) flags |= 0x04\n\n const versionBytes = encoder.encode(version)\n const dataBytes = isDataBinary\n ? (payload.data as Uint8Array)\n : encoder.encode(payload.data as string)\n\n const buf = new Uint8Array(1 + 4 + versionBytes.length + dataBytes.length)\n const view = new DataView(buf.buffer)\n\n buf[0] = flags\n view.setUint32(1, versionBytes.length, false) // big-endian\n buf.set(versionBytes, 5)\n buf.set(dataBytes, 5 + versionBytes.length)\n\n return buf\n}\n\nexport function decodeStoreEntry(bytes: Uint8Array): StoreEntry {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n\n const flagByte = bytes[0]\n if (flagByte === undefined) throw new Error(\"empty store entry bytes\")\n const flags = flagByte\n const kind = (flags & 0x01) !== 0 ? \"since\" : \"entirety\"\n const encoding = (flags & 0x02) !== 0 ? \"binary\" : \"json\"\n const isDataBinary = (flags & 0x04) !== 0\n\n const versionLen = view.getUint32(1, false)\n const version = decoder.decode(bytes.subarray(5, 5 + versionLen))\n\n const dataStart = 5 + versionLen\n const rawData = bytes.subarray(dataStart)\n\n const data: string | Uint8Array = isDataBinary\n ? new Uint8Array(rawData)\n : decoder.decode(rawData)\n\n return {\n payload: { kind, encoding, data },\n version,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Key helpers\n// ---------------------------------------------------------------------------\n\nfunction metaKey(docId: DocId): string {\n return `${META_PREFIX}${docId}`\n}\n\nfunction entryPrefix(docId: DocId): string {\n return `${ENTRY_PREFIX}${docId}${SEP}`\n}\n\nfunction entryKey(docId: DocId, seqNo: number): string {\n return `${entryPrefix(docId)}${String(seqNo).padStart(SEQ_PAD, \"0\")}`\n}\n\nfunction parseDocIdFromMetaKey(key: string): DocId {\n return key.slice(META_PREFIX.length)\n}\n\nfunction parseSeqNoFromEntryKey(key: string, docId: DocId): number {\n const prefix = entryPrefix(docId)\n return Number.parseInt(key.slice(prefix.length), 10)\n}\n\n// ---------------------------------------------------------------------------\n// LevelDBStore\n// ---------------------------------------------------------------------------\n\nexport class LevelDBStore implements Store {\n readonly #db: ClassicLevel<string, Uint8Array>\n readonly #seqNos: Map<DocId, number> = new Map()\n\n constructor(dbPath: string) {\n this.#db = new ClassicLevel(dbPath, {\n valueEncoding: \"binary\",\n })\n }\n\n // -------------------------------------------------------------------------\n // Private — seqNo management\n // -------------------------------------------------------------------------\n\n /**\n * Get the next seqNo for a doc. On first call after reboot, discovers\n * the max existing seqNo via a single reverse-iterator seek.\n */\n async #nextSeqNo(docId: DocId): Promise<number> {\n const cached = this.#seqNos.get(docId)\n if (cached !== undefined) {\n const next = cached + 1\n this.#seqNos.set(docId, next)\n return next\n }\n\n // Lazy discovery: reverse iterate to find the highest existing seqNo\n const prefix = entryPrefix(docId)\n let maxSeq = -1\n for await (const key of this.#db.keys({\n gte: prefix,\n lt: `${prefix}\\xff`,\n reverse: true,\n limit: 1,\n })) {\n maxSeq = parseSeqNoFromEntryKey(key, docId)\n }\n\n const next = maxSeq + 1\n this.#seqNos.set(docId, next)\n return next\n }\n\n // -------------------------------------------------------------------------\n // Store interface\n // -------------------------------------------------------------------------\n\n async lookup(docId: DocId): Promise<DocMetadata | null> {\n try {\n const raw = await this.#db.get(metaKey(docId))\n return JSON.parse(decoder.decode(raw)) as DocMetadata\n } catch (error: any) {\n if (error.code === \"LEVEL_NOT_FOUND\") return null\n throw error\n }\n }\n\n async ensureDoc(docId: DocId, metadata: DocMetadata): Promise<void> {\n try {\n await this.#db.get(metaKey(docId))\n // Already exists — no-op (first call wins)\n } catch (error: any) {\n if (error.code === \"LEVEL_NOT_FOUND\") {\n await this.#db.put(\n metaKey(docId),\n encoder.encode(JSON.stringify(metadata)),\n )\n return\n }\n throw error\n }\n }\n\n async append(docId: DocId, entry: StoreEntry): Promise<void> {\n const seq = await this.#nextSeqNo(docId)\n await this.#db.put(entryKey(docId, seq), encodeStoreEntry(entry))\n }\n\n async *loadAll(docId: DocId): AsyncIterable<StoreEntry> {\n const prefix = entryPrefix(docId)\n for await (const value of this.#db.values({\n gte: prefix,\n lt: `${prefix}\\xff`,\n })) {\n yield decodeStoreEntry(value)\n }\n }\n\n async replace(docId: DocId, entry: StoreEntry): Promise<void> {\n const prefix = entryPrefix(docId)\n\n // Collect keys to delete\n const keysToDelete: string[] = []\n for await (const key of this.#db.keys({\n gte: prefix,\n lt: `${prefix}\\xff`,\n })) {\n keysToDelete.push(key)\n }\n\n // Atomic batch: delete all existing entries + write the single replacement\n const ops: Array<\n | { type: \"del\"; key: string }\n | { type: \"put\"; key: string; value: Uint8Array }\n > = keysToDelete.map(key => ({ type: \"del\" as const, key }))\n ops.push({\n type: \"put\",\n key: entryKey(docId, 0),\n value: encodeStoreEntry(entry),\n })\n await this.#db.batch(ops)\n\n // Reset seqNo counter\n this.#seqNos.set(docId, 0)\n }\n\n async delete(docId: DocId): Promise<void> {\n const prefix = entryPrefix(docId)\n\n // Collect entry keys to delete\n const keysToDelete: string[] = [metaKey(docId)]\n for await (const key of this.#db.keys({\n gte: prefix,\n lt: `${prefix}\\xff`,\n })) {\n keysToDelete.push(key)\n }\n\n await this.#db.batch(\n keysToDelete.map(key => ({ type: \"del\" as const, key })),\n )\n\n // Remove from in-memory seqNo tracker\n this.#seqNos.delete(docId)\n }\n\n async *listDocIds(): AsyncIterable<DocId> {\n for await (const key of this.#db.keys({\n gte: META_PREFIX,\n lt: `${META_PREFIX}\\xff`,\n })) {\n yield parseDocIdFromMetaKey(key)\n }\n }\n\n async close(): Promise<void> {\n await this.#db.close()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Create a LevelDB storage backend for server-side persistence.\n *\n * Returns a `Store` — pass directly to `Exchange({ stores: [...] })`.\n *\n * @param dbPath - Directory path where LevelDB stores its files\n *\n * @example\n * ```typescript\n * import { createLevelDBStore } from \"@kyneta/leveldb-store/server\"\n *\n * const exchange = new Exchange({\n * stores: [createLevelDBStore(\"./data/exchange-db\")],\n * })\n * ```\n */\nexport function createLevelDBStore(dbPath: string): Store {\n return new LevelDBStore(dbPath)\n}\n"],"mappings":"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","names":["next"]}