@livestore/adapter-expo 0.0.0-snapshot-a953343ad2d7468c6573bcb5e26f0eab4302078f

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/src/common.ts ADDED
@@ -0,0 +1,115 @@
1
+ import type { PreparedStatement, SqliteDb } from '@livestore/common'
2
+ import { base64, shouldNeverHappen } from '@livestore/utils'
3
+ import { Effect } from '@livestore/utils/effect'
4
+ import * as ExpoFS from 'expo-file-system'
5
+ import type * as SQLite from 'expo-sqlite'
6
+
7
+ export const makeSqliteDb = (db: SQLite.SQLiteDatabase): SqliteDb => {
8
+ const stmts: PreparedStatement[] = []
9
+
10
+ const sqliteDb: SqliteDb<any> = {
11
+ metadata: { fileName: db.databasePath },
12
+ _tag: 'SqliteDb',
13
+ prepare: (queryStr) => {
14
+ try {
15
+ const dbStmt = db.prepareSync(queryStr)
16
+ const stmt = {
17
+ execute: (bindValues) => {
18
+ // console.log('execute', queryStr, bindValues)
19
+ const res = dbStmt.executeSync(bindValues ?? ([] as any))
20
+ res.resetSync()
21
+ return () => res.changes
22
+ },
23
+ select: (bindValues) => {
24
+ const res = dbStmt.executeSync(bindValues ?? ([] as any))
25
+ try {
26
+ return res.getAllSync() as any
27
+ } finally {
28
+ res.resetSync()
29
+ }
30
+ },
31
+ finalize: () => dbStmt.finalizeSync(),
32
+ sql: queryStr,
33
+ } satisfies PreparedStatement
34
+ stmts.push(stmt)
35
+ return stmt
36
+ } catch (e) {
37
+ console.error(`Error preparing statement: ${queryStr}`, e)
38
+ return shouldNeverHappen(`Error preparing statement: ${queryStr}`)
39
+ }
40
+ },
41
+ execute: (queryStr, bindValues) => {
42
+ const stmt = db.prepareSync(queryStr)
43
+ try {
44
+ const res = stmt.executeSync(bindValues ?? ([] as any))
45
+ return () => res.changes
46
+ } finally {
47
+ stmt.finalizeSync()
48
+ }
49
+ },
50
+ export: () => {
51
+ return db.serializeSync()
52
+ },
53
+ select: (queryStr, bindValues) => {
54
+ const stmt = sqliteDb.prepare(queryStr)
55
+ const res = stmt.select(bindValues)
56
+ stmt.finalize()
57
+ return res as any
58
+ },
59
+ // TODO
60
+ destroy: () => {},
61
+ close: () => {
62
+ for (const stmt of stmts) {
63
+ stmt.finalize()
64
+ }
65
+ return db.closeSync()
66
+ },
67
+ import: () => {
68
+ throw new Error('Not implemented')
69
+ // TODO properly implement this as it seems to require importing to a temporary in-memory db,
70
+ // save it to a file, and then reopen the DB from that file? (see `overwriteDbFile` below)
71
+ },
72
+ session: () => {
73
+ return {
74
+ changeset: () => new Uint8Array(),
75
+ finish: () => {},
76
+ }
77
+ },
78
+ makeChangeset: (data) => {
79
+ return {
80
+ invert: () => {
81
+ return sqliteDb.makeChangeset(data)
82
+ },
83
+ apply: () => {
84
+ // TODO
85
+ },
86
+ }
87
+ },
88
+ } satisfies SqliteDb
89
+
90
+ return sqliteDb
91
+ }
92
+
93
+ export type DbPairRef = {
94
+ current:
95
+ | {
96
+ db: SQLite.SQLiteDatabase
97
+ sqliteDb: SqliteDb
98
+ }
99
+ | undefined
100
+ }
101
+
102
+ export const getDbFilePath = (dbName: string) => {
103
+ return `${ExpoFS.documentDirectory}SQLite/${dbName}`
104
+ }
105
+
106
+ export const overwriteDbFile = (dbName: string, data: Uint8Array) =>
107
+ Effect.gen(function* () {
108
+ const path = getDbFilePath(dbName)
109
+
110
+ yield* Effect.promise(() => ExpoFS.deleteAsync(path, { idempotent: true }))
111
+
112
+ // TODO avoid converting to string once the ExpoFS API supports binary data
113
+ const b64String = base64.encode(data)
114
+ yield* Effect.promise(() => ExpoFS.writeAsStringAsync(path, b64String, { encoding: ExpoFS.EncodingType.Base64 }))
115
+ })
@@ -0,0 +1,292 @@
1
+ // @ts-nocheck
2
+ import type { ClientSession, ConnectDevtoolsToStore } from '@livestore/common'
3
+ import {
4
+ Devtools,
5
+ IntentionalShutdownCause,
6
+ liveStoreVersion,
7
+ MUTATION_LOG_META_TABLE,
8
+ SCHEMA_META_TABLE,
9
+ SCHEMA_MUTATIONS_META_TABLE,
10
+ UnexpectedError,
11
+ } from '@livestore/common'
12
+ import type { PullQueueItem } from '@livestore/common/leader-thread'
13
+ import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
14
+ import { makeExpoDevtoolsChannel } from '@livestore/devtools-expo-common/web-channel'
15
+ import type { ParseResult, Scope } from '@livestore/utils/effect'
16
+ import { Cause, Effect, Queue, Schema, Stream, SubscriptionRef, WebChannel } from '@livestore/utils/effect'
17
+ import * as SQLite from 'expo-sqlite'
18
+
19
+ import type { DbPairRef } from './common.js'
20
+ import { makeSqliteDb, overwriteDbFile } from './common.js'
21
+
22
+ export type BootedDevtools = {
23
+ onMutation: ({
24
+ mutationEventEncoded,
25
+ }: {
26
+ mutationEventEncoded: MutationEvent.AnyEncoded
27
+ }) => Effect.Effect<void, UnexpectedError, never>
28
+ }
29
+
30
+ export const bootDevtools = ({
31
+ connectDevtoolsToStore,
32
+ clientSession,
33
+ schema,
34
+ shutdown,
35
+ dbRef,
36
+ dbMutationLogRef,
37
+ incomingSyncMutationsQueue,
38
+ }: {
39
+ connectDevtoolsToStore: ConnectDevtoolsToStore
40
+ clientSession: ClientSession
41
+ schema: LiveStoreSchema
42
+ dbRef: DbPairRef
43
+ dbMutationLogRef: DbPairRef
44
+ shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => Effect.Effect<void>
45
+ incomingSyncMutationsQueue: Queue.Queue<PullQueueItem>
46
+ }): Effect.Effect<BootedDevtools, UnexpectedError | ParseResult.ParseError, Scope.Scope> =>
47
+ Effect.gen(function* () {
48
+ const appHostId = 'expo'
49
+ const isLeader = true
50
+
51
+ const expoDevtoolsChannel = yield* makeExpoDevtoolsChannel({
52
+ listenSchema: Schema.Union(Devtools.MessageToApp, Devtools.MessageToApp),
53
+ sendSchema: Schema.Union(Devtools.MessageFromApp, Devtools.MessageFromApp),
54
+ })
55
+
56
+ const isConnected = yield* SubscriptionRef.make(false)
57
+
58
+ /**
59
+ * Used to forward messages from `expoDevtoolsChannel` to a "filtered" `storeDevtoolsChannel`
60
+ * which is expected by the `connectDevtoolsToStore` function.
61
+ */
62
+ const storeDevtoolsChannelProxy = yield* WebChannel.queueChannelProxy({
63
+ schema: { listen: Devtools.MessageToApp, send: Devtools.MessageFromApp },
64
+ })
65
+
66
+ yield* storeDevtoolsChannelProxy.sendQueue.pipe(
67
+ Stream.fromQueue,
68
+ Stream.tap((msg) => expoDevtoolsChannel.send(msg)),
69
+ Stream.runDrain,
70
+ Effect.forkScoped,
71
+ )
72
+
73
+ const getDatabaseName = (db: DbPairRef) =>
74
+ db.current!.db.databasePath.slice(db.current!.db.databasePath.lastIndexOf('/') + 1)
75
+
76
+ yield* expoDevtoolsChannel.listen.pipe(
77
+ Stream.flatten(),
78
+ Stream.tap((decodedEvent) =>
79
+ Effect.gen(function* () {
80
+ if (Schema.is(Devtools.MessageToApp)(decodedEvent)) {
81
+ yield* storeDevtoolsChannelProxy.listenQueue.pipe(Queue.offer(decodedEvent))
82
+ return
83
+ }
84
+
85
+ // if (decodedEvent._tag === 'LSD.DevtoolsReady') {
86
+ // if ((yield* isConnected.get) === false) {
87
+ // // yield* expoDevtoolsChannel.send(Devtools.AppHostReady.make({ appHostId, liveStoreVersion, isLeader }))
88
+ // }
89
+
90
+ // return
91
+ // }
92
+
93
+ // if (decodedEvent._tag === 'LSD.DevtoolsConnected') {
94
+ // if (yield* isConnected.get) {
95
+ // console.warn('devtools already connected')
96
+ // return
97
+ // }
98
+
99
+ // yield* connectDevtoolsToStore(storeDevtoolsChannelProxy.webChannel).pipe(
100
+ // Effect.tapCauseLogPretty,
101
+ // Effect.forkScoped,
102
+ // )
103
+
104
+ // yield* SubscriptionRef.set(isConnected, true)
105
+ // return
106
+ // }
107
+
108
+ // if (decodedEvent._tag === 'LSD.Disconnect') {
109
+ // yield* SubscriptionRef.set(isConnected, false)
110
+
111
+ // // yield* disconnect
112
+
113
+ // // TODO is there a better place for this?
114
+ // yield* expoDevtoolsChannel.send(Devtools.AppHostReady.make({ appHostId, liveStoreVersion, isLeader }))
115
+
116
+ // return
117
+ // }
118
+
119
+ const { requestId } = decodedEvent
120
+ const reqPayload = { requestId, appHostId, liveStoreVersion }
121
+
122
+ switch (decodedEvent._tag) {
123
+ case 'LSD.Ping': {
124
+ yield* expoDevtoolsChannel.send(Devtools.Pong.make({ ...reqPayload }))
125
+ return
126
+ }
127
+ case 'LSD.Leader.SnapshotReq': {
128
+ const data = yield* clientSession.leaderThread.export
129
+
130
+ yield* expoDevtoolsChannel.send(Devtools.SnapshotRes.make({ snapshot: data!, ...reqPayload }))
131
+
132
+ return
133
+ }
134
+ case 'LSD.Leader.LoadDatabaseFileReq': {
135
+ const { data } = decodedEvent
136
+
137
+ let tableNames: Set<string>
138
+
139
+ try {
140
+ const tmpExpoDb = SQLite.deserializeDatabaseSync(data)
141
+ const tmpDb = makeSqliteDb(tmpExpoDb)
142
+ const tableNameResults = tmpDb.select<{ name: string }>(
143
+ `select name from sqlite_master where type = 'table'`,
144
+ )
145
+
146
+ tableNames = new Set(tableNameResults.map((_) => _.name))
147
+
148
+ tmpExpoDb.closeSync()
149
+ } catch (e) {
150
+ yield* expoDevtoolsChannel.send(
151
+ Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-file' }),
152
+ )
153
+
154
+ console.error(e)
155
+
156
+ return
157
+ }
158
+
159
+ if (tableNames.has(MUTATION_LOG_META_TABLE)) {
160
+ // yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
161
+
162
+ dbMutationLogRef.current!.db.closeSync()
163
+
164
+ yield* overwriteDbFile(getDatabaseName(dbMutationLogRef), data)
165
+
166
+ dbMutationLogRef.current = undefined
167
+
168
+ dbRef.current!.db.closeSync()
169
+ SQLite.deleteDatabaseSync(getDatabaseName(dbRef))
170
+ } else if (tableNames.has(SCHEMA_META_TABLE) && tableNames.has(SCHEMA_MUTATIONS_META_TABLE)) {
171
+ // yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
172
+
173
+ // yield* db.import(data)
174
+
175
+ dbRef.current!.db.closeSync()
176
+
177
+ yield* overwriteDbFile(getDatabaseName(dbRef), data)
178
+ } else {
179
+ yield* expoDevtoolsChannel.send(
180
+ Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-database' }),
181
+ )
182
+ return
183
+ }
184
+
185
+ yield* expoDevtoolsChannel.send(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'ok' }))
186
+
187
+ yield* shutdown(Cause.fail(IntentionalShutdownCause.make({ reason: 'devtools-import' })))
188
+
189
+ return
190
+ }
191
+ case 'LSD.Leader.ResetAllDataReq': {
192
+ const { mode } = decodedEvent
193
+
194
+ dbRef.current!.db.closeSync()
195
+ SQLite.deleteDatabaseSync(getDatabaseName(dbRef))
196
+
197
+ if (mode === 'all-data') {
198
+ dbMutationLogRef.current!.db.closeSync()
199
+ SQLite.deleteDatabaseSync(getDatabaseName(dbMutationLogRef))
200
+ }
201
+
202
+ yield* expoDevtoolsChannel.send(Devtools.ResetAllDataRes.make({ ...reqPayload }))
203
+
204
+ yield* shutdown(Cause.fail(IntentionalShutdownCause.make({ reason: 'devtools-reset' })))
205
+
206
+ return
207
+ }
208
+ case 'LSD.Leader.DatabaseFileInfoReq': {
209
+ const dbSizeQuery = `SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();`
210
+ const dbFileSize = dbRef.current!.db.prepareSync(dbSizeQuery).executeSync<any>().getFirstSync()!
211
+ .size as number
212
+ const mutationLogFileSize = dbMutationLogRef
213
+ .current!.db.prepareSync(dbSizeQuery)
214
+ .executeSync<any>()
215
+ .getFirstSync()!.size as number
216
+
217
+ yield* expoDevtoolsChannel.send(
218
+ Devtools.DatabaseFileInfoRes.make({
219
+ readModel: { fileSize: dbFileSize, persistenceInfo: { fileName: 'livestore.db' } },
220
+ mutationLog: {
221
+ fileSize: mutationLogFileSize,
222
+ persistenceInfo: { fileName: 'livestore-mutationlog.db' },
223
+ },
224
+ ...reqPayload,
225
+ }),
226
+ )
227
+
228
+ return
229
+ }
230
+ case 'LSD.Leader.MutationLogReq': {
231
+ const mutationLog = yield* clientSession.leaderThread.getMutationLogData
232
+
233
+ yield* expoDevtoolsChannel.send(Devtools.MutationLogRes.make({ mutationLog, ...reqPayload }))
234
+
235
+ return
236
+ }
237
+ case 'LSD.Leader.RunMutationReq': {
238
+ const { mutationEventEncoded: mutationEventEncoded_ } = decodedEvent
239
+ const mutationDef = schema.mutations.get(mutationEventEncoded_.mutation)!
240
+ // const nextMutationEventIdPair = clientSession.mutations.nextMutationEventIdPair({
241
+ // clientOnly: mutationDef.options.clientOnly,
242
+ // })
243
+
244
+ // const mutationEventEncoded = new MutationEvent.EncodedWithMeta({
245
+ // ...mutationEventEncoded_,
246
+ // // ...nextMutationEventIdPair,
247
+ // })
248
+
249
+ // const mutationEventDecoded = yield* Schema.decode(mutationEventSchema)(mutationEventEncoded)
250
+ // yield* Queue.offer(incomingSyncMutationsQueue, {
251
+ // payload: { _tag: 'upstream-advance', newEvents: [mutationEventEncoded] },
252
+ // remaining: 0,
253
+ // })
254
+
255
+ // const mutationDef =
256
+ // schema.mutations.get(mutationEventEncoded.mutation) ??
257
+ // shouldNeverHappen(`Unknown mutation: ${mutationEventEncoded.mutation}`)
258
+
259
+ // yield* clientSession.mutations.push([mutationEventEncoded])
260
+
261
+ yield* expoDevtoolsChannel.send(Devtools.RunMutationRes.make({ ...reqPayload }))
262
+
263
+ return
264
+ }
265
+ case 'LSD.Leader.SyncingInfoReq': {
266
+ const syncingInfo = Devtools.SyncingInfo.make({
267
+ enabled: false,
268
+ metadata: {},
269
+ })
270
+
271
+ yield* expoDevtoolsChannel.send(Devtools.SyncingInfoRes.make({ syncingInfo, ...reqPayload }))
272
+
273
+ return
274
+ }
275
+ }
276
+ }),
277
+ ),
278
+ Stream.runDrain,
279
+ Effect.tapCauseLogPretty,
280
+ Effect.forkScoped,
281
+ )
282
+ // yield* expoDevtoolsChannel.send(Devtools.AppHostReady.make({ appHostId, isLeader, liveStoreVersion }))
283
+
284
+ const onMutation = ({ mutationEventEncoded }: { mutationEventEncoded: MutationEvent.AnyEncoded }) =>
285
+ expoDevtoolsChannel
286
+ .send(Devtools.MutationBroadcast.make({ mutationEventEncoded, liveStoreVersion }))
287
+ .pipe(UnexpectedError.mapToUnexpectedError)
288
+
289
+ return {
290
+ onMutation,
291
+ }
292
+ }).pipe(Effect.withSpan('@livestore/adapter-expo:bootDevtools'))
package/src/index.ts ADDED
@@ -0,0 +1,248 @@
1
+ import type { Adapter, ClientSession, LockStatus, PreparedBindValues } from '@livestore/common'
2
+ import {
3
+ getExecArgsFromMutation,
4
+ initializeSingletonTables,
5
+ liveStoreStorageFormatVersion,
6
+ migrateDb,
7
+ migrateTable,
8
+ rehydrateFromMutationLog,
9
+ sql,
10
+ UnexpectedError,
11
+ } from '@livestore/common'
12
+ import type { PullQueueItem } from '@livestore/common/leader-thread'
13
+ import type { MutationLogMetaRow } from '@livestore/common/schema'
14
+ import { EventId, MUTATION_LOG_META_TABLE, MutationEvent, mutationLogMetaTable } from '@livestore/common/schema'
15
+ import { insertRowPrepared, makeBindValues } from '@livestore/common/sql-queries'
16
+ import { casesHandled, shouldNeverHappen } from '@livestore/utils'
17
+ import { Effect, Option, Queue, Schema, Stream, SubscriptionRef } from '@livestore/utils/effect'
18
+ import * as SQLite from 'expo-sqlite'
19
+
20
+ import { makeSqliteDb } from './common.js'
21
+ import type { BootedDevtools } from './devtools.js'
22
+ import { bootDevtools } from './devtools.js'
23
+
24
+ export type MakeDbOptions = {
25
+ fileNamePrefix?: string
26
+ subDirectory?: string
27
+ // syncBackend?: TODO
28
+ }
29
+
30
+ // TODO refactor with leader-thread code from `@livestore/common/leader-thread`
31
+ export const makeAdapter =
32
+ (options?: MakeDbOptions): Adapter =>
33
+ ({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled }) =>
34
+ Effect.gen(function* () {
35
+ const { fileNamePrefix, subDirectory } = options ?? {}
36
+ const migrationOptions = schema.migrationOptions
37
+ const subDirectoryPath = subDirectory ? subDirectory.replace(/\/$/, '') + '/' : ''
38
+ const fullDbFilePath = `${subDirectoryPath}${fileNamePrefix ?? 'livestore-'}${schema.hash}@${liveStoreStorageFormatVersion}.db`
39
+ const db = SQLite.openDatabaseSync(fullDbFilePath)
40
+
41
+ const dbRef = { current: { db, sqliteDb: makeSqliteDb(db) } }
42
+
43
+ const dbWasEmptyWhenOpened = dbRef.current.sqliteDb.select('SELECT 1 FROM sqlite_master').length === 0
44
+
45
+ const dbMutationLog = SQLite.openDatabaseSync(
46
+ `${subDirectory ?? ''}${fileNamePrefix ?? 'livestore-'}mutationlog@${liveStoreStorageFormatVersion}.db`,
47
+ )
48
+
49
+ const dbMutationLogRef = { current: { db: dbMutationLog, sqliteDb: makeSqliteDb(dbMutationLog) } }
50
+
51
+ const dbMutationLogWasEmptyWhenOpened =
52
+ dbMutationLogRef.current.sqliteDb.select('SELECT 1 FROM sqlite_master').length === 0
53
+
54
+ yield* Effect.addFinalizer(() =>
55
+ Effect.gen(function* () {
56
+ // Ignoring in case the database is already closed
57
+ yield* Effect.try(() => db.closeSync()).pipe(Effect.ignore)
58
+ yield* Effect.try(() => dbMutationLog.closeSync()).pipe(Effect.ignore)
59
+ }),
60
+ )
61
+
62
+ if (dbMutationLogWasEmptyWhenOpened) {
63
+ yield* migrateTable({
64
+ db: dbMutationLogRef.current.sqliteDb,
65
+ behaviour: 'create-if-not-exists',
66
+ tableAst: mutationLogMetaTable.sqliteDef.ast,
67
+ skipMetaTable: true,
68
+ })
69
+ }
70
+
71
+ if (dbWasEmptyWhenOpened) {
72
+ yield* migrateDb({ db: dbRef.current.sqliteDb, schema })
73
+
74
+ initializeSingletonTables(schema, dbRef.current.sqliteDb)
75
+
76
+ switch (migrationOptions.strategy) {
77
+ case 'from-mutation-log': {
78
+ // TODO bring back
79
+ // yield* rehydrateFromMutationLog({
80
+ // db: dbRef.current.sqliteDb,
81
+ // logDb: dbMutationLogRef.current.sqliteDb,
82
+ // schema,
83
+ // migrationOptions,
84
+ // onProgress: () => Effect.void,
85
+ // })
86
+
87
+ break
88
+ }
89
+ case 'hard-reset': {
90
+ // This is already the case by note doing anything now
91
+
92
+ break
93
+ }
94
+ case 'manual': {
95
+ // const migrateFn = migrationStrategy.migrate
96
+ console.warn('Manual migration strategy not implemented yet')
97
+
98
+ // TODO figure out a way to get previous database file to pass to the migration function
99
+
100
+ break
101
+ }
102
+ default: {
103
+ casesHandled(migrationOptions)
104
+ }
105
+ }
106
+ }
107
+
108
+ const mutationLogExclude =
109
+ migrationOptions.strategy === 'from-mutation-log'
110
+ ? (migrationOptions.excludeMutations ?? new Set(['livestore.RawSql']))
111
+ : new Set(['livestore.RawSql'])
112
+
113
+ const mutationEventSchema = MutationEvent.makeMutationEventSchema(schema)
114
+ const mutationDefSchemaHashMap = new Map(
115
+ [...schema.mutations.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
116
+ )
117
+
118
+ const newMutationLogStmt = dbMutationLogRef.current.sqliteDb.prepare(
119
+ insertRowPrepared({ tableName: MUTATION_LOG_META_TABLE, columns: mutationLogMetaTable.sqliteDef.columns }),
120
+ )
121
+
122
+ const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
123
+
124
+ const incomingSyncMutationsQueue = yield* Queue.unbounded<PullQueueItem>().pipe(
125
+ Effect.acquireRelease(Queue.shutdown),
126
+ )
127
+
128
+ const initialMutationEventIdSchema = mutationLogMetaTable.schema.pipe(
129
+ Schema.pick('idGlobal', 'idClient'),
130
+ Schema.transform(EventId.EventId, {
131
+ encode: (_) => ({ idGlobal: _.global, idClient: _.client }),
132
+ decode: (_) => EventId.make({ global: _.idGlobal, client: _.idClient }),
133
+ strict: false,
134
+ }),
135
+ Schema.Array,
136
+ Schema.headOrElse(() => EventId.make({ global: 0, client: 0 })),
137
+ )
138
+
139
+ const initialMutationEventId = yield* Schema.decode(initialMutationEventIdSchema)(
140
+ dbMutationLogRef.current.sqliteDb.select(
141
+ sql`SELECT idGlobal, idClient FROM ${MUTATION_LOG_META_TABLE} ORDER BY idGlobal DESC, idClient DESC LIMIT 1`,
142
+ ),
143
+ )
144
+
145
+ let devtools: BootedDevtools | undefined
146
+
147
+ const clientSession = {
148
+ devtools: { enabled: false },
149
+ lockStatus,
150
+ // Expo doesn't support multiple client sessions, so we just use a fixed session id
151
+ clientId: 'expo',
152
+ sessionId: 'expo',
153
+ leaderThread: {
154
+ mutations: {
155
+ pull: Stream.fromQueue(incomingSyncMutationsQueue),
156
+ push: (batch): Effect.Effect<void, UnexpectedError> =>
157
+ Effect.gen(function* () {
158
+ for (const mutationEventEncoded of batch) {
159
+ if (migrationOptions.strategy !== 'from-mutation-log') return
160
+
161
+ const mutation = mutationEventEncoded.mutation
162
+ const mutationDef =
163
+ schema.mutations.get(mutation) ?? shouldNeverHappen(`Unknown mutation: ${mutation}`)
164
+
165
+ const execArgsArr = getExecArgsFromMutation({
166
+ mutationDef,
167
+ mutationEvent: { decoded: undefined, encoded: mutationEventEncoded },
168
+ })
169
+
170
+ // write to mutation_log
171
+ if (
172
+ mutationLogExclude.has(mutation) === false &&
173
+ execArgsArr.some((_) => _.statementSql.includes('__livestore')) === false
174
+ ) {
175
+ const mutationDefSchemaHash =
176
+ mutationDefSchemaHashMap.get(mutation) ?? shouldNeverHappen(`Unknown mutation: ${mutation}`)
177
+
178
+ const argsJson = JSON.stringify(mutationEventEncoded.args ?? {})
179
+ const mutationLogRowValues = {
180
+ idGlobal: mutationEventEncoded.id.global,
181
+ idClient: mutationEventEncoded.id.client,
182
+ mutation: mutationEventEncoded.mutation,
183
+ argsJson,
184
+ schemaHash: mutationDefSchemaHash,
185
+ syncMetadataJson: Option.none(),
186
+ parentIdGlobal: mutationEventEncoded.parentId.global,
187
+ parentIdClient: mutationEventEncoded.parentId.client,
188
+ } satisfies MutationLogMetaRow
189
+
190
+ try {
191
+ newMutationLogStmt.execute(
192
+ makeBindValues({
193
+ columns: mutationLogMetaTable.sqliteDef.columns,
194
+ values: mutationLogRowValues,
195
+ variablePrefix: '$',
196
+ }) as PreparedBindValues,
197
+ )
198
+ } catch (e) {
199
+ console.error('Error writing to mutation_log', e, mutationLogRowValues)
200
+ debugger
201
+ throw e
202
+ }
203
+ } else {
204
+ // console.debug('livestore-webworker: skipping mutation log write', mutation, statementSql, bindValues)
205
+ }
206
+
207
+ yield* devtools?.onMutation({ mutationEventEncoded }) ?? Effect.void
208
+ }
209
+ }),
210
+ },
211
+ initialState: {
212
+ migrationsReport: {
213
+ migrations: [],
214
+ },
215
+ leaderHead: initialMutationEventId,
216
+ },
217
+ export: Effect.sync(() => dbRef.current.sqliteDb.export()),
218
+ getMutationLogData: Effect.sync(() => dbMutationLogRef.current.sqliteDb.export()),
219
+ networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
220
+ Effect.runSync,
221
+ ),
222
+ sendDevtoolsMessage: () => Effect.dieMessage('Not implemented'),
223
+ getSyncState: Effect.dieMessage('Not implemented'),
224
+ },
225
+ shutdown: () => Effect.dieMessage('TODO implement shutdown'),
226
+ sqliteDb: dbRef.current.sqliteDb,
227
+ } satisfies ClientSession
228
+
229
+ if (devtoolsEnabled) {
230
+ devtools = yield* bootDevtools({
231
+ connectDevtoolsToStore,
232
+ clientSession,
233
+ schema,
234
+ dbRef,
235
+ dbMutationLogRef,
236
+ shutdown,
237
+ incomingSyncMutationsQueue,
238
+ }).pipe(
239
+ Effect.tapCauseLogPretty,
240
+ Effect.catchAll(() => Effect.succeed(undefined)),
241
+ )
242
+ }
243
+
244
+ return clientSession
245
+ }).pipe(
246
+ Effect.mapError((cause) => (cause._tag === 'LiveStore.UnexpectedError' ? cause : new UnexpectedError({ cause }))),
247
+ Effect.tapCauseLogPretty,
248
+ )
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "tsBuildInfoFile": "./dist/.tsbuildinfo"
7
+ },
8
+ "include": ["./src"],
9
+ "references": [{ "path": "../common" } ,{ "path": "../utils" }, { "path": "../devtools-expo-common" }]
10
+ }