@tanstack/node-db-sqlite-persistence 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @tanstack/node-db-sqlite-persistence
2
+
3
+ Thin Node SQLite persistence for TanStack DB.
4
+
5
+ ## Public API
6
+
7
+ - `createNodeSQLitePersistence(...)`
8
+ - `persistedCollectionOptions(...)` (re-exported from core)
9
+
10
+ ## Quick start
11
+
12
+ ```ts
13
+ import { createCollection } from '@tanstack/db'
14
+ import {
15
+ createNodeSQLitePersistence,
16
+ persistedCollectionOptions,
17
+ } from '@tanstack/node-db-sqlite-persistence'
18
+ import Database from 'better-sqlite3'
19
+
20
+ type Todo = {
21
+ id: string
22
+ title: string
23
+ completed: boolean
24
+ }
25
+
26
+ // You own database lifecycle directly.
27
+ const database = new Database(`./tanstack-db.sqlite`)
28
+
29
+ // One shared persistence instance for the whole database.
30
+ const persistence = createNodeSQLitePersistence({
31
+ database,
32
+ })
33
+
34
+ export const todosCollection = createCollection(
35
+ persistedCollectionOptions<Todo, string>({
36
+ id: `todos`,
37
+ getKey: (todo) => todo.id,
38
+ persistence,
39
+ schemaVersion: 1, // Per-collection schema version
40
+ }),
41
+ )
42
+ ```
43
+
44
+ ## Notes
45
+
46
+ - `createNodeSQLitePersistence` is shared across collections; it resolves
47
+ mode-specific behavior (`sync-present` vs `sync-absent`) automatically.
48
+ - `schemaVersion` is specified per collection via `persistedCollectionOptions`.
49
+ - Call `database.close()` when your app shuts down.
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const nodePersistence = require("./node-persistence.cjs");
4
+ const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
5
+ exports.createNodeSQLitePersistence = nodePersistence.createNodeSQLitePersistence;
6
+ Object.defineProperty(exports, "persistedCollectionOptions", {
7
+ enumerable: true,
8
+ get: () => dbSqlitePersistenceCore.persistedCollectionOptions
9
+ });
10
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
@@ -0,0 +1,4 @@
1
+ export { createNodeSQLitePersistence } from './node-persistence.cjs';
2
+ export type { BetterSqlite3Database, NodeSQLitePersistenceOptions, NodeSQLiteSchemaMismatchPolicy, } from './node-persistence.cjs';
3
+ export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core';
4
+ export type { PersistedCollectionCoordinator, PersistedCollectionPersistence, } from '@tanstack/db-sqlite-persistence-core';
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const node_async_hooks = require("node:async_hooks");
4
+ const BetterSqlite3 = require("better-sqlite3");
5
+ const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
6
+ const DEFAULT_PRAGMAS = [
7
+ `journal_mode = WAL`,
8
+ `synchronous = NORMAL`,
9
+ `foreign_keys = ON`
10
+ ];
11
+ const INVALID_PRAGMA_PATTERN = /(;|--|\/\*)/;
12
+ function assertTransactionCallbackHasDriverArg(fn) {
13
+ if (fn.length > 0) {
14
+ return;
15
+ }
16
+ throw new dbSqlitePersistenceCore.InvalidPersistedCollectionConfigError(
17
+ `SQLiteDriver.transaction callback must accept the transaction driver argument`
18
+ );
19
+ }
20
+ function hasExistingDatabase(options) {
21
+ return `database` in options;
22
+ }
23
+ class BetterSqlite3SQLiteDriver {
24
+ constructor(options) {
25
+ this.transactionContext = new node_async_hooks.AsyncLocalStorage();
26
+ this.queue = Promise.resolve();
27
+ this.nextSavepointId = 1;
28
+ if (hasExistingDatabase(options)) {
29
+ this.database = options.database;
30
+ this.ownsDatabase = false;
31
+ this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS);
32
+ return;
33
+ }
34
+ if (options.filename.trim().length === 0) {
35
+ throw new dbSqlitePersistenceCore.InvalidPersistedCollectionConfigError(
36
+ `Node SQLite driver filename cannot be empty`
37
+ );
38
+ }
39
+ this.database = new BetterSqlite3(options.filename, options.options);
40
+ this.ownsDatabase = true;
41
+ this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS);
42
+ }
43
+ async exec(sql) {
44
+ if (this.isInsideTransaction()) {
45
+ this.database.exec(sql);
46
+ return;
47
+ }
48
+ await this.enqueue(() => {
49
+ this.database.exec(sql);
50
+ });
51
+ }
52
+ async query(sql, params = []) {
53
+ if (this.isInsideTransaction()) {
54
+ return this.executeQuery(sql, params);
55
+ }
56
+ return this.enqueue(() => this.executeQuery(sql, params));
57
+ }
58
+ async run(sql, params = []) {
59
+ if (this.isInsideTransaction()) {
60
+ this.executeRun(sql, params);
61
+ return;
62
+ }
63
+ await this.enqueue(() => {
64
+ this.executeRun(sql, params);
65
+ });
66
+ }
67
+ async transaction(fn) {
68
+ assertTransactionCallbackHasDriverArg(fn);
69
+ if (this.isInsideTransaction()) {
70
+ return this.runNestedTransaction(fn);
71
+ }
72
+ return this.enqueue(async () => {
73
+ this.database.exec(`BEGIN IMMEDIATE`);
74
+ try {
75
+ const result = await this.transactionContext.run(
76
+ { depth: 1 },
77
+ async () => fn(this)
78
+ );
79
+ this.database.exec(`COMMIT`);
80
+ return result;
81
+ } catch (error) {
82
+ try {
83
+ this.database.exec(`ROLLBACK`);
84
+ } catch {
85
+ }
86
+ throw error;
87
+ }
88
+ });
89
+ }
90
+ async transactionWithDriver(fn) {
91
+ return this.transaction(fn);
92
+ }
93
+ close() {
94
+ if (!this.ownsDatabase) {
95
+ return;
96
+ }
97
+ this.database.close();
98
+ }
99
+ getDatabase() {
100
+ return this.database;
101
+ }
102
+ applyPragmas(pragmas) {
103
+ for (const pragma of pragmas) {
104
+ const trimmedPragma = pragma.trim();
105
+ if (trimmedPragma.length === 0) {
106
+ continue;
107
+ }
108
+ if (INVALID_PRAGMA_PATTERN.test(trimmedPragma)) {
109
+ throw new dbSqlitePersistenceCore.InvalidPersistedCollectionConfigError(
110
+ `Invalid SQLite PRAGMA: "${pragma}"`
111
+ );
112
+ }
113
+ this.database.exec(`PRAGMA ${trimmedPragma}`);
114
+ }
115
+ }
116
+ isInsideTransaction() {
117
+ return this.transactionContext.getStore() !== void 0;
118
+ }
119
+ executeQuery(sql, params) {
120
+ const statement = this.database.prepare(sql);
121
+ if (params.length === 0) {
122
+ return statement.all();
123
+ }
124
+ return statement.all(...params);
125
+ }
126
+ executeRun(sql, params) {
127
+ const statement = this.database.prepare(sql);
128
+ if (params.length === 0) {
129
+ statement.run();
130
+ return;
131
+ }
132
+ statement.run(...params);
133
+ }
134
+ enqueue(operation) {
135
+ const queuedOperation = this.queue.then(operation, operation);
136
+ this.queue = queuedOperation.then(
137
+ () => void 0,
138
+ () => void 0
139
+ );
140
+ return queuedOperation;
141
+ }
142
+ async runNestedTransaction(fn) {
143
+ const context = this.transactionContext.getStore();
144
+ if (!context) {
145
+ return fn(this);
146
+ }
147
+ const savepointName = `tsdb_sp_${this.nextSavepointId}`;
148
+ this.nextSavepointId++;
149
+ this.database.exec(`SAVEPOINT ${savepointName}`);
150
+ try {
151
+ const result = await this.transactionContext.run(
152
+ { depth: context.depth + 1 },
153
+ async () => fn(this)
154
+ );
155
+ this.database.exec(`RELEASE SAVEPOINT ${savepointName}`);
156
+ return result;
157
+ } catch (error) {
158
+ this.database.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`);
159
+ this.database.exec(`RELEASE SAVEPOINT ${savepointName}`);
160
+ throw error;
161
+ }
162
+ }
163
+ }
164
+ exports.BetterSqlite3SQLiteDriver = BetterSqlite3SQLiteDriver;
165
+ //# sourceMappingURL=node-driver.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-driver.cjs","sources":["../../src/node-driver.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport BetterSqlite3 from 'better-sqlite3'\nimport { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'\nimport type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'\n\nconst DEFAULT_PRAGMAS = [\n `journal_mode = WAL`,\n `synchronous = NORMAL`,\n `foreign_keys = ON`,\n] as const\n\nconst INVALID_PRAGMA_PATTERN = /(;|--|\\/\\*)/\n\nexport type BetterSqlite3Database = InstanceType<typeof BetterSqlite3>\nexport type BetterSqlite3OpenOptions = ConstructorParameters<\n typeof BetterSqlite3\n>[1]\n\ntype BetterSqlite3ExistingDatabaseOptions = {\n database: BetterSqlite3Database\n pragmas?: ReadonlyArray<string>\n}\n\ntype BetterSqlite3OpenFileOptions = {\n filename: string\n options?: BetterSqlite3OpenOptions\n pragmas?: ReadonlyArray<string>\n}\n\nexport type BetterSqlite3DriverOptions =\n | BetterSqlite3ExistingDatabaseOptions\n | BetterSqlite3OpenFileOptions\n\ntype TransactionContext = {\n depth: number\n}\n\nfunction assertTransactionCallbackHasDriverArg(\n fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,\n): void {\n if (fn.length > 0) {\n return\n }\n\n throw new InvalidPersistedCollectionConfigError(\n `SQLiteDriver.transaction callback must accept the transaction driver argument`,\n )\n}\n\nfunction hasExistingDatabase(\n options: BetterSqlite3DriverOptions,\n): options is BetterSqlite3ExistingDatabaseOptions {\n return `database` in options\n}\n\nexport class BetterSqlite3SQLiteDriver implements SQLiteDriver {\n private readonly database: BetterSqlite3Database\n private readonly ownsDatabase: boolean\n private readonly transactionContext =\n new AsyncLocalStorage<TransactionContext>()\n private queue: Promise<void> = Promise.resolve()\n private nextSavepointId = 1\n\n constructor(options: BetterSqlite3DriverOptions) {\n if (hasExistingDatabase(options)) {\n this.database = options.database\n this.ownsDatabase = false\n this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS)\n return\n }\n\n if (options.filename.trim().length === 0) {\n throw new InvalidPersistedCollectionConfigError(\n `Node SQLite driver filename cannot be empty`,\n )\n }\n\n this.database = new BetterSqlite3(options.filename, options.options)\n this.ownsDatabase = true\n this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS)\n }\n\n async exec(sql: string): Promise<void> {\n if (this.isInsideTransaction()) {\n this.database.exec(sql)\n return\n }\n\n await this.enqueue(() => {\n this.database.exec(sql)\n })\n }\n\n async query<T>(\n sql: string,\n params: ReadonlyArray<unknown> = [],\n ): Promise<ReadonlyArray<T>> {\n if (this.isInsideTransaction()) {\n return this.executeQuery<T>(sql, params)\n }\n\n return this.enqueue(() => this.executeQuery<T>(sql, params))\n }\n\n async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {\n if (this.isInsideTransaction()) {\n this.executeRun(sql, params)\n return\n }\n\n await this.enqueue(() => {\n this.executeRun(sql, params)\n })\n }\n\n async transaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn)\n\n if (this.isInsideTransaction()) {\n return this.runNestedTransaction(fn)\n }\n\n return this.enqueue(async () => {\n this.database.exec(`BEGIN IMMEDIATE`)\n try {\n const result = await this.transactionContext.run(\n { depth: 1 },\n async () => fn(this),\n )\n this.database.exec(`COMMIT`)\n return result\n } catch (error) {\n try {\n this.database.exec(`ROLLBACK`)\n } catch {\n // Keep the original transaction error as the primary failure.\n }\n throw error\n }\n })\n }\n\n async transactionWithDriver<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n return this.transaction(fn)\n }\n\n close(): void {\n if (!this.ownsDatabase) {\n return\n }\n\n this.database.close()\n }\n\n getDatabase(): BetterSqlite3Database {\n return this.database\n }\n\n private applyPragmas(pragmas: ReadonlyArray<string>): void {\n for (const pragma of pragmas) {\n const trimmedPragma = pragma.trim()\n if (trimmedPragma.length === 0) {\n continue\n }\n\n if (INVALID_PRAGMA_PATTERN.test(trimmedPragma)) {\n throw new InvalidPersistedCollectionConfigError(\n `Invalid SQLite PRAGMA: \"${pragma}\"`,\n )\n }\n\n this.database.exec(`PRAGMA ${trimmedPragma}`)\n }\n }\n\n private isInsideTransaction(): boolean {\n return this.transactionContext.getStore() !== undefined\n }\n\n private executeQuery<T>(\n sql: string,\n params: ReadonlyArray<unknown>,\n ): ReadonlyArray<T> {\n const statement = this.database.prepare(sql)\n if (params.length === 0) {\n return statement.all() as ReadonlyArray<T>\n }\n\n return statement.all(...params) as ReadonlyArray<T>\n }\n\n private executeRun(sql: string, params: ReadonlyArray<unknown>): void {\n const statement = this.database.prepare(sql)\n if (params.length === 0) {\n statement.run()\n return\n }\n\n statement.run(...params)\n }\n\n private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {\n const queuedOperation = this.queue.then(operation, operation)\n this.queue = queuedOperation.then(\n () => undefined,\n () => undefined,\n )\n return queuedOperation\n }\n\n private async runNestedTransaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n const context = this.transactionContext.getStore()\n if (!context) {\n return fn(this)\n }\n\n const savepointName = `tsdb_sp_${this.nextSavepointId}`\n this.nextSavepointId++\n this.database.exec(`SAVEPOINT ${savepointName}`)\n\n try {\n const result = await this.transactionContext.run(\n { depth: context.depth + 1 },\n async () => fn(this),\n )\n this.database.exec(`RELEASE SAVEPOINT ${savepointName}`)\n return result\n } catch (error) {\n this.database.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`)\n this.database.exec(`RELEASE SAVEPOINT ${savepointName}`)\n throw error\n }\n }\n}\n\nexport function createBetterSqlite3Driver(\n options: BetterSqlite3DriverOptions,\n): BetterSqlite3SQLiteDriver {\n return new BetterSqlite3SQLiteDriver(options)\n}\n"],"names":["InvalidPersistedCollectionConfigError","AsyncLocalStorage"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { default as BetterSqlite3 } from 'better-sqlite3';
2
+ import { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core';
3
+ export type BetterSqlite3Database = InstanceType<typeof BetterSqlite3>;
4
+ export type BetterSqlite3OpenOptions = ConstructorParameters<typeof BetterSqlite3>[1];
5
+ type BetterSqlite3ExistingDatabaseOptions = {
6
+ database: BetterSqlite3Database;
7
+ pragmas?: ReadonlyArray<string>;
8
+ };
9
+ type BetterSqlite3OpenFileOptions = {
10
+ filename: string;
11
+ options?: BetterSqlite3OpenOptions;
12
+ pragmas?: ReadonlyArray<string>;
13
+ };
14
+ export type BetterSqlite3DriverOptions = BetterSqlite3ExistingDatabaseOptions | BetterSqlite3OpenFileOptions;
15
+ export declare class BetterSqlite3SQLiteDriver implements SQLiteDriver {
16
+ private readonly database;
17
+ private readonly ownsDatabase;
18
+ private readonly transactionContext;
19
+ private queue;
20
+ private nextSavepointId;
21
+ constructor(options: BetterSqlite3DriverOptions);
22
+ exec(sql: string): Promise<void>;
23
+ query<T>(sql: string, params?: ReadonlyArray<unknown>): Promise<ReadonlyArray<T>>;
24
+ run(sql: string, params?: ReadonlyArray<unknown>): Promise<void>;
25
+ transaction<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
26
+ transactionWithDriver<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
27
+ close(): void;
28
+ getDatabase(): BetterSqlite3Database;
29
+ private applyPragmas;
30
+ private isInsideTransaction;
31
+ private executeQuery;
32
+ private executeRun;
33
+ private enqueue;
34
+ private runNestedTransaction;
35
+ }
36
+ export declare function createBetterSqlite3Driver(options: BetterSqlite3DriverOptions): BetterSqlite3SQLiteDriver;
37
+ export {};
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
4
+ const nodeDriver = require("./node-driver.cjs");
5
+ function normalizeSchemaMismatchPolicy(policy) {
6
+ if (policy === `throw`) {
7
+ return `sync-absent-error`;
8
+ }
9
+ return policy;
10
+ }
11
+ function resolveSchemaMismatchPolicy(explicitPolicy, mode) {
12
+ if (explicitPolicy) {
13
+ return normalizeSchemaMismatchPolicy(explicitPolicy);
14
+ }
15
+ return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`;
16
+ }
17
+ function createAdapterCacheKey(schemaMismatchPolicy, schemaVersion) {
18
+ const schemaVersionKey = schemaVersion === void 0 ? `schema:default` : `schema:${schemaVersion}`;
19
+ return `${schemaMismatchPolicy}|${schemaVersionKey}`;
20
+ }
21
+ function createInternalSQLiteDriver(options) {
22
+ return new nodeDriver.BetterSqlite3SQLiteDriver({
23
+ database: options.database,
24
+ ...options.pragmas ? { pragmas: options.pragmas } : {}
25
+ });
26
+ }
27
+ function resolveAdapterBaseOptions(options) {
28
+ return {
29
+ appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
30
+ appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
31
+ pullSinceReloadThreshold: options.pullSinceReloadThreshold
32
+ };
33
+ }
34
+ function createNodeSQLitePersistence(options) {
35
+ const { coordinator, schemaMismatchPolicy } = options;
36
+ const driver = createInternalSQLiteDriver(options);
37
+ const adapterBaseOptions = resolveAdapterBaseOptions(options);
38
+ const resolvedCoordinator = coordinator ?? new dbSqlitePersistenceCore.SingleProcessCoordinator();
39
+ const adapterCache = /* @__PURE__ */ new Map();
40
+ const getAdapterForCollection = (mode, schemaVersion) => {
41
+ const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(
42
+ schemaMismatchPolicy,
43
+ mode
44
+ );
45
+ const cacheKey = createAdapterCacheKey(
46
+ resolvedSchemaMismatchPolicy,
47
+ schemaVersion
48
+ );
49
+ const cachedAdapter = adapterCache.get(cacheKey);
50
+ if (cachedAdapter) {
51
+ return cachedAdapter;
52
+ }
53
+ const adapter = dbSqlitePersistenceCore.createSQLiteCorePersistenceAdapter({
54
+ ...adapterBaseOptions,
55
+ driver,
56
+ schemaMismatchPolicy: resolvedSchemaMismatchPolicy,
57
+ ...schemaVersion === void 0 ? {} : { schemaVersion }
58
+ });
59
+ adapterCache.set(cacheKey, adapter);
60
+ return adapter;
61
+ };
62
+ const createCollectionPersistence = (mode, schemaVersion) => ({
63
+ adapter: getAdapterForCollection(
64
+ mode,
65
+ schemaVersion
66
+ ),
67
+ coordinator: resolvedCoordinator
68
+ });
69
+ const defaultPersistence = createCollectionPersistence(
70
+ `sync-absent`,
71
+ void 0
72
+ );
73
+ return {
74
+ ...defaultPersistence,
75
+ resolvePersistenceForCollection: ({ mode, schemaVersion }) => createCollectionPersistence(mode, schemaVersion),
76
+ // Backward compatible fallback for older callers.
77
+ resolvePersistenceForMode: (mode) => createCollectionPersistence(mode, void 0)
78
+ };
79
+ }
80
+ exports.createNodeSQLitePersistence = createNodeSQLitePersistence;
81
+ //# sourceMappingURL=node-persistence.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-persistence.cjs","sources":["../../src/node-persistence.ts"],"sourcesContent":["import {\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { BetterSqlite3SQLiteDriver } from './node-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { BetterSqlite3Database } from './node-driver'\n\nexport type { BetterSqlite3Database } from './node-driver'\n\ntype NodeSQLiteCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type NodeSQLiteSchemaMismatchPolicy =\n | NodeSQLiteCoreSchemaMismatchPolicy\n | `throw`\n\ntype NodeSQLitePersistenceBaseOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n database: BetterSqlite3Database\n pragmas?: ReadonlyArray<string>\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: NodeSQLiteSchemaMismatchPolicy\n}\n\nexport type NodeSQLitePersistenceOptions = NodeSQLitePersistenceBaseOptions\n\nfunction normalizeSchemaMismatchPolicy(\n policy: NodeSQLiteSchemaMismatchPolicy,\n): NodeSQLiteCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: NodeSQLiteSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): NodeSQLiteCoreSchemaMismatchPolicy {\n if (explicitPolicy) {\n return normalizeSchemaMismatchPolicy(explicitPolicy)\n }\n\n return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`\n}\n\nfunction createAdapterCacheKey(\n schemaMismatchPolicy: NodeSQLiteCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction createInternalSQLiteDriver(\n options: NodeSQLitePersistenceOptions,\n): SQLiteDriver {\n return new BetterSqlite3SQLiteDriver({\n database: options.database,\n ...(options.pragmas ? { pragmas: options.pragmas } : {}),\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: NodeSQLitePersistenceOptions,\n): Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> {\n return {\n appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,\n appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,\n pullSinceReloadThreshold: options.pullSinceReloadThreshold,\n }\n}\n\n/**\n * Creates a shared SQLite persistence instance that can be reused by many\n * collections on the same database. Collection-specific schema versions are\n * resolved by `persistedCollectionOptions` via `resolvePersistenceForCollection`.\n */\nexport function createNodeSQLitePersistence<\n T extends object,\n TKey extends string | number = string | number,\n>(\n options: NodeSQLitePersistenceOptions,\n): PersistedCollectionPersistence<T, TKey> {\n const { coordinator, schemaMismatchPolicy } = options\n const driver = createInternalSQLiteDriver(options)\n const adapterBaseOptions = resolveAdapterBaseOptions(options)\n const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator()\n const adapterCache = new Map<\n string,\n ReturnType<\n typeof createSQLiteCorePersistenceAdapter<\n Record<string, unknown>,\n string | number\n >\n >\n >()\n\n const getAdapterForCollection = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ) => {\n const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(\n schemaMismatchPolicy,\n mode,\n )\n const cacheKey = createAdapterCacheKey(\n resolvedSchemaMismatchPolicy,\n schemaVersion,\n )\n const cachedAdapter = adapterCache.get(cacheKey)\n if (cachedAdapter) {\n return cachedAdapter\n }\n\n const adapter = createSQLiteCorePersistenceAdapter<\n Record<string, unknown>,\n string | number\n >({\n ...adapterBaseOptions,\n driver,\n schemaMismatchPolicy: resolvedSchemaMismatchPolicy,\n ...(schemaVersion === undefined ? {} : { schemaVersion }),\n })\n adapterCache.set(cacheKey, adapter)\n return adapter\n }\n\n const createCollectionPersistence = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ): PersistedCollectionPersistence<T, TKey> => ({\n adapter: getAdapterForCollection(\n mode,\n schemaVersion,\n ) as unknown as PersistedCollectionPersistence<T, TKey>[`adapter`],\n coordinator: resolvedCoordinator,\n })\n\n const defaultPersistence = createCollectionPersistence(\n `sync-absent`,\n undefined,\n )\n\n return {\n ...defaultPersistence,\n resolvePersistenceForCollection: ({ mode, schemaVersion }) =>\n createCollectionPersistence(mode, schemaVersion),\n // Backward compatible fallback for older callers.\n resolvePersistenceForMode: (mode) =>\n createCollectionPersistence(mode, undefined),\n }\n}\n"],"names":["BetterSqlite3SQLiteDriver","SingleProcessCoordinator","createSQLiteCorePersistenceAdapter"],"mappings":";;;;AAqCA,SAAS,8BACP,QACoC;AACpC,MAAI,WAAW,SAAS;AACtB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,4BACP,gBACA,MACoC;AACpC,MAAI,gBAAgB;AAClB,WAAO,8BAA8B,cAAc;AAAA,EACrD;AAEA,SAAO,SAAS,iBAAiB,uBAAuB;AAC1D;AAEA,SAAS,sBACP,sBACA,eACQ;AACR,QAAM,mBACJ,kBAAkB,SAAY,mBAAmB,UAAU,aAAa;AAC1E,SAAO,GAAG,oBAAoB,IAAI,gBAAgB;AACpD;AAEA,SAAS,2BACP,SACc;AACd,SAAO,IAAIA,WAAAA,0BAA0B;AAAA,IACnC,UAAU,QAAQ;AAAA,IAClB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAA,IAAY,CAAA;AAAA,EAAC,CACvD;AACH;AAEA,SAAS,0BACP,SAIA;AACA,SAAO;AAAA,IACL,uBAAuB,QAAQ;AAAA,IAC/B,6BAA6B,QAAQ;AAAA,IACrC,0BAA0B,QAAQ;AAAA,EAAA;AAEtC;AAOO,SAAS,4BAId,SACyC;AACzC,QAAM,EAAE,aAAa,qBAAA,IAAyB;AAC9C,QAAM,SAAS,2BAA2B,OAAO;AACjD,QAAM,qBAAqB,0BAA0B,OAAO;AAC5D,QAAM,sBAAsB,eAAe,IAAIC,iDAAA;AAC/C,QAAM,mCAAmB,IAAA;AAUzB,QAAM,0BAA0B,CAC9B,MACA,kBACG;AACH,UAAM,+BAA+B;AAAA,MACnC;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,WAAW;AAAA,MACf;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,gBAAgB,aAAa,IAAI,QAAQ;AAC/C,QAAI,eAAe;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAUC,wBAAAA,mCAGd;AAAA,MACA,GAAG;AAAA,MACH;AAAA,MACA,sBAAsB;AAAA,MACtB,GAAI,kBAAkB,SAAY,CAAA,IAAK,EAAE,cAAA;AAAA,IAAc,CACxD;AACD,iBAAa,IAAI,UAAU,OAAO;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,8BAA8B,CAClC,MACA,mBAC6C;AAAA,IAC7C,SAAS;AAAA,MACP;AAAA,MACA;AAAA,IAAA;AAAA,IAEF,aAAa;AAAA,EAAA;AAGf,QAAM,qBAAqB;AAAA,IACzB;AAAA,IACA;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,iCAAiC,CAAC,EAAE,MAAM,oBACxC,4BAA4B,MAAM,aAAa;AAAA;AAAA,IAEjD,2BAA2B,CAAC,SAC1B,4BAA4B,MAAM,MAAS;AAAA,EAAA;AAEjD;;"}
@@ -0,0 +1,18 @@
1
+ import { PersistedCollectionCoordinator, PersistedCollectionPersistence, SQLiteCoreAdapterOptions } from '@tanstack/db-sqlite-persistence-core';
2
+ import { BetterSqlite3Database } from './node-driver.cjs';
3
+ export type { BetterSqlite3Database } from './node-driver.cjs';
4
+ type NodeSQLiteCoreSchemaMismatchPolicy = `sync-present-reset` | `sync-absent-error` | `reset`;
5
+ export type NodeSQLiteSchemaMismatchPolicy = NodeSQLiteCoreSchemaMismatchPolicy | `throw`;
6
+ type NodeSQLitePersistenceBaseOptions = Omit<SQLiteCoreAdapterOptions, `driver` | `schemaVersion` | `schemaMismatchPolicy`> & {
7
+ database: BetterSqlite3Database;
8
+ pragmas?: ReadonlyArray<string>;
9
+ coordinator?: PersistedCollectionCoordinator;
10
+ schemaMismatchPolicy?: NodeSQLiteSchemaMismatchPolicy;
11
+ };
12
+ export type NodeSQLitePersistenceOptions = NodeSQLitePersistenceBaseOptions;
13
+ /**
14
+ * Creates a shared SQLite persistence instance that can be reused by many
15
+ * collections on the same database. Collection-specific schema versions are
16
+ * resolved by `persistedCollectionOptions` via `resolvePersistenceForCollection`.
17
+ */
18
+ export declare function createNodeSQLitePersistence<T extends object, TKey extends string | number = string | number>(options: NodeSQLitePersistenceOptions): PersistedCollectionPersistence<T, TKey>;
@@ -0,0 +1,4 @@
1
+ export { createNodeSQLitePersistence } from './node-persistence.js';
2
+ export type { BetterSqlite3Database, NodeSQLitePersistenceOptions, NodeSQLiteSchemaMismatchPolicy, } from './node-persistence.js';
3
+ export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core';
4
+ export type { PersistedCollectionCoordinator, PersistedCollectionPersistence, } from '@tanstack/db-sqlite-persistence-core';
@@ -0,0 +1,7 @@
1
+ import { createNodeSQLitePersistence } from "./node-persistence.js";
2
+ import { persistedCollectionOptions } from "@tanstack/db-sqlite-persistence-core";
3
+ export {
4
+ createNodeSQLitePersistence,
5
+ persistedCollectionOptions
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,37 @@
1
+ import { default as BetterSqlite3 } from 'better-sqlite3';
2
+ import { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core';
3
+ export type BetterSqlite3Database = InstanceType<typeof BetterSqlite3>;
4
+ export type BetterSqlite3OpenOptions = ConstructorParameters<typeof BetterSqlite3>[1];
5
+ type BetterSqlite3ExistingDatabaseOptions = {
6
+ database: BetterSqlite3Database;
7
+ pragmas?: ReadonlyArray<string>;
8
+ };
9
+ type BetterSqlite3OpenFileOptions = {
10
+ filename: string;
11
+ options?: BetterSqlite3OpenOptions;
12
+ pragmas?: ReadonlyArray<string>;
13
+ };
14
+ export type BetterSqlite3DriverOptions = BetterSqlite3ExistingDatabaseOptions | BetterSqlite3OpenFileOptions;
15
+ export declare class BetterSqlite3SQLiteDriver implements SQLiteDriver {
16
+ private readonly database;
17
+ private readonly ownsDatabase;
18
+ private readonly transactionContext;
19
+ private queue;
20
+ private nextSavepointId;
21
+ constructor(options: BetterSqlite3DriverOptions);
22
+ exec(sql: string): Promise<void>;
23
+ query<T>(sql: string, params?: ReadonlyArray<unknown>): Promise<ReadonlyArray<T>>;
24
+ run(sql: string, params?: ReadonlyArray<unknown>): Promise<void>;
25
+ transaction<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
26
+ transactionWithDriver<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
27
+ close(): void;
28
+ getDatabase(): BetterSqlite3Database;
29
+ private applyPragmas;
30
+ private isInsideTransaction;
31
+ private executeQuery;
32
+ private executeRun;
33
+ private enqueue;
34
+ private runNestedTransaction;
35
+ }
36
+ export declare function createBetterSqlite3Driver(options: BetterSqlite3DriverOptions): BetterSqlite3SQLiteDriver;
37
+ export {};
@@ -0,0 +1,165 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import BetterSqlite3 from "better-sqlite3";
3
+ import { InvalidPersistedCollectionConfigError } from "@tanstack/db-sqlite-persistence-core";
4
+ const DEFAULT_PRAGMAS = [
5
+ `journal_mode = WAL`,
6
+ `synchronous = NORMAL`,
7
+ `foreign_keys = ON`
8
+ ];
9
+ const INVALID_PRAGMA_PATTERN = /(;|--|\/\*)/;
10
+ function assertTransactionCallbackHasDriverArg(fn) {
11
+ if (fn.length > 0) {
12
+ return;
13
+ }
14
+ throw new InvalidPersistedCollectionConfigError(
15
+ `SQLiteDriver.transaction callback must accept the transaction driver argument`
16
+ );
17
+ }
18
+ function hasExistingDatabase(options) {
19
+ return `database` in options;
20
+ }
21
+ class BetterSqlite3SQLiteDriver {
22
+ constructor(options) {
23
+ this.transactionContext = new AsyncLocalStorage();
24
+ this.queue = Promise.resolve();
25
+ this.nextSavepointId = 1;
26
+ if (hasExistingDatabase(options)) {
27
+ this.database = options.database;
28
+ this.ownsDatabase = false;
29
+ this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS);
30
+ return;
31
+ }
32
+ if (options.filename.trim().length === 0) {
33
+ throw new InvalidPersistedCollectionConfigError(
34
+ `Node SQLite driver filename cannot be empty`
35
+ );
36
+ }
37
+ this.database = new BetterSqlite3(options.filename, options.options);
38
+ this.ownsDatabase = true;
39
+ this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS);
40
+ }
41
+ async exec(sql) {
42
+ if (this.isInsideTransaction()) {
43
+ this.database.exec(sql);
44
+ return;
45
+ }
46
+ await this.enqueue(() => {
47
+ this.database.exec(sql);
48
+ });
49
+ }
50
+ async query(sql, params = []) {
51
+ if (this.isInsideTransaction()) {
52
+ return this.executeQuery(sql, params);
53
+ }
54
+ return this.enqueue(() => this.executeQuery(sql, params));
55
+ }
56
+ async run(sql, params = []) {
57
+ if (this.isInsideTransaction()) {
58
+ this.executeRun(sql, params);
59
+ return;
60
+ }
61
+ await this.enqueue(() => {
62
+ this.executeRun(sql, params);
63
+ });
64
+ }
65
+ async transaction(fn) {
66
+ assertTransactionCallbackHasDriverArg(fn);
67
+ if (this.isInsideTransaction()) {
68
+ return this.runNestedTransaction(fn);
69
+ }
70
+ return this.enqueue(async () => {
71
+ this.database.exec(`BEGIN IMMEDIATE`);
72
+ try {
73
+ const result = await this.transactionContext.run(
74
+ { depth: 1 },
75
+ async () => fn(this)
76
+ );
77
+ this.database.exec(`COMMIT`);
78
+ return result;
79
+ } catch (error) {
80
+ try {
81
+ this.database.exec(`ROLLBACK`);
82
+ } catch {
83
+ }
84
+ throw error;
85
+ }
86
+ });
87
+ }
88
+ async transactionWithDriver(fn) {
89
+ return this.transaction(fn);
90
+ }
91
+ close() {
92
+ if (!this.ownsDatabase) {
93
+ return;
94
+ }
95
+ this.database.close();
96
+ }
97
+ getDatabase() {
98
+ return this.database;
99
+ }
100
+ applyPragmas(pragmas) {
101
+ for (const pragma of pragmas) {
102
+ const trimmedPragma = pragma.trim();
103
+ if (trimmedPragma.length === 0) {
104
+ continue;
105
+ }
106
+ if (INVALID_PRAGMA_PATTERN.test(trimmedPragma)) {
107
+ throw new InvalidPersistedCollectionConfigError(
108
+ `Invalid SQLite PRAGMA: "${pragma}"`
109
+ );
110
+ }
111
+ this.database.exec(`PRAGMA ${trimmedPragma}`);
112
+ }
113
+ }
114
+ isInsideTransaction() {
115
+ return this.transactionContext.getStore() !== void 0;
116
+ }
117
+ executeQuery(sql, params) {
118
+ const statement = this.database.prepare(sql);
119
+ if (params.length === 0) {
120
+ return statement.all();
121
+ }
122
+ return statement.all(...params);
123
+ }
124
+ executeRun(sql, params) {
125
+ const statement = this.database.prepare(sql);
126
+ if (params.length === 0) {
127
+ statement.run();
128
+ return;
129
+ }
130
+ statement.run(...params);
131
+ }
132
+ enqueue(operation) {
133
+ const queuedOperation = this.queue.then(operation, operation);
134
+ this.queue = queuedOperation.then(
135
+ () => void 0,
136
+ () => void 0
137
+ );
138
+ return queuedOperation;
139
+ }
140
+ async runNestedTransaction(fn) {
141
+ const context = this.transactionContext.getStore();
142
+ if (!context) {
143
+ return fn(this);
144
+ }
145
+ const savepointName = `tsdb_sp_${this.nextSavepointId}`;
146
+ this.nextSavepointId++;
147
+ this.database.exec(`SAVEPOINT ${savepointName}`);
148
+ try {
149
+ const result = await this.transactionContext.run(
150
+ { depth: context.depth + 1 },
151
+ async () => fn(this)
152
+ );
153
+ this.database.exec(`RELEASE SAVEPOINT ${savepointName}`);
154
+ return result;
155
+ } catch (error) {
156
+ this.database.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`);
157
+ this.database.exec(`RELEASE SAVEPOINT ${savepointName}`);
158
+ throw error;
159
+ }
160
+ }
161
+ }
162
+ export {
163
+ BetterSqlite3SQLiteDriver
164
+ };
165
+ //# sourceMappingURL=node-driver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-driver.js","sources":["../../src/node-driver.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport BetterSqlite3 from 'better-sqlite3'\nimport { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'\nimport type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'\n\nconst DEFAULT_PRAGMAS = [\n `journal_mode = WAL`,\n `synchronous = NORMAL`,\n `foreign_keys = ON`,\n] as const\n\nconst INVALID_PRAGMA_PATTERN = /(;|--|\\/\\*)/\n\nexport type BetterSqlite3Database = InstanceType<typeof BetterSqlite3>\nexport type BetterSqlite3OpenOptions = ConstructorParameters<\n typeof BetterSqlite3\n>[1]\n\ntype BetterSqlite3ExistingDatabaseOptions = {\n database: BetterSqlite3Database\n pragmas?: ReadonlyArray<string>\n}\n\ntype BetterSqlite3OpenFileOptions = {\n filename: string\n options?: BetterSqlite3OpenOptions\n pragmas?: ReadonlyArray<string>\n}\n\nexport type BetterSqlite3DriverOptions =\n | BetterSqlite3ExistingDatabaseOptions\n | BetterSqlite3OpenFileOptions\n\ntype TransactionContext = {\n depth: number\n}\n\nfunction assertTransactionCallbackHasDriverArg(\n fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,\n): void {\n if (fn.length > 0) {\n return\n }\n\n throw new InvalidPersistedCollectionConfigError(\n `SQLiteDriver.transaction callback must accept the transaction driver argument`,\n )\n}\n\nfunction hasExistingDatabase(\n options: BetterSqlite3DriverOptions,\n): options is BetterSqlite3ExistingDatabaseOptions {\n return `database` in options\n}\n\nexport class BetterSqlite3SQLiteDriver implements SQLiteDriver {\n private readonly database: BetterSqlite3Database\n private readonly ownsDatabase: boolean\n private readonly transactionContext =\n new AsyncLocalStorage<TransactionContext>()\n private queue: Promise<void> = Promise.resolve()\n private nextSavepointId = 1\n\n constructor(options: BetterSqlite3DriverOptions) {\n if (hasExistingDatabase(options)) {\n this.database = options.database\n this.ownsDatabase = false\n this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS)\n return\n }\n\n if (options.filename.trim().length === 0) {\n throw new InvalidPersistedCollectionConfigError(\n `Node SQLite driver filename cannot be empty`,\n )\n }\n\n this.database = new BetterSqlite3(options.filename, options.options)\n this.ownsDatabase = true\n this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS)\n }\n\n async exec(sql: string): Promise<void> {\n if (this.isInsideTransaction()) {\n this.database.exec(sql)\n return\n }\n\n await this.enqueue(() => {\n this.database.exec(sql)\n })\n }\n\n async query<T>(\n sql: string,\n params: ReadonlyArray<unknown> = [],\n ): Promise<ReadonlyArray<T>> {\n if (this.isInsideTransaction()) {\n return this.executeQuery<T>(sql, params)\n }\n\n return this.enqueue(() => this.executeQuery<T>(sql, params))\n }\n\n async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {\n if (this.isInsideTransaction()) {\n this.executeRun(sql, params)\n return\n }\n\n await this.enqueue(() => {\n this.executeRun(sql, params)\n })\n }\n\n async transaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn)\n\n if (this.isInsideTransaction()) {\n return this.runNestedTransaction(fn)\n }\n\n return this.enqueue(async () => {\n this.database.exec(`BEGIN IMMEDIATE`)\n try {\n const result = await this.transactionContext.run(\n { depth: 1 },\n async () => fn(this),\n )\n this.database.exec(`COMMIT`)\n return result\n } catch (error) {\n try {\n this.database.exec(`ROLLBACK`)\n } catch {\n // Keep the original transaction error as the primary failure.\n }\n throw error\n }\n })\n }\n\n async transactionWithDriver<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n return this.transaction(fn)\n }\n\n close(): void {\n if (!this.ownsDatabase) {\n return\n }\n\n this.database.close()\n }\n\n getDatabase(): BetterSqlite3Database {\n return this.database\n }\n\n private applyPragmas(pragmas: ReadonlyArray<string>): void {\n for (const pragma of pragmas) {\n const trimmedPragma = pragma.trim()\n if (trimmedPragma.length === 0) {\n continue\n }\n\n if (INVALID_PRAGMA_PATTERN.test(trimmedPragma)) {\n throw new InvalidPersistedCollectionConfigError(\n `Invalid SQLite PRAGMA: \"${pragma}\"`,\n )\n }\n\n this.database.exec(`PRAGMA ${trimmedPragma}`)\n }\n }\n\n private isInsideTransaction(): boolean {\n return this.transactionContext.getStore() !== undefined\n }\n\n private executeQuery<T>(\n sql: string,\n params: ReadonlyArray<unknown>,\n ): ReadonlyArray<T> {\n const statement = this.database.prepare(sql)\n if (params.length === 0) {\n return statement.all() as ReadonlyArray<T>\n }\n\n return statement.all(...params) as ReadonlyArray<T>\n }\n\n private executeRun(sql: string, params: ReadonlyArray<unknown>): void {\n const statement = this.database.prepare(sql)\n if (params.length === 0) {\n statement.run()\n return\n }\n\n statement.run(...params)\n }\n\n private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {\n const queuedOperation = this.queue.then(operation, operation)\n this.queue = queuedOperation.then(\n () => undefined,\n () => undefined,\n )\n return queuedOperation\n }\n\n private async runNestedTransaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n const context = this.transactionContext.getStore()\n if (!context) {\n return fn(this)\n }\n\n const savepointName = `tsdb_sp_${this.nextSavepointId}`\n this.nextSavepointId++\n this.database.exec(`SAVEPOINT ${savepointName}`)\n\n try {\n const result = await this.transactionContext.run(\n { depth: context.depth + 1 },\n async () => fn(this),\n )\n this.database.exec(`RELEASE SAVEPOINT ${savepointName}`)\n return result\n } catch (error) {\n this.database.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`)\n this.database.exec(`RELEASE SAVEPOINT ${savepointName}`)\n throw error\n }\n }\n}\n\nexport function createBetterSqlite3Driver(\n options: BetterSqlite3DriverOptions,\n): BetterSqlite3SQLiteDriver {\n return new BetterSqlite3SQLiteDriver(options)\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { PersistedCollectionCoordinator, PersistedCollectionPersistence, SQLiteCoreAdapterOptions } from '@tanstack/db-sqlite-persistence-core';
2
+ import { BetterSqlite3Database } from './node-driver.js';
3
+ export type { BetterSqlite3Database } from './node-driver.js';
4
+ type NodeSQLiteCoreSchemaMismatchPolicy = `sync-present-reset` | `sync-absent-error` | `reset`;
5
+ export type NodeSQLiteSchemaMismatchPolicy = NodeSQLiteCoreSchemaMismatchPolicy | `throw`;
6
+ type NodeSQLitePersistenceBaseOptions = Omit<SQLiteCoreAdapterOptions, `driver` | `schemaVersion` | `schemaMismatchPolicy`> & {
7
+ database: BetterSqlite3Database;
8
+ pragmas?: ReadonlyArray<string>;
9
+ coordinator?: PersistedCollectionCoordinator;
10
+ schemaMismatchPolicy?: NodeSQLiteSchemaMismatchPolicy;
11
+ };
12
+ export type NodeSQLitePersistenceOptions = NodeSQLitePersistenceBaseOptions;
13
+ /**
14
+ * Creates a shared SQLite persistence instance that can be reused by many
15
+ * collections on the same database. Collection-specific schema versions are
16
+ * resolved by `persistedCollectionOptions` via `resolvePersistenceForCollection`.
17
+ */
18
+ export declare function createNodeSQLitePersistence<T extends object, TKey extends string | number = string | number>(options: NodeSQLitePersistenceOptions): PersistedCollectionPersistence<T, TKey>;
@@ -0,0 +1,81 @@
1
+ import { SingleProcessCoordinator, createSQLiteCorePersistenceAdapter } from "@tanstack/db-sqlite-persistence-core";
2
+ import { BetterSqlite3SQLiteDriver } from "./node-driver.js";
3
+ function normalizeSchemaMismatchPolicy(policy) {
4
+ if (policy === `throw`) {
5
+ return `sync-absent-error`;
6
+ }
7
+ return policy;
8
+ }
9
+ function resolveSchemaMismatchPolicy(explicitPolicy, mode) {
10
+ if (explicitPolicy) {
11
+ return normalizeSchemaMismatchPolicy(explicitPolicy);
12
+ }
13
+ return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`;
14
+ }
15
+ function createAdapterCacheKey(schemaMismatchPolicy, schemaVersion) {
16
+ const schemaVersionKey = schemaVersion === void 0 ? `schema:default` : `schema:${schemaVersion}`;
17
+ return `${schemaMismatchPolicy}|${schemaVersionKey}`;
18
+ }
19
+ function createInternalSQLiteDriver(options) {
20
+ return new BetterSqlite3SQLiteDriver({
21
+ database: options.database,
22
+ ...options.pragmas ? { pragmas: options.pragmas } : {}
23
+ });
24
+ }
25
+ function resolveAdapterBaseOptions(options) {
26
+ return {
27
+ appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
28
+ appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
29
+ pullSinceReloadThreshold: options.pullSinceReloadThreshold
30
+ };
31
+ }
32
+ function createNodeSQLitePersistence(options) {
33
+ const { coordinator, schemaMismatchPolicy } = options;
34
+ const driver = createInternalSQLiteDriver(options);
35
+ const adapterBaseOptions = resolveAdapterBaseOptions(options);
36
+ const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator();
37
+ const adapterCache = /* @__PURE__ */ new Map();
38
+ const getAdapterForCollection = (mode, schemaVersion) => {
39
+ const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(
40
+ schemaMismatchPolicy,
41
+ mode
42
+ );
43
+ const cacheKey = createAdapterCacheKey(
44
+ resolvedSchemaMismatchPolicy,
45
+ schemaVersion
46
+ );
47
+ const cachedAdapter = adapterCache.get(cacheKey);
48
+ if (cachedAdapter) {
49
+ return cachedAdapter;
50
+ }
51
+ const adapter = createSQLiteCorePersistenceAdapter({
52
+ ...adapterBaseOptions,
53
+ driver,
54
+ schemaMismatchPolicy: resolvedSchemaMismatchPolicy,
55
+ ...schemaVersion === void 0 ? {} : { schemaVersion }
56
+ });
57
+ adapterCache.set(cacheKey, adapter);
58
+ return adapter;
59
+ };
60
+ const createCollectionPersistence = (mode, schemaVersion) => ({
61
+ adapter: getAdapterForCollection(
62
+ mode,
63
+ schemaVersion
64
+ ),
65
+ coordinator: resolvedCoordinator
66
+ });
67
+ const defaultPersistence = createCollectionPersistence(
68
+ `sync-absent`,
69
+ void 0
70
+ );
71
+ return {
72
+ ...defaultPersistence,
73
+ resolvePersistenceForCollection: ({ mode, schemaVersion }) => createCollectionPersistence(mode, schemaVersion),
74
+ // Backward compatible fallback for older callers.
75
+ resolvePersistenceForMode: (mode) => createCollectionPersistence(mode, void 0)
76
+ };
77
+ }
78
+ export {
79
+ createNodeSQLitePersistence
80
+ };
81
+ //# sourceMappingURL=node-persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-persistence.js","sources":["../../src/node-persistence.ts"],"sourcesContent":["import {\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { BetterSqlite3SQLiteDriver } from './node-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { BetterSqlite3Database } from './node-driver'\n\nexport type { BetterSqlite3Database } from './node-driver'\n\ntype NodeSQLiteCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type NodeSQLiteSchemaMismatchPolicy =\n | NodeSQLiteCoreSchemaMismatchPolicy\n | `throw`\n\ntype NodeSQLitePersistenceBaseOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n database: BetterSqlite3Database\n pragmas?: ReadonlyArray<string>\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: NodeSQLiteSchemaMismatchPolicy\n}\n\nexport type NodeSQLitePersistenceOptions = NodeSQLitePersistenceBaseOptions\n\nfunction normalizeSchemaMismatchPolicy(\n policy: NodeSQLiteSchemaMismatchPolicy,\n): NodeSQLiteCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: NodeSQLiteSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): NodeSQLiteCoreSchemaMismatchPolicy {\n if (explicitPolicy) {\n return normalizeSchemaMismatchPolicy(explicitPolicy)\n }\n\n return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`\n}\n\nfunction createAdapterCacheKey(\n schemaMismatchPolicy: NodeSQLiteCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction createInternalSQLiteDriver(\n options: NodeSQLitePersistenceOptions,\n): SQLiteDriver {\n return new BetterSqlite3SQLiteDriver({\n database: options.database,\n ...(options.pragmas ? { pragmas: options.pragmas } : {}),\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: NodeSQLitePersistenceOptions,\n): Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> {\n return {\n appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,\n appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,\n pullSinceReloadThreshold: options.pullSinceReloadThreshold,\n }\n}\n\n/**\n * Creates a shared SQLite persistence instance that can be reused by many\n * collections on the same database. Collection-specific schema versions are\n * resolved by `persistedCollectionOptions` via `resolvePersistenceForCollection`.\n */\nexport function createNodeSQLitePersistence<\n T extends object,\n TKey extends string | number = string | number,\n>(\n options: NodeSQLitePersistenceOptions,\n): PersistedCollectionPersistence<T, TKey> {\n const { coordinator, schemaMismatchPolicy } = options\n const driver = createInternalSQLiteDriver(options)\n const adapterBaseOptions = resolveAdapterBaseOptions(options)\n const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator()\n const adapterCache = new Map<\n string,\n ReturnType<\n typeof createSQLiteCorePersistenceAdapter<\n Record<string, unknown>,\n string | number\n >\n >\n >()\n\n const getAdapterForCollection = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ) => {\n const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(\n schemaMismatchPolicy,\n mode,\n )\n const cacheKey = createAdapterCacheKey(\n resolvedSchemaMismatchPolicy,\n schemaVersion,\n )\n const cachedAdapter = adapterCache.get(cacheKey)\n if (cachedAdapter) {\n return cachedAdapter\n }\n\n const adapter = createSQLiteCorePersistenceAdapter<\n Record<string, unknown>,\n string | number\n >({\n ...adapterBaseOptions,\n driver,\n schemaMismatchPolicy: resolvedSchemaMismatchPolicy,\n ...(schemaVersion === undefined ? {} : { schemaVersion }),\n })\n adapterCache.set(cacheKey, adapter)\n return adapter\n }\n\n const createCollectionPersistence = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ): PersistedCollectionPersistence<T, TKey> => ({\n adapter: getAdapterForCollection(\n mode,\n schemaVersion,\n ) as unknown as PersistedCollectionPersistence<T, TKey>[`adapter`],\n coordinator: resolvedCoordinator,\n })\n\n const defaultPersistence = createCollectionPersistence(\n `sync-absent`,\n undefined,\n )\n\n return {\n ...defaultPersistence,\n resolvePersistenceForCollection: ({ mode, schemaVersion }) =>\n createCollectionPersistence(mode, schemaVersion),\n // Backward compatible fallback for older callers.\n resolvePersistenceForMode: (mode) =>\n createCollectionPersistence(mode, undefined),\n }\n}\n"],"names":[],"mappings":";;AAqCA,SAAS,8BACP,QACoC;AACpC,MAAI,WAAW,SAAS;AACtB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,4BACP,gBACA,MACoC;AACpC,MAAI,gBAAgB;AAClB,WAAO,8BAA8B,cAAc;AAAA,EACrD;AAEA,SAAO,SAAS,iBAAiB,uBAAuB;AAC1D;AAEA,SAAS,sBACP,sBACA,eACQ;AACR,QAAM,mBACJ,kBAAkB,SAAY,mBAAmB,UAAU,aAAa;AAC1E,SAAO,GAAG,oBAAoB,IAAI,gBAAgB;AACpD;AAEA,SAAS,2BACP,SACc;AACd,SAAO,IAAI,0BAA0B;AAAA,IACnC,UAAU,QAAQ;AAAA,IAClB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAA,IAAY,CAAA;AAAA,EAAC,CACvD;AACH;AAEA,SAAS,0BACP,SAIA;AACA,SAAO;AAAA,IACL,uBAAuB,QAAQ;AAAA,IAC/B,6BAA6B,QAAQ;AAAA,IACrC,0BAA0B,QAAQ;AAAA,EAAA;AAEtC;AAOO,SAAS,4BAId,SACyC;AACzC,QAAM,EAAE,aAAa,qBAAA,IAAyB;AAC9C,QAAM,SAAS,2BAA2B,OAAO;AACjD,QAAM,qBAAqB,0BAA0B,OAAO;AAC5D,QAAM,sBAAsB,eAAe,IAAI,yBAAA;AAC/C,QAAM,mCAAmB,IAAA;AAUzB,QAAM,0BAA0B,CAC9B,MACA,kBACG;AACH,UAAM,+BAA+B;AAAA,MACnC;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,WAAW;AAAA,MACf;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,gBAAgB,aAAa,IAAI,QAAQ;AAC/C,QAAI,eAAe;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,mCAGd;AAAA,MACA,GAAG;AAAA,MACH;AAAA,MACA,sBAAsB;AAAA,MACtB,GAAI,kBAAkB,SAAY,CAAA,IAAK,EAAE,cAAA;AAAA,IAAc,CACxD;AACD,iBAAa,IAAI,UAAU,OAAO;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,8BAA8B,CAClC,MACA,mBAC6C;AAAA,IAC7C,SAAS;AAAA,MACP;AAAA,MACA;AAAA,IAAA;AAAA,IAEF,aAAa;AAAA,EAAA;AAGf,QAAM,qBAAqB;AAAA,IACzB;AAAA,IACA;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,iCAAiC,CAAC,EAAE,MAAM,oBACxC,4BAA4B,MAAM,aAAa;AAAA;AAAA,IAEjD,2BAA2B,CAAC,SAC1B,4BAA4B,MAAM,MAAS;AAAA,EAAA;AAEjD;"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@tanstack/node-db-sqlite-persistence",
3
+ "version": "0.1.2",
4
+ "description": "Node SQLite persisted collection adapter for TanStack DB",
5
+ "author": "TanStack Team",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/db.git",
10
+ "directory": "packages/node-db-sqlite-persistence"
11
+ },
12
+ "homepage": "https://tanstack.com/db",
13
+ "keywords": [
14
+ "sqlite",
15
+ "node",
16
+ "persistence",
17
+ "optimistic",
18
+ "typescript"
19
+ ],
20
+ "scripts": {
21
+ "build": "vite build",
22
+ "dev": "vite build --watch",
23
+ "lint": "eslint . --fix",
24
+ "test": "vitest --run",
25
+ "test:e2e": "pnpm --filter @tanstack/db-ivm build && pnpm --filter @tanstack/db build && pnpm --filter @tanstack/db-sqlite-persistence-core build && pnpm --filter @tanstack/node-db-sqlite-persistence build && vitest --config vitest.e2e.config.ts --run"
26
+ },
27
+ "type": "module",
28
+ "main": "dist/cjs/index.cjs",
29
+ "module": "dist/esm/index.js",
30
+ "types": "dist/esm/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": {
34
+ "types": "./dist/esm/index.d.ts",
35
+ "default": "./dist/esm/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/cjs/index.d.cts",
39
+ "default": "./dist/cjs/index.cjs"
40
+ }
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "sideEffects": false,
45
+ "files": [
46
+ "dist",
47
+ "src"
48
+ ],
49
+ "dependencies": {
50
+ "@tanstack/db-sqlite-persistence-core": "workspace:*",
51
+ "better-sqlite3": "^12.6.2"
52
+ },
53
+ "peerDependencies": {
54
+ "typescript": ">=4.7"
55
+ },
56
+ "devDependencies": {
57
+ "@types/better-sqlite3": "^7.6.13",
58
+ "@vitest/coverage-istanbul": "^3.2.4"
59
+ }
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { createNodeSQLitePersistence } from './node-persistence'
2
+ export type {
3
+ BetterSqlite3Database,
4
+ NodeSQLitePersistenceOptions,
5
+ NodeSQLiteSchemaMismatchPolicy,
6
+ } from './node-persistence'
7
+ export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core'
8
+ export type {
9
+ PersistedCollectionCoordinator,
10
+ PersistedCollectionPersistence,
11
+ } from '@tanstack/db-sqlite-persistence-core'
@@ -0,0 +1,246 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import BetterSqlite3 from 'better-sqlite3'
3
+ import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
4
+ import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'
5
+
6
+ const DEFAULT_PRAGMAS = [
7
+ `journal_mode = WAL`,
8
+ `synchronous = NORMAL`,
9
+ `foreign_keys = ON`,
10
+ ] as const
11
+
12
+ const INVALID_PRAGMA_PATTERN = /(;|--|\/\*)/
13
+
14
+ export type BetterSqlite3Database = InstanceType<typeof BetterSqlite3>
15
+ export type BetterSqlite3OpenOptions = ConstructorParameters<
16
+ typeof BetterSqlite3
17
+ >[1]
18
+
19
+ type BetterSqlite3ExistingDatabaseOptions = {
20
+ database: BetterSqlite3Database
21
+ pragmas?: ReadonlyArray<string>
22
+ }
23
+
24
+ type BetterSqlite3OpenFileOptions = {
25
+ filename: string
26
+ options?: BetterSqlite3OpenOptions
27
+ pragmas?: ReadonlyArray<string>
28
+ }
29
+
30
+ export type BetterSqlite3DriverOptions =
31
+ | BetterSqlite3ExistingDatabaseOptions
32
+ | BetterSqlite3OpenFileOptions
33
+
34
+ type TransactionContext = {
35
+ depth: number
36
+ }
37
+
38
+ function assertTransactionCallbackHasDriverArg(
39
+ fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,
40
+ ): void {
41
+ if (fn.length > 0) {
42
+ return
43
+ }
44
+
45
+ throw new InvalidPersistedCollectionConfigError(
46
+ `SQLiteDriver.transaction callback must accept the transaction driver argument`,
47
+ )
48
+ }
49
+
50
+ function hasExistingDatabase(
51
+ options: BetterSqlite3DriverOptions,
52
+ ): options is BetterSqlite3ExistingDatabaseOptions {
53
+ return `database` in options
54
+ }
55
+
56
+ export class BetterSqlite3SQLiteDriver implements SQLiteDriver {
57
+ private readonly database: BetterSqlite3Database
58
+ private readonly ownsDatabase: boolean
59
+ private readonly transactionContext =
60
+ new AsyncLocalStorage<TransactionContext>()
61
+ private queue: Promise<void> = Promise.resolve()
62
+ private nextSavepointId = 1
63
+
64
+ constructor(options: BetterSqlite3DriverOptions) {
65
+ if (hasExistingDatabase(options)) {
66
+ this.database = options.database
67
+ this.ownsDatabase = false
68
+ this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS)
69
+ return
70
+ }
71
+
72
+ if (options.filename.trim().length === 0) {
73
+ throw new InvalidPersistedCollectionConfigError(
74
+ `Node SQLite driver filename cannot be empty`,
75
+ )
76
+ }
77
+
78
+ this.database = new BetterSqlite3(options.filename, options.options)
79
+ this.ownsDatabase = true
80
+ this.applyPragmas(options.pragmas ?? DEFAULT_PRAGMAS)
81
+ }
82
+
83
+ async exec(sql: string): Promise<void> {
84
+ if (this.isInsideTransaction()) {
85
+ this.database.exec(sql)
86
+ return
87
+ }
88
+
89
+ await this.enqueue(() => {
90
+ this.database.exec(sql)
91
+ })
92
+ }
93
+
94
+ async query<T>(
95
+ sql: string,
96
+ params: ReadonlyArray<unknown> = [],
97
+ ): Promise<ReadonlyArray<T>> {
98
+ if (this.isInsideTransaction()) {
99
+ return this.executeQuery<T>(sql, params)
100
+ }
101
+
102
+ return this.enqueue(() => this.executeQuery<T>(sql, params))
103
+ }
104
+
105
+ async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
106
+ if (this.isInsideTransaction()) {
107
+ this.executeRun(sql, params)
108
+ return
109
+ }
110
+
111
+ await this.enqueue(() => {
112
+ this.executeRun(sql, params)
113
+ })
114
+ }
115
+
116
+ async transaction<T>(
117
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
118
+ ): Promise<T> {
119
+ assertTransactionCallbackHasDriverArg(fn)
120
+
121
+ if (this.isInsideTransaction()) {
122
+ return this.runNestedTransaction(fn)
123
+ }
124
+
125
+ return this.enqueue(async () => {
126
+ this.database.exec(`BEGIN IMMEDIATE`)
127
+ try {
128
+ const result = await this.transactionContext.run(
129
+ { depth: 1 },
130
+ async () => fn(this),
131
+ )
132
+ this.database.exec(`COMMIT`)
133
+ return result
134
+ } catch (error) {
135
+ try {
136
+ this.database.exec(`ROLLBACK`)
137
+ } catch {
138
+ // Keep the original transaction error as the primary failure.
139
+ }
140
+ throw error
141
+ }
142
+ })
143
+ }
144
+
145
+ async transactionWithDriver<T>(
146
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
147
+ ): Promise<T> {
148
+ return this.transaction(fn)
149
+ }
150
+
151
+ close(): void {
152
+ if (!this.ownsDatabase) {
153
+ return
154
+ }
155
+
156
+ this.database.close()
157
+ }
158
+
159
+ getDatabase(): BetterSqlite3Database {
160
+ return this.database
161
+ }
162
+
163
+ private applyPragmas(pragmas: ReadonlyArray<string>): void {
164
+ for (const pragma of pragmas) {
165
+ const trimmedPragma = pragma.trim()
166
+ if (trimmedPragma.length === 0) {
167
+ continue
168
+ }
169
+
170
+ if (INVALID_PRAGMA_PATTERN.test(trimmedPragma)) {
171
+ throw new InvalidPersistedCollectionConfigError(
172
+ `Invalid SQLite PRAGMA: "${pragma}"`,
173
+ )
174
+ }
175
+
176
+ this.database.exec(`PRAGMA ${trimmedPragma}`)
177
+ }
178
+ }
179
+
180
+ private isInsideTransaction(): boolean {
181
+ return this.transactionContext.getStore() !== undefined
182
+ }
183
+
184
+ private executeQuery<T>(
185
+ sql: string,
186
+ params: ReadonlyArray<unknown>,
187
+ ): ReadonlyArray<T> {
188
+ const statement = this.database.prepare(sql)
189
+ if (params.length === 0) {
190
+ return statement.all() as ReadonlyArray<T>
191
+ }
192
+
193
+ return statement.all(...params) as ReadonlyArray<T>
194
+ }
195
+
196
+ private executeRun(sql: string, params: ReadonlyArray<unknown>): void {
197
+ const statement = this.database.prepare(sql)
198
+ if (params.length === 0) {
199
+ statement.run()
200
+ return
201
+ }
202
+
203
+ statement.run(...params)
204
+ }
205
+
206
+ private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {
207
+ const queuedOperation = this.queue.then(operation, operation)
208
+ this.queue = queuedOperation.then(
209
+ () => undefined,
210
+ () => undefined,
211
+ )
212
+ return queuedOperation
213
+ }
214
+
215
+ private async runNestedTransaction<T>(
216
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
217
+ ): Promise<T> {
218
+ const context = this.transactionContext.getStore()
219
+ if (!context) {
220
+ return fn(this)
221
+ }
222
+
223
+ const savepointName = `tsdb_sp_${this.nextSavepointId}`
224
+ this.nextSavepointId++
225
+ this.database.exec(`SAVEPOINT ${savepointName}`)
226
+
227
+ try {
228
+ const result = await this.transactionContext.run(
229
+ { depth: context.depth + 1 },
230
+ async () => fn(this),
231
+ )
232
+ this.database.exec(`RELEASE SAVEPOINT ${savepointName}`)
233
+ return result
234
+ } catch (error) {
235
+ this.database.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`)
236
+ this.database.exec(`RELEASE SAVEPOINT ${savepointName}`)
237
+ throw error
238
+ }
239
+ }
240
+ }
241
+
242
+ export function createBetterSqlite3Driver(
243
+ options: BetterSqlite3DriverOptions,
244
+ ): BetterSqlite3SQLiteDriver {
245
+ return new BetterSqlite3SQLiteDriver(options)
246
+ }
@@ -0,0 +1,169 @@
1
+ import {
2
+ SingleProcessCoordinator,
3
+ createSQLiteCorePersistenceAdapter,
4
+ } from '@tanstack/db-sqlite-persistence-core'
5
+ import { BetterSqlite3SQLiteDriver } from './node-driver'
6
+ import type {
7
+ PersistedCollectionCoordinator,
8
+ PersistedCollectionMode,
9
+ PersistedCollectionPersistence,
10
+ SQLiteCoreAdapterOptions,
11
+ SQLiteDriver,
12
+ } from '@tanstack/db-sqlite-persistence-core'
13
+ import type { BetterSqlite3Database } from './node-driver'
14
+
15
+ export type { BetterSqlite3Database } from './node-driver'
16
+
17
+ type NodeSQLiteCoreSchemaMismatchPolicy =
18
+ | `sync-present-reset`
19
+ | `sync-absent-error`
20
+ | `reset`
21
+
22
+ export type NodeSQLiteSchemaMismatchPolicy =
23
+ | NodeSQLiteCoreSchemaMismatchPolicy
24
+ | `throw`
25
+
26
+ type NodeSQLitePersistenceBaseOptions = Omit<
27
+ SQLiteCoreAdapterOptions,
28
+ `driver` | `schemaVersion` | `schemaMismatchPolicy`
29
+ > & {
30
+ database: BetterSqlite3Database
31
+ pragmas?: ReadonlyArray<string>
32
+ coordinator?: PersistedCollectionCoordinator
33
+ schemaMismatchPolicy?: NodeSQLiteSchemaMismatchPolicy
34
+ }
35
+
36
+ export type NodeSQLitePersistenceOptions = NodeSQLitePersistenceBaseOptions
37
+
38
+ function normalizeSchemaMismatchPolicy(
39
+ policy: NodeSQLiteSchemaMismatchPolicy,
40
+ ): NodeSQLiteCoreSchemaMismatchPolicy {
41
+ if (policy === `throw`) {
42
+ return `sync-absent-error`
43
+ }
44
+
45
+ return policy
46
+ }
47
+
48
+ function resolveSchemaMismatchPolicy(
49
+ explicitPolicy: NodeSQLiteSchemaMismatchPolicy | undefined,
50
+ mode: PersistedCollectionMode,
51
+ ): NodeSQLiteCoreSchemaMismatchPolicy {
52
+ if (explicitPolicy) {
53
+ return normalizeSchemaMismatchPolicy(explicitPolicy)
54
+ }
55
+
56
+ return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`
57
+ }
58
+
59
+ function createAdapterCacheKey(
60
+ schemaMismatchPolicy: NodeSQLiteCoreSchemaMismatchPolicy,
61
+ schemaVersion: number | undefined,
62
+ ): string {
63
+ const schemaVersionKey =
64
+ schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`
65
+ return `${schemaMismatchPolicy}|${schemaVersionKey}`
66
+ }
67
+
68
+ function createInternalSQLiteDriver(
69
+ options: NodeSQLitePersistenceOptions,
70
+ ): SQLiteDriver {
71
+ return new BetterSqlite3SQLiteDriver({
72
+ database: options.database,
73
+ ...(options.pragmas ? { pragmas: options.pragmas } : {}),
74
+ })
75
+ }
76
+
77
+ function resolveAdapterBaseOptions(
78
+ options: NodeSQLitePersistenceOptions,
79
+ ): Omit<
80
+ SQLiteCoreAdapterOptions,
81
+ `driver` | `schemaVersion` | `schemaMismatchPolicy`
82
+ > {
83
+ return {
84
+ appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
85
+ appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
86
+ pullSinceReloadThreshold: options.pullSinceReloadThreshold,
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Creates a shared SQLite persistence instance that can be reused by many
92
+ * collections on the same database. Collection-specific schema versions are
93
+ * resolved by `persistedCollectionOptions` via `resolvePersistenceForCollection`.
94
+ */
95
+ export function createNodeSQLitePersistence<
96
+ T extends object,
97
+ TKey extends string | number = string | number,
98
+ >(
99
+ options: NodeSQLitePersistenceOptions,
100
+ ): PersistedCollectionPersistence<T, TKey> {
101
+ const { coordinator, schemaMismatchPolicy } = options
102
+ const driver = createInternalSQLiteDriver(options)
103
+ const adapterBaseOptions = resolveAdapterBaseOptions(options)
104
+ const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator()
105
+ const adapterCache = new Map<
106
+ string,
107
+ ReturnType<
108
+ typeof createSQLiteCorePersistenceAdapter<
109
+ Record<string, unknown>,
110
+ string | number
111
+ >
112
+ >
113
+ >()
114
+
115
+ const getAdapterForCollection = (
116
+ mode: PersistedCollectionMode,
117
+ schemaVersion: number | undefined,
118
+ ) => {
119
+ const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(
120
+ schemaMismatchPolicy,
121
+ mode,
122
+ )
123
+ const cacheKey = createAdapterCacheKey(
124
+ resolvedSchemaMismatchPolicy,
125
+ schemaVersion,
126
+ )
127
+ const cachedAdapter = adapterCache.get(cacheKey)
128
+ if (cachedAdapter) {
129
+ return cachedAdapter
130
+ }
131
+
132
+ const adapter = createSQLiteCorePersistenceAdapter<
133
+ Record<string, unknown>,
134
+ string | number
135
+ >({
136
+ ...adapterBaseOptions,
137
+ driver,
138
+ schemaMismatchPolicy: resolvedSchemaMismatchPolicy,
139
+ ...(schemaVersion === undefined ? {} : { schemaVersion }),
140
+ })
141
+ adapterCache.set(cacheKey, adapter)
142
+ return adapter
143
+ }
144
+
145
+ const createCollectionPersistence = (
146
+ mode: PersistedCollectionMode,
147
+ schemaVersion: number | undefined,
148
+ ): PersistedCollectionPersistence<T, TKey> => ({
149
+ adapter: getAdapterForCollection(
150
+ mode,
151
+ schemaVersion,
152
+ ) as unknown as PersistedCollectionPersistence<T, TKey>[`adapter`],
153
+ coordinator: resolvedCoordinator,
154
+ })
155
+
156
+ const defaultPersistence = createCollectionPersistence(
157
+ `sync-absent`,
158
+ undefined,
159
+ )
160
+
161
+ return {
162
+ ...defaultPersistence,
163
+ resolvePersistenceForCollection: ({ mode, schemaVersion }) =>
164
+ createCollectionPersistence(mode, schemaVersion),
165
+ // Backward compatible fallback for older callers.
166
+ resolvePersistenceForMode: (mode) =>
167
+ createCollectionPersistence(mode, undefined),
168
+ }
169
+ }