@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 +49 -0
- package/dist/cjs/index.cjs +10 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +4 -0
- package/dist/cjs/node-driver.cjs +165 -0
- package/dist/cjs/node-driver.cjs.map +1 -0
- package/dist/cjs/node-driver.d.cts +37 -0
- package/dist/cjs/node-persistence.cjs +81 -0
- package/dist/cjs/node-persistence.cjs.map +1 -0
- package/dist/cjs/node-persistence.d.cts +18 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/node-driver.d.ts +37 -0
- package/dist/esm/node-driver.js +165 -0
- package/dist/esm/node-driver.js.map +1 -0
- package/dist/esm/node-persistence.d.ts +18 -0
- package/dist/esm/node-persistence.js +81 -0
- package/dist/esm/node-persistence.js.map +1 -0
- package/package.json +60 -0
- package/src/index.ts +11 -0
- package/src/node-driver.ts +246 -0
- package/src/node-persistence.ts +169 -0
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 @@
|
|
|
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":";;;AAKA,MAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,yBAAyB;AA0B/B,SAAS,sCACP,IACM;AACN,MAAI,GAAG,SAAS,GAAG;AACjB;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EAAA;AAEJ;AAEA,SAAS,oBACP,SACiD;AACjD,SAAO,cAAc;AACvB;AAEO,MAAM,0BAAkD;AAAA,EAQ7D,YAAY,SAAqC;AALjD,SAAiB,qBACf,IAAI,kBAAA;AACN,SAAQ,QAAuB,QAAQ,QAAA;AACvC,SAAQ,kBAAkB;AAGxB,QAAI,oBAAoB,OAAO,GAAG;AAChC,WAAK,WAAW,QAAQ;AACxB,WAAK,eAAe;AACpB,WAAK,aAAa,QAAQ,WAAW,eAAe;AACpD;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,KAAA,EAAO,WAAW,GAAG;AACxC,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAEJ;AAEA,SAAK,WAAW,IAAI,cAAc,QAAQ,UAAU,QAAQ,OAAO;AACnE,SAAK,eAAe;AACpB,SAAK,aAAa,QAAQ,WAAW,eAAe;AAAA,EACtD;AAAA,EAEA,MAAM,KAAK,KAA4B;AACrC,QAAI,KAAK,uBAAuB;AAC9B,WAAK,SAAS,KAAK,GAAG;AACtB;AAAA,IACF;AAEA,UAAM,KAAK,QAAQ,MAAM;AACvB,WAAK,SAAS,KAAK,GAAG;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,MACJ,KACA,SAAiC,IACN;AAC3B,QAAI,KAAK,uBAAuB;AAC9B,aAAO,KAAK,aAAgB,KAAK,MAAM;AAAA,IACzC;AAEA,WAAO,KAAK,QAAQ,MAAM,KAAK,aAAgB,KAAK,MAAM,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,IAAI,KAAa,SAAiC,IAAmB;AACzE,QAAI,KAAK,uBAAuB;AAC9B,WAAK,WAAW,KAAK,MAAM;AAC3B;AAAA,IACF;AAEA,UAAM,KAAK,QAAQ,MAAM;AACvB,WAAK,WAAW,KAAK,MAAM;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,YACJ,IACY;AACZ,0CAAsC,EAAE;AAExC,QAAI,KAAK,uBAAuB;AAC9B,aAAO,KAAK,qBAAqB,EAAE;AAAA,IACrC;AAEA,WAAO,KAAK,QAAQ,YAAY;AAC9B,WAAK,SAAS,KAAK,iBAAiB;AACpC,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,mBAAmB;AAAA,UAC3C,EAAE,OAAO,EAAA;AAAA,UACT,YAAY,GAAG,IAAI;AAAA,QAAA;AAErB,aAAK,SAAS,KAAK,QAAQ;AAC3B,eAAO;AAAA,MACT,SAAS,OAAO;AACd,YAAI;AACF,eAAK,SAAS,KAAK,UAAU;AAAA,QAC/B,QAAQ;AAAA,QAER;AACA,cAAM;AAAA,MACR;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,sBACJ,IACY;AACZ,WAAO,KAAK,YAAY,EAAE;AAAA,EAC5B;AAAA,EAEA,QAAc;AACZ,QAAI,CAAC,KAAK,cAAc;AACtB;AAAA,IACF;AAEA,SAAK,SAAS,MAAA;AAAA,EAChB;AAAA,EAEA,cAAqC;AACnC,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,aAAa,SAAsC;AACzD,eAAW,UAAU,SAAS;AAC5B,YAAM,gBAAgB,OAAO,KAAA;AAC7B,UAAI,cAAc,WAAW,GAAG;AAC9B;AAAA,MACF;AAEA,UAAI,uBAAuB,KAAK,aAAa,GAAG;AAC9C,cAAM,IAAI;AAAA,UACR,2BAA2B,MAAM;AAAA,QAAA;AAAA,MAErC;AAEA,WAAK,SAAS,KAAK,UAAU,aAAa,EAAE;AAAA,IAC9C;AAAA,EACF;AAAA,EAEQ,sBAA+B;AACrC,WAAO,KAAK,mBAAmB,SAAA,MAAe;AAAA,EAChD;AAAA,EAEQ,aACN,KACA,QACkB;AAClB,UAAM,YAAY,KAAK,SAAS,QAAQ,GAAG;AAC3C,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO,UAAU,IAAA;AAAA,IACnB;AAEA,WAAO,UAAU,IAAI,GAAG,MAAM;AAAA,EAChC;AAAA,EAEQ,WAAW,KAAa,QAAsC;AACpE,UAAM,YAAY,KAAK,SAAS,QAAQ,GAAG;AAC3C,QAAI,OAAO,WAAW,GAAG;AACvB,gBAAU,IAAA;AACV;AAAA,IACF;AAEA,cAAU,IAAI,GAAG,MAAM;AAAA,EACzB;AAAA,EAEQ,QAAW,WAA6C;AAC9D,UAAM,kBAAkB,KAAK,MAAM,KAAK,WAAW,SAAS;AAC5D,SAAK,QAAQ,gBAAgB;AAAA,MAC3B,MAAM;AAAA,MACN,MAAM;AAAA,IAAA;AAER,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,qBACZ,IACY;AACZ,UAAM,UAAU,KAAK,mBAAmB,SAAA;AACxC,QAAI,CAAC,SAAS;AACZ,aAAO,GAAG,IAAI;AAAA,IAChB;AAEA,UAAM,gBAAgB,WAAW,KAAK,eAAe;AACrD,SAAK;AACL,SAAK,SAAS,KAAK,aAAa,aAAa,EAAE;AAE/C,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,mBAAmB;AAAA,QAC3C,EAAE,OAAO,QAAQ,QAAQ,EAAA;AAAA,QACzB,YAAY,GAAG,IAAI;AAAA,MAAA;AAErB,WAAK,SAAS,KAAK,qBAAqB,aAAa,EAAE;AACvD,aAAO;AAAA,IACT,SAAS,OAAO;AACd,WAAK,SAAS,KAAK,yBAAyB,aAAa,EAAE;AAC3D,WAAK,SAAS,KAAK,qBAAqB,aAAa,EAAE;AACvD,YAAM;AAAA,IACR;AAAA,EACF;AACF;"}
|
|
@@ -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
|
+
}
|