@livestore/adapter-cloudflare 0.4.0-dev.9 → 0.4.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 +73 -4
- 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 +58 -45
- 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 +60 -17
- package/src/WebSocket.ts +23 -18
- package/src/create-store-do.ts +86 -11
- package/src/make-adapter.ts +76 -53
- package/src/make-sqlite-db.ts +52 -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,21 @@ import {
|
|
|
5
5
|
liveStoreStorageFormatVersion,
|
|
6
6
|
makeClientSession,
|
|
7
7
|
type SyncOptions,
|
|
8
|
-
|
|
8
|
+
UnknownError,
|
|
9
9
|
} from '@livestore/common'
|
|
10
|
-
import
|
|
10
|
+
import type { CfTypes } from '@livestore/common-cf'
|
|
11
|
+
import {
|
|
12
|
+
type DevtoolsOptions,
|
|
13
|
+
Eventlog,
|
|
14
|
+
LeaderThreadCtx,
|
|
15
|
+
makeLeaderThreadLayer,
|
|
16
|
+
streamEventsWithSyncState,
|
|
17
|
+
} from '@livestore/common/leader-thread'
|
|
11
18
|
import { LiveStoreEvent } from '@livestore/livestore'
|
|
12
|
-
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'
|
|
13
21
|
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
14
22
|
import { Effect, FetchHttpClient, Layer, Schedule, SubscriptionRef, WebChannel } from '@livestore/utils/effect'
|
|
15
|
-
import type * as CfWorker from './cf-types.ts'
|
|
16
23
|
|
|
17
24
|
export const makeAdapter =
|
|
18
25
|
({
|
|
@@ -22,7 +29,7 @@ export const makeAdapter =
|
|
|
22
29
|
sessionId,
|
|
23
30
|
resetPersistence = false,
|
|
24
31
|
}: {
|
|
25
|
-
storage:
|
|
32
|
+
storage: CfTypes.DurableObjectStorage
|
|
26
33
|
clientId: string
|
|
27
34
|
syncOptions: SyncOptions
|
|
28
35
|
sessionId: string
|
|
@@ -30,7 +37,13 @@ export const makeAdapter =
|
|
|
30
37
|
}): Adapter =>
|
|
31
38
|
(adapterArgs) =>
|
|
32
39
|
Effect.gen(function* () {
|
|
33
|
-
const {
|
|
40
|
+
const {
|
|
41
|
+
storeId,
|
|
42
|
+
/* devtoolsEnabled, shutdown, bootStatusQueue, */
|
|
43
|
+
syncPayloadEncoded,
|
|
44
|
+
syncPayloadSchema,
|
|
45
|
+
schema,
|
|
46
|
+
} = adapterArgs
|
|
34
47
|
|
|
35
48
|
const devtoolsOptions = { enabled: false } as DevtoolsOptions
|
|
36
49
|
|
|
@@ -39,41 +52,39 @@ export const makeAdapter =
|
|
|
39
52
|
const makeSqliteDb = sqliteDbFactory({ sqlite3 })
|
|
40
53
|
|
|
41
54
|
const syncInMemoryDb = yield* makeSqliteDb({ _tag: 'in-memory', storage, configureDb: () => {} }).pipe(
|
|
42
|
-
|
|
55
|
+
UnknownError.mapToUnknownError,
|
|
43
56
|
)
|
|
44
57
|
|
|
45
58
|
const schemaHashSuffix =
|
|
46
59
|
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
47
60
|
|
|
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
|
|
|
65
|
+
const stateDbFileName = getStateDbFileName(schemaHashSuffix)
|
|
66
|
+
|
|
59
67
|
const dbState = yield* makeSqliteDb({
|
|
60
68
|
_tag: 'storage',
|
|
61
69
|
storage,
|
|
62
70
|
fileName: stateDbFileName,
|
|
71
|
+
configureDb: (db) =>
|
|
72
|
+
db.execute(
|
|
73
|
+
[...CF_SQL_VFS_REQUIRED_PRAGMAS, 'cache_size=-8000'].map((p) => `PRAGMA ${p}`).join(';\n'),
|
|
74
|
+
),
|
|
75
|
+
}).pipe(UnknownError.mapToUnknownError)
|
|
76
|
+
|
|
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,
|
|
63
83
|
configureDb: () => {},
|
|
64
|
-
}).pipe(
|
|
65
|
-
|
|
66
|
-
const dbEventlog = yield* makeSqliteDb({
|
|
67
|
-
_tag: 'storage',
|
|
68
|
-
storage,
|
|
69
|
-
fileName: eventlogDbFileName,
|
|
70
|
-
configureDb: () => {},
|
|
71
|
-
}).pipe(UnexpectedError.mapToUnexpectedError)
|
|
84
|
+
}).pipe(UnknownError.mapToUnknownError)
|
|
72
85
|
|
|
73
86
|
const shutdownChannel = yield* WebChannel.noopChannel<any, any>()
|
|
74
87
|
|
|
75
|
-
// Use Durable Object sync backend if no backend is specified
|
|
76
|
-
|
|
77
88
|
const layer = yield* Layer.build(
|
|
78
89
|
makeLeaderThreadLayer({
|
|
79
90
|
schema,
|
|
@@ -85,12 +96,14 @@ export const makeAdapter =
|
|
|
85
96
|
dbEventlog,
|
|
86
97
|
devtoolsOptions,
|
|
87
98
|
shutdownChannel,
|
|
88
|
-
|
|
99
|
+
syncPayloadEncoded,
|
|
100
|
+
syncPayloadSchema,
|
|
89
101
|
}),
|
|
90
102
|
)
|
|
91
103
|
|
|
92
104
|
const { leaderThread, initialSnapshot } = yield* Effect.gen(function* () {
|
|
93
|
-
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } =
|
|
105
|
+
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
|
|
106
|
+
yield* LeaderThreadCtx
|
|
94
107
|
|
|
95
108
|
const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
|
|
96
109
|
// const initialLeaderHead = EventSequenceNumber.ROOT
|
|
@@ -101,15 +114,26 @@ export const makeAdapter =
|
|
|
101
114
|
pull: ({ cursor }) => syncProcessor.pull({ cursor }),
|
|
102
115
|
push: (batch) =>
|
|
103
116
|
syncProcessor.push(
|
|
104
|
-
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
117
|
+
batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
|
|
105
118
|
{ waitForProcessing: true },
|
|
106
119
|
),
|
|
120
|
+
stream: (options) =>
|
|
121
|
+
streamEventsWithSyncState({
|
|
122
|
+
dbEventlog,
|
|
123
|
+
syncState: syncProcessor.syncState,
|
|
124
|
+
options,
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
initialState: {
|
|
128
|
+
leaderHead: initialLeaderHead,
|
|
129
|
+
migrationsReport: initialState.migrationsReport,
|
|
130
|
+
storageMode: 'persisted',
|
|
107
131
|
},
|
|
108
|
-
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
109
132
|
export: Effect.sync(() => dbState.export()),
|
|
110
133
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
111
|
-
|
|
134
|
+
syncState: syncProcessor.syncState,
|
|
112
135
|
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
136
|
+
networkStatus,
|
|
113
137
|
},
|
|
114
138
|
{
|
|
115
139
|
// overrides: testing?.overrides?.clientSession?.leaderThreadProxy
|
|
@@ -130,14 +154,14 @@ export const makeAdapter =
|
|
|
130
154
|
sqliteDb: syncInMemoryDb,
|
|
131
155
|
webmeshMode: 'proxy',
|
|
132
156
|
connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode }) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
if (devtoolsOptions.enabled === true) {
|
|
158
|
+
console.log('connectWebmeshNode', { webmeshNode })
|
|
159
|
+
// yield* Webmesh.connectViaWebSocket({
|
|
160
|
+
// node: webmeshNode,
|
|
161
|
+
// url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
|
|
162
|
+
// openTimeout: 500,
|
|
163
|
+
// }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
164
|
+
}
|
|
141
165
|
}),
|
|
142
166
|
leaderThread,
|
|
143
167
|
lockStatus,
|
|
@@ -146,6 +170,7 @@ export const makeAdapter =
|
|
|
146
170
|
isLeader: true,
|
|
147
171
|
// Not really applicable for node as there is no "reload the app" concept
|
|
148
172
|
registerBeforeUnload: (_onBeforeUnload) => () => {},
|
|
173
|
+
origin: undefined,
|
|
149
174
|
})
|
|
150
175
|
|
|
151
176
|
return clientSession
|
|
@@ -156,28 +181,26 @@ export const makeAdapter =
|
|
|
156
181
|
|
|
157
182
|
const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
|
|
158
183
|
|
|
159
|
-
const getEventlogDbFileName = () => `eventlog@${liveStoreStorageFormatVersion}.db`
|
|
160
|
-
|
|
161
184
|
const resetDurableObjectPersistence = ({
|
|
162
185
|
storage,
|
|
163
186
|
storeId,
|
|
164
|
-
dbFileNames,
|
|
165
187
|
}: {
|
|
166
|
-
storage:
|
|
188
|
+
storage: CfTypes.DurableObjectStorage
|
|
167
189
|
storeId: string
|
|
168
|
-
dbFileNames: ReadonlyArray<string>
|
|
169
190
|
}) =>
|
|
170
191
|
Effect.try({
|
|
171
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
|
|
172
197
|
storage.transactionSync(() => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
safeSqlExec(storage, 'DELETE FROM vfs_files WHERE file_path LIKE ?', likePattern)
|
|
177
|
-
}
|
|
198
|
+
safeSqlExec(storage, 'DELETE FROM vfs_pages')
|
|
199
|
+
safeSqlExec(storage, 'DELETE FROM eventlog')
|
|
200
|
+
safeSqlExec(storage, 'DELETE FROM __livestore_sync_status')
|
|
178
201
|
}),
|
|
179
202
|
catch: (cause) =>
|
|
180
|
-
new
|
|
203
|
+
new UnknownError({
|
|
181
204
|
cause,
|
|
182
205
|
note: `@livestore/adapter-cloudflare: Failed to reset persistence for store ${storeId}`,
|
|
183
206
|
}),
|
|
@@ -186,11 +209,11 @@ const resetDurableObjectPersistence = ({
|
|
|
186
209
|
Effect.withSpan('@livestore/adapter-cloudflare:resetPersistence', { attributes: { storeId } }),
|
|
187
210
|
)
|
|
188
211
|
|
|
189
|
-
const safeSqlExec = (storage:
|
|
212
|
+
const safeSqlExec = (storage: CfTypes.DurableObjectStorage, query: string, binding?: string) => {
|
|
190
213
|
try {
|
|
191
|
-
storage.sql.exec(query, binding)
|
|
214
|
+
binding !== undefined ? storage.sql.exec(query, binding) : storage.sql.exec(query)
|
|
192
215
|
} catch (error) {
|
|
193
|
-
if (
|
|
216
|
+
if (isMissingTableError(error) === true) {
|
|
194
217
|
return
|
|
195
218
|
}
|
|
196
219
|
|
|
@@ -198,5 +221,5 @@ const safeSqlExec = (storage: CfWorker.DurableObjectStorage, query: string, bind
|
|
|
198
221
|
}
|
|
199
222
|
}
|
|
200
223
|
|
|
201
|
-
const
|
|
224
|
+
const isMissingTableError = (error: unknown): boolean =>
|
|
202
225
|
error instanceof Error && error.message.toLowerCase().includes('no such table')
|
package/src/make-sqlite-db.ts
CHANGED
|
@@ -8,23 +8,26 @@ 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
|
+
|
|
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) {
|
|
@@ -46,9 +49,9 @@ class CloudflarePreparedStatement implements PreparedStatement {
|
|
|
46
49
|
|
|
47
50
|
select = <T>(bindValues?: PreparedBindValues): readonly T[] => {
|
|
48
51
|
try {
|
|
49
|
-
const cursor = this.sqlStorage.exec<Record<string,
|
|
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
|
|
|
@@ -84,12 +87,12 @@ type CloudflareDatabaseInput =
|
|
|
84
87
|
_tag: 'file'
|
|
85
88
|
// databaseName: string
|
|
86
89
|
// directory: string
|
|
87
|
-
db:
|
|
90
|
+
db: CfTypes.SqlStorage
|
|
88
91
|
configureDb: (db: SqliteDb) => void
|
|
89
92
|
}
|
|
90
93
|
| {
|
|
91
94
|
_tag: 'in-memory'
|
|
92
|
-
db:
|
|
95
|
+
db: CfTypes.SqlStorage
|
|
93
96
|
configureDb: (db: SqliteDb) => void
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -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,14 +130,13 @@ 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
|
>({
|
|
137
136
|
sqlStorage,
|
|
138
137
|
metadata,
|
|
139
138
|
}: {
|
|
140
|
-
sqlStorage:
|
|
139
|
+
sqlStorage: CfTypes.SqlStorage
|
|
141
140
|
metadata: TMetadata
|
|
142
141
|
}): SqliteDb<TMetadata> => {
|
|
143
142
|
const preparedStmts: PreparedStatement[] = []
|
|
@@ -149,7 +148,7 @@ export const makeSqliteDb_ = <
|
|
|
149
148
|
metadata,
|
|
150
149
|
debug: {
|
|
151
150
|
// Setting initially to root but will be set to correct value shortly after
|
|
152
|
-
head: EventSequenceNumber.ROOT,
|
|
151
|
+
head: EventSequenceNumber.Client.ROOT,
|
|
153
152
|
},
|
|
154
153
|
prepare: (queryStr) => {
|
|
155
154
|
try {
|
|
@@ -188,18 +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
|
-
|
|
197
|
-
// if (metadata._tag === 'opfs') {
|
|
198
|
-
// metadata.vfs.resetAccessHandle(metadata.fileName)
|
|
199
|
-
// }
|
|
200
194
|
},
|
|
201
195
|
close: () => {
|
|
202
|
-
if (isClosed) {
|
|
196
|
+
if (isClosed === true) {
|
|
203
197
|
return
|
|
204
198
|
}
|
|
205
199
|
|
|
@@ -259,3 +253,40 @@ export const makeSqliteDb_ = <
|
|
|
259
253
|
|
|
260
254
|
return sqliteDb
|
|
261
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'
|
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'
|