@kyneta/leveldb-store 1.5.1 → 1.6.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.
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;iBAuDgB,iBAAA,CAAkB,MAAA,EAAQ,WAAA,GAAc,UAAU;AAAA,iBAmClD,iBAAA,CAAkB,KAAA,EAAO,UAAA,GAAa,WAAW;AAAA,cAsEpD,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;;;;;;;AA5MgD;AAsEjE;;;;;;;;;iBA+JgB,kBAAA,CAAmB,MAAA,WAAiB,KAAK"}
package/dist/index.js.map CHANGED
@@ -1 +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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"}
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":";;;AA8BA,MAAM,MAAM;AACZ,MAAM,cAAc,OAAO;AAC3B,MAAM,gBAAgB,SAAS;AAC/B,MAAM,UAAU;AAmBhB,MAAM,UAAU,IAAI,YAAY;AAChC,MAAM,UAAU,IAAI,YAAY;AAEhC,SAAgB,kBAAkB,QAAiC;CACjE,IAAI,OAAO,SAAS,QAAQ;EAC1B,MAAM,YAAY,QAAQ,OAAO,KAAK,UAAU,OAAO,IAAI,CAAC;EAC5D,MAAM,MAAM,IAAI,WAAW,IAAI,UAAU,MAAM;EAC/C,IAAI,KAAK;EACT,IAAI,IAAI,WAAW,CAAC;EACpB,OAAO;CACT;CAGA,MAAM,EAAE,SAAS,YAAY;CAE7B,IAAI,QAAQ;CACZ,IAAI,QAAQ,SAAS,SAAS,SAAS;CACvC,IAAI,QAAQ,aAAa,UAAU,SAAS;CAE5C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,IAAI,cAAc,SAAS;CAE3B,MAAM,eAAe,QAAQ,OAAO,OAAO;CAC3C,MAAM,YAAY,eACb,QAAQ,OACT,QAAQ,OAAO,QAAQ,IAAc;CAEzC,MAAM,MAAM,IAAI,WAAW,IAAQ,aAAa,SAAS,UAAU,MAAM;CACzE,MAAM,OAAO,IAAI,SAAS,IAAI,MAAM;CAEpC,IAAI,KAAK;CACT,KAAK,UAAU,GAAG,aAAa,QAAQ,KAAK;CAC5C,IAAI,IAAI,cAAc,CAAC;CACvB,IAAI,IAAI,WAAW,IAAI,aAAa,MAAM;CAE1C,OAAO;AACT;AAEA,SAAgB,kBAAkB,OAAgC;CAChE,MAAM,WAAW,MAAM;CACvB,IAAI,aAAa,KAAA,GAAW,MAAM,IAAI,MAAM,0BAA0B;CACtE,MAAM,QAAQ;CAGd,KAAK,QAAQ,SAAU,GACrB,MAAM,IAAI,MAAM,qDAAqD;CAKvE,KAFgB,QAAQ,OAAU,GAEtB;EACV,MAAM,WAAW,QAAQ,OAAO,MAAM,SAAS,CAAC,CAAC;EACjD,OAAO;GAAE,MAAM;GAAQ,MAAM,KAAK,MAAM,QAAQ;EAAe;CACjE;CAGA,MAAM,OAAO,IAAI,SAAS,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;CAE1E,MAAM,QAAQ,QAAQ,OAAU,IAAI,UAAU;CAC9C,MAAM,YAAY,QAAQ,OAAU,IAAI,WAAW;CACnD,MAAM,gBAAgB,QAAQ,OAAU;CAExC,MAAM,aAAa,KAAK,UAAU,GAAG,KAAK;CAC1C,MAAM,UAAU,QAAQ,OAAO,MAAM,SAAS,GAAG,IAAI,UAAU,CAAC;CAEhE,MAAM,YAAY,IAAI;CACtB,MAAM,UAAU,MAAM,SAAS,SAAS;CAMxC,OAAO;EACL,MAAM;EACN,SAAS;GAAE;GAAM;GAAU,MANK,eAC9B,IAAI,WAAW,OAAO,IACtB,QAAQ,OAAO,OAAO;EAIQ;EAChC;CACF;AACF;AAMA,SAAS,QAAQ,OAAsB;CACrC,OAAO,GAAG,cAAc;AAC1B;AAEA,SAAS,aAAa,OAAsB;CAC1C,OAAO,GAAG,gBAAgB,QAAQ;AACpC;AAEA,SAAS,UAAU,OAAc,OAAuB;CACtD,OAAO,GAAG,aAAa,KAAK,IAAI,OAAO,KAAK,EAAE,SAAS,SAAS,GAAG;AACrE;AAEA,SAAS,sBAAsB,KAAoB;CACjD,OAAO,IAAI,MAAM,YAAY,MAAM;AACrC;AAEA,SAAS,wBAAwB,KAAa,OAAsB;CAClE,MAAM,SAAS,aAAa,KAAK;CACjC,OAAO,OAAO,SAAS,IAAI,MAAM,OAAO,MAAM,GAAG,EAAE;AACrD;AAMA,IAAa,eAAb,MAA2C;CACzC;CACA,UAAmB,IAAI,aAAa;CAEpC,YAAY,QAAgB;EAC1B,KAAKA,MAAM,IAAI,aAAa,QAAQ,EAClC,eAAe,SACjB,CAAC;CACH;CAMA,MAAM,YAAY,OAAyC;EACzD,IAAI;GACF,MAAM,MAAM,MAAM,KAAKA,IAAI,IAAI,QAAQ,KAAK,CAAC;GAC7C,OAAO,KAAK,MAAM,QAAQ,OAAO,GAAG,CAAC;EACvC,SAAS,OAAY;GACnB,IAAI,MAAM,SAAS,mBAAmB,OAAO;GAC7C,MAAM;EACR;CACF;CAEA,MAAM,OAAO,OAAc,QAAoC;EAE7D,MAAM,WAAW,eAAe,OAAO,QAAQ,MADpB,KAAK,YAAY,KAAK,CACU;EAE3D,IAAI,aAAa,MACf,MAAM,KAAKA,IAAI,IACb,QAAQ,KAAK,GACb,QAAQ,OAAO,KAAK,UAAU,QAAQ,CAAC,CACzC;EAGF,MAAM,MAAM,MAAM,KAAKC,QAAQ,KAAK,OAAO,YAAY;GACrD,MAAM,SAAS,aAAa,KAAK;GACjC,WAAW,MAAM,OAAO,KAAKD,IAAI,KAAK;IACpC,KAAK;IACL,IAAI,GAAG,OAAO;IACd,SAAS;IACT,OAAO;GACT,CAAC,GACC,OAAO,wBAAwB,KAAK,KAAK;GAE3C,OAAO;EACT,CAAC;EACD,MAAM,KAAKA,IAAI,IAAI,UAAU,OAAO,GAAG,GAAG,kBAAkB,MAAM,CAAC;CACrE;CAEA,OAAO,QAAQ,OAA0C;EACvD,MAAM,SAAS,aAAa,KAAK;EACjC,WAAW,MAAM,SAAS,KAAKA,IAAI,OAAO;GACxC,KAAK;GACL,IAAI,GAAG,OAAO;EAChB,CAAC,GACC,MAAM,kBAAkB,KAAK;CAEjC;CAEA,MAAM,QAAQ,OAAc,SAAuC;EAIjE,MAAM,WAAW,qBAAqB,SAAS,MAHpB,KAAK,YAAY,KAAK,CAGU;EAE3D,MAAM,SAAS,aAAa,KAAK;EAGjC,MAAM,eAAyB,CAAC;EAChC,WAAW,MAAM,OAAO,KAAKA,IAAI,KAAK;GACpC,KAAK;GACL,IAAI,GAAG,OAAO;EAChB,CAAC,GACC,aAAa,KAAK,GAAG;EAIvB,MAAM,MAGF,aAAa,KAAI,SAAQ;GAAE,MAAM;GAAgB;EAAI,EAAE;EAE3D,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAClC,IAAI,KAAK;GACP,MAAM;GACN,KAAK,UAAU,OAAO,CAAC;GACvB,OAAO,kBAAkB,QAAQ,EAAG;EACtC,CAAC;EAGH,IAAI,KAAK;GACP,MAAM;GACN,KAAK,QAAQ,KAAK;GAClB,OAAO,QAAQ,OAAO,KAAK,UAAU,QAAQ,CAAC;EAChD,CAAC;EAED,MAAM,KAAKA,IAAI,MAAM,GAAG;EAGxB,KAAKC,QAAQ,MAAM,OAAO,QAAQ,SAAS,CAAC;CAC9C;CAEA,MAAM,OAAO,OAA6B;EACxC,MAAM,SAAS,aAAa,KAAK;EAGjC,MAAM,eAAyB,CAAC,QAAQ,KAAK,CAAC;EAC9C,WAAW,MAAM,OAAO,KAAKD,IAAI,KAAK;GACpC,KAAK;GACL,IAAI,GAAG,OAAO;EAChB,CAAC,GACC,aAAa,KAAK,GAAG;EAGvB,MAAM,KAAKA,IAAI,MACb,aAAa,KAAI,SAAQ;GAAE,MAAM;GAAgB;EAAI,EAAE,CACzD;EAGA,KAAKC,QAAQ,OAAO,KAAK;CAC3B;CAEA,OAAO,WAAW,QAAuC;EACvD,MAAM,cACJ,WAAW,KAAA,IAAY,GAAG,cAAc,WAAW;EACrD,WAAW,MAAM,OAAO,KAAKD,IAAI,KAAK;GACpC,KAAK;GACL,IAAI,GAAG,YAAY;EACrB,CAAC,GACC,MAAM,sBAAsB,GAAG;CAEnC;CAEA,MAAM,QAAuB;EAC3B,MAAM,KAAKA,IAAI,MAAM;CACvB;AACF;;;;;;;;;;;;;;;;;AAsBA,SAAgB,mBAAmB,QAAuB;CACxD,OAAO,IAAI,aAAa,MAAM;AAChC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyneta/leveldb-store",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "LevelDB storage backend for @kyneta/exchange — server-side persistent storage",
5
5
  "author": "Duane Johnson",
6
6
  "license": "MIT",
@@ -30,19 +30,19 @@
30
30
  "./src/*": "./src/*"
31
31
  },
32
32
  "peerDependencies": {
33
- "@kyneta/exchange": "^1.5.1",
34
- "@kyneta/schema": "^1.5.1"
33
+ "@kyneta/exchange": "^1.6.0",
34
+ "@kyneta/schema": "^1.6.0"
35
35
  },
36
36
  "dependencies": {
37
37
  "classic-level": "^1.4.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22",
41
- "tsdown": "^0.21.9",
41
+ "tsdown": "^0.22.0",
42
42
  "typescript": "^5.9.2",
43
43
  "vitest": "^4.0.17",
44
- "@kyneta/exchange": "^1.5.1",
45
- "@kyneta/schema": "^1.5.1"
44
+ "@kyneta/schema": "^1.6.0",
45
+ "@kyneta/exchange": "^1.6.0"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsdown",