@tanstack/cloudflare-durable-objects-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 +48 -0
- package/dist/cjs/do-driver.cjs +160 -0
- package/dist/cjs/do-driver.cjs.map +1 -0
- package/dist/cjs/do-driver.d.cts +42 -0
- package/dist/cjs/do-persistence.cjs +80 -0
- package/dist/cjs/do-persistence.cjs.map +1 -0
- package/dist/cjs/do-persistence.d.cts +16 -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/esm/do-driver.d.ts +42 -0
- package/dist/esm/do-driver.js +160 -0
- package/dist/esm/do-driver.js.map +1 -0
- package/dist/esm/do-persistence.d.ts +16 -0
- package/dist/esm/do-persistence.js +80 -0
- package/dist/esm/do-persistence.js.map +1 -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/package.json +61 -0
- package/src/do-driver.ts +263 -0
- package/src/do-persistence.ts +166 -0
- package/src/index.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @tanstack/cloudflare-durable-objects-db-sqlite-persistence
|
|
2
|
+
|
|
3
|
+
Thin SQLite persistence for Cloudflare Durable Objects.
|
|
4
|
+
|
|
5
|
+
## Public API
|
|
6
|
+
|
|
7
|
+
- `createCloudflareDOSQLitePersistence(...)`
|
|
8
|
+
- `persistedCollectionOptions(...)` (re-exported from core)
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createCollection } from '@tanstack/db'
|
|
14
|
+
import {
|
|
15
|
+
createCloudflareDOSQLitePersistence,
|
|
16
|
+
persistedCollectionOptions,
|
|
17
|
+
} from '@tanstack/cloudflare-durable-objects-db-sqlite-persistence'
|
|
18
|
+
|
|
19
|
+
type Todo = {
|
|
20
|
+
id: string
|
|
21
|
+
title: string
|
|
22
|
+
completed: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class TodosObject extends DurableObject {
|
|
26
|
+
persistence = createCloudflareDOSQLitePersistence({
|
|
27
|
+
// Pass full storage to use native DO transaction support.
|
|
28
|
+
storage: this.ctx.storage,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
todos = createCollection(
|
|
32
|
+
persistedCollectionOptions<Todo, string>({
|
|
33
|
+
id: `todos`,
|
|
34
|
+
getKey: (todo) => todo.id,
|
|
35
|
+
persistence: this.persistence,
|
|
36
|
+
schemaVersion: 1, // Per-collection schema version
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
- One shared persistence instance can serve multiple collections.
|
|
45
|
+
- Mode defaults are inferred from collection usage:
|
|
46
|
+
- sync config present => `sync-present-reset`
|
|
47
|
+
- no sync config => `sync-absent-error`
|
|
48
|
+
- You can still override with `schemaMismatchPolicy` if needed.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
|
|
4
|
+
function assertTransactionCallbackHasDriverArg(fn) {
|
|
5
|
+
if (fn.length > 0) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
throw new dbSqlitePersistenceCore.InvalidPersistedCollectionConfigError(
|
|
9
|
+
`SQLiteDriver.transaction callback must accept the transaction driver argument`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
function isIterableRecord(value) {
|
|
13
|
+
if (!value || typeof value !== `object`) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const iterator = value[Symbol.iterator];
|
|
17
|
+
return typeof iterator === `function`;
|
|
18
|
+
}
|
|
19
|
+
function toRowArray(result, sql) {
|
|
20
|
+
if (result == null) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(result)) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
const cursorResult = result;
|
|
27
|
+
if (typeof cursorResult.toArray === `function`) {
|
|
28
|
+
return cursorResult.toArray();
|
|
29
|
+
}
|
|
30
|
+
if (isIterableRecord(cursorResult)) {
|
|
31
|
+
return Array.from(cursorResult);
|
|
32
|
+
}
|
|
33
|
+
throw new dbSqlitePersistenceCore.InvalidPersistedCollectionConfigError(
|
|
34
|
+
`Unsupported Durable Object SQL result shape for query "${sql}"`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
class CloudflareDOSQLiteDriver {
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.queue = Promise.resolve();
|
|
40
|
+
this.nextSavepointId = 1;
|
|
41
|
+
const resolvedStorage = `storage` in options ? options.storage : {
|
|
42
|
+
sql: options.sql,
|
|
43
|
+
...typeof options.transaction === `function` ? { transaction: options.transaction } : {}
|
|
44
|
+
};
|
|
45
|
+
const resolvedSqlStorage = resolvedStorage.sql;
|
|
46
|
+
if (typeof resolvedSqlStorage.exec !== `function`) {
|
|
47
|
+
throw new dbSqlitePersistenceCore.InvalidPersistedCollectionConfigError(
|
|
48
|
+
`Cloudflare DO SQL driver requires a sql.exec function`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
this.storage = resolvedStorage;
|
|
52
|
+
this.sqlStorage = resolvedSqlStorage;
|
|
53
|
+
if (typeof resolvedStorage.transaction === `function`) {
|
|
54
|
+
const transactionMethod = resolvedStorage.transaction;
|
|
55
|
+
this.transactionExecutor = (fn) => Promise.resolve(
|
|
56
|
+
transactionMethod.call(resolvedStorage, fn)
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
this.transactionExecutor = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async exec(sql) {
|
|
63
|
+
await this.enqueue(() => {
|
|
64
|
+
this.execute(sql);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async query(sql, params = []) {
|
|
68
|
+
return this.enqueue(() => this.executeQuery(sql, params));
|
|
69
|
+
}
|
|
70
|
+
async run(sql, params = []) {
|
|
71
|
+
await this.enqueue(() => {
|
|
72
|
+
this.execute(sql, params);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async transaction(fn) {
|
|
76
|
+
assertTransactionCallbackHasDriverArg(fn);
|
|
77
|
+
return this.enqueue(async () => {
|
|
78
|
+
const transactionDriver = this.createTransactionDriver();
|
|
79
|
+
if (this.transactionExecutor) {
|
|
80
|
+
return this.transactionExecutor(() => fn(transactionDriver));
|
|
81
|
+
}
|
|
82
|
+
this.execute(`BEGIN IMMEDIATE`);
|
|
83
|
+
try {
|
|
84
|
+
const result = await fn(transactionDriver);
|
|
85
|
+
this.execute(`COMMIT`);
|
|
86
|
+
return result;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
try {
|
|
89
|
+
this.execute(`ROLLBACK`);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async transactionWithDriver(fn) {
|
|
97
|
+
return this.transaction(fn);
|
|
98
|
+
}
|
|
99
|
+
getStorage() {
|
|
100
|
+
return this.storage;
|
|
101
|
+
}
|
|
102
|
+
execute(sql, params = []) {
|
|
103
|
+
return this.sqlStorage.exec(sql, ...params);
|
|
104
|
+
}
|
|
105
|
+
executeQuery(sql, params) {
|
|
106
|
+
const result = this.execute(sql, params);
|
|
107
|
+
return toRowArray(
|
|
108
|
+
result,
|
|
109
|
+
sql
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
enqueue(operation) {
|
|
113
|
+
const queuedOperation = this.queue.then(operation, operation);
|
|
114
|
+
this.queue = queuedOperation.then(
|
|
115
|
+
() => void 0,
|
|
116
|
+
() => void 0
|
|
117
|
+
);
|
|
118
|
+
return queuedOperation;
|
|
119
|
+
}
|
|
120
|
+
createTransactionDriver() {
|
|
121
|
+
const transactionDriver = {
|
|
122
|
+
exec: (sql) => {
|
|
123
|
+
this.execute(sql);
|
|
124
|
+
return Promise.resolve();
|
|
125
|
+
},
|
|
126
|
+
query: (sql, params = []) => Promise.resolve(this.executeQuery(sql, params)),
|
|
127
|
+
run: (sql, params = []) => {
|
|
128
|
+
this.execute(sql, params);
|
|
129
|
+
return Promise.resolve();
|
|
130
|
+
},
|
|
131
|
+
transaction: (fn) => {
|
|
132
|
+
assertTransactionCallbackHasDriverArg(fn);
|
|
133
|
+
return this.runNestedTransaction(transactionDriver, fn);
|
|
134
|
+
},
|
|
135
|
+
transactionWithDriver: (fn) => this.runNestedTransaction(transactionDriver, fn)
|
|
136
|
+
};
|
|
137
|
+
return transactionDriver;
|
|
138
|
+
}
|
|
139
|
+
async runNestedTransaction(transactionDriver, fn) {
|
|
140
|
+
if (this.transactionExecutor) {
|
|
141
|
+
throw new dbSqlitePersistenceCore.InvalidPersistedCollectionConfigError(
|
|
142
|
+
`Nested SQL savepoints are not supported when using Durable Object transaction API`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const savepointName = `tsdb_sp_${this.nextSavepointId}`;
|
|
146
|
+
this.nextSavepointId++;
|
|
147
|
+
this.execute(`SAVEPOINT ${savepointName}`);
|
|
148
|
+
try {
|
|
149
|
+
const result = await fn(transactionDriver);
|
|
150
|
+
this.execute(`RELEASE SAVEPOINT ${savepointName}`);
|
|
151
|
+
return result;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
154
|
+
this.execute(`RELEASE SAVEPOINT ${savepointName}`);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.CloudflareDOSQLiteDriver = CloudflareDOSQLiteDriver;
|
|
160
|
+
//# sourceMappingURL=do-driver.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"do-driver.cjs","sources":["../../src/do-driver.ts"],"sourcesContent":["import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'\nimport type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'\n\ntype DurableObjectSqlRow = Record<string, unknown>\n\ntype DurableObjectSqlCursorLike = Iterable<DurableObjectSqlRow> & {\n toArray?: () => Array<DurableObjectSqlRow>\n}\n\nexport type DurableObjectSqlStorageLike = {\n exec: (\n sql: string,\n ...params: ReadonlyArray<unknown>\n ) => DurableObjectSqlCursorLike | ReadonlyArray<DurableObjectSqlRow> | null\n}\n\nexport type DurableObjectTransactionExecutor = <T>(\n fn: () => Promise<T>,\n) => Promise<T>\n\nexport type DurableObjectStorageLike = {\n sql: DurableObjectSqlStorageLike\n transaction?: DurableObjectTransactionExecutor\n}\n\ntype CloudflareDOProvidedSqlOptions = {\n sql: DurableObjectSqlStorageLike\n transaction?: DurableObjectTransactionExecutor\n}\n\ntype CloudflareDOProvidedStorageOptions = {\n storage: DurableObjectStorageLike\n}\n\nexport type CloudflareDOSQLiteDriverOptions =\n | CloudflareDOProvidedSqlOptions\n | CloudflareDOProvidedStorageOptions\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 isIterableRecord(\n value: unknown,\n): value is Iterable<DurableObjectSqlRow> {\n if (!value || typeof value !== `object`) {\n return false\n }\n\n const iterator = (value as { [Symbol.iterator]?: unknown })[Symbol.iterator]\n return typeof iterator === `function`\n}\n\nfunction toRowArray<T>(\n result: ReturnType<DurableObjectSqlStorageLike[`exec`]>,\n sql: string,\n): ReadonlyArray<T> {\n if (result == null) {\n return []\n }\n\n if (Array.isArray(result)) {\n return result as ReadonlyArray<T>\n }\n\n const cursorResult = result as DurableObjectSqlCursorLike\n if (typeof cursorResult.toArray === `function`) {\n return cursorResult.toArray() as ReadonlyArray<T>\n }\n\n if (isIterableRecord(cursorResult)) {\n return Array.from(cursorResult as Iterable<T>)\n }\n\n throw new InvalidPersistedCollectionConfigError(\n `Unsupported Durable Object SQL result shape for query \"${sql}\"`,\n )\n}\n\nexport class CloudflareDOSQLiteDriver implements SQLiteDriver {\n private readonly sqlStorage: DurableObjectSqlStorageLike\n private readonly storage: DurableObjectStorageLike\n private readonly transactionExecutor: DurableObjectTransactionExecutor | null\n private queue: Promise<void> = Promise.resolve()\n private nextSavepointId = 1\n\n constructor(options: CloudflareDOSQLiteDriverOptions) {\n const resolvedStorage: DurableObjectStorageLike =\n `storage` in options\n ? options.storage\n : {\n sql: options.sql,\n ...(typeof options.transaction === `function`\n ? { transaction: options.transaction }\n : {}),\n }\n const resolvedSqlStorage = resolvedStorage.sql\n if (typeof resolvedSqlStorage.exec !== `function`) {\n throw new InvalidPersistedCollectionConfigError(\n `Cloudflare DO SQL driver requires a sql.exec function`,\n )\n }\n this.storage = resolvedStorage\n this.sqlStorage = resolvedSqlStorage\n if (typeof resolvedStorage.transaction === `function`) {\n const transactionMethod = resolvedStorage.transaction\n this.transactionExecutor = <T>(fn: () => Promise<T>) =>\n Promise.resolve(\n transactionMethod.call(resolvedStorage, fn) as Promise<T> | T,\n )\n } else {\n this.transactionExecutor = null\n }\n }\n\n async exec(sql: string): Promise<void> {\n await this.enqueue(() => {\n this.execute(sql)\n })\n }\n\n async query<T>(\n sql: string,\n params: ReadonlyArray<unknown> = [],\n ): Promise<ReadonlyArray<T>> {\n return this.enqueue(() => this.executeQuery<T>(sql, params))\n }\n\n async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {\n await this.enqueue(() => {\n this.execute(sql, params)\n })\n }\n\n async transaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn)\n\n return this.enqueue(async () => {\n const transactionDriver = this.createTransactionDriver()\n if (this.transactionExecutor) {\n return this.transactionExecutor(() => fn(transactionDriver))\n }\n\n this.execute(`BEGIN IMMEDIATE`)\n try {\n const result = await fn(transactionDriver)\n this.execute(`COMMIT`)\n return result\n } catch (error) {\n try {\n this.execute(`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 getStorage(): DurableObjectStorageLike {\n return this.storage\n }\n\n private execute(sql: string, params: ReadonlyArray<unknown> = []): unknown {\n return this.sqlStorage.exec(sql, ...params)\n }\n\n private executeQuery<T>(\n sql: string,\n params: ReadonlyArray<unknown>,\n ): ReadonlyArray<T> {\n const result = this.execute(sql, params)\n return toRowArray<T>(\n result as ReturnType<DurableObjectSqlStorageLike[`exec`]>,\n sql,\n )\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 createTransactionDriver(): SQLiteDriver {\n const transactionDriver: SQLiteDriver = {\n exec: (sql) => {\n this.execute(sql)\n return Promise.resolve()\n },\n query: <T>(\n sql: string,\n params: ReadonlyArray<unknown> = [],\n ): Promise<ReadonlyArray<T>> =>\n Promise.resolve(this.executeQuery<T>(sql, params)),\n run: (sql, params = []) => {\n this.execute(sql, params)\n return Promise.resolve()\n },\n transaction: <T>(\n fn: (nestedDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> => {\n assertTransactionCallbackHasDriverArg(fn)\n return this.runNestedTransaction(transactionDriver, fn)\n },\n transactionWithDriver: <T>(\n fn: (nestedDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> => this.runNestedTransaction(transactionDriver, fn),\n }\n\n return transactionDriver\n }\n\n private async runNestedTransaction<T>(\n transactionDriver: SQLiteDriver,\n fn: (nestedDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n if (this.transactionExecutor) {\n throw new InvalidPersistedCollectionConfigError(\n `Nested SQL savepoints are not supported when using Durable Object transaction API`,\n )\n }\n\n const savepointName = `tsdb_sp_${this.nextSavepointId}`\n this.nextSavepointId++\n this.execute(`SAVEPOINT ${savepointName}`)\n\n try {\n const result = await fn(transactionDriver)\n this.execute(`RELEASE SAVEPOINT ${savepointName}`)\n return result\n } catch (error) {\n this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`)\n this.execute(`RELEASE SAVEPOINT ${savepointName}`)\n throw error\n }\n }\n}\n\nexport function createCloudflareDOSQLiteDriver(\n options: CloudflareDOSQLiteDriverOptions,\n): CloudflareDOSQLiteDriver {\n return new CloudflareDOSQLiteDriver(options)\n}\n"],"names":["InvalidPersistedCollectionConfigError"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core';
|
|
2
|
+
type DurableObjectSqlRow = Record<string, unknown>;
|
|
3
|
+
type DurableObjectSqlCursorLike = Iterable<DurableObjectSqlRow> & {
|
|
4
|
+
toArray?: () => Array<DurableObjectSqlRow>;
|
|
5
|
+
};
|
|
6
|
+
export type DurableObjectSqlStorageLike = {
|
|
7
|
+
exec: (sql: string, ...params: ReadonlyArray<unknown>) => DurableObjectSqlCursorLike | ReadonlyArray<DurableObjectSqlRow> | null;
|
|
8
|
+
};
|
|
9
|
+
export type DurableObjectTransactionExecutor = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
10
|
+
export type DurableObjectStorageLike = {
|
|
11
|
+
sql: DurableObjectSqlStorageLike;
|
|
12
|
+
transaction?: DurableObjectTransactionExecutor;
|
|
13
|
+
};
|
|
14
|
+
type CloudflareDOProvidedSqlOptions = {
|
|
15
|
+
sql: DurableObjectSqlStorageLike;
|
|
16
|
+
transaction?: DurableObjectTransactionExecutor;
|
|
17
|
+
};
|
|
18
|
+
type CloudflareDOProvidedStorageOptions = {
|
|
19
|
+
storage: DurableObjectStorageLike;
|
|
20
|
+
};
|
|
21
|
+
export type CloudflareDOSQLiteDriverOptions = CloudflareDOProvidedSqlOptions | CloudflareDOProvidedStorageOptions;
|
|
22
|
+
export declare class CloudflareDOSQLiteDriver implements SQLiteDriver {
|
|
23
|
+
private readonly sqlStorage;
|
|
24
|
+
private readonly storage;
|
|
25
|
+
private readonly transactionExecutor;
|
|
26
|
+
private queue;
|
|
27
|
+
private nextSavepointId;
|
|
28
|
+
constructor(options: CloudflareDOSQLiteDriverOptions);
|
|
29
|
+
exec(sql: string): Promise<void>;
|
|
30
|
+
query<T>(sql: string, params?: ReadonlyArray<unknown>): Promise<ReadonlyArray<T>>;
|
|
31
|
+
run(sql: string, params?: ReadonlyArray<unknown>): Promise<void>;
|
|
32
|
+
transaction<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
|
|
33
|
+
transactionWithDriver<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
|
|
34
|
+
getStorage(): DurableObjectStorageLike;
|
|
35
|
+
private execute;
|
|
36
|
+
private executeQuery;
|
|
37
|
+
private enqueue;
|
|
38
|
+
private createTransactionDriver;
|
|
39
|
+
private runNestedTransaction;
|
|
40
|
+
}
|
|
41
|
+
export declare function createCloudflareDOSQLiteDriver(options: CloudflareDOSQLiteDriverOptions): CloudflareDOSQLiteDriver;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
|
|
4
|
+
const doDriver = require("./do-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 resolveSQLiteDriver(options) {
|
|
22
|
+
return new doDriver.CloudflareDOSQLiteDriver({
|
|
23
|
+
storage: options.storage
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function resolveAdapterBaseOptions(options) {
|
|
27
|
+
return {
|
|
28
|
+
appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
|
|
29
|
+
appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
|
|
30
|
+
pullSinceReloadThreshold: options.pullSinceReloadThreshold
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createCloudflareDOSQLitePersistence(options) {
|
|
34
|
+
const { coordinator, schemaMismatchPolicy } = options;
|
|
35
|
+
const driver = resolveSQLiteDriver(options);
|
|
36
|
+
const adapterBaseOptions = resolveAdapterBaseOptions(options);
|
|
37
|
+
const resolvedCoordinator = coordinator ?? new dbSqlitePersistenceCore.SingleProcessCoordinator();
|
|
38
|
+
const adapterCache = /* @__PURE__ */ new Map();
|
|
39
|
+
const getAdapterForCollection = (mode, schemaVersion) => {
|
|
40
|
+
const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(
|
|
41
|
+
schemaMismatchPolicy,
|
|
42
|
+
mode
|
|
43
|
+
);
|
|
44
|
+
const cacheKey = createAdapterCacheKey(
|
|
45
|
+
resolvedSchemaMismatchPolicy,
|
|
46
|
+
schemaVersion
|
|
47
|
+
);
|
|
48
|
+
const cachedAdapter = adapterCache.get(cacheKey);
|
|
49
|
+
if (cachedAdapter) {
|
|
50
|
+
return cachedAdapter;
|
|
51
|
+
}
|
|
52
|
+
const adapter = dbSqlitePersistenceCore.createSQLiteCorePersistenceAdapter({
|
|
53
|
+
...adapterBaseOptions,
|
|
54
|
+
driver,
|
|
55
|
+
schemaMismatchPolicy: resolvedSchemaMismatchPolicy,
|
|
56
|
+
...schemaVersion === void 0 ? {} : { schemaVersion }
|
|
57
|
+
});
|
|
58
|
+
adapterCache.set(cacheKey, adapter);
|
|
59
|
+
return adapter;
|
|
60
|
+
};
|
|
61
|
+
const createCollectionPersistence = (mode, schemaVersion) => ({
|
|
62
|
+
adapter: getAdapterForCollection(
|
|
63
|
+
mode,
|
|
64
|
+
schemaVersion
|
|
65
|
+
),
|
|
66
|
+
coordinator: resolvedCoordinator
|
|
67
|
+
});
|
|
68
|
+
const defaultPersistence = createCollectionPersistence(
|
|
69
|
+
`sync-absent`,
|
|
70
|
+
void 0
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
...defaultPersistence,
|
|
74
|
+
resolvePersistenceForCollection: ({ mode, schemaVersion }) => createCollectionPersistence(mode, schemaVersion),
|
|
75
|
+
// Backward compatible fallback for older callers.
|
|
76
|
+
resolvePersistenceForMode: (mode) => createCollectionPersistence(mode, void 0)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
exports.createCloudflareDOSQLitePersistence = createCloudflareDOSQLitePersistence;
|
|
80
|
+
//# sourceMappingURL=do-persistence.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"do-persistence.cjs","sources":["../../src/do-persistence.ts"],"sourcesContent":["import {\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { CloudflareDOSQLiteDriver } from './do-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { DurableObjectStorageLike } from './do-driver'\n\nexport type { DurableObjectStorageLike } from './do-driver'\n\ntype CloudflareDOCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type CloudflareDOSchemaMismatchPolicy =\n | CloudflareDOCoreSchemaMismatchPolicy\n | `throw`\n\ntype CloudflareDOSQLitePersistenceBaseOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n storage: DurableObjectStorageLike\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: CloudflareDOSchemaMismatchPolicy\n}\n\nexport type CloudflareDOSQLitePersistenceOptions =\n CloudflareDOSQLitePersistenceBaseOptions\n\nfunction normalizeSchemaMismatchPolicy(\n policy: CloudflareDOSchemaMismatchPolicy,\n): CloudflareDOCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: CloudflareDOSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): CloudflareDOCoreSchemaMismatchPolicy {\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: CloudflareDOCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction resolveSQLiteDriver(\n options: CloudflareDOSQLitePersistenceOptions,\n): SQLiteDriver {\n return new CloudflareDOSQLiteDriver({\n storage: options.storage,\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: CloudflareDOSQLitePersistenceOptions,\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 Durable Object SQLite persistence instance that can be reused\n * by many collections in a single Durable Object storage.\n */\nexport function createCloudflareDOSQLitePersistence<\n T extends object,\n TKey extends string | number = string | number,\n>(\n options: CloudflareDOSQLitePersistenceOptions,\n): PersistedCollectionPersistence<T, TKey> {\n const { coordinator, schemaMismatchPolicy } = options\n const driver = resolveSQLiteDriver(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":["CloudflareDOSQLiteDriver","SingleProcessCoordinator","createSQLiteCorePersistenceAdapter"],"mappings":";;;;AAqCA,SAAS,8BACP,QACsC;AACtC,MAAI,WAAW,SAAS;AACtB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,4BACP,gBACA,MACsC;AACtC,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,oBACP,SACc;AACd,SAAO,IAAIA,SAAAA,yBAAyB;AAAA,IAClC,SAAS,QAAQ;AAAA,EAAA,CAClB;AACH;AAEA,SAAS,0BACP,SAIA;AACA,SAAO;AAAA,IACL,uBAAuB,QAAQ;AAAA,IAC/B,6BAA6B,QAAQ;AAAA,IACrC,0BAA0B,QAAQ;AAAA,EAAA;AAEtC;AAMO,SAAS,oCAId,SACyC;AACzC,QAAM,EAAE,aAAa,qBAAA,IAAyB;AAC9C,QAAM,SAAS,oBAAoB,OAAO;AAC1C,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,16 @@
|
|
|
1
|
+
import { PersistedCollectionCoordinator, PersistedCollectionPersistence, SQLiteCoreAdapterOptions } from '@tanstack/db-sqlite-persistence-core';
|
|
2
|
+
import { DurableObjectStorageLike } from './do-driver.cjs';
|
|
3
|
+
export type { DurableObjectStorageLike } from './do-driver.cjs';
|
|
4
|
+
type CloudflareDOCoreSchemaMismatchPolicy = `sync-present-reset` | `sync-absent-error` | `reset`;
|
|
5
|
+
export type CloudflareDOSchemaMismatchPolicy = CloudflareDOCoreSchemaMismatchPolicy | `throw`;
|
|
6
|
+
type CloudflareDOSQLitePersistenceBaseOptions = Omit<SQLiteCoreAdapterOptions, `driver` | `schemaVersion` | `schemaMismatchPolicy`> & {
|
|
7
|
+
storage: DurableObjectStorageLike;
|
|
8
|
+
coordinator?: PersistedCollectionCoordinator;
|
|
9
|
+
schemaMismatchPolicy?: CloudflareDOSchemaMismatchPolicy;
|
|
10
|
+
};
|
|
11
|
+
export type CloudflareDOSQLitePersistenceOptions = CloudflareDOSQLitePersistenceBaseOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a shared Durable Object SQLite persistence instance that can be reused
|
|
14
|
+
* by many collections in a single Durable Object storage.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createCloudflareDOSQLitePersistence<T extends object, TKey extends string | number = string | number>(options: CloudflareDOSQLitePersistenceOptions): PersistedCollectionPersistence<T, TKey>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const doPersistence = require("./do-persistence.cjs");
|
|
4
|
+
const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
|
|
5
|
+
exports.createCloudflareDOSQLitePersistence = doPersistence.createCloudflareDOSQLitePersistence;
|
|
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 { createCloudflareDOSQLitePersistence } from './do-persistence.cjs';
|
|
2
|
+
export type { CloudflareDOSchemaMismatchPolicy, CloudflareDOSQLitePersistenceOptions, DurableObjectStorageLike, } from './do-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,42 @@
|
|
|
1
|
+
import { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core';
|
|
2
|
+
type DurableObjectSqlRow = Record<string, unknown>;
|
|
3
|
+
type DurableObjectSqlCursorLike = Iterable<DurableObjectSqlRow> & {
|
|
4
|
+
toArray?: () => Array<DurableObjectSqlRow>;
|
|
5
|
+
};
|
|
6
|
+
export type DurableObjectSqlStorageLike = {
|
|
7
|
+
exec: (sql: string, ...params: ReadonlyArray<unknown>) => DurableObjectSqlCursorLike | ReadonlyArray<DurableObjectSqlRow> | null;
|
|
8
|
+
};
|
|
9
|
+
export type DurableObjectTransactionExecutor = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
10
|
+
export type DurableObjectStorageLike = {
|
|
11
|
+
sql: DurableObjectSqlStorageLike;
|
|
12
|
+
transaction?: DurableObjectTransactionExecutor;
|
|
13
|
+
};
|
|
14
|
+
type CloudflareDOProvidedSqlOptions = {
|
|
15
|
+
sql: DurableObjectSqlStorageLike;
|
|
16
|
+
transaction?: DurableObjectTransactionExecutor;
|
|
17
|
+
};
|
|
18
|
+
type CloudflareDOProvidedStorageOptions = {
|
|
19
|
+
storage: DurableObjectStorageLike;
|
|
20
|
+
};
|
|
21
|
+
export type CloudflareDOSQLiteDriverOptions = CloudflareDOProvidedSqlOptions | CloudflareDOProvidedStorageOptions;
|
|
22
|
+
export declare class CloudflareDOSQLiteDriver implements SQLiteDriver {
|
|
23
|
+
private readonly sqlStorage;
|
|
24
|
+
private readonly storage;
|
|
25
|
+
private readonly transactionExecutor;
|
|
26
|
+
private queue;
|
|
27
|
+
private nextSavepointId;
|
|
28
|
+
constructor(options: CloudflareDOSQLiteDriverOptions);
|
|
29
|
+
exec(sql: string): Promise<void>;
|
|
30
|
+
query<T>(sql: string, params?: ReadonlyArray<unknown>): Promise<ReadonlyArray<T>>;
|
|
31
|
+
run(sql: string, params?: ReadonlyArray<unknown>): Promise<void>;
|
|
32
|
+
transaction<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
|
|
33
|
+
transactionWithDriver<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T>;
|
|
34
|
+
getStorage(): DurableObjectStorageLike;
|
|
35
|
+
private execute;
|
|
36
|
+
private executeQuery;
|
|
37
|
+
private enqueue;
|
|
38
|
+
private createTransactionDriver;
|
|
39
|
+
private runNestedTransaction;
|
|
40
|
+
}
|
|
41
|
+
export declare function createCloudflareDOSQLiteDriver(options: CloudflareDOSQLiteDriverOptions): CloudflareDOSQLiteDriver;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { InvalidPersistedCollectionConfigError } from "@tanstack/db-sqlite-persistence-core";
|
|
2
|
+
function assertTransactionCallbackHasDriverArg(fn) {
|
|
3
|
+
if (fn.length > 0) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
7
|
+
`SQLiteDriver.transaction callback must accept the transaction driver argument`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function isIterableRecord(value) {
|
|
11
|
+
if (!value || typeof value !== `object`) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const iterator = value[Symbol.iterator];
|
|
15
|
+
return typeof iterator === `function`;
|
|
16
|
+
}
|
|
17
|
+
function toRowArray(result, sql) {
|
|
18
|
+
if (result == null) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(result)) {
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
const cursorResult = result;
|
|
25
|
+
if (typeof cursorResult.toArray === `function`) {
|
|
26
|
+
return cursorResult.toArray();
|
|
27
|
+
}
|
|
28
|
+
if (isIterableRecord(cursorResult)) {
|
|
29
|
+
return Array.from(cursorResult);
|
|
30
|
+
}
|
|
31
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
32
|
+
`Unsupported Durable Object SQL result shape for query "${sql}"`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
class CloudflareDOSQLiteDriver {
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.queue = Promise.resolve();
|
|
38
|
+
this.nextSavepointId = 1;
|
|
39
|
+
const resolvedStorage = `storage` in options ? options.storage : {
|
|
40
|
+
sql: options.sql,
|
|
41
|
+
...typeof options.transaction === `function` ? { transaction: options.transaction } : {}
|
|
42
|
+
};
|
|
43
|
+
const resolvedSqlStorage = resolvedStorage.sql;
|
|
44
|
+
if (typeof resolvedSqlStorage.exec !== `function`) {
|
|
45
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
46
|
+
`Cloudflare DO SQL driver requires a sql.exec function`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
this.storage = resolvedStorage;
|
|
50
|
+
this.sqlStorage = resolvedSqlStorage;
|
|
51
|
+
if (typeof resolvedStorage.transaction === `function`) {
|
|
52
|
+
const transactionMethod = resolvedStorage.transaction;
|
|
53
|
+
this.transactionExecutor = (fn) => Promise.resolve(
|
|
54
|
+
transactionMethod.call(resolvedStorage, fn)
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
this.transactionExecutor = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async exec(sql) {
|
|
61
|
+
await this.enqueue(() => {
|
|
62
|
+
this.execute(sql);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async query(sql, params = []) {
|
|
66
|
+
return this.enqueue(() => this.executeQuery(sql, params));
|
|
67
|
+
}
|
|
68
|
+
async run(sql, params = []) {
|
|
69
|
+
await this.enqueue(() => {
|
|
70
|
+
this.execute(sql, params);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async transaction(fn) {
|
|
74
|
+
assertTransactionCallbackHasDriverArg(fn);
|
|
75
|
+
return this.enqueue(async () => {
|
|
76
|
+
const transactionDriver = this.createTransactionDriver();
|
|
77
|
+
if (this.transactionExecutor) {
|
|
78
|
+
return this.transactionExecutor(() => fn(transactionDriver));
|
|
79
|
+
}
|
|
80
|
+
this.execute(`BEGIN IMMEDIATE`);
|
|
81
|
+
try {
|
|
82
|
+
const result = await fn(transactionDriver);
|
|
83
|
+
this.execute(`COMMIT`);
|
|
84
|
+
return result;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
try {
|
|
87
|
+
this.execute(`ROLLBACK`);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async transactionWithDriver(fn) {
|
|
95
|
+
return this.transaction(fn);
|
|
96
|
+
}
|
|
97
|
+
getStorage() {
|
|
98
|
+
return this.storage;
|
|
99
|
+
}
|
|
100
|
+
execute(sql, params = []) {
|
|
101
|
+
return this.sqlStorage.exec(sql, ...params);
|
|
102
|
+
}
|
|
103
|
+
executeQuery(sql, params) {
|
|
104
|
+
const result = this.execute(sql, params);
|
|
105
|
+
return toRowArray(
|
|
106
|
+
result,
|
|
107
|
+
sql
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
enqueue(operation) {
|
|
111
|
+
const queuedOperation = this.queue.then(operation, operation);
|
|
112
|
+
this.queue = queuedOperation.then(
|
|
113
|
+
() => void 0,
|
|
114
|
+
() => void 0
|
|
115
|
+
);
|
|
116
|
+
return queuedOperation;
|
|
117
|
+
}
|
|
118
|
+
createTransactionDriver() {
|
|
119
|
+
const transactionDriver = {
|
|
120
|
+
exec: (sql) => {
|
|
121
|
+
this.execute(sql);
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
},
|
|
124
|
+
query: (sql, params = []) => Promise.resolve(this.executeQuery(sql, params)),
|
|
125
|
+
run: (sql, params = []) => {
|
|
126
|
+
this.execute(sql, params);
|
|
127
|
+
return Promise.resolve();
|
|
128
|
+
},
|
|
129
|
+
transaction: (fn) => {
|
|
130
|
+
assertTransactionCallbackHasDriverArg(fn);
|
|
131
|
+
return this.runNestedTransaction(transactionDriver, fn);
|
|
132
|
+
},
|
|
133
|
+
transactionWithDriver: (fn) => this.runNestedTransaction(transactionDriver, fn)
|
|
134
|
+
};
|
|
135
|
+
return transactionDriver;
|
|
136
|
+
}
|
|
137
|
+
async runNestedTransaction(transactionDriver, fn) {
|
|
138
|
+
if (this.transactionExecutor) {
|
|
139
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
140
|
+
`Nested SQL savepoints are not supported when using Durable Object transaction API`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const savepointName = `tsdb_sp_${this.nextSavepointId}`;
|
|
144
|
+
this.nextSavepointId++;
|
|
145
|
+
this.execute(`SAVEPOINT ${savepointName}`);
|
|
146
|
+
try {
|
|
147
|
+
const result = await fn(transactionDriver);
|
|
148
|
+
this.execute(`RELEASE SAVEPOINT ${savepointName}`);
|
|
149
|
+
return result;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
152
|
+
this.execute(`RELEASE SAVEPOINT ${savepointName}`);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
CloudflareDOSQLiteDriver
|
|
159
|
+
};
|
|
160
|
+
//# sourceMappingURL=do-driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"do-driver.js","sources":["../../src/do-driver.ts"],"sourcesContent":["import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'\nimport type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'\n\ntype DurableObjectSqlRow = Record<string, unknown>\n\ntype DurableObjectSqlCursorLike = Iterable<DurableObjectSqlRow> & {\n toArray?: () => Array<DurableObjectSqlRow>\n}\n\nexport type DurableObjectSqlStorageLike = {\n exec: (\n sql: string,\n ...params: ReadonlyArray<unknown>\n ) => DurableObjectSqlCursorLike | ReadonlyArray<DurableObjectSqlRow> | null\n}\n\nexport type DurableObjectTransactionExecutor = <T>(\n fn: () => Promise<T>,\n) => Promise<T>\n\nexport type DurableObjectStorageLike = {\n sql: DurableObjectSqlStorageLike\n transaction?: DurableObjectTransactionExecutor\n}\n\ntype CloudflareDOProvidedSqlOptions = {\n sql: DurableObjectSqlStorageLike\n transaction?: DurableObjectTransactionExecutor\n}\n\ntype CloudflareDOProvidedStorageOptions = {\n storage: DurableObjectStorageLike\n}\n\nexport type CloudflareDOSQLiteDriverOptions =\n | CloudflareDOProvidedSqlOptions\n | CloudflareDOProvidedStorageOptions\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 isIterableRecord(\n value: unknown,\n): value is Iterable<DurableObjectSqlRow> {\n if (!value || typeof value !== `object`) {\n return false\n }\n\n const iterator = (value as { [Symbol.iterator]?: unknown })[Symbol.iterator]\n return typeof iterator === `function`\n}\n\nfunction toRowArray<T>(\n result: ReturnType<DurableObjectSqlStorageLike[`exec`]>,\n sql: string,\n): ReadonlyArray<T> {\n if (result == null) {\n return []\n }\n\n if (Array.isArray(result)) {\n return result as ReadonlyArray<T>\n }\n\n const cursorResult = result as DurableObjectSqlCursorLike\n if (typeof cursorResult.toArray === `function`) {\n return cursorResult.toArray() as ReadonlyArray<T>\n }\n\n if (isIterableRecord(cursorResult)) {\n return Array.from(cursorResult as Iterable<T>)\n }\n\n throw new InvalidPersistedCollectionConfigError(\n `Unsupported Durable Object SQL result shape for query \"${sql}\"`,\n )\n}\n\nexport class CloudflareDOSQLiteDriver implements SQLiteDriver {\n private readonly sqlStorage: DurableObjectSqlStorageLike\n private readonly storage: DurableObjectStorageLike\n private readonly transactionExecutor: DurableObjectTransactionExecutor | null\n private queue: Promise<void> = Promise.resolve()\n private nextSavepointId = 1\n\n constructor(options: CloudflareDOSQLiteDriverOptions) {\n const resolvedStorage: DurableObjectStorageLike =\n `storage` in options\n ? options.storage\n : {\n sql: options.sql,\n ...(typeof options.transaction === `function`\n ? { transaction: options.transaction }\n : {}),\n }\n const resolvedSqlStorage = resolvedStorage.sql\n if (typeof resolvedSqlStorage.exec !== `function`) {\n throw new InvalidPersistedCollectionConfigError(\n `Cloudflare DO SQL driver requires a sql.exec function`,\n )\n }\n this.storage = resolvedStorage\n this.sqlStorage = resolvedSqlStorage\n if (typeof resolvedStorage.transaction === `function`) {\n const transactionMethod = resolvedStorage.transaction\n this.transactionExecutor = <T>(fn: () => Promise<T>) =>\n Promise.resolve(\n transactionMethod.call(resolvedStorage, fn) as Promise<T> | T,\n )\n } else {\n this.transactionExecutor = null\n }\n }\n\n async exec(sql: string): Promise<void> {\n await this.enqueue(() => {\n this.execute(sql)\n })\n }\n\n async query<T>(\n sql: string,\n params: ReadonlyArray<unknown> = [],\n ): Promise<ReadonlyArray<T>> {\n return this.enqueue(() => this.executeQuery<T>(sql, params))\n }\n\n async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {\n await this.enqueue(() => {\n this.execute(sql, params)\n })\n }\n\n async transaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn)\n\n return this.enqueue(async () => {\n const transactionDriver = this.createTransactionDriver()\n if (this.transactionExecutor) {\n return this.transactionExecutor(() => fn(transactionDriver))\n }\n\n this.execute(`BEGIN IMMEDIATE`)\n try {\n const result = await fn(transactionDriver)\n this.execute(`COMMIT`)\n return result\n } catch (error) {\n try {\n this.execute(`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 getStorage(): DurableObjectStorageLike {\n return this.storage\n }\n\n private execute(sql: string, params: ReadonlyArray<unknown> = []): unknown {\n return this.sqlStorage.exec(sql, ...params)\n }\n\n private executeQuery<T>(\n sql: string,\n params: ReadonlyArray<unknown>,\n ): ReadonlyArray<T> {\n const result = this.execute(sql, params)\n return toRowArray<T>(\n result as ReturnType<DurableObjectSqlStorageLike[`exec`]>,\n sql,\n )\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 createTransactionDriver(): SQLiteDriver {\n const transactionDriver: SQLiteDriver = {\n exec: (sql) => {\n this.execute(sql)\n return Promise.resolve()\n },\n query: <T>(\n sql: string,\n params: ReadonlyArray<unknown> = [],\n ): Promise<ReadonlyArray<T>> =>\n Promise.resolve(this.executeQuery<T>(sql, params)),\n run: (sql, params = []) => {\n this.execute(sql, params)\n return Promise.resolve()\n },\n transaction: <T>(\n fn: (nestedDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> => {\n assertTransactionCallbackHasDriverArg(fn)\n return this.runNestedTransaction(transactionDriver, fn)\n },\n transactionWithDriver: <T>(\n fn: (nestedDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> => this.runNestedTransaction(transactionDriver, fn),\n }\n\n return transactionDriver\n }\n\n private async runNestedTransaction<T>(\n transactionDriver: SQLiteDriver,\n fn: (nestedDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n if (this.transactionExecutor) {\n throw new InvalidPersistedCollectionConfigError(\n `Nested SQL savepoints are not supported when using Durable Object transaction API`,\n )\n }\n\n const savepointName = `tsdb_sp_${this.nextSavepointId}`\n this.nextSavepointId++\n this.execute(`SAVEPOINT ${savepointName}`)\n\n try {\n const result = await fn(transactionDriver)\n this.execute(`RELEASE SAVEPOINT ${savepointName}`)\n return result\n } catch (error) {\n this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`)\n this.execute(`RELEASE SAVEPOINT ${savepointName}`)\n throw error\n }\n }\n}\n\nexport function createCloudflareDOSQLiteDriver(\n options: CloudflareDOSQLiteDriverOptions,\n): CloudflareDOSQLiteDriver {\n return new CloudflareDOSQLiteDriver(options)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PersistedCollectionCoordinator, PersistedCollectionPersistence, SQLiteCoreAdapterOptions } from '@tanstack/db-sqlite-persistence-core';
|
|
2
|
+
import { DurableObjectStorageLike } from './do-driver.js';
|
|
3
|
+
export type { DurableObjectStorageLike } from './do-driver.js';
|
|
4
|
+
type CloudflareDOCoreSchemaMismatchPolicy = `sync-present-reset` | `sync-absent-error` | `reset`;
|
|
5
|
+
export type CloudflareDOSchemaMismatchPolicy = CloudflareDOCoreSchemaMismatchPolicy | `throw`;
|
|
6
|
+
type CloudflareDOSQLitePersistenceBaseOptions = Omit<SQLiteCoreAdapterOptions, `driver` | `schemaVersion` | `schemaMismatchPolicy`> & {
|
|
7
|
+
storage: DurableObjectStorageLike;
|
|
8
|
+
coordinator?: PersistedCollectionCoordinator;
|
|
9
|
+
schemaMismatchPolicy?: CloudflareDOSchemaMismatchPolicy;
|
|
10
|
+
};
|
|
11
|
+
export type CloudflareDOSQLitePersistenceOptions = CloudflareDOSQLitePersistenceBaseOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a shared Durable Object SQLite persistence instance that can be reused
|
|
14
|
+
* by many collections in a single Durable Object storage.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createCloudflareDOSQLitePersistence<T extends object, TKey extends string | number = string | number>(options: CloudflareDOSQLitePersistenceOptions): PersistedCollectionPersistence<T, TKey>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SingleProcessCoordinator, createSQLiteCorePersistenceAdapter } from "@tanstack/db-sqlite-persistence-core";
|
|
2
|
+
import { CloudflareDOSQLiteDriver } from "./do-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 resolveSQLiteDriver(options) {
|
|
20
|
+
return new CloudflareDOSQLiteDriver({
|
|
21
|
+
storage: options.storage
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function resolveAdapterBaseOptions(options) {
|
|
25
|
+
return {
|
|
26
|
+
appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
|
|
27
|
+
appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
|
|
28
|
+
pullSinceReloadThreshold: options.pullSinceReloadThreshold
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createCloudflareDOSQLitePersistence(options) {
|
|
32
|
+
const { coordinator, schemaMismatchPolicy } = options;
|
|
33
|
+
const driver = resolveSQLiteDriver(options);
|
|
34
|
+
const adapterBaseOptions = resolveAdapterBaseOptions(options);
|
|
35
|
+
const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator();
|
|
36
|
+
const adapterCache = /* @__PURE__ */ new Map();
|
|
37
|
+
const getAdapterForCollection = (mode, schemaVersion) => {
|
|
38
|
+
const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(
|
|
39
|
+
schemaMismatchPolicy,
|
|
40
|
+
mode
|
|
41
|
+
);
|
|
42
|
+
const cacheKey = createAdapterCacheKey(
|
|
43
|
+
resolvedSchemaMismatchPolicy,
|
|
44
|
+
schemaVersion
|
|
45
|
+
);
|
|
46
|
+
const cachedAdapter = adapterCache.get(cacheKey);
|
|
47
|
+
if (cachedAdapter) {
|
|
48
|
+
return cachedAdapter;
|
|
49
|
+
}
|
|
50
|
+
const adapter = createSQLiteCorePersistenceAdapter({
|
|
51
|
+
...adapterBaseOptions,
|
|
52
|
+
driver,
|
|
53
|
+
schemaMismatchPolicy: resolvedSchemaMismatchPolicy,
|
|
54
|
+
...schemaVersion === void 0 ? {} : { schemaVersion }
|
|
55
|
+
});
|
|
56
|
+
adapterCache.set(cacheKey, adapter);
|
|
57
|
+
return adapter;
|
|
58
|
+
};
|
|
59
|
+
const createCollectionPersistence = (mode, schemaVersion) => ({
|
|
60
|
+
adapter: getAdapterForCollection(
|
|
61
|
+
mode,
|
|
62
|
+
schemaVersion
|
|
63
|
+
),
|
|
64
|
+
coordinator: resolvedCoordinator
|
|
65
|
+
});
|
|
66
|
+
const defaultPersistence = createCollectionPersistence(
|
|
67
|
+
`sync-absent`,
|
|
68
|
+
void 0
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
...defaultPersistence,
|
|
72
|
+
resolvePersistenceForCollection: ({ mode, schemaVersion }) => createCollectionPersistence(mode, schemaVersion),
|
|
73
|
+
// Backward compatible fallback for older callers.
|
|
74
|
+
resolvePersistenceForMode: (mode) => createCollectionPersistence(mode, void 0)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
createCloudflareDOSQLitePersistence
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=do-persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"do-persistence.js","sources":["../../src/do-persistence.ts"],"sourcesContent":["import {\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { CloudflareDOSQLiteDriver } from './do-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { DurableObjectStorageLike } from './do-driver'\n\nexport type { DurableObjectStorageLike } from './do-driver'\n\ntype CloudflareDOCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type CloudflareDOSchemaMismatchPolicy =\n | CloudflareDOCoreSchemaMismatchPolicy\n | `throw`\n\ntype CloudflareDOSQLitePersistenceBaseOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n storage: DurableObjectStorageLike\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: CloudflareDOSchemaMismatchPolicy\n}\n\nexport type CloudflareDOSQLitePersistenceOptions =\n CloudflareDOSQLitePersistenceBaseOptions\n\nfunction normalizeSchemaMismatchPolicy(\n policy: CloudflareDOSchemaMismatchPolicy,\n): CloudflareDOCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: CloudflareDOSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): CloudflareDOCoreSchemaMismatchPolicy {\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: CloudflareDOCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction resolveSQLiteDriver(\n options: CloudflareDOSQLitePersistenceOptions,\n): SQLiteDriver {\n return new CloudflareDOSQLiteDriver({\n storage: options.storage,\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: CloudflareDOSQLitePersistenceOptions,\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 Durable Object SQLite persistence instance that can be reused\n * by many collections in a single Durable Object storage.\n */\nexport function createCloudflareDOSQLitePersistence<\n T extends object,\n TKey extends string | number = string | number,\n>(\n options: CloudflareDOSQLitePersistenceOptions,\n): PersistedCollectionPersistence<T, TKey> {\n const { coordinator, schemaMismatchPolicy } = options\n const driver = resolveSQLiteDriver(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,QACsC;AACtC,MAAI,WAAW,SAAS;AACtB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,4BACP,gBACA,MACsC;AACtC,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,oBACP,SACc;AACd,SAAO,IAAI,yBAAyB;AAAA,IAClC,SAAS,QAAQ;AAAA,EAAA,CAClB;AACH;AAEA,SAAS,0BACP,SAIA;AACA,SAAO;AAAA,IACL,uBAAuB,QAAQ;AAAA,IAC/B,6BAA6B,QAAQ;AAAA,IACrC,0BAA0B,QAAQ;AAAA,EAAA;AAEtC;AAMO,SAAS,oCAId,SACyC;AACzC,QAAM,EAAE,aAAa,qBAAA,IAAyB;AAC9C,QAAM,SAAS,oBAAoB,OAAO;AAC1C,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;"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createCloudflareDOSQLitePersistence } from './do-persistence.js';
|
|
2
|
+
export type { CloudflareDOSchemaMismatchPolicy, CloudflareDOSQLitePersistenceOptions, DurableObjectStorageLike, } from './do-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 { createCloudflareDOSQLitePersistence } from "./do-persistence.js";
|
|
2
|
+
import { persistedCollectionOptions } from "@tanstack/db-sqlite-persistence-core";
|
|
3
|
+
export {
|
|
4
|
+
createCloudflareDOSQLitePersistence,
|
|
5
|
+
persistedCollectionOptions
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/cloudflare-durable-objects-db-sqlite-persistence",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Cloudflare Durable Object 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/cloudflare-durable-objects-db-sqlite-persistence"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://tanstack.com/db",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"sqlite",
|
|
15
|
+
"cloudflare",
|
|
16
|
+
"durable-objects",
|
|
17
|
+
"persistence",
|
|
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/cloudflare-durable-objects-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
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"typescript": ">=4.7"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
57
|
+
"@vitest/coverage-istanbul": "^3.2.4",
|
|
58
|
+
"better-sqlite3": "^12.6.2",
|
|
59
|
+
"wrangler": "^4.64.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/do-driver.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
|
|
2
|
+
import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'
|
|
3
|
+
|
|
4
|
+
type DurableObjectSqlRow = Record<string, unknown>
|
|
5
|
+
|
|
6
|
+
type DurableObjectSqlCursorLike = Iterable<DurableObjectSqlRow> & {
|
|
7
|
+
toArray?: () => Array<DurableObjectSqlRow>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type DurableObjectSqlStorageLike = {
|
|
11
|
+
exec: (
|
|
12
|
+
sql: string,
|
|
13
|
+
...params: ReadonlyArray<unknown>
|
|
14
|
+
) => DurableObjectSqlCursorLike | ReadonlyArray<DurableObjectSqlRow> | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DurableObjectTransactionExecutor = <T>(
|
|
18
|
+
fn: () => Promise<T>,
|
|
19
|
+
) => Promise<T>
|
|
20
|
+
|
|
21
|
+
export type DurableObjectStorageLike = {
|
|
22
|
+
sql: DurableObjectSqlStorageLike
|
|
23
|
+
transaction?: DurableObjectTransactionExecutor
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type CloudflareDOProvidedSqlOptions = {
|
|
27
|
+
sql: DurableObjectSqlStorageLike
|
|
28
|
+
transaction?: DurableObjectTransactionExecutor
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CloudflareDOProvidedStorageOptions = {
|
|
32
|
+
storage: DurableObjectStorageLike
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CloudflareDOSQLiteDriverOptions =
|
|
36
|
+
| CloudflareDOProvidedSqlOptions
|
|
37
|
+
| CloudflareDOProvidedStorageOptions
|
|
38
|
+
|
|
39
|
+
function assertTransactionCallbackHasDriverArg(
|
|
40
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,
|
|
41
|
+
): void {
|
|
42
|
+
if (fn.length > 0) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
47
|
+
`SQLiteDriver.transaction callback must accept the transaction driver argument`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isIterableRecord(
|
|
52
|
+
value: unknown,
|
|
53
|
+
): value is Iterable<DurableObjectSqlRow> {
|
|
54
|
+
if (!value || typeof value !== `object`) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const iterator = (value as { [Symbol.iterator]?: unknown })[Symbol.iterator]
|
|
59
|
+
return typeof iterator === `function`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toRowArray<T>(
|
|
63
|
+
result: ReturnType<DurableObjectSqlStorageLike[`exec`]>,
|
|
64
|
+
sql: string,
|
|
65
|
+
): ReadonlyArray<T> {
|
|
66
|
+
if (result == null) {
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(result)) {
|
|
71
|
+
return result as ReadonlyArray<T>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cursorResult = result as DurableObjectSqlCursorLike
|
|
75
|
+
if (typeof cursorResult.toArray === `function`) {
|
|
76
|
+
return cursorResult.toArray() as ReadonlyArray<T>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isIterableRecord(cursorResult)) {
|
|
80
|
+
return Array.from(cursorResult as Iterable<T>)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
84
|
+
`Unsupported Durable Object SQL result shape for query "${sql}"`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class CloudflareDOSQLiteDriver implements SQLiteDriver {
|
|
89
|
+
private readonly sqlStorage: DurableObjectSqlStorageLike
|
|
90
|
+
private readonly storage: DurableObjectStorageLike
|
|
91
|
+
private readonly transactionExecutor: DurableObjectTransactionExecutor | null
|
|
92
|
+
private queue: Promise<void> = Promise.resolve()
|
|
93
|
+
private nextSavepointId = 1
|
|
94
|
+
|
|
95
|
+
constructor(options: CloudflareDOSQLiteDriverOptions) {
|
|
96
|
+
const resolvedStorage: DurableObjectStorageLike =
|
|
97
|
+
`storage` in options
|
|
98
|
+
? options.storage
|
|
99
|
+
: {
|
|
100
|
+
sql: options.sql,
|
|
101
|
+
...(typeof options.transaction === `function`
|
|
102
|
+
? { transaction: options.transaction }
|
|
103
|
+
: {}),
|
|
104
|
+
}
|
|
105
|
+
const resolvedSqlStorage = resolvedStorage.sql
|
|
106
|
+
if (typeof resolvedSqlStorage.exec !== `function`) {
|
|
107
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
108
|
+
`Cloudflare DO SQL driver requires a sql.exec function`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
this.storage = resolvedStorage
|
|
112
|
+
this.sqlStorage = resolvedSqlStorage
|
|
113
|
+
if (typeof resolvedStorage.transaction === `function`) {
|
|
114
|
+
const transactionMethod = resolvedStorage.transaction
|
|
115
|
+
this.transactionExecutor = <T>(fn: () => Promise<T>) =>
|
|
116
|
+
Promise.resolve(
|
|
117
|
+
transactionMethod.call(resolvedStorage, fn) as Promise<T> | T,
|
|
118
|
+
)
|
|
119
|
+
} else {
|
|
120
|
+
this.transactionExecutor = null
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async exec(sql: string): Promise<void> {
|
|
125
|
+
await this.enqueue(() => {
|
|
126
|
+
this.execute(sql)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async query<T>(
|
|
131
|
+
sql: string,
|
|
132
|
+
params: ReadonlyArray<unknown> = [],
|
|
133
|
+
): Promise<ReadonlyArray<T>> {
|
|
134
|
+
return this.enqueue(() => this.executeQuery<T>(sql, params))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
|
|
138
|
+
await this.enqueue(() => {
|
|
139
|
+
this.execute(sql, params)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async transaction<T>(
|
|
144
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<T>,
|
|
145
|
+
): Promise<T> {
|
|
146
|
+
assertTransactionCallbackHasDriverArg(fn)
|
|
147
|
+
|
|
148
|
+
return this.enqueue(async () => {
|
|
149
|
+
const transactionDriver = this.createTransactionDriver()
|
|
150
|
+
if (this.transactionExecutor) {
|
|
151
|
+
return this.transactionExecutor(() => fn(transactionDriver))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.execute(`BEGIN IMMEDIATE`)
|
|
155
|
+
try {
|
|
156
|
+
const result = await fn(transactionDriver)
|
|
157
|
+
this.execute(`COMMIT`)
|
|
158
|
+
return result
|
|
159
|
+
} catch (error) {
|
|
160
|
+
try {
|
|
161
|
+
this.execute(`ROLLBACK`)
|
|
162
|
+
} catch {
|
|
163
|
+
// Keep the original transaction error as the primary failure.
|
|
164
|
+
}
|
|
165
|
+
throw error
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async transactionWithDriver<T>(
|
|
171
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<T>,
|
|
172
|
+
): Promise<T> {
|
|
173
|
+
return this.transaction(fn)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getStorage(): DurableObjectStorageLike {
|
|
177
|
+
return this.storage
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private execute(sql: string, params: ReadonlyArray<unknown> = []): unknown {
|
|
181
|
+
return this.sqlStorage.exec(sql, ...params)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private executeQuery<T>(
|
|
185
|
+
sql: string,
|
|
186
|
+
params: ReadonlyArray<unknown>,
|
|
187
|
+
): ReadonlyArray<T> {
|
|
188
|
+
const result = this.execute(sql, params)
|
|
189
|
+
return toRowArray<T>(
|
|
190
|
+
result as ReturnType<DurableObjectSqlStorageLike[`exec`]>,
|
|
191
|
+
sql,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {
|
|
196
|
+
const queuedOperation = this.queue.then(operation, operation)
|
|
197
|
+
this.queue = queuedOperation.then(
|
|
198
|
+
() => undefined,
|
|
199
|
+
() => undefined,
|
|
200
|
+
)
|
|
201
|
+
return queuedOperation
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private createTransactionDriver(): SQLiteDriver {
|
|
205
|
+
const transactionDriver: SQLiteDriver = {
|
|
206
|
+
exec: (sql) => {
|
|
207
|
+
this.execute(sql)
|
|
208
|
+
return Promise.resolve()
|
|
209
|
+
},
|
|
210
|
+
query: <T>(
|
|
211
|
+
sql: string,
|
|
212
|
+
params: ReadonlyArray<unknown> = [],
|
|
213
|
+
): Promise<ReadonlyArray<T>> =>
|
|
214
|
+
Promise.resolve(this.executeQuery<T>(sql, params)),
|
|
215
|
+
run: (sql, params = []) => {
|
|
216
|
+
this.execute(sql, params)
|
|
217
|
+
return Promise.resolve()
|
|
218
|
+
},
|
|
219
|
+
transaction: <T>(
|
|
220
|
+
fn: (nestedDriver: SQLiteDriver) => Promise<T>,
|
|
221
|
+
): Promise<T> => {
|
|
222
|
+
assertTransactionCallbackHasDriverArg(fn)
|
|
223
|
+
return this.runNestedTransaction(transactionDriver, fn)
|
|
224
|
+
},
|
|
225
|
+
transactionWithDriver: <T>(
|
|
226
|
+
fn: (nestedDriver: SQLiteDriver) => Promise<T>,
|
|
227
|
+
): Promise<T> => this.runNestedTransaction(transactionDriver, fn),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return transactionDriver
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async runNestedTransaction<T>(
|
|
234
|
+
transactionDriver: SQLiteDriver,
|
|
235
|
+
fn: (nestedDriver: SQLiteDriver) => Promise<T>,
|
|
236
|
+
): Promise<T> {
|
|
237
|
+
if (this.transactionExecutor) {
|
|
238
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
239
|
+
`Nested SQL savepoints are not supported when using Durable Object transaction API`,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const savepointName = `tsdb_sp_${this.nextSavepointId}`
|
|
244
|
+
this.nextSavepointId++
|
|
245
|
+
this.execute(`SAVEPOINT ${savepointName}`)
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const result = await fn(transactionDriver)
|
|
249
|
+
this.execute(`RELEASE SAVEPOINT ${savepointName}`)
|
|
250
|
+
return result
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`)
|
|
253
|
+
this.execute(`RELEASE SAVEPOINT ${savepointName}`)
|
|
254
|
+
throw error
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function createCloudflareDOSQLiteDriver(
|
|
260
|
+
options: CloudflareDOSQLiteDriverOptions,
|
|
261
|
+
): CloudflareDOSQLiteDriver {
|
|
262
|
+
return new CloudflareDOSQLiteDriver(options)
|
|
263
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SingleProcessCoordinator,
|
|
3
|
+
createSQLiteCorePersistenceAdapter,
|
|
4
|
+
} from '@tanstack/db-sqlite-persistence-core'
|
|
5
|
+
import { CloudflareDOSQLiteDriver } from './do-driver'
|
|
6
|
+
import type {
|
|
7
|
+
PersistedCollectionCoordinator,
|
|
8
|
+
PersistedCollectionMode,
|
|
9
|
+
PersistedCollectionPersistence,
|
|
10
|
+
SQLiteCoreAdapterOptions,
|
|
11
|
+
SQLiteDriver,
|
|
12
|
+
} from '@tanstack/db-sqlite-persistence-core'
|
|
13
|
+
import type { DurableObjectStorageLike } from './do-driver'
|
|
14
|
+
|
|
15
|
+
export type { DurableObjectStorageLike } from './do-driver'
|
|
16
|
+
|
|
17
|
+
type CloudflareDOCoreSchemaMismatchPolicy =
|
|
18
|
+
| `sync-present-reset`
|
|
19
|
+
| `sync-absent-error`
|
|
20
|
+
| `reset`
|
|
21
|
+
|
|
22
|
+
export type CloudflareDOSchemaMismatchPolicy =
|
|
23
|
+
| CloudflareDOCoreSchemaMismatchPolicy
|
|
24
|
+
| `throw`
|
|
25
|
+
|
|
26
|
+
type CloudflareDOSQLitePersistenceBaseOptions = Omit<
|
|
27
|
+
SQLiteCoreAdapterOptions,
|
|
28
|
+
`driver` | `schemaVersion` | `schemaMismatchPolicy`
|
|
29
|
+
> & {
|
|
30
|
+
storage: DurableObjectStorageLike
|
|
31
|
+
coordinator?: PersistedCollectionCoordinator
|
|
32
|
+
schemaMismatchPolicy?: CloudflareDOSchemaMismatchPolicy
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CloudflareDOSQLitePersistenceOptions =
|
|
36
|
+
CloudflareDOSQLitePersistenceBaseOptions
|
|
37
|
+
|
|
38
|
+
function normalizeSchemaMismatchPolicy(
|
|
39
|
+
policy: CloudflareDOSchemaMismatchPolicy,
|
|
40
|
+
): CloudflareDOCoreSchemaMismatchPolicy {
|
|
41
|
+
if (policy === `throw`) {
|
|
42
|
+
return `sync-absent-error`
|
|
43
|
+
}
|
|
44
|
+
return policy
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveSchemaMismatchPolicy(
|
|
48
|
+
explicitPolicy: CloudflareDOSchemaMismatchPolicy | undefined,
|
|
49
|
+
mode: PersistedCollectionMode,
|
|
50
|
+
): CloudflareDOCoreSchemaMismatchPolicy {
|
|
51
|
+
if (explicitPolicy) {
|
|
52
|
+
return normalizeSchemaMismatchPolicy(explicitPolicy)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createAdapterCacheKey(
|
|
59
|
+
schemaMismatchPolicy: CloudflareDOCoreSchemaMismatchPolicy,
|
|
60
|
+
schemaVersion: number | undefined,
|
|
61
|
+
): string {
|
|
62
|
+
const schemaVersionKey =
|
|
63
|
+
schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`
|
|
64
|
+
return `${schemaMismatchPolicy}|${schemaVersionKey}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveSQLiteDriver(
|
|
68
|
+
options: CloudflareDOSQLitePersistenceOptions,
|
|
69
|
+
): SQLiteDriver {
|
|
70
|
+
return new CloudflareDOSQLiteDriver({
|
|
71
|
+
storage: options.storage,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveAdapterBaseOptions(
|
|
76
|
+
options: CloudflareDOSQLitePersistenceOptions,
|
|
77
|
+
): Omit<
|
|
78
|
+
SQLiteCoreAdapterOptions,
|
|
79
|
+
`driver` | `schemaVersion` | `schemaMismatchPolicy`
|
|
80
|
+
> {
|
|
81
|
+
return {
|
|
82
|
+
appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
|
|
83
|
+
appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
|
|
84
|
+
pullSinceReloadThreshold: options.pullSinceReloadThreshold,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates a shared Durable Object SQLite persistence instance that can be reused
|
|
90
|
+
* by many collections in a single Durable Object storage.
|
|
91
|
+
*/
|
|
92
|
+
export function createCloudflareDOSQLitePersistence<
|
|
93
|
+
T extends object,
|
|
94
|
+
TKey extends string | number = string | number,
|
|
95
|
+
>(
|
|
96
|
+
options: CloudflareDOSQLitePersistenceOptions,
|
|
97
|
+
): PersistedCollectionPersistence<T, TKey> {
|
|
98
|
+
const { coordinator, schemaMismatchPolicy } = options
|
|
99
|
+
const driver = resolveSQLiteDriver(options)
|
|
100
|
+
const adapterBaseOptions = resolveAdapterBaseOptions(options)
|
|
101
|
+
const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator()
|
|
102
|
+
const adapterCache = new Map<
|
|
103
|
+
string,
|
|
104
|
+
ReturnType<
|
|
105
|
+
typeof createSQLiteCorePersistenceAdapter<
|
|
106
|
+
Record<string, unknown>,
|
|
107
|
+
string | number
|
|
108
|
+
>
|
|
109
|
+
>
|
|
110
|
+
>()
|
|
111
|
+
|
|
112
|
+
const getAdapterForCollection = (
|
|
113
|
+
mode: PersistedCollectionMode,
|
|
114
|
+
schemaVersion: number | undefined,
|
|
115
|
+
) => {
|
|
116
|
+
const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(
|
|
117
|
+
schemaMismatchPolicy,
|
|
118
|
+
mode,
|
|
119
|
+
)
|
|
120
|
+
const cacheKey = createAdapterCacheKey(
|
|
121
|
+
resolvedSchemaMismatchPolicy,
|
|
122
|
+
schemaVersion,
|
|
123
|
+
)
|
|
124
|
+
const cachedAdapter = adapterCache.get(cacheKey)
|
|
125
|
+
if (cachedAdapter) {
|
|
126
|
+
return cachedAdapter
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const adapter = createSQLiteCorePersistenceAdapter<
|
|
130
|
+
Record<string, unknown>,
|
|
131
|
+
string | number
|
|
132
|
+
>({
|
|
133
|
+
...adapterBaseOptions,
|
|
134
|
+
driver,
|
|
135
|
+
schemaMismatchPolicy: resolvedSchemaMismatchPolicy,
|
|
136
|
+
...(schemaVersion === undefined ? {} : { schemaVersion }),
|
|
137
|
+
})
|
|
138
|
+
adapterCache.set(cacheKey, adapter)
|
|
139
|
+
return adapter
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const createCollectionPersistence = (
|
|
143
|
+
mode: PersistedCollectionMode,
|
|
144
|
+
schemaVersion: number | undefined,
|
|
145
|
+
): PersistedCollectionPersistence<T, TKey> => ({
|
|
146
|
+
adapter: getAdapterForCollection(
|
|
147
|
+
mode,
|
|
148
|
+
schemaVersion,
|
|
149
|
+
) as unknown as PersistedCollectionPersistence<T, TKey>[`adapter`],
|
|
150
|
+
coordinator: resolvedCoordinator,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const defaultPersistence = createCollectionPersistence(
|
|
154
|
+
`sync-absent`,
|
|
155
|
+
undefined,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...defaultPersistence,
|
|
160
|
+
resolvePersistenceForCollection: ({ mode, schemaVersion }) =>
|
|
161
|
+
createCollectionPersistence(mode, schemaVersion),
|
|
162
|
+
// Backward compatible fallback for older callers.
|
|
163
|
+
resolvePersistenceForMode: (mode) =>
|
|
164
|
+
createCollectionPersistence(mode, undefined),
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createCloudflareDOSQLitePersistence } from './do-persistence'
|
|
2
|
+
export type {
|
|
3
|
+
CloudflareDOSchemaMismatchPolicy,
|
|
4
|
+
CloudflareDOSQLitePersistenceOptions,
|
|
5
|
+
DurableObjectStorageLike,
|
|
6
|
+
} from './do-persistence'
|
|
7
|
+
export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core'
|
|
8
|
+
export type {
|
|
9
|
+
PersistedCollectionCoordinator,
|
|
10
|
+
PersistedCollectionPersistence,
|
|
11
|
+
} from '@tanstack/db-sqlite-persistence-core'
|