@kyneta/sqlite-store 1.5.2 → 1.6.1

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":";;;;;;AAmCA;;;;UAAiB,aAAA;EACf,IAAA,CAAK,GAAA,aAAgB,MAAA;EACrB,OAAA,KAAY,MAAA,mBACV,GAAA,aACG,MAAA,cACF,QAAA,CAAS,CAAA;EACZ,WAAA,IAAe,EAAA,QAAU,CAAA,GAAI,CAAA;EAC7B,KAAA;AAAA;;;;;;;;;;;;;iBAmBc,iBAAA,CAAkB,EAAA,EAAI,qBAAA,GAAwB,aAAA;;;;;;AAA9D;;;;;;;iBAgCgB,aAAA,CAAc,EAAA,EAAI,iBAAA,GAAoB,aAAA;;UAyB5C,qBAAA;EACR,OAAA,CAAQ,GAAA;IACN,GAAA,IAAO,MAAA;IACP,OAAA,IAAW,MAAA,cAAoB,gBAAA;EAAA;EAEjC,WAAA,IAAe,EAAA,QAAU,CAAA,SAAU,CAAA;EACnC,KAAA;AAAA;;UAIQ,iBAAA;EACR,GAAA,CAAI,GAAA,aAAgB,MAAA;EACpB,KAAA,CAAM,GAAA;IACJ,OAAA,IAAW,MAAA,cAAoB,gBAAA;EAAA;EAEjC,WAAA,IAAe,EAAA,QAAU,CAAA,SAAU,CAAA;EACnC,KAAA;AAAA;AAAA,UAOe,kBAAA;EAtBP;;;;;;;EA8BR,MAAA,GAAS,OAAA,CAAQ,UAAA;AAAA;AAAA,cAON,WAAA,YAAuB,KAAA;EAAA;cAKtB,OAAA,EAAS,aAAA,EAAe,OAAA,GAAS,kBAAA;EA6BvC,MAAA,CAAO,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,WAAA,GAAc,OAAA;EAkC1C,OAAA,CAAQ,KAAA,EAAO,KAAA,GAAQ,aAAA,CAAc,WAAA;EAStC,OAAA,CAAQ,KAAA,EAAO,KAAA,EAAO,OAAA,EAAS,WAAA,KAAgB,OAAA;EAoC/C,MAAA,CAAO,KAAA,EAAO,KAAA,GAAQ,OAAA;EActB,WAAA,CAAY,KAAA,EAAO,KAAA,GAAQ,OAAA,CAAQ,SAAA;EASlC,UAAA,CAAW,MAAA,YAAkB,aAAA,CAAc,KAAA;EAe5C,KAAA,CAAA,GAAS,OAAA;AAAA;AAAA,iBAsBD,iBAAA,CACd,OAAA,EAAS,aAAA,EACT,OAAA,GAAU,kBAAA,GACT,KAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;AAmCA;;;;UAAiB,aAAA;EACf,IAAA,CAAK,GAAA,aAAgB,MAAA;EACrB,OAAA,KAAY,MAAA,mBACV,GAAA,aACG,MAAA,cACF,QAAA,CAAS,CAAA;EACZ,WAAA,IAAe,EAAA,QAAU,CAAA,GAAI,CAAA;EAC7B,KAAA;AAAA;;;;;;;;;;;;;iBAmBc,iBAAA,CAAkB,EAAA,EAAI,qBAAA,GAAwB,aAAa;;;;;AAnBpE;AAmBP;;;;;;;iBAgCgB,aAAA,CAAc,EAAA,EAAI,iBAAA,GAAoB,aAAa;AAhCQ;AAAA,UAyDjE,qBAAA;EACR,OAAA,CAAQ,GAAA;IACN,GAAA,IAAO,MAAA;IACP,OAAA,IAAW,MAAA,cAAoB,gBAAA;EAAA;EAEjC,WAAA,IAAe,EAAA,QAAU,CAAA,SAAU,CAAA;EACnC,KAAA;AAAA;AA/BiE;AAAA,UAmCzD,iBAAA;EACR,GAAA,CAAI,GAAA,aAAgB,MAAA;EACpB,KAAA,CAAM,GAAA;IACJ,OAAA,IAAW,MAAA,cAAoB,gBAAA;EAAA;EAEjC,WAAA,IAAe,EAAA,QAAU,CAAA,SAAU,CAAA;EACnC,KAAA;AAAA;AAAA,UAOe,kBAAA;EAtBP;;;;;;;EA8BR,MAAA,GAAS,OAAO,CAAC,UAAA;AAAA;AAAA,cAON,WAAA,YAAuB,KAAA;EAAA;cAKtB,OAAA,EAAS,aAAA,EAAe,OAAA,GAAS,kBAAA;EA6BvC,MAAA,CAAO,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,WAAA,GAAc,OAAA;EAkC1C,OAAA,CAAQ,KAAA,EAAO,KAAA,GAAQ,aAAA,CAAc,WAAA;EAStC,OAAA,CAAQ,KAAA,EAAO,KAAA,EAAO,OAAA,EAAS,WAAA,KAAgB,OAAA;EAoC/C,MAAA,CAAO,KAAA,EAAO,KAAA,GAAQ,OAAA;EActB,WAAA,CAAY,KAAA,EAAO,KAAA,GAAQ,OAAA,CAAQ,SAAA;EASlC,UAAA,CAAW,MAAA,YAAkB,aAAA,CAAc,KAAA;EAe5C,KAAA,CAAA,GAAS,OAAA;AAAA;AAAA,iBAsBD,iBAAA,CACd,OAAA,EAAS,aAAA,EACT,OAAA,GAAU,kBAAA,GACT,KAAA"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#adapter","#seqNos","#tables","#ensureSchema"],"sources":["../src/index.ts"],"sourcesContent":["// SQLite Store backend.\n//\n// Why a thin adapter rather than a direct better-sqlite3 dependency: the\n// adapter shape is deliberately synchronous because every supported\n// SQLite binding is sync (better-sqlite3, bun:sqlite, Cloudflare DO's\n// ctx.storage.sql). Forcing async here would dilute that ergonomics for\n// no benefit, since postgres-store and prisma-store get their own\n// async-native packages.\n\nimport {\n type DocId,\n SeqNoTracker,\n type Store,\n type StoreMeta,\n type StoreRecord,\n} from \"@kyneta/exchange\"\nimport {\n fromRow,\n planAppend,\n planReplace,\n type RowShape,\n resolveTables,\n type TableNames,\n} from \"@kyneta/sql-store-core\"\n\n// ---------------------------------------------------------------------------\n// SqliteAdapter — minimal synchronous database interface\n// ---------------------------------------------------------------------------\n\n/**\n * `iterate` returns `Iterable<T>` rather than `T[]` so `loadAll` can\n * stream million-record stores without materializing them all in\n * memory. Cloudflare DO's `ctx.storage.sql.exec` returns a cursor for\n * the same reason; this shape is chosen to pass through.\n */\nexport interface SqliteAdapter {\n exec(sql: string, ...params: unknown[]): void\n iterate<T = Record<string, unknown>>(\n sql: string,\n ...params: unknown[]\n ): Iterable<T>\n transaction<R>(fn: () => R): R\n close(): void\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factories\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a `better-sqlite3` Database as a `SqliteAdapter`.\n *\n * @example\n * ```typescript\n * import Database from \"better-sqlite3\"\n * import { SqliteStore, fromBetterSqlite3 } from \"@kyneta/sqlite-store\"\n *\n * const db = new Database(\"exchange.db\")\n * const store = new SqliteStore(fromBetterSqlite3(db))\n * ```\n */\nexport function fromBetterSqlite3(db: BetterSqlite3Database): SqliteAdapter {\n return {\n exec(sql: string, ...params: unknown[]): void {\n db.prepare(sql).run(...params)\n },\n iterate<T = Record<string, unknown>>(\n sql: string,\n ...params: unknown[]\n ): Iterable<T> {\n return db.prepare(sql).iterate(...params) as IterableIterator<T>\n },\n transaction<R>(fn: () => R): R {\n return db.transaction(fn)()\n },\n close(): void {\n db.close()\n },\n }\n}\n\n/**\n * Wrap a `bun:sqlite` Database as a `SqliteAdapter`.\n *\n * @example\n * ```typescript\n * import { Database } from \"bun:sqlite\"\n * import { SqliteStore, fromBunSqlite } from \"@kyneta/sqlite-store\"\n *\n * const db = new Database(\"exchange.db\")\n * const store = new SqliteStore(fromBunSqlite(db))\n * ```\n */\nexport function fromBunSqlite(db: BunSqliteDatabase): SqliteAdapter {\n return {\n exec(sql: string, ...params: unknown[]): void {\n db.run(sql, ...params)\n },\n iterate<T = Record<string, unknown>>(\n sql: string,\n ...params: unknown[]\n ): Iterable<T> {\n return db.query(sql).iterate(...params) as IterableIterator<T>\n },\n transaction<R>(fn: () => R): R {\n return db.transaction(fn)()\n },\n close(): void {\n db.close()\n },\n }\n}\n\n// Minimal structural types for the two primary SQLite bindings.\n// These avoid a hard dependency on `better-sqlite3` or `bun:sqlite` types\n// at runtime — the caller provides the concrete database instance.\n\n/** Structural type for a `better-sqlite3` Database instance. */\ninterface BetterSqlite3Database {\n prepare(sql: string): {\n run(...params: unknown[]): unknown\n iterate(...params: unknown[]): IterableIterator<unknown>\n }\n transaction<R>(fn: () => R): () => R\n close(): void\n}\n\n/** Structural type for a `bun:sqlite` Database instance. */\ninterface BunSqliteDatabase {\n run(sql: string, ...params: unknown[]): void\n query(sql: string): {\n iterate(...params: unknown[]): IterableIterator<unknown>\n }\n transaction<R>(fn: () => R): () => R\n close(): void\n}\n\n// ---------------------------------------------------------------------------\n// SqliteStore options\n// ---------------------------------------------------------------------------\n\nexport interface SqliteStoreOptions {\n /**\n * Override the default table names (`kyneta_meta` and `kyneta_records`).\n *\n * Use when co-locating Exchange tables alongside application tables in\n * the same SQLite database, or when running multiple isolated Exchange\n * instances in one database. Either or both names may be overridden.\n */\n tables?: Partial<TableNames>\n}\n\n// ---------------------------------------------------------------------------\n// SqliteStore\n// ---------------------------------------------------------------------------\n\nexport class SqliteStore implements Store {\n readonly #adapter: SqliteAdapter\n readonly #seqNos = new SeqNoTracker()\n readonly #tables: TableNames\n\n constructor(adapter: SqliteAdapter, options: SqliteStoreOptions = {}) {\n this.#adapter = adapter\n this.#tables = resolveTables(options)\n this.#ensureSchema()\n }\n\n #ensureSchema(): void {\n this.#adapter.exec(`\n CREATE TABLE IF NOT EXISTS ${this.#tables.meta} (\n doc_id TEXT PRIMARY KEY,\n data TEXT NOT NULL\n ) WITHOUT ROWID\n `)\n this.#adapter.exec(`\n CREATE TABLE IF NOT EXISTS ${this.#tables.records} (\n doc_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n kind TEXT NOT NULL,\n payload TEXT,\n blob BLOB,\n PRIMARY KEY (doc_id, seq)\n ) WITHOUT ROWID\n `)\n }\n\n // -------------------------------------------------------------------------\n // Store interface\n // -------------------------------------------------------------------------\n\n async append(docId: DocId, record: StoreRecord): Promise<void> {\n const existingMeta = await this.currentMeta(docId)\n const seq = await this.#seqNos.next(docId, async () => {\n const [row] = this.#adapter.iterate<{ max_seq: number | null }>(\n `SELECT MAX(seq) AS max_seq FROM ${this.#tables.records} WHERE doc_id = ?`,\n docId,\n )\n return row?.max_seq ?? null\n })\n\n const plan = planAppend(docId, record, existingMeta, seq)\n\n // Both writes must commit together or neither — a crash between\n // them used to leave meta updated with no corresponding row.\n this.#adapter.transaction(() => {\n if (plan.upsertMeta !== null) {\n this.#adapter.exec(\n `INSERT OR REPLACE INTO ${this.#tables.meta} (doc_id, data) VALUES (?, ?)`,\n docId,\n plan.upsertMeta.data,\n )\n }\n const { row } = plan.insertRecord\n this.#adapter.exec(\n `INSERT INTO ${this.#tables.records} (doc_id, seq, kind, payload, blob) VALUES (?, ?, ?, ?, ?)`,\n docId,\n plan.insertRecord.seq,\n row.kind,\n row.payload,\n row.blob,\n )\n })\n }\n\n async *loadAll(docId: DocId): AsyncIterable<StoreRecord> {\n for (const row of this.#adapter.iterate<RowShape>(\n `SELECT kind, payload, blob FROM ${this.#tables.records} WHERE doc_id = ? ORDER BY seq`,\n docId,\n )) {\n yield fromRow(row)\n }\n }\n\n async replace(docId: DocId, records: StoreRecord[]): Promise<void> {\n const existingMeta = await this.currentMeta(docId)\n const plan = planReplace(records, existingMeta)\n\n this.#adapter.transaction(() => {\n this.#adapter.exec(\n `DELETE FROM ${this.#tables.records} WHERE doc_id = ?`,\n docId,\n )\n\n for (const { seq, row } of plan.records) {\n this.#adapter.exec(\n `INSERT INTO ${this.#tables.records} (doc_id, seq, kind, payload, blob) VALUES (?, ?, ?, ?, ?)`,\n docId,\n seq,\n row.kind,\n row.payload,\n row.blob,\n )\n }\n\n this.#adapter.exec(\n `INSERT OR REPLACE INTO ${this.#tables.meta} (doc_id, data) VALUES (?, ?)`,\n docId,\n plan.upsertMeta.data,\n )\n })\n\n // Must run after the transaction commits. If `transaction()` throws,\n // control jumps past this line; the cache stays unmutated. Moving\n // this inside the callback or before the call would corrupt the\n // cache on rollback — the next append would compute a seq that\n // collides with restored rows on the (doc_id, seq) primary key.\n this.#seqNos.reset(docId, records.length - 1)\n }\n\n async delete(docId: DocId): Promise<void> {\n this.#adapter.transaction(() => {\n this.#adapter.exec(\n `DELETE FROM ${this.#tables.records} WHERE doc_id = ?`,\n docId,\n )\n this.#adapter.exec(\n `DELETE FROM ${this.#tables.meta} WHERE doc_id = ?`,\n docId,\n )\n })\n this.#seqNos.remove(docId)\n }\n\n async currentMeta(docId: DocId): Promise<StoreMeta | null> {\n const [row] = this.#adapter.iterate<{ data: string }>(\n `SELECT data FROM ${this.#tables.meta} WHERE doc_id = ?`,\n docId,\n )\n if (row === undefined) return null\n return JSON.parse(row.data) as StoreMeta\n }\n\n async *listDocIds(prefix?: string): AsyncIterable<DocId> {\n const rows =\n prefix !== undefined\n ? this.#adapter.iterate<{ doc_id: string }>(\n `SELECT doc_id FROM ${this.#tables.meta} WHERE doc_id LIKE ? ESCAPE '\\\\'`,\n `${escapeLike(prefix)}%`,\n )\n : this.#adapter.iterate<{ doc_id: string }>(\n `SELECT doc_id FROM ${this.#tables.meta}`,\n )\n for (const row of rows) {\n yield row.doc_id\n }\n }\n\n async close(): Promise<void> {\n this.#adapter.close()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * SQLite's LIKE treats `%` and `_` as wildcards. Escape them (and the\n * escape char itself) so doc IDs containing those characters are\n * matched literally. The query declares `ESCAPE '\\'`.\n */\nfunction escapeLike(value: string): string {\n return value.replace(/[%_\\\\]/g, ch => `\\\\${ch}`)\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\nexport function createSqliteStore(\n adapter: SqliteAdapter,\n options?: SqliteStoreOptions,\n): Store {\n return new SqliteStore(adapter, options)\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6DA,SAAgB,kBAAkB,IAA0C;AAC1E,QAAO;EACL,KAAK,KAAa,GAAG,QAAyB;AAC5C,MAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO;;EAEhC,QACE,KACA,GAAG,QACU;AACb,UAAO,GAAG,QAAQ,IAAI,CAAC,QAAQ,GAAG,OAAO;;EAE3C,YAAe,IAAgB;AAC7B,UAAO,GAAG,YAAY,GAAG,EAAE;;EAE7B,QAAc;AACZ,MAAG,OAAO;;EAEb;;;;;;;;;;;;;;AAeH,SAAgB,cAAc,IAAsC;AAClE,QAAO;EACL,KAAK,KAAa,GAAG,QAAyB;AAC5C,MAAG,IAAI,KAAK,GAAG,OAAO;;EAExB,QACE,KACA,GAAG,QACU;AACb,UAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,GAAG,OAAO;;EAEzC,YAAe,IAAgB;AAC7B,UAAO,GAAG,YAAY,GAAG,EAAE;;EAE7B,QAAc;AACZ,MAAG,OAAO;;EAEb;;AA8CH,IAAa,cAAb,MAA0C;CACxC;CACA,UAAmB,IAAI,cAAc;CACrC;CAEA,YAAY,SAAwB,UAA8B,EAAE,EAAE;AACpE,QAAA,UAAgB;AAChB,QAAA,SAAe,cAAc,QAAQ;AACrC,QAAA,cAAoB;;CAGtB,gBAAsB;AACpB,QAAA,QAAc,KAAK;mCACY,MAAA,OAAa,KAAK;;;;MAI/C;AACF,QAAA,QAAc,KAAK;mCACY,MAAA,OAAa,QAAQ;;;;;;;;MAQlD;;CAOJ,MAAM,OAAO,OAAc,QAAoC;EAU7D,MAAM,OAAO,WAAW,OAAO,QATV,MAAM,KAAK,YAAY,MAAM,EACtC,MAAM,MAAA,OAAa,KAAK,OAAO,YAAY;GACrD,MAAM,CAAC,OAAO,MAAA,QAAc,QAC1B,mCAAmC,MAAA,OAAa,QAAQ,oBACxD,MACD;AACD,UAAO,KAAK,WAAW;IACvB,CAEuD;AAIzD,QAAA,QAAc,kBAAkB;AAC9B,OAAI,KAAK,eAAe,KACtB,OAAA,QAAc,KACZ,0BAA0B,MAAA,OAAa,KAAK,gCAC5C,OACA,KAAK,WAAW,KACjB;GAEH,MAAM,EAAE,QAAQ,KAAK;AACrB,SAAA,QAAc,KACZ,eAAe,MAAA,OAAa,QAAQ,6DACpC,OACA,KAAK,aAAa,KAClB,IAAI,MACJ,IAAI,SACJ,IAAI,KACL;IACD;;CAGJ,OAAO,QAAQ,OAA0C;AACvD,OAAK,MAAM,OAAO,MAAA,QAAc,QAC9B,mCAAmC,MAAA,OAAa,QAAQ,iCACxD,MACD,CACC,OAAM,QAAQ,IAAI;;CAItB,MAAM,QAAQ,OAAc,SAAuC;EAEjE,MAAM,OAAO,YAAY,SADJ,MAAM,KAAK,YAAY,MAAM,CACH;AAE/C,QAAA,QAAc,kBAAkB;AAC9B,SAAA,QAAc,KACZ,eAAe,MAAA,OAAa,QAAQ,oBACpC,MACD;AAED,QAAK,MAAM,EAAE,KAAK,SAAS,KAAK,QAC9B,OAAA,QAAc,KACZ,eAAe,MAAA,OAAa,QAAQ,6DACpC,OACA,KACA,IAAI,MACJ,IAAI,SACJ,IAAI,KACL;AAGH,SAAA,QAAc,KACZ,0BAA0B,MAAA,OAAa,KAAK,gCAC5C,OACA,KAAK,WAAW,KACjB;IACD;AAOF,QAAA,OAAa,MAAM,OAAO,QAAQ,SAAS,EAAE;;CAG/C,MAAM,OAAO,OAA6B;AACxC,QAAA,QAAc,kBAAkB;AAC9B,SAAA,QAAc,KACZ,eAAe,MAAA,OAAa,QAAQ,oBACpC,MACD;AACD,SAAA,QAAc,KACZ,eAAe,MAAA,OAAa,KAAK,oBACjC,MACD;IACD;AACF,QAAA,OAAa,OAAO,MAAM;;CAG5B,MAAM,YAAY,OAAyC;EACzD,MAAM,CAAC,OAAO,MAAA,QAAc,QAC1B,oBAAoB,MAAA,OAAa,KAAK,oBACtC,MACD;AACD,MAAI,QAAQ,KAAA,EAAW,QAAO;AAC9B,SAAO,KAAK,MAAM,IAAI,KAAK;;CAG7B,OAAO,WAAW,QAAuC;EACvD,MAAM,OACJ,WAAW,KAAA,IACP,MAAA,QAAc,QACZ,sBAAsB,MAAA,OAAa,KAAK,mCACxC,GAAG,WAAW,OAAO,CAAC,GACvB,GACD,MAAA,QAAc,QACZ,sBAAsB,MAAA,OAAa,OACpC;AACP,OAAK,MAAM,OAAO,KAChB,OAAM,IAAI;;CAId,MAAM,QAAuB;AAC3B,QAAA,QAAc,OAAO;;;;;;;;AAazB,SAAS,WAAW,OAAuB;AACzC,QAAO,MAAM,QAAQ,YAAW,OAAM,KAAK,KAAK;;AAOlD,SAAgB,kBACd,SACA,SACO;AACP,QAAO,IAAI,YAAY,SAAS,QAAQ"}
1
+ {"version":3,"file":"index.js","names":["#adapter","#seqNos","#tables","#ensureSchema"],"sources":["../src/index.ts"],"sourcesContent":["// SQLite Store backend.\n//\n// Why a thin adapter rather than a direct better-sqlite3 dependency: the\n// adapter shape is deliberately synchronous because every supported\n// SQLite binding is sync (better-sqlite3, bun:sqlite, Cloudflare DO's\n// ctx.storage.sql). Forcing async here would dilute that ergonomics for\n// no benefit, since postgres-store and prisma-store get their own\n// async-native packages.\n\nimport {\n type DocId,\n SeqNoTracker,\n type Store,\n type StoreMeta,\n type StoreRecord,\n} from \"@kyneta/exchange\"\nimport {\n fromRow,\n planAppend,\n planReplace,\n type RowShape,\n resolveTables,\n type TableNames,\n} from \"@kyneta/sql-store-core\"\n\n// ---------------------------------------------------------------------------\n// SqliteAdapter — minimal synchronous database interface\n// ---------------------------------------------------------------------------\n\n/**\n * `iterate` returns `Iterable<T>` rather than `T[]` so `loadAll` can\n * stream million-record stores without materializing them all in\n * memory. Cloudflare DO's `ctx.storage.sql.exec` returns a cursor for\n * the same reason; this shape is chosen to pass through.\n */\nexport interface SqliteAdapter {\n exec(sql: string, ...params: unknown[]): void\n iterate<T = Record<string, unknown>>(\n sql: string,\n ...params: unknown[]\n ): Iterable<T>\n transaction<R>(fn: () => R): R\n close(): void\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factories\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a `better-sqlite3` Database as a `SqliteAdapter`.\n *\n * @example\n * ```typescript\n * import Database from \"better-sqlite3\"\n * import { SqliteStore, fromBetterSqlite3 } from \"@kyneta/sqlite-store\"\n *\n * const db = new Database(\"exchange.db\")\n * const store = new SqliteStore(fromBetterSqlite3(db))\n * ```\n */\nexport function fromBetterSqlite3(db: BetterSqlite3Database): SqliteAdapter {\n return {\n exec(sql: string, ...params: unknown[]): void {\n db.prepare(sql).run(...params)\n },\n iterate<T = Record<string, unknown>>(\n sql: string,\n ...params: unknown[]\n ): Iterable<T> {\n return db.prepare(sql).iterate(...params) as IterableIterator<T>\n },\n transaction<R>(fn: () => R): R {\n return db.transaction(fn)()\n },\n close(): void {\n db.close()\n },\n }\n}\n\n/**\n * Wrap a `bun:sqlite` Database as a `SqliteAdapter`.\n *\n * @example\n * ```typescript\n * import { Database } from \"bun:sqlite\"\n * import { SqliteStore, fromBunSqlite } from \"@kyneta/sqlite-store\"\n *\n * const db = new Database(\"exchange.db\")\n * const store = new SqliteStore(fromBunSqlite(db))\n * ```\n */\nexport function fromBunSqlite(db: BunSqliteDatabase): SqliteAdapter {\n return {\n exec(sql: string, ...params: unknown[]): void {\n db.run(sql, ...params)\n },\n iterate<T = Record<string, unknown>>(\n sql: string,\n ...params: unknown[]\n ): Iterable<T> {\n return db.query(sql).iterate(...params) as IterableIterator<T>\n },\n transaction<R>(fn: () => R): R {\n return db.transaction(fn)()\n },\n close(): void {\n db.close()\n },\n }\n}\n\n// Minimal structural types for the two primary SQLite bindings.\n// These avoid a hard dependency on `better-sqlite3` or `bun:sqlite` types\n// at runtime — the caller provides the concrete database instance.\n\n/** Structural type for a `better-sqlite3` Database instance. */\ninterface BetterSqlite3Database {\n prepare(sql: string): {\n run(...params: unknown[]): unknown\n iterate(...params: unknown[]): IterableIterator<unknown>\n }\n transaction<R>(fn: () => R): () => R\n close(): void\n}\n\n/** Structural type for a `bun:sqlite` Database instance. */\ninterface BunSqliteDatabase {\n run(sql: string, ...params: unknown[]): void\n query(sql: string): {\n iterate(...params: unknown[]): IterableIterator<unknown>\n }\n transaction<R>(fn: () => R): () => R\n close(): void\n}\n\n// ---------------------------------------------------------------------------\n// SqliteStore options\n// ---------------------------------------------------------------------------\n\nexport interface SqliteStoreOptions {\n /**\n * Override the default table names (`kyneta_meta` and `kyneta_records`).\n *\n * Use when co-locating Exchange tables alongside application tables in\n * the same SQLite database, or when running multiple isolated Exchange\n * instances in one database. Either or both names may be overridden.\n */\n tables?: Partial<TableNames>\n}\n\n// ---------------------------------------------------------------------------\n// SqliteStore\n// ---------------------------------------------------------------------------\n\nexport class SqliteStore implements Store {\n readonly #adapter: SqliteAdapter\n readonly #seqNos = new SeqNoTracker()\n readonly #tables: TableNames\n\n constructor(adapter: SqliteAdapter, options: SqliteStoreOptions = {}) {\n this.#adapter = adapter\n this.#tables = resolveTables(options)\n this.#ensureSchema()\n }\n\n #ensureSchema(): void {\n this.#adapter.exec(`\n CREATE TABLE IF NOT EXISTS ${this.#tables.meta} (\n doc_id TEXT PRIMARY KEY,\n data TEXT NOT NULL\n ) WITHOUT ROWID\n `)\n this.#adapter.exec(`\n CREATE TABLE IF NOT EXISTS ${this.#tables.records} (\n doc_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n kind TEXT NOT NULL,\n payload TEXT,\n blob BLOB,\n PRIMARY KEY (doc_id, seq)\n ) WITHOUT ROWID\n `)\n }\n\n // -------------------------------------------------------------------------\n // Store interface\n // -------------------------------------------------------------------------\n\n async append(docId: DocId, record: StoreRecord): Promise<void> {\n const existingMeta = await this.currentMeta(docId)\n const seq = await this.#seqNos.next(docId, async () => {\n const [row] = this.#adapter.iterate<{ max_seq: number | null }>(\n `SELECT MAX(seq) AS max_seq FROM ${this.#tables.records} WHERE doc_id = ?`,\n docId,\n )\n return row?.max_seq ?? null\n })\n\n const plan = planAppend(docId, record, existingMeta, seq)\n\n // Both writes must commit together or neither — a crash between\n // them used to leave meta updated with no corresponding row.\n this.#adapter.transaction(() => {\n if (plan.upsertMeta !== null) {\n this.#adapter.exec(\n `INSERT OR REPLACE INTO ${this.#tables.meta} (doc_id, data) VALUES (?, ?)`,\n docId,\n plan.upsertMeta.data,\n )\n }\n const { row } = plan.insertRecord\n this.#adapter.exec(\n `INSERT INTO ${this.#tables.records} (doc_id, seq, kind, payload, blob) VALUES (?, ?, ?, ?, ?)`,\n docId,\n plan.insertRecord.seq,\n row.kind,\n row.payload,\n row.blob,\n )\n })\n }\n\n async *loadAll(docId: DocId): AsyncIterable<StoreRecord> {\n for (const row of this.#adapter.iterate<RowShape>(\n `SELECT kind, payload, blob FROM ${this.#tables.records} WHERE doc_id = ? ORDER BY seq`,\n docId,\n )) {\n yield fromRow(row)\n }\n }\n\n async replace(docId: DocId, records: StoreRecord[]): Promise<void> {\n const existingMeta = await this.currentMeta(docId)\n const plan = planReplace(records, existingMeta)\n\n this.#adapter.transaction(() => {\n this.#adapter.exec(\n `DELETE FROM ${this.#tables.records} WHERE doc_id = ?`,\n docId,\n )\n\n for (const { seq, row } of plan.records) {\n this.#adapter.exec(\n `INSERT INTO ${this.#tables.records} (doc_id, seq, kind, payload, blob) VALUES (?, ?, ?, ?, ?)`,\n docId,\n seq,\n row.kind,\n row.payload,\n row.blob,\n )\n }\n\n this.#adapter.exec(\n `INSERT OR REPLACE INTO ${this.#tables.meta} (doc_id, data) VALUES (?, ?)`,\n docId,\n plan.upsertMeta.data,\n )\n })\n\n // Must run after the transaction commits. If `transaction()` throws,\n // control jumps past this line; the cache stays unmutated. Moving\n // this inside the callback or before the call would corrupt the\n // cache on rollback — the next append would compute a seq that\n // collides with restored rows on the (doc_id, seq) primary key.\n this.#seqNos.reset(docId, records.length - 1)\n }\n\n async delete(docId: DocId): Promise<void> {\n this.#adapter.transaction(() => {\n this.#adapter.exec(\n `DELETE FROM ${this.#tables.records} WHERE doc_id = ?`,\n docId,\n )\n this.#adapter.exec(\n `DELETE FROM ${this.#tables.meta} WHERE doc_id = ?`,\n docId,\n )\n })\n this.#seqNos.remove(docId)\n }\n\n async currentMeta(docId: DocId): Promise<StoreMeta | null> {\n const [row] = this.#adapter.iterate<{ data: string }>(\n `SELECT data FROM ${this.#tables.meta} WHERE doc_id = ?`,\n docId,\n )\n if (row === undefined) return null\n return JSON.parse(row.data) as StoreMeta\n }\n\n async *listDocIds(prefix?: string): AsyncIterable<DocId> {\n const rows =\n prefix !== undefined\n ? this.#adapter.iterate<{ doc_id: string }>(\n `SELECT doc_id FROM ${this.#tables.meta} WHERE doc_id LIKE ? ESCAPE '\\\\'`,\n `${escapeLike(prefix)}%`,\n )\n : this.#adapter.iterate<{ doc_id: string }>(\n `SELECT doc_id FROM ${this.#tables.meta}`,\n )\n for (const row of rows) {\n yield row.doc_id\n }\n }\n\n async close(): Promise<void> {\n this.#adapter.close()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * SQLite's LIKE treats `%` and `_` as wildcards. Escape them (and the\n * escape char itself) so doc IDs containing those characters are\n * matched literally. The query declares `ESCAPE '\\'`.\n */\nfunction escapeLike(value: string): string {\n return value.replace(/[%_\\\\]/g, ch => `\\\\${ch}`)\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\nexport function createSqliteStore(\n adapter: SqliteAdapter,\n options?: SqliteStoreOptions,\n): Store {\n return new SqliteStore(adapter, options)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyneta/sqlite-store",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "description": "SQLite storage backend for @kyneta/exchange — universal persistent storage",
5
5
  "author": "Duane Johnson",
6
6
  "license": "MIT",
@@ -30,20 +30,20 @@
30
30
  "./src/*": "./src/*"
31
31
  },
32
32
  "peerDependencies": {
33
- "@kyneta/exchange": "^1.5.2",
34
- "@kyneta/schema": "^1.5.2",
35
- "@kyneta/sql-store-core": "^1.5.2"
33
+ "@kyneta/exchange": "^1.6.1",
34
+ "@kyneta/sql-store-core": "^1.6.1",
35
+ "@kyneta/schema": "^1.6.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/better-sqlite3": "^7.6.12",
39
39
  "@types/node": "^22",
40
40
  "better-sqlite3": "^11.9.1",
41
- "tsdown": "^0.21.9",
41
+ "tsdown": "^0.22.0",
42
42
  "typescript": "^5.9.2",
43
43
  "vitest": "^4.0.17",
44
- "@kyneta/schema": "^1.5.2",
45
- "@kyneta/sql-store-core": "^1.5.2",
46
- "@kyneta/exchange": "^1.5.2"
44
+ "@kyneta/exchange": "^1.6.1",
45
+ "@kyneta/sql-store-core": "^1.6.1",
46
+ "@kyneta/schema": "^1.6.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsdown",