@livestore/adapter-cloudflare 0.4.0-dev.9 → 0.5.0-dev.0
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 +5 -5
- package/dist/WebSocket.d.ts.map +1 -1
- package/dist/WebSocket.js +18 -15
- package/dist/WebSocket.js.map +1 -1
- package/dist/create-store-do.d.ts +78 -7
- package/dist/create-store-do.d.ts.map +1 -1
- package/dist/create-store-do.js +72 -5
- package/dist/create-store-do.js.map +1 -1
- package/dist/make-adapter.d.ts +2 -2
- package/dist/make-adapter.d.ts.map +1 -1
- package/dist/make-adapter.js +62 -51
- package/dist/make-adapter.js.map +1 -1
- package/dist/make-sqlite-db.d.ts +4 -4
- package/dist/make-sqlite-db.d.ts.map +1 -1
- package/dist/make-sqlite-db.js +41 -12
- 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 +50 -17
- package/src/WebSocket.ts +23 -18
- package/src/create-store-do.ts +91 -12
- package/src/make-adapter.ts +79 -66
- package/src/make-sqlite-db.ts +51 -21
- package/src/mod.ts +1 -6
- package/dist/cf-types.d.ts +0 -2
- package/dist/cf-types.d.ts.map +0 -1
- package/dist/cf-types.js +0 -2
- package/dist/cf-types.js.map +0 -1
- package/dist/make-client-durable-object.d.ts +0 -48
- package/dist/make-client-durable-object.d.ts.map +0 -1
- package/dist/make-client-durable-object.js +0 -26
- package/dist/make-client-durable-object.js.map +0 -1
- package/src/cf-types.ts +0 -20
package/src/make-adapter.ts
CHANGED
|
@@ -5,14 +5,24 @@ import {
|
|
|
5
5
|
liveStoreStorageFormatVersion,
|
|
6
6
|
makeClientSession,
|
|
7
7
|
type SyncOptions,
|
|
8
|
-
|
|
8
|
+
UnknownError,
|
|
9
|
+
StateHead,
|
|
9
10
|
} from '@livestore/common'
|
|
10
|
-
import
|
|
11
|
+
import type { CfTypes } from '@livestore/common-cf'
|
|
12
|
+
import {
|
|
13
|
+
type DevtoolsOptions,
|
|
14
|
+
Eventlog,
|
|
15
|
+
LeaderThreadCtx,
|
|
16
|
+
makeLeaderThreadLayer,
|
|
17
|
+
streamEventsWithSyncState,
|
|
18
|
+
} from '@livestore/common/leader-thread'
|
|
19
|
+
import { getStateDbBaseName } from '@livestore/common/schema'
|
|
11
20
|
import { LiveStoreEvent } from '@livestore/livestore'
|
|
12
|
-
import { sqliteDbFactory } from '@livestore/sqlite-wasm/cf'
|
|
21
|
+
import { CF_SQL_VFS_REQUIRED_PRAGMAS, sqliteDbFactory } from '@livestore/sqlite-wasm/cf'
|
|
13
22
|
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
14
|
-
import { Effect, FetchHttpClient, Layer, Schedule, SubscriptionRef, WebChannel } from '@livestore/utils/effect'
|
|
15
|
-
|
|
23
|
+
import { Effect, FetchHttpClient, Layer, Queue, Schedule, SubscriptionRef, WebChannel } from '@livestore/utils/effect'
|
|
24
|
+
|
|
25
|
+
import { makeSqliteDb as makeDoSqliteDb } from './make-sqlite-db.ts'
|
|
16
26
|
|
|
17
27
|
export const makeAdapter =
|
|
18
28
|
({
|
|
@@ -22,7 +32,7 @@ export const makeAdapter =
|
|
|
22
32
|
sessionId,
|
|
23
33
|
resetPersistence = false,
|
|
24
34
|
}: {
|
|
25
|
-
storage:
|
|
35
|
+
storage: CfTypes.DurableObjectStorage
|
|
26
36
|
clientId: string
|
|
27
37
|
syncOptions: SyncOptions
|
|
28
38
|
sessionId: string
|
|
@@ -30,7 +40,13 @@ export const makeAdapter =
|
|
|
30
40
|
}): Adapter =>
|
|
31
41
|
(adapterArgs) =>
|
|
32
42
|
Effect.gen(function* () {
|
|
33
|
-
const {
|
|
43
|
+
const {
|
|
44
|
+
storeId,
|
|
45
|
+
/* devtoolsEnabled, shutdown, bootStatusQueue, */
|
|
46
|
+
syncPayloadEncoded,
|
|
47
|
+
syncPayloadSchema,
|
|
48
|
+
schema,
|
|
49
|
+
} = adapterArgs
|
|
34
50
|
|
|
35
51
|
const devtoolsOptions = { enabled: false } as DevtoolsOptions
|
|
36
52
|
|
|
@@ -39,41 +55,32 @@ export const makeAdapter =
|
|
|
39
55
|
const makeSqliteDb = sqliteDbFactory({ sqlite3 })
|
|
40
56
|
|
|
41
57
|
const syncInMemoryDb = yield* makeSqliteDb({ _tag: 'in-memory', storage, configureDb: () => {} }).pipe(
|
|
42
|
-
|
|
58
|
+
UnknownError.mapToUnknownError,
|
|
43
59
|
)
|
|
44
60
|
|
|
45
|
-
const schemaHashSuffix =
|
|
46
|
-
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
47
|
-
|
|
48
|
-
const stateDbFileName = getStateDbFileName(schemaHashSuffix)
|
|
49
|
-
const eventlogDbFileName = getEventlogDbFileName()
|
|
50
|
-
|
|
51
61
|
if (resetPersistence === true) {
|
|
52
|
-
yield* resetDurableObjectPersistence({
|
|
53
|
-
storage,
|
|
54
|
-
storeId,
|
|
55
|
-
dbFileNames: [stateDbFileName, eventlogDbFileName],
|
|
56
|
-
})
|
|
62
|
+
yield* resetDurableObjectPersistence({ storage, storeId })
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
const dbState = yield* makeSqliteDb({
|
|
60
66
|
_tag: 'storage',
|
|
61
67
|
storage,
|
|
62
|
-
fileName:
|
|
63
|
-
configureDb: () =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
fileName: `${getStateDbBaseName(schema)}@${liveStoreStorageFormatVersion}.db`,
|
|
69
|
+
configureDb: (db) =>
|
|
70
|
+
db.execute([...CF_SQL_VFS_REQUIRED_PRAGMAS, 'cache_size=-8000'].map((p) => `PRAGMA ${p}`).join(';\n')),
|
|
71
|
+
}).pipe(UnknownError.mapToUnknownError)
|
|
72
|
+
|
|
73
|
+
// dbEventlog runs on DO SQLite directly (not through the VFS). SQL-level transaction
|
|
74
|
+
// control (BEGIN/COMMIT/ROLLBACK) is silently dropped — see isTransactionControlStatement
|
|
75
|
+
// in make-sqlite-db.ts for details on why this is safe.
|
|
76
|
+
const dbEventlog = yield* makeDoSqliteDb({
|
|
77
|
+
_tag: 'file',
|
|
78
|
+
db: storage.sql,
|
|
70
79
|
configureDb: () => {},
|
|
71
|
-
}).pipe(
|
|
80
|
+
}).pipe(UnknownError.mapToUnknownError)
|
|
72
81
|
|
|
73
82
|
const shutdownChannel = yield* WebChannel.noopChannel<any, any>()
|
|
74
83
|
|
|
75
|
-
// Use Durable Object sync backend if no backend is specified
|
|
76
|
-
|
|
77
84
|
const layer = yield* Layer.build(
|
|
78
85
|
makeLeaderThreadLayer({
|
|
79
86
|
schema,
|
|
@@ -85,12 +92,14 @@ export const makeAdapter =
|
|
|
85
92
|
dbEventlog,
|
|
86
93
|
devtoolsOptions,
|
|
87
94
|
shutdownChannel,
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
syncPayloadEncoded,
|
|
96
|
+
syncPayloadSchema,
|
|
97
|
+
}).pipe(Layer.provide(StateHead.layer({ dbState }))),
|
|
90
98
|
)
|
|
91
99
|
|
|
92
100
|
const { leaderThread, initialSnapshot } = yield* Effect.gen(function* () {
|
|
93
|
-
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } =
|
|
101
|
+
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
|
|
102
|
+
yield* LeaderThreadCtx
|
|
94
103
|
|
|
95
104
|
const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
|
|
96
105
|
// const initialLeaderHead = EventSequenceNumber.ROOT
|
|
@@ -99,17 +108,24 @@ export const makeAdapter =
|
|
|
99
108
|
{
|
|
100
109
|
events: {
|
|
101
110
|
pull: ({ cursor }) => syncProcessor.pull({ cursor }),
|
|
102
|
-
push: (batch) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
push: (batch) => syncProcessor.push(batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item))),
|
|
112
|
+
stream: (options) =>
|
|
113
|
+
streamEventsWithSyncState({
|
|
114
|
+
dbEventlog,
|
|
115
|
+
syncState: syncProcessor.syncState,
|
|
116
|
+
options,
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
initialState: {
|
|
120
|
+
leaderHead: initialLeaderHead,
|
|
121
|
+
migrationsReport: initialState.migrationsReport,
|
|
122
|
+
storageMode: 'persisted',
|
|
107
123
|
},
|
|
108
|
-
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
109
124
|
export: Effect.sync(() => dbState.export()),
|
|
110
125
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
111
|
-
|
|
112
|
-
sendDevtoolsMessage: (message) =>
|
|
126
|
+
syncState: syncProcessor.syncState,
|
|
127
|
+
sendDevtoolsMessage: (message) => Queue.offer(extraIncomingMessagesQueue, message),
|
|
128
|
+
networkStatus,
|
|
113
129
|
},
|
|
114
130
|
{
|
|
115
131
|
// overrides: testing?.overrides?.clientSession?.leaderThreadProxy
|
|
@@ -130,14 +146,14 @@ export const makeAdapter =
|
|
|
130
146
|
sqliteDb: syncInMemoryDb,
|
|
131
147
|
webmeshMode: 'proxy',
|
|
132
148
|
connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode }) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
if (devtoolsOptions.enabled === true) {
|
|
150
|
+
console.log('connectWebmeshNode', { webmeshNode })
|
|
151
|
+
// yield* Webmesh.connectViaWebSocket({
|
|
152
|
+
// node: webmeshNode,
|
|
153
|
+
// url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
|
|
154
|
+
// openTimeout: 500,
|
|
155
|
+
// }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
156
|
+
}
|
|
141
157
|
}),
|
|
142
158
|
leaderThread,
|
|
143
159
|
lockStatus,
|
|
@@ -146,6 +162,7 @@ export const makeAdapter =
|
|
|
146
162
|
isLeader: true,
|
|
147
163
|
// Not really applicable for node as there is no "reload the app" concept
|
|
148
164
|
registerBeforeUnload: (_onBeforeUnload) => () => {},
|
|
165
|
+
origin: undefined,
|
|
149
166
|
})
|
|
150
167
|
|
|
151
168
|
return clientSession
|
|
@@ -154,30 +171,26 @@ export const makeAdapter =
|
|
|
154
171
|
Effect.provide(FetchHttpClient.layer),
|
|
155
172
|
)
|
|
156
173
|
|
|
157
|
-
const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
|
|
158
|
-
|
|
159
|
-
const getEventlogDbFileName = () => `eventlog@${liveStoreStorageFormatVersion}.db`
|
|
160
|
-
|
|
161
174
|
const resetDurableObjectPersistence = ({
|
|
162
175
|
storage,
|
|
163
176
|
storeId,
|
|
164
|
-
dbFileNames,
|
|
165
177
|
}: {
|
|
166
|
-
storage:
|
|
178
|
+
storage: CfTypes.DurableObjectStorage
|
|
167
179
|
storeId: string
|
|
168
|
-
dbFileNames: ReadonlyArray<string>
|
|
169
180
|
}) =>
|
|
170
181
|
Effect.try({
|
|
171
182
|
try: () =>
|
|
183
|
+
// All three tables live in the DO's single storage.sql database but are
|
|
184
|
+
// owned by different layers during normal operation:
|
|
185
|
+
// - vfs_pages: written by the wa-sqlite VFS layer (backs dbState)
|
|
186
|
+
// - eventlog, __livestore_sync_status: written directly by dbEventlog via storage.sql
|
|
172
187
|
storage.transactionSync(() => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
safeSqlExec(storage, 'DELETE FROM vfs_files WHERE file_path LIKE ?', likePattern)
|
|
177
|
-
}
|
|
188
|
+
safeSqlExec(storage, 'DELETE FROM vfs_pages')
|
|
189
|
+
safeSqlExec(storage, 'DELETE FROM eventlog')
|
|
190
|
+
safeSqlExec(storage, 'DELETE FROM __livestore_sync_status')
|
|
178
191
|
}),
|
|
179
192
|
catch: (cause) =>
|
|
180
|
-
new
|
|
193
|
+
new UnknownError({
|
|
181
194
|
cause,
|
|
182
195
|
note: `@livestore/adapter-cloudflare: Failed to reset persistence for store ${storeId}`,
|
|
183
196
|
}),
|
|
@@ -186,11 +199,11 @@ const resetDurableObjectPersistence = ({
|
|
|
186
199
|
Effect.withSpan('@livestore/adapter-cloudflare:resetPersistence', { attributes: { storeId } }),
|
|
187
200
|
)
|
|
188
201
|
|
|
189
|
-
const safeSqlExec = (storage:
|
|
202
|
+
const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, binding?: string) => {
|
|
190
203
|
try {
|
|
191
|
-
storage.sql.exec(query, binding)
|
|
204
|
+
binding !== undefined ? storage.sql.exec(query, binding) : storage.sql.exec(query)
|
|
192
205
|
} catch (error) {
|
|
193
|
-
if (
|
|
206
|
+
if (isMissingTableError(error) === true) {
|
|
194
207
|
return
|
|
195
208
|
}
|
|
196
209
|
|
|
@@ -198,5 +211,5 @@ const safeSqlExec = (storage: CfWorker.DurableObjectStorage, query: string, bind
|
|
|
198
211
|
}
|
|
199
212
|
}
|
|
200
213
|
|
|
201
|
-
const
|
|
214
|
+
const isMissingTableError = (error: unknown): boolean =>
|
|
202
215
|
error instanceof Error && error.message.toLowerCase().includes('no such table')
|
package/src/make-sqlite-db.ts
CHANGED
|
@@ -8,23 +8,25 @@ import type {
|
|
|
8
8
|
SqliteDbSession,
|
|
9
9
|
} from '@livestore/common'
|
|
10
10
|
import { SqliteDbHelper, SqliteError } from '@livestore/common'
|
|
11
|
+
import type { CfTypes } from '@livestore/common-cf'
|
|
11
12
|
import { EventSequenceNumber } from '@livestore/common/schema'
|
|
12
13
|
import { Effect } from '@livestore/utils/effect'
|
|
13
|
-
import type * as CfWorker from './cf-types.ts'
|
|
14
14
|
|
|
15
15
|
// Simplified prepared statement implementation using only public API
|
|
16
16
|
class CloudflarePreparedStatement implements PreparedStatement {
|
|
17
|
-
private sqlStorage:
|
|
17
|
+
private sqlStorage: CfTypes.SqlStorage
|
|
18
18
|
public readonly sql: string
|
|
19
19
|
|
|
20
|
-
constructor(sqlStorage:
|
|
20
|
+
constructor(sqlStorage: CfTypes.SqlStorage, sql: string) {
|
|
21
21
|
this.sqlStorage = sqlStorage
|
|
22
22
|
this.sql = sql
|
|
23
23
|
}
|
|
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
|
+
const cursor = this.sqlStorage.exec(this.sql, ...(bindValues !== undefined ? Object.values(bindValues) : []))
|
|
28
30
|
|
|
29
31
|
// Count affected rows by iterating through cursor
|
|
30
32
|
let changedCount = 0
|
|
@@ -32,7 +34,7 @@ class CloudflarePreparedStatement implements PreparedStatement {
|
|
|
32
34
|
changedCount++
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
if (options?.onRowsChanged) {
|
|
37
|
+
if (options?.onRowsChanged !== undefined) {
|
|
36
38
|
options.onRowsChanged(changedCount)
|
|
37
39
|
}
|
|
38
40
|
} catch (e) {
|
|
@@ -46,9 +48,9 @@ class CloudflarePreparedStatement implements PreparedStatement {
|
|
|
46
48
|
|
|
47
49
|
select = <T>(bindValues?: PreparedBindValues): readonly T[] => {
|
|
48
50
|
try {
|
|
49
|
-
const cursor = this.sqlStorage.exec<Record<string,
|
|
51
|
+
const cursor = this.sqlStorage.exec<Record<string, CfTypes.SqlStorageValue>>(
|
|
50
52
|
this.sql,
|
|
51
|
-
...(bindValues ? Object.values(bindValues) : []),
|
|
53
|
+
...(bindValues !== undefined ? Object.values(bindValues) : []),
|
|
52
54
|
)
|
|
53
55
|
const results: T[] = []
|
|
54
56
|
|
|
@@ -84,12 +86,12 @@ type CloudflareDatabaseInput =
|
|
|
84
86
|
_tag: 'file'
|
|
85
87
|
// databaseName: string
|
|
86
88
|
// directory: string
|
|
87
|
-
db:
|
|
89
|
+
db: CfTypes.SqlStorage
|
|
88
90
|
configureDb: (db: SqliteDb) => void
|
|
89
91
|
}
|
|
90
92
|
| {
|
|
91
93
|
_tag: 'in-memory'
|
|
92
|
-
db:
|
|
94
|
+
db: CfTypes.SqlStorage
|
|
93
95
|
configureDb: (db: SqliteDb) => void
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -97,14 +99,12 @@ export type MakeCloudflareSqliteDb = MakeSqliteDb<Metadata, CloudflareDatabaseIn
|
|
|
97
99
|
|
|
98
100
|
export const makeSqliteDb: MakeCloudflareSqliteDb = (input: CloudflareDatabaseInput) =>
|
|
99
101
|
Effect.gen(function* () {
|
|
100
|
-
// console.log('makeSqliteDb', input)
|
|
101
102
|
if (input._tag === 'in-memory') {
|
|
102
103
|
return makeSqliteDb_<Metadata>({
|
|
103
104
|
sqlStorage: input.db,
|
|
104
105
|
metadata: {
|
|
105
106
|
_tag: 'file' as const,
|
|
106
107
|
dbPointer: 0,
|
|
107
|
-
// persistenceInfo: { fileName: ':memory:' },
|
|
108
108
|
persistenceInfo: { fileName: 'cf' },
|
|
109
109
|
input,
|
|
110
110
|
configureDb: input.configureDb,
|
|
@@ -118,7 +118,6 @@ export const makeSqliteDb: MakeCloudflareSqliteDb = (input: CloudflareDatabaseIn
|
|
|
118
118
|
metadata: {
|
|
119
119
|
_tag: 'file' as const,
|
|
120
120
|
dbPointer: 0,
|
|
121
|
-
// persistenceInfo: { fileName: `${input.directory}/${input.databaseName}` },
|
|
122
121
|
persistenceInfo: { fileName: 'cf' },
|
|
123
122
|
input,
|
|
124
123
|
configureDb: input.configureDb,
|
|
@@ -130,14 +129,13 @@ export const makeSqliteDb: MakeCloudflareSqliteDb = (input: CloudflareDatabaseIn
|
|
|
130
129
|
export const makeSqliteDb_ = <
|
|
131
130
|
TMetadata extends {
|
|
132
131
|
persistenceInfo: PersistenceInfo
|
|
133
|
-
// deleteDb: () => void
|
|
134
132
|
configureDb: (db: SqliteDb<TMetadata>) => void
|
|
135
133
|
},
|
|
136
134
|
>({
|
|
137
135
|
sqlStorage,
|
|
138
136
|
metadata,
|
|
139
137
|
}: {
|
|
140
|
-
sqlStorage:
|
|
138
|
+
sqlStorage: CfTypes.SqlStorage
|
|
141
139
|
metadata: TMetadata
|
|
142
140
|
}): SqliteDb<TMetadata> => {
|
|
143
141
|
const preparedStmts: PreparedStatement[] = []
|
|
@@ -149,7 +147,7 @@ export const makeSqliteDb_ = <
|
|
|
149
147
|
metadata,
|
|
150
148
|
debug: {
|
|
151
149
|
// Setting initially to root but will be set to correct value shortly after
|
|
152
|
-
head: EventSequenceNumber.ROOT,
|
|
150
|
+
head: EventSequenceNumber.Client.ROOT,
|
|
153
151
|
},
|
|
154
152
|
prepare: (queryStr) => {
|
|
155
153
|
try {
|
|
@@ -188,18 +186,13 @@ export const makeSqliteDb_ = <
|
|
|
188
186
|
destroy: () => {
|
|
189
187
|
sqliteDb.close()
|
|
190
188
|
|
|
191
|
-
// metadata.deleteDb()
|
|
192
189
|
throw new SqliteError({
|
|
193
190
|
code: -1,
|
|
194
191
|
cause: 'Database destroy not supported with public SqlStorage API',
|
|
195
192
|
})
|
|
196
|
-
|
|
197
|
-
// if (metadata._tag === 'opfs') {
|
|
198
|
-
// metadata.vfs.resetAccessHandle(metadata.fileName)
|
|
199
|
-
// }
|
|
200
193
|
},
|
|
201
194
|
close: () => {
|
|
202
|
-
if (isClosed) {
|
|
195
|
+
if (isClosed === true) {
|
|
203
196
|
return
|
|
204
197
|
}
|
|
205
198
|
|
|
@@ -259,3 +252,40 @@ export const makeSqliteDb_ = <
|
|
|
259
252
|
|
|
260
253
|
return sqliteDb
|
|
261
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* CF DO SQLite rejects SQL-level transaction control and requires `storage.transactionSync()` instead.
|
|
258
|
+
* The current adapter only detects and suppresses those SQL statements. It does not yet translate the
|
|
259
|
+
* caller's transaction intent into a shared Durable Object storage transaction.
|
|
260
|
+
*
|
|
261
|
+
* ## Consistency implications
|
|
262
|
+
*
|
|
263
|
+
* `LeaderSyncProcessor.materializeEventsBatch()` wraps both `dbState` and `dbEventlog` in
|
|
264
|
+
* `BEGIN`/`COMMIT` to keep them consistent. Because this adapter drops those statements,
|
|
265
|
+
* eventlog INSERTs are auto-committed individually while `dbState` (VFS-backed) still has
|
|
266
|
+
* real transaction boundaries. If a batch partially fails, earlier eventlog rows survive
|
|
267
|
+
* while `dbState` rolls back.
|
|
268
|
+
*
|
|
269
|
+
* This is safe because:
|
|
270
|
+
* - The eventlog is append-only and idempotent — replaying already-inserted events is a no-op.
|
|
271
|
+
* - State is always rebuildable from the eventlog on cold start (`recreateDb`).
|
|
272
|
+
* - A Durable Object is single-threaded, so no concurrent reader can observe the
|
|
273
|
+
* intermediate inconsistency.
|
|
274
|
+
*
|
|
275
|
+
* Uses prefix matching to cover all SQLite variants:
|
|
276
|
+
* - `BEGIN [DEFERRED | IMMEDIATE | EXCLUSIVE] [TRANSACTION]`
|
|
277
|
+
* - `COMMIT [TRANSACTION]` / `END [TRANSACTION]`
|
|
278
|
+
* - `ROLLBACK [TRANSACTION] [TO [SAVEPOINT] name]`
|
|
279
|
+
* - `SAVEPOINT name` / `RELEASE [SAVEPOINT] name`
|
|
280
|
+
*/
|
|
281
|
+
const isTransactionControlStatement = (sql: string): boolean => {
|
|
282
|
+
const upper = sql.trim().toUpperCase()
|
|
283
|
+
return (
|
|
284
|
+
upper.startsWith('BEGIN') === true ||
|
|
285
|
+
upper.startsWith('COMMIT') === true ||
|
|
286
|
+
upper.startsWith('END') === true ||
|
|
287
|
+
upper.startsWith('ROLLBACK') === true ||
|
|
288
|
+
upper.startsWith('SAVEPOINT') === true ||
|
|
289
|
+
upper.startsWith('RELEASE') === true
|
|
290
|
+
)
|
|
291
|
+
}
|
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'
|
package/dist/cf-types.d.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export { type D1Database, type D1Result, type DurableObject, type DurableObjectNamespace, type DurableObjectState, type DurableObjectStorage, type DurableObjectStub, type MessageEvent, Request, Response, Rpc, type SqlStorage, SqlStorageCursor, SqlStorageStatement, type SqlStorageValue, WebSocket, WebSocketPair, WebSocketRequestResponsePair, } from '@cloudflare/workers-types';
|
|
2
|
-
//# sourceMappingURL=cf-types.d.ts.map
|
package/dist/cf-types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cf-types.d.ts","sourceRoot":"","sources":["../src/cf-types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,OAAO,EACP,QAAQ,EACR,GAAG,EACH,KAAK,UAAU,EACf,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,eAAe,EACpB,SAAS,EACT,aAAa,EACb,4BAA4B,GAC7B,MAAM,2BAA2B,CAAA"}
|
package/dist/cf-types.js
DELETED
package/dist/cf-types.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cf-types.js","sourceRoot":"","sources":["../src/cf-types.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,OAAO,EACP,QAAQ,EACR,GAAG,EAEH,gBAAgB,EAChB,mBAAmB,EAEnB,SAAS,EACT,aAAa,EACb,4BAA4B,GAC7B,MAAM,2BAA2B,CAAA"}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { UnexpectedError } from '@livestore/common';
|
|
2
|
-
import { type LiveStoreSchema, type Store, type Unsubscribe } from '@livestore/livestore';
|
|
3
|
-
import type * as CfSyncBackend from '@livestore/sync-cf/cf-worker';
|
|
4
|
-
import { Effect } from '@livestore/utils/effect';
|
|
5
|
-
import type * as CfWorker from './cf-types.ts';
|
|
6
|
-
export type MakeDurableObjectClassOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any> = {
|
|
7
|
-
schema: TSchema;
|
|
8
|
-
clientId: string;
|
|
9
|
-
sessionId: string;
|
|
10
|
-
onStoreReady?: (store: Store<TSchema>) => Effect.SyncOrPromiseOrEffect<void, UnexpectedError>;
|
|
11
|
-
registerQueries?: (store: Store<TSchema>) => Effect.SyncOrPromiseOrEffect<ReadonlyArray<Unsubscribe>>;
|
|
12
|
-
syncBackendUrl?: string;
|
|
13
|
-
handleCustomRequest?: (request: CfWorker.Request, ensureStore: Effect.Effect<Store<TSchema>, UnexpectedError, never>) => Effect.SyncOrPromiseOrEffect<CfWorker.Response | undefined, UnexpectedError>;
|
|
14
|
-
};
|
|
15
|
-
export type Env = {
|
|
16
|
-
SYNC_BACKEND_DO: CfWorker.DurableObjectNamespace;
|
|
17
|
-
};
|
|
18
|
-
export type MakeDurableObjectClass = <TSchema extends LiveStoreSchema = LiveStoreSchema.Any>(options: MakeDurableObjectClassOptions<TSchema>) => {
|
|
19
|
-
new (ctx: CfWorker.DurableObjectState, env: Env): CfWorker.DurableObject & CfWorker.Rpc.DurableObjectBranded;
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Options used to initialize the LiveStore Durable Object runtime.
|
|
23
|
-
*/
|
|
24
|
-
export type CreateStoreDoOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any> = {
|
|
25
|
-
/** LiveStore schema that defines state, migrations, and validators. */
|
|
26
|
-
schema: TSchema;
|
|
27
|
-
/** Logical identifier for the store instance persisted inside the Durable Object. */
|
|
28
|
-
storeId: string;
|
|
29
|
-
/** Unique identifier for the client that owns the Durable Object instance. */
|
|
30
|
-
clientId: string;
|
|
31
|
-
/** Identifier for the LiveStore session running inside the Durable Object. */
|
|
32
|
-
sessionId: string;
|
|
33
|
-
/** Cloudflare Durable Object storage binding backing the local SQLite files. */
|
|
34
|
-
storage: CfWorker.DurableObjectStorage;
|
|
35
|
-
/** RPC stub pointing at the sync backend Durable Object used for replication. */
|
|
36
|
-
syncBackendDurableObject: CfWorker.DurableObjectStub<CfSyncBackend.SyncBackendRpcInterface>;
|
|
37
|
-
/** Durable Object identifier for the current instance, forwarded to the sync backend. */
|
|
38
|
-
durableObjectId: string;
|
|
39
|
-
/** Binding name Cloudflare uses to reach this Durable Object from other workers. */
|
|
40
|
-
bindingName: string;
|
|
41
|
-
/** Enables live pull mode to receive sync updates via Durable Object RPC callbacks. */
|
|
42
|
-
livePull?: boolean;
|
|
43
|
-
/** Clears existing Durable Object persistence before bootstrapping the store. */
|
|
44
|
-
resetPersistence?: boolean;
|
|
45
|
-
};
|
|
46
|
-
export declare const createStoreDo: <TSchema extends LiveStoreSchema = LiveStoreSchema.Any>({ schema, storeId, clientId, sessionId, storage, syncBackendDurableObject, durableObjectId, bindingName, livePull, resetPersistence, }: CreateStoreDoOptions<TSchema>) => Effect.Effect<Store<TSchema, {}>, UnexpectedError, never>;
|
|
47
|
-
export declare const createStoreDoPromise: <TSchema extends LiveStoreSchema = LiveStoreSchema.Any>(options: CreateStoreDoOptions<TSchema>) => Promise<Store<TSchema, {}>>;
|
|
48
|
-
//# sourceMappingURL=make-client-durable-object.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"make-client-durable-object.d.ts","sourceRoot":"","sources":["../src/make-client-durable-object.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAe,KAAK,eAAe,EAAe,KAAK,KAAK,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACnH,OAAO,KAAK,KAAK,aAAa,MAAM,8BAA8B,CAAA;AAElE,OAAO,EAAE,MAAM,EAA2B,MAAM,yBAAyB,CAAA;AACzE,OAAO,KAAK,KAAK,QAAQ,MAAM,eAAe,CAAA;AAK9C,MAAM,MAAM,6BAA6B,CAAC,OAAO,SAAS,eAAe,GAAG,eAAe,CAAC,GAAG,IAAI;IACjG,MAAM,EAAE,OAAO,CAAA;IAEf,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,qBAAqB,CAAC,IAAI,EAAE,eAAe,CAAC,CAAA;IAG7F,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,qBAAqB,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAA;IACrG,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,QAAQ,CAAC,OAAO,EACzB,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,KAC/D,MAAM,CAAC,qBAAqB,CAAC,QAAQ,CAAC,QAAQ,GAAG,SAAS,EAAE,eAAe,CAAC,CAAA;CAClF,CAAA;AAED,MAAM,MAAM,GAAG,GAAG;IAChB,eAAe,EAAE,QAAQ,CAAC,sBAAsB,CAAA;CACjD,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,SAAS,eAAe,GAAG,eAAe,CAAC,GAAG,EACzF,OAAO,EAAE,6BAA6B,CAAC,OAAO,CAAC,KAC5C;IACH,KAAK,GAAG,EAAE,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE,GAAG,GAAG,QAAQ,CAAC,aAAa,GAAG,QAAQ,CAAC,GAAG,CAAC,oBAAoB,CAAA;CAC7G,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,CAAC,OAAO,SAAS,eAAe,GAAG,eAAe,CAAC,GAAG,IAAI;IACxF,uEAAuE;IACvE,MAAM,EAAE,OAAO,CAAA;IACf,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;IAChB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,OAAO,EAAE,QAAQ,CAAC,oBAAoB,CAAA;IACtC,iFAAiF;IACjF,wBAAwB,EAAE,QAAQ,CAAC,iBAAiB,CAAC,aAAa,CAAC,uBAAuB,CAAC,CAAA;IAC3F,yFAAyF;IACzF,eAAe,EAAE,MAAM,CAAA;IACvB,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAA;IACnB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,SAAS,eAAe,GAAG,eAAe,CAAC,GAAG,EAAE,wIAWlF,oBAAoB,CAAC,OAAO,CAAC,8DAsB5B,CAAA;AAEJ,eAAO,MAAM,oBAAoB,GAAI,OAAO,SAAS,eAAe,GAAG,eAAe,CAAC,GAAG,EACxF,SAAS,oBAAoB,CAAC,OAAO,CAAC,gCAOrC,CAAA"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { createStore, provideOtel } from '@livestore/livestore';
|
|
2
|
-
import { makeDoRpcSync } from '@livestore/sync-cf/client';
|
|
3
|
-
import { Effect, Logger, LogLevel, Scope } from '@livestore/utils/effect';
|
|
4
|
-
import { makeAdapter } from "./make-adapter.js";
|
|
5
|
-
export const createStoreDo = ({ schema, storeId, clientId, sessionId, storage, syncBackendDurableObject, durableObjectId, bindingName, livePull = false, resetPersistence = false, }) => Effect.gen(function* () {
|
|
6
|
-
const scope = yield* Scope.make();
|
|
7
|
-
const adapter = makeAdapter({
|
|
8
|
-
clientId,
|
|
9
|
-
sessionId,
|
|
10
|
-
storage,
|
|
11
|
-
resetPersistence,
|
|
12
|
-
syncOptions: {
|
|
13
|
-
backend: makeDoRpcSync({
|
|
14
|
-
syncBackendStub: syncBackendDurableObject,
|
|
15
|
-
durableObjectContext: { bindingName, durableObjectId },
|
|
16
|
-
}),
|
|
17
|
-
livePull, // Uses DO RPC callbacks for reactive pull
|
|
18
|
-
// backend: makeHttpSync({ url: `http://localhost:8787`, livePull: { pollInterval: 500 } }),
|
|
19
|
-
initialSyncOptions: { _tag: 'Blocking', timeout: 500 },
|
|
20
|
-
// backend: makeWsSyncProviderClient({ durableObject: syncBackendDurableObject }),
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
return yield* createStore({ schema, adapter, storeId }).pipe(Scope.extend(scope), provideOtel({}));
|
|
24
|
-
});
|
|
25
|
-
export const createStoreDoPromise = (options) => createStoreDo(options).pipe(Logger.withMinimumLogLevel(LogLevel.Debug), Effect.provide(Logger.consoleWithThread('DoClient')), Effect.tapCauseLogPretty, Effect.runPromise);
|
|
26
|
-
//# sourceMappingURL=make-client-durable-object.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"make-client-durable-object.js","sourceRoot":"","sources":["../src/make-client-durable-object.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAwB,WAAW,EAAgC,MAAM,sBAAsB,CAAA;AAEnH,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAEzE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAyD/C,MAAM,CAAC,MAAM,aAAa,GAAG,CAAwD,EACnF,MAAM,EACN,OAAO,EACP,QAAQ,EACR,SAAS,EACT,OAAO,EACP,wBAAwB,EACxB,eAAe,EACf,WAAW,EACX,QAAQ,GAAG,KAAK,EAChB,gBAAgB,GAAG,KAAK,GACM,EAAE,EAAE,CAClC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IAEjC,MAAM,OAAO,GAAG,WAAW,CAAC;QAC1B,QAAQ;QACR,SAAS;QACT,OAAO;QACP,gBAAgB;QAChB,WAAW,EAAE;YACX,OAAO,EAAE,aAAa,CAAC;gBACrB,eAAe,EAAE,wBAAwB;gBACzC,oBAAoB,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE;aACvD,CAAC;YACF,QAAQ,EAAE,0CAA0C;YACpD,4FAA4F;YAC5F,kBAAkB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE;YACtD,kFAAkF;SACnF;KACF,CAAC,CAAA;IAEF,OAAO,KAAK,CAAC,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,CAAA;AACpG,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,OAAsC,EACtC,EAAE,CACF,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CACzB,MAAM,CAAC,mBAAmB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAC1C,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,EACpD,MAAM,CAAC,iBAAiB,EACxB,MAAM,CAAC,UAAU,CAClB,CAAA"}
|
package/src/cf-types.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
type D1Database,
|
|
3
|
-
type D1Result,
|
|
4
|
-
type DurableObject,
|
|
5
|
-
type DurableObjectNamespace,
|
|
6
|
-
type DurableObjectState,
|
|
7
|
-
type DurableObjectStorage,
|
|
8
|
-
type DurableObjectStub,
|
|
9
|
-
type MessageEvent,
|
|
10
|
-
Request,
|
|
11
|
-
Response,
|
|
12
|
-
Rpc,
|
|
13
|
-
type SqlStorage,
|
|
14
|
-
SqlStorageCursor,
|
|
15
|
-
SqlStorageStatement,
|
|
16
|
-
type SqlStorageValue,
|
|
17
|
-
WebSocket,
|
|
18
|
-
WebSocketPair,
|
|
19
|
-
WebSocketRequestResponsePair,
|
|
20
|
-
} from '@cloudflare/workers-types'
|