@livestore/adapter-cloudflare 0.4.0-dev.22 → 0.4.0-dev.23
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/dist/.tsbuildinfo +1 -1
- package/dist/WebSocket.d.ts.map +1 -1
- package/dist/WebSocket.js +6 -4
- package/dist/WebSocket.js.map +1 -1
- package/dist/create-store-do.d.ts +8 -2
- package/dist/create-store-do.d.ts.map +1 -1
- package/dist/create-store-do.js +2 -2
- package/dist/create-store-do.js.map +1 -1
- package/dist/make-adapter.d.ts.map +1 -1
- package/dist/make-adapter.js +25 -26
- package/dist/make-adapter.js.map +1 -1
- package/dist/make-sqlite-db.d.ts.map +1 -1
- package/dist/make-sqlite-db.js +40 -8
- package/dist/make-sqlite-db.js.map +1 -1
- package/dist/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -1
- package/dist/mod.js.map +1 -1
- package/package.json +58 -15
- package/src/WebSocket.ts +8 -4
- package/src/create-store-do.ts +10 -2
- package/src/make-adapter.ts +28 -31
- package/src/make-sqlite-db.ts +45 -10
- package/src/mod.ts +1 -6
package/src/make-adapter.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type SyncOptions,
|
|
8
8
|
UnknownError,
|
|
9
9
|
} from '@livestore/common'
|
|
10
|
+
import type { CfTypes } from '@livestore/common-cf'
|
|
10
11
|
import {
|
|
11
12
|
type DevtoolsOptions,
|
|
12
13
|
Eventlog,
|
|
@@ -14,9 +15,9 @@ import {
|
|
|
14
15
|
makeLeaderThreadLayer,
|
|
15
16
|
streamEventsWithSyncState,
|
|
16
17
|
} from '@livestore/common/leader-thread'
|
|
17
|
-
import type { CfTypes } from '@livestore/common-cf'
|
|
18
18
|
import { LiveStoreEvent } from '@livestore/livestore'
|
|
19
|
-
import { sqliteDbFactory } from '@livestore/sqlite-wasm/cf'
|
|
19
|
+
import { CF_SQL_VFS_REQUIRED_PRAGMAS, sqliteDbFactory } from '@livestore/sqlite-wasm/cf'
|
|
20
|
+
import { makeSqliteDb as makeDoSqliteDb } from './make-sqlite-db.ts'
|
|
20
21
|
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
21
22
|
import { Effect, FetchHttpClient, Layer, Schedule, SubscriptionRef, WebChannel } from '@livestore/utils/effect'
|
|
22
23
|
|
|
@@ -57,35 +58,33 @@ export const makeAdapter =
|
|
|
57
58
|
const schemaHashSuffix =
|
|
58
59
|
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
59
60
|
|
|
60
|
-
const stateDbFileName = getStateDbFileName(schemaHashSuffix)
|
|
61
|
-
const eventlogDbFileName = getEventlogDbFileName()
|
|
62
|
-
|
|
63
61
|
if (resetPersistence === true) {
|
|
64
|
-
yield* resetDurableObjectPersistence({
|
|
65
|
-
storage,
|
|
66
|
-
storeId,
|
|
67
|
-
dbFileNames: [stateDbFileName, eventlogDbFileName],
|
|
68
|
-
})
|
|
62
|
+
yield* resetDurableObjectPersistence({ storage, storeId })
|
|
69
63
|
}
|
|
70
64
|
|
|
65
|
+
const stateDbFileName = getStateDbFileName(schemaHashSuffix)
|
|
66
|
+
|
|
71
67
|
const dbState = yield* makeSqliteDb({
|
|
72
68
|
_tag: 'storage',
|
|
73
69
|
storage,
|
|
74
70
|
fileName: stateDbFileName,
|
|
75
|
-
configureDb: () =>
|
|
71
|
+
configureDb: (db) =>
|
|
72
|
+
db.execute(
|
|
73
|
+
[...CF_SQL_VFS_REQUIRED_PRAGMAS, 'cache_size=-8000'].map((p) => `PRAGMA ${p}`).join(';\n'),
|
|
74
|
+
),
|
|
76
75
|
}).pipe(UnknownError.mapToUnknownError)
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// dbEventlog runs on DO SQLite directly (not through the VFS). SQL-level transaction
|
|
78
|
+
// control (BEGIN/COMMIT/ROLLBACK) is silently dropped — see isTransactionControlStatement
|
|
79
|
+
// in make-sqlite-db.ts for details on why this is safe.
|
|
80
|
+
const dbEventlog = yield* makeDoSqliteDb({
|
|
81
|
+
_tag: 'file',
|
|
82
|
+
db: storage.sql,
|
|
82
83
|
configureDb: () => {},
|
|
83
84
|
}).pipe(UnknownError.mapToUnknownError)
|
|
84
85
|
|
|
85
86
|
const shutdownChannel = yield* WebChannel.noopChannel<any, any>()
|
|
86
87
|
|
|
87
|
-
// Use Durable Object sync backend if no backend is specified
|
|
88
|
-
|
|
89
88
|
const layer = yield* Layer.build(
|
|
90
89
|
makeLeaderThreadLayer({
|
|
91
90
|
schema,
|
|
@@ -155,7 +154,7 @@ export const makeAdapter =
|
|
|
155
154
|
sqliteDb: syncInMemoryDb,
|
|
156
155
|
webmeshMode: 'proxy',
|
|
157
156
|
connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode }) {
|
|
158
|
-
if (devtoolsOptions.enabled) {
|
|
157
|
+
if (devtoolsOptions.enabled === true) {
|
|
159
158
|
console.log('connectWebmeshNode', { webmeshNode })
|
|
160
159
|
// yield* Webmesh.connectViaWebSocket({
|
|
161
160
|
// node: webmeshNode,
|
|
@@ -182,25 +181,23 @@ export const makeAdapter =
|
|
|
182
181
|
|
|
183
182
|
const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
|
|
184
183
|
|
|
185
|
-
const getEventlogDbFileName = () => `eventlog@${liveStoreStorageFormatVersion}.db`
|
|
186
|
-
|
|
187
184
|
const resetDurableObjectPersistence = ({
|
|
188
185
|
storage,
|
|
189
186
|
storeId,
|
|
190
|
-
dbFileNames,
|
|
191
187
|
}: {
|
|
192
188
|
storage: CfTypes.DurableObjectStorage
|
|
193
189
|
storeId: string
|
|
194
|
-
dbFileNames: ReadonlyArray<string>
|
|
195
190
|
}) =>
|
|
196
191
|
Effect.try({
|
|
197
192
|
try: () =>
|
|
193
|
+
// All three tables live in the DO's single storage.sql database but are
|
|
194
|
+
// owned by different layers during normal operation:
|
|
195
|
+
// - vfs_pages: written by the wa-sqlite VFS layer (backs dbState)
|
|
196
|
+
// - eventlog, __livestore_sync_status: written directly by dbEventlog via storage.sql
|
|
198
197
|
storage.transactionSync(() => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
safeSqlExec(storage, 'DELETE FROM vfs_files WHERE file_path LIKE ?', likePattern)
|
|
203
|
-
}
|
|
198
|
+
safeSqlExec(storage, 'DELETE FROM vfs_pages')
|
|
199
|
+
safeSqlExec(storage, 'DELETE FROM eventlog')
|
|
200
|
+
safeSqlExec(storage, 'DELETE FROM __livestore_sync_status')
|
|
204
201
|
}),
|
|
205
202
|
catch: (cause) =>
|
|
206
203
|
new UnknownError({
|
|
@@ -212,11 +209,11 @@ const resetDurableObjectPersistence = ({
|
|
|
212
209
|
Effect.withSpan('@livestore/adapter-cloudflare:resetPersistence', { attributes: { storeId } }),
|
|
213
210
|
)
|
|
214
211
|
|
|
215
|
-
const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, binding
|
|
212
|
+
const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, binding?: string) => {
|
|
216
213
|
try {
|
|
217
|
-
storage.sql.exec(query, binding)
|
|
214
|
+
binding !== undefined ? storage.sql.exec(query, binding) : storage.sql.exec(query)
|
|
218
215
|
} catch (error) {
|
|
219
|
-
if (
|
|
216
|
+
if (isMissingTableError(error) === true) {
|
|
220
217
|
return
|
|
221
218
|
}
|
|
222
219
|
|
|
@@ -224,5 +221,5 @@ const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, bindi
|
|
|
224
221
|
}
|
|
225
222
|
}
|
|
226
223
|
|
|
227
|
-
const
|
|
224
|
+
const isMissingTableError = (error: unknown): boolean =>
|
|
228
225
|
error instanceof Error && error.message.toLowerCase().includes('no such table')
|
package/src/make-sqlite-db.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type {
|
|
|
8
8
|
SqliteDbSession,
|
|
9
9
|
} from '@livestore/common'
|
|
10
10
|
import { SqliteDbHelper, SqliteError } from '@livestore/common'
|
|
11
|
-
import { EventSequenceNumber } from '@livestore/common/schema'
|
|
12
11
|
import type { CfTypes } from '@livestore/common-cf'
|
|
12
|
+
import { EventSequenceNumber } from '@livestore/common/schema'
|
|
13
13
|
import { Effect } from '@livestore/utils/effect'
|
|
14
14
|
|
|
15
15
|
// Simplified prepared statement implementation using only public API
|
|
@@ -24,7 +24,10 @@ class CloudflarePreparedStatement implements PreparedStatement {
|
|
|
24
24
|
|
|
25
25
|
execute = (bindValues?: PreparedBindValues, options?: { onRowsChanged?: (count: number) => void }) => {
|
|
26
26
|
try {
|
|
27
|
-
|
|
27
|
+
if (isTransactionControlStatement(this.sql) === true) return
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const cursor = this.sqlStorage.exec(this.sql, ...(bindValues !== undefined ? Object.values(bindValues) : []))
|
|
28
31
|
|
|
29
32
|
// Count affected rows by iterating through cursor
|
|
30
33
|
let changedCount = 0
|
|
@@ -32,7 +35,7 @@ class CloudflarePreparedStatement implements PreparedStatement {
|
|
|
32
35
|
changedCount++
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
if (options?.onRowsChanged) {
|
|
38
|
+
if (options?.onRowsChanged !== undefined) {
|
|
36
39
|
options.onRowsChanged(changedCount)
|
|
37
40
|
}
|
|
38
41
|
} catch (e) {
|
|
@@ -48,7 +51,7 @@ class CloudflarePreparedStatement implements PreparedStatement {
|
|
|
48
51
|
try {
|
|
49
52
|
const cursor = this.sqlStorage.exec<Record<string, CfTypes.SqlStorageValue>>(
|
|
50
53
|
this.sql,
|
|
51
|
-
...(bindValues ? Object.values(bindValues) : []),
|
|
54
|
+
...(bindValues !== undefined ? Object.values(bindValues) : []),
|
|
52
55
|
)
|
|
53
56
|
const results: T[] = []
|
|
54
57
|
|
|
@@ -97,14 +100,12 @@ export type MakeCloudflareSqliteDb = MakeSqliteDb<Metadata, CloudflareDatabaseIn
|
|
|
97
100
|
|
|
98
101
|
export const makeSqliteDb: MakeCloudflareSqliteDb = (input: CloudflareDatabaseInput) =>
|
|
99
102
|
Effect.gen(function* () {
|
|
100
|
-
// console.log('makeSqliteDb', input)
|
|
101
103
|
if (input._tag === 'in-memory') {
|
|
102
104
|
return makeSqliteDb_<Metadata>({
|
|
103
105
|
sqlStorage: input.db,
|
|
104
106
|
metadata: {
|
|
105
107
|
_tag: 'file' as const,
|
|
106
108
|
dbPointer: 0,
|
|
107
|
-
// persistenceInfo: { fileName: ':memory:' },
|
|
108
109
|
persistenceInfo: { fileName: 'cf' },
|
|
109
110
|
input,
|
|
110
111
|
configureDb: input.configureDb,
|
|
@@ -118,7 +119,6 @@ export const makeSqliteDb: MakeCloudflareSqliteDb = (input: CloudflareDatabaseIn
|
|
|
118
119
|
metadata: {
|
|
119
120
|
_tag: 'file' as const,
|
|
120
121
|
dbPointer: 0,
|
|
121
|
-
// persistenceInfo: { fileName: `${input.directory}/${input.databaseName}` },
|
|
122
122
|
persistenceInfo: { fileName: 'cf' },
|
|
123
123
|
input,
|
|
124
124
|
configureDb: input.configureDb,
|
|
@@ -130,7 +130,6 @@ export const makeSqliteDb: MakeCloudflareSqliteDb = (input: CloudflareDatabaseIn
|
|
|
130
130
|
export const makeSqliteDb_ = <
|
|
131
131
|
TMetadata extends {
|
|
132
132
|
persistenceInfo: PersistenceInfo
|
|
133
|
-
// deleteDb: () => void
|
|
134
133
|
configureDb: (db: SqliteDb<TMetadata>) => void
|
|
135
134
|
},
|
|
136
135
|
>({
|
|
@@ -188,14 +187,13 @@ export const makeSqliteDb_ = <
|
|
|
188
187
|
destroy: () => {
|
|
189
188
|
sqliteDb.close()
|
|
190
189
|
|
|
191
|
-
// metadata.deleteDb()
|
|
192
190
|
throw new SqliteError({
|
|
193
191
|
code: -1,
|
|
194
192
|
cause: 'Database destroy not supported with public SqlStorage API',
|
|
195
193
|
})
|
|
196
194
|
},
|
|
197
195
|
close: () => {
|
|
198
|
-
if (isClosed) {
|
|
196
|
+
if (isClosed === true) {
|
|
199
197
|
return
|
|
200
198
|
}
|
|
201
199
|
|
|
@@ -255,3 +253,40 @@ export const makeSqliteDb_ = <
|
|
|
255
253
|
|
|
256
254
|
return sqliteDb
|
|
257
255
|
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* CF DO SQLite rejects SQL-level transaction control and requires `storage.transactionSync()` instead.
|
|
259
|
+
* The current adapter only detects and suppresses those SQL statements. It does not yet translate the
|
|
260
|
+
* caller's transaction intent into a shared Durable Object storage transaction.
|
|
261
|
+
*
|
|
262
|
+
* ## Consistency implications
|
|
263
|
+
*
|
|
264
|
+
* `LeaderSyncProcessor.materializeEventsBatch()` wraps both `dbState` and `dbEventlog` in
|
|
265
|
+
* `BEGIN`/`COMMIT` to keep them consistent. Because this adapter drops those statements,
|
|
266
|
+
* eventlog INSERTs are auto-committed individually while `dbState` (VFS-backed) still has
|
|
267
|
+
* real transaction boundaries. If a batch partially fails, earlier eventlog rows survive
|
|
268
|
+
* while `dbState` rolls back.
|
|
269
|
+
*
|
|
270
|
+
* This is safe because:
|
|
271
|
+
* - The eventlog is append-only and idempotent — replaying already-inserted events is a no-op.
|
|
272
|
+
* - State is always rebuildable from the eventlog on cold start (`recreateDb`).
|
|
273
|
+
* - A Durable Object is single-threaded, so no concurrent reader can observe the
|
|
274
|
+
* intermediate inconsistency.
|
|
275
|
+
*
|
|
276
|
+
* Uses prefix matching to cover all SQLite variants:
|
|
277
|
+
* - `BEGIN [DEFERRED | IMMEDIATE | EXCLUSIVE] [TRANSACTION]`
|
|
278
|
+
* - `COMMIT [TRANSACTION]` / `END [TRANSACTION]`
|
|
279
|
+
* - `ROLLBACK [TRANSACTION] [TO [SAVEPOINT] name]`
|
|
280
|
+
* - `SAVEPOINT name` / `RELEASE [SAVEPOINT] name`
|
|
281
|
+
*/
|
|
282
|
+
const isTransactionControlStatement = (sql: string): boolean => {
|
|
283
|
+
const upper = sql.trim().toUpperCase()
|
|
284
|
+
return (
|
|
285
|
+
upper.startsWith('BEGIN') === true ||
|
|
286
|
+
upper.startsWith('COMMIT') === true ||
|
|
287
|
+
upper.startsWith('END') === true ||
|
|
288
|
+
upper.startsWith('ROLLBACK') === true ||
|
|
289
|
+
upper.startsWith('SAVEPOINT') === true ||
|
|
290
|
+
upper.startsWith('RELEASE') === true
|
|
291
|
+
)
|
|
292
|
+
}
|
package/src/mod.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import './polyfill.ts'
|
|
2
2
|
|
|
3
3
|
export type { ClientDoWithRpcCallback } from '@livestore/common-cf'
|
|
4
|
-
export {
|
|
5
|
-
type CreateStoreDoOptions,
|
|
6
|
-
createStoreDo,
|
|
7
|
-
createStoreDoPromise,
|
|
8
|
-
type Env,
|
|
9
|
-
} from './create-store-do.ts'
|
|
4
|
+
export { type CreateStoreDoOptions, createStoreDo, createStoreDoPromise, type Env } from './create-store-do.ts'
|
|
10
5
|
export { makeAdapter } from './make-adapter.ts'
|