@livestore/adapter-cloudflare 0.4.0-dev.21 → 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 +30 -27
- 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 +33 -32
- 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,
|
|
@@ -125,7 +124,11 @@ export const makeAdapter =
|
|
|
125
124
|
options,
|
|
126
125
|
}),
|
|
127
126
|
},
|
|
128
|
-
initialState: {
|
|
127
|
+
initialState: {
|
|
128
|
+
leaderHead: initialLeaderHead,
|
|
129
|
+
migrationsReport: initialState.migrationsReport,
|
|
130
|
+
storageMode: 'persisted',
|
|
131
|
+
},
|
|
129
132
|
export: Effect.sync(() => dbState.export()),
|
|
130
133
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
131
134
|
syncState: syncProcessor.syncState,
|
|
@@ -151,7 +154,7 @@ export const makeAdapter =
|
|
|
151
154
|
sqliteDb: syncInMemoryDb,
|
|
152
155
|
webmeshMode: 'proxy',
|
|
153
156
|
connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode }) {
|
|
154
|
-
if (devtoolsOptions.enabled) {
|
|
157
|
+
if (devtoolsOptions.enabled === true) {
|
|
155
158
|
console.log('connectWebmeshNode', { webmeshNode })
|
|
156
159
|
// yield* Webmesh.connectViaWebSocket({
|
|
157
160
|
// node: webmeshNode,
|
|
@@ -178,25 +181,23 @@ export const makeAdapter =
|
|
|
178
181
|
|
|
179
182
|
const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
|
|
180
183
|
|
|
181
|
-
const getEventlogDbFileName = () => `eventlog@${liveStoreStorageFormatVersion}.db`
|
|
182
|
-
|
|
183
184
|
const resetDurableObjectPersistence = ({
|
|
184
185
|
storage,
|
|
185
186
|
storeId,
|
|
186
|
-
dbFileNames,
|
|
187
187
|
}: {
|
|
188
188
|
storage: CfTypes.DurableObjectStorage
|
|
189
189
|
storeId: string
|
|
190
|
-
dbFileNames: ReadonlyArray<string>
|
|
191
190
|
}) =>
|
|
192
191
|
Effect.try({
|
|
193
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
|
|
194
197
|
storage.transactionSync(() => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
safeSqlExec(storage, 'DELETE FROM vfs_files WHERE file_path LIKE ?', likePattern)
|
|
199
|
-
}
|
|
198
|
+
safeSqlExec(storage, 'DELETE FROM vfs_pages')
|
|
199
|
+
safeSqlExec(storage, 'DELETE FROM eventlog')
|
|
200
|
+
safeSqlExec(storage, 'DELETE FROM __livestore_sync_status')
|
|
200
201
|
}),
|
|
201
202
|
catch: (cause) =>
|
|
202
203
|
new UnknownError({
|
|
@@ -208,11 +209,11 @@ const resetDurableObjectPersistence = ({
|
|
|
208
209
|
Effect.withSpan('@livestore/adapter-cloudflare:resetPersistence', { attributes: { storeId } }),
|
|
209
210
|
)
|
|
210
211
|
|
|
211
|
-
const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, binding
|
|
212
|
+
const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, binding?: string) => {
|
|
212
213
|
try {
|
|
213
|
-
storage.sql.exec(query, binding)
|
|
214
|
+
binding !== undefined ? storage.sql.exec(query, binding) : storage.sql.exec(query)
|
|
214
215
|
} catch (error) {
|
|
215
|
-
if (
|
|
216
|
+
if (isMissingTableError(error) === true) {
|
|
216
217
|
return
|
|
217
218
|
}
|
|
218
219
|
|
|
@@ -220,5 +221,5 @@ const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, bindi
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
const
|
|
224
|
+
const isMissingTableError = (error: unknown): boolean =>
|
|
224
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'
|