@livestore/adapter-expo 0.3.0-dev.18 → 0.3.0-dev.19

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/index.ts CHANGED
@@ -1,251 +1,102 @@
1
- import type { Adapter, ClientSession, LockStatus, PreparedBindValues } from '@livestore/common'
1
+ import type { Adapter, ClientSession, ClientSessionLeaderThreadProxy, LockStatus, SyncOptions } from '@livestore/common'
2
+ import { Devtools, liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
3
+ import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
4
+ import { getClientHeadFromDb, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
5
+ import type { LiveStoreSchema } from '@livestore/common/schema'
6
+ import { MutationEvent } from '@livestore/common/schema'
2
7
  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 {
15
- EventId,
16
- getMutationDef,
17
- MUTATION_LOG_META_TABLE,
18
- MutationEvent,
19
- mutationLogMetaTable,
20
- } from '@livestore/common/schema'
21
- import { insertRowPrepared, makeBindValues } from '@livestore/common/sql-queries'
22
- import { casesHandled, shouldNeverHappen } from '@livestore/utils'
23
- import { Effect, Option, Queue, Schema, Stream, SubscriptionRef } from '@livestore/utils/effect'
8
+ makeChannelForConnectedMeshNode,
9
+ makeExpoDevtoolsConnectedMeshNode,
10
+ } from '@livestore/devtools-expo-common/web-channel'
11
+ import type { Scope } from '@livestore/utils/effect'
12
+ import { Cause, Effect, FetchHttpClient, Layer, Stream, SubscriptionRef } from '@livestore/utils/effect'
13
+ import type { MeshNode } from '@livestore/webmesh'
24
14
  import * as SQLite from 'expo-sqlite'
25
15
 
26
- import { makeSqliteDb } from './common.js'
27
- import type { BootedDevtools } from './devtools.js'
28
- import { bootDevtools } from './devtools.js'
16
+ import type { MakeExpoSqliteDb } from './make-sqlite-db.js'
17
+ import { makeSqliteDb } from './make-sqlite-db.js'
18
+ import { makeShutdownChannel } from './shutdown-channel.js'
29
19
 
30
20
  export type MakeDbOptions = {
31
- fileNamePrefix?: string
32
- subDirectory?: string
21
+ sync?: SyncOptions
22
+ storage?: {
23
+ /**
24
+ * Relative to expo-sqlite's default directory
25
+ *
26
+ * Example of a resulting path for `subDirectory: 'my-app'`:
27
+ * `/data/Containers/Data/Application/<APP_UUID>/Documents/ExponentExperienceData/@<USERNAME>/<APPNAME>/SQLite/my-app/<STORE_ID>/livestore-mutationlog@3.db`
28
+ */
29
+ subDirectory?: string
30
+ }
33
31
  // syncBackend?: TODO
32
+ /** @default 'expo' */
33
+ clientId?: string
34
+ /** @default 'expo' */
35
+ sessionId?: string
34
36
  }
35
37
 
36
38
  // TODO refactor with leader-thread code from `@livestore/common/leader-thread`
37
39
  export const makeAdapter =
38
- (options?: MakeDbOptions): Adapter =>
39
- ({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled }) =>
40
+ (options: MakeDbOptions = {}): Adapter =>
41
+ ({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled, storeId, bootStatusQueue, debugInstanceId }) =>
40
42
  Effect.gen(function* () {
41
- const { fileNamePrefix, subDirectory } = options ?? {}
42
- const migrationOptions = schema.migrationOptions
43
- const subDirectoryPath = subDirectory ? subDirectory.replace(/\/$/, '') + '/' : ''
44
- const fullDbFilePath = `${subDirectoryPath}${fileNamePrefix ?? 'livestore-'}${schema.hash}@${liveStoreStorageFormatVersion}.db`
45
- const db = SQLite.openDatabaseSync(fullDbFilePath)
46
-
47
- const dbRef = { current: { db, sqliteDb: makeSqliteDb(db) } }
48
-
49
- const dbWasEmptyWhenOpened = dbRef.current.sqliteDb.select('SELECT 1 FROM sqlite_master').length === 0
50
-
51
- const dbMutationLog = SQLite.openDatabaseSync(
52
- `${subDirectory ?? ''}${fileNamePrefix ?? 'livestore-'}mutationlog@${liveStoreStorageFormatVersion}.db`,
53
- )
54
-
55
- const dbMutationLogRef = { current: { db: dbMutationLog, sqliteDb: makeSqliteDb(dbMutationLog) } }
56
-
57
- const dbMutationLogWasEmptyWhenOpened =
58
- dbMutationLogRef.current.sqliteDb.select('SELECT 1 FROM sqlite_master').length === 0
59
-
60
- yield* Effect.addFinalizer(() =>
61
- Effect.gen(function* () {
62
- // Ignoring in case the database is already closed
63
- yield* Effect.try(() => db.closeSync()).pipe(Effect.ignore)
64
- yield* Effect.try(() => dbMutationLog.closeSync()).pipe(Effect.ignore)
65
- }),
66
- )
67
-
68
- if (dbMutationLogWasEmptyWhenOpened) {
69
- yield* migrateTable({
70
- db: dbMutationLogRef.current.sqliteDb,
71
- behaviour: 'create-if-not-exists',
72
- tableAst: mutationLogMetaTable.sqliteDef.ast,
73
- skipMetaTable: true,
74
- })
75
- }
76
-
77
- if (dbWasEmptyWhenOpened) {
78
- yield* migrateDb({ db: dbRef.current.sqliteDb, schema })
79
-
80
- initializeSingletonTables(schema, dbRef.current.sqliteDb)
81
-
82
- switch (migrationOptions.strategy) {
83
- case 'from-mutation-log': {
84
- // TODO bring back
85
- // yield* rehydrateFromMutationLog({
86
- // db: dbRef.current.sqliteDb,
87
- // logDb: dbMutationLogRef.current.sqliteDb,
88
- // schema,
89
- // migrationOptions,
90
- // onProgress: () => Effect.void,
91
- // })
92
-
93
- break
94
- }
95
- case 'hard-reset': {
96
- // This is already the case by note doing anything now
97
-
98
- break
99
- }
100
- case 'manual': {
101
- // const migrateFn = migrationStrategy.migrate
102
- console.warn('Manual migration strategy not implemented yet')
103
-
104
- // TODO figure out a way to get previous database file to pass to the migration function
105
-
106
- break
107
- }
108
- default: {
109
- casesHandled(migrationOptions)
110
- }
111
- }
112
- }
113
-
114
- const mutationLogExclude =
115
- migrationOptions.strategy === 'from-mutation-log'
116
- ? (migrationOptions.excludeMutations ?? new Set(['livestore.RawSql']))
117
- : new Set(['livestore.RawSql'])
118
-
119
- const mutationEventSchema = MutationEvent.makeMutationEventSchema(schema)
120
- const mutationDefSchemaHashMap = new Map(
121
- [...schema.mutations.map.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
122
- )
123
-
124
- const newMutationLogStmt = dbMutationLogRef.current.sqliteDb.prepare(
125
- insertRowPrepared({ tableName: MUTATION_LOG_META_TABLE, columns: mutationLogMetaTable.sqliteDef.columns }),
126
- )
43
+ const { storage, clientId = 'expo', sessionId = 'expo', sync: syncOptions } = options
127
44
 
128
- const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
45
+ const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
129
46
 
130
- const incomingSyncMutationsQueue = yield* Queue.unbounded<PullQueueItem>().pipe(
131
- Effect.acquireRelease(Queue.shutdown),
132
- )
47
+ const shutdownChannel = yield* makeShutdownChannel(storeId)
133
48
 
134
- const initialMutationEventIdSchema = mutationLogMetaTable.schema.pipe(
135
- Schema.pick('idGlobal', 'idClient'),
136
- Schema.transform(EventId.EventId, {
137
- encode: (_) => ({ idGlobal: _.global, idClient: _.client }),
138
- decode: (_) => EventId.make({ global: _.idGlobal, client: _.idClient }),
139
- strict: false,
140
- }),
141
- Schema.Array,
142
- Schema.headOrElse(() => EventId.make({ global: 0, client: 0 })),
143
- )
144
-
145
- const initialMutationEventId = yield* Schema.decode(initialMutationEventIdSchema)(
146
- dbMutationLogRef.current.sqliteDb.select(
147
- sql`SELECT idGlobal, idClient FROM ${MUTATION_LOG_META_TABLE} ORDER BY idGlobal DESC, idClient DESC LIMIT 1`,
148
- ),
49
+ yield* shutdownChannel.listen.pipe(
50
+ Stream.flatten(),
51
+ Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
52
+ Stream.runDrain,
53
+ Effect.interruptible,
54
+ Effect.tapCauseLogPretty,
55
+ Effect.forkScoped,
149
56
  )
150
57
 
151
- let devtools: BootedDevtools | undefined
58
+ const devtoolsWebmeshNode = devtoolsEnabled
59
+ ? yield* makeExpoDevtoolsConnectedMeshNode({
60
+ nodeName: `expo-${storeId}-${clientId}-${sessionId}`,
61
+ target: `devtools-${storeId}-${clientId}-${sessionId}`,
62
+ })
63
+ : undefined
64
+
65
+ const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
66
+ storeId,
67
+ clientId,
68
+ sessionId,
69
+ schema,
70
+ makeSqliteDb,
71
+ syncOptions,
72
+ storage: storage ?? {},
73
+ devtoolsEnabled,
74
+ devtoolsWebmeshNode,
75
+ })
76
+
77
+ const sqliteDb = yield* makeSqliteDb({ _tag: 'in-memory' })
78
+ sqliteDb.import(initialSnapshot)
152
79
 
153
80
  const clientSession = {
154
81
  devtools: { enabled: false },
155
82
  lockStatus,
156
- // Expo doesn't support multiple client sessions, so we just use a fixed session id
157
- clientId: 'expo',
158
- sessionId: 'expo',
159
- leaderThread: {
160
- mutations: {
161
- pull: Stream.fromQueue(incomingSyncMutationsQueue),
162
- push: (batch): Effect.Effect<void, UnexpectedError> =>
163
- Effect.gen(function* () {
164
- for (const mutationEventEncoded of batch) {
165
- if (migrationOptions.strategy !== 'from-mutation-log') return
166
-
167
- const mutation = mutationEventEncoded.mutation
168
- const mutationDef = getMutationDef(schema, mutation)
169
-
170
- const execArgsArr = getExecArgsFromMutation({
171
- mutationDef,
172
- mutationEvent: { decoded: undefined, encoded: mutationEventEncoded },
173
- })
174
-
175
- // write to mutation_log
176
- if (
177
- mutationLogExclude.has(mutation) === false &&
178
- execArgsArr.some((_) => _.statementSql.includes('__livestore')) === false
179
- ) {
180
- const mutationDefSchemaHash =
181
- mutationDefSchemaHashMap.get(mutation) ?? shouldNeverHappen(`Unknown mutation: ${mutation}`)
182
-
183
- const argsJson = JSON.stringify(mutationEventEncoded.args ?? {})
184
- const mutationLogRowValues = {
185
- idGlobal: mutationEventEncoded.id.global,
186
- idClient: mutationEventEncoded.id.client,
187
- mutation: mutationEventEncoded.mutation,
188
- argsJson,
189
- schemaHash: mutationDefSchemaHash,
190
- syncMetadataJson: Option.none(),
191
- parentIdGlobal: mutationEventEncoded.parentId.global,
192
- parentIdClient: mutationEventEncoded.parentId.client,
193
- clientId: 'expo',
194
- sessionId: 'expo',
195
- } satisfies MutationLogMetaRow
196
-
197
- try {
198
- newMutationLogStmt.execute(
199
- makeBindValues({
200
- columns: mutationLogMetaTable.sqliteDef.columns,
201
- values: mutationLogRowValues,
202
- variablePrefix: '$',
203
- }) as PreparedBindValues,
204
- )
205
- } catch (e) {
206
- console.error('Error writing to mutation_log', e, mutationLogRowValues)
207
- debugger
208
- throw e
209
- }
210
- } else {
211
- // console.debug('livestore-webworker: skipping mutation log write', mutation, statementSql, bindValues)
212
- }
213
-
214
- yield* devtools?.onMutation({ mutationEventEncoded }) ?? Effect.void
215
- }
216
- }),
217
- },
218
- initialState: {
219
- migrationsReport: {
220
- migrations: [],
221
- },
222
- leaderHead: initialMutationEventId,
223
- },
224
- export: Effect.sync(() => dbRef.current.sqliteDb.export()),
225
- getMutationLogData: Effect.sync(() => dbMutationLogRef.current.sqliteDb.export()),
226
- networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
227
- Effect.runSync,
228
- ),
229
- sendDevtoolsMessage: () => Effect.dieMessage('Not implemented'),
230
- getSyncState: Effect.dieMessage('Not implemented'),
231
- },
83
+ clientId,
84
+ sessionId,
85
+ leaderThread,
232
86
  shutdown: () => Effect.dieMessage('TODO implement shutdown'),
233
- sqliteDb: dbRef.current.sqliteDb,
87
+ sqliteDb,
234
88
  } satisfies ClientSession
235
89
 
236
90
  if (devtoolsEnabled) {
237
- devtools = yield* bootDevtools({
238
- connectDevtoolsToStore,
239
- clientSession,
240
- schema,
241
- dbRef,
242
- dbMutationLogRef,
243
- shutdown,
244
- incomingSyncMutationsQueue,
245
- }).pipe(
246
- Effect.tapCauseLogPretty,
247
- Effect.catchAll(() => Effect.succeed(undefined)),
248
- )
91
+ yield* Effect.gen(function* () {
92
+ const storeDevtoolsChannel = yield* makeChannelForConnectedMeshNode({
93
+ target: `devtools-${storeId}-${clientId}-${sessionId}`,
94
+ node: devtoolsWebmeshNode!,
95
+ schema: { listen: Devtools.ClientSession.MessageToApp, send: Devtools.ClientSession.MessageFromApp },
96
+ channelType: 'clientSession',
97
+ })
98
+ yield* connectDevtoolsToStore(storeDevtoolsChannel)
99
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
249
100
  }
250
101
 
251
102
  return clientSession
@@ -253,3 +104,151 @@ export const makeAdapter =
253
104
  Effect.mapError((cause) => (cause._tag === 'LiveStore.UnexpectedError' ? cause : new UnexpectedError({ cause }))),
254
105
  Effect.tapCauseLogPretty,
255
106
  )
107
+
108
+ const makeLeaderThread = ({
109
+ storeId,
110
+ clientId,
111
+ sessionId,
112
+ schema,
113
+ makeSqliteDb,
114
+ syncOptions,
115
+ storage,
116
+ devtoolsEnabled,
117
+ devtoolsWebmeshNode,
118
+ }: {
119
+ storeId: string
120
+ clientId: string
121
+ sessionId: string
122
+ schema: LiveStoreSchema
123
+ makeSqliteDb: MakeExpoSqliteDb
124
+ syncOptions: SyncOptions | undefined
125
+ storage: {
126
+ subDirectory?: string
127
+ }
128
+ devtoolsEnabled: boolean
129
+ devtoolsWebmeshNode: MeshNode | undefined
130
+ }) =>
131
+ Effect.gen(function* () {
132
+ const subDirectory = storage.subDirectory ? storage.subDirectory.replace(/\/$/, '') + '/' : ''
133
+ const pathJoin = (...paths: string[]) => paths.join('/').replaceAll(/\/+/g, '/')
134
+ const directory = pathJoin(SQLite.defaultDatabaseDirectory, subDirectory, storeId)
135
+
136
+ const readModelDatabaseName = `${'livestore-'}${schema.hash}@${liveStoreStorageFormatVersion}.db`
137
+ const dbMutationLogPath = `${'livestore-'}mutationlog@${liveStoreStorageFormatVersion}.db`
138
+
139
+ const dbReadModel = yield* makeSqliteDb({ _tag: 'expo', databaseName: readModelDatabaseName, directory })
140
+ const dbMutationLog = yield* makeSqliteDb({ _tag: 'expo', databaseName: dbMutationLogPath, directory })
141
+
142
+ const devtoolsOptions = yield* makeDevtoolsOptions({
143
+ devtoolsEnabled,
144
+ dbReadModel,
145
+ dbMutationLog,
146
+ storeId,
147
+ clientId,
148
+ sessionId,
149
+ devtoolsWebmeshNode,
150
+ })
151
+
152
+ const layer = yield* Layer.memoize(
153
+ makeLeaderThreadLayer({
154
+ clientId,
155
+ dbReadModel,
156
+ dbMutationLog,
157
+ devtoolsOptions,
158
+ makeSqliteDb,
159
+ schema,
160
+ // NOTE we're creating a separate channel here since you can't listen to your own channel messages
161
+ shutdownChannel: yield* makeShutdownChannel(storeId),
162
+ storeId,
163
+ syncOptions,
164
+ }).pipe(Layer.provideMerge(FetchHttpClient.layer)),
165
+ )
166
+
167
+ return yield* Effect.gen(function* () {
168
+ const {
169
+ dbReadModel: db,
170
+ dbMutationLog,
171
+ syncProcessor,
172
+ connectedClientSessionPullQueues,
173
+ extraIncomingMessagesQueue,
174
+ initialState,
175
+ } = yield* LeaderThreadCtx
176
+
177
+ const initialLeaderHead = getClientHeadFromDb(dbMutationLog)
178
+ const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(initialLeaderHead)
179
+
180
+ const leaderThread = {
181
+ mutations: {
182
+ pull: Stream.fromQueue(pullQueue),
183
+ push: (batch) =>
184
+ syncProcessor
185
+ .push(
186
+ batch.map((item) => new MutationEvent.EncodedWithMeta(item)),
187
+ { waitForProcessing: true },
188
+ )
189
+ .pipe(Effect.provide(layer), Effect.scoped),
190
+ },
191
+ initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
192
+ export: Effect.sync(() => db.export()),
193
+ getMutationLogData: Effect.sync(() => dbMutationLog.export()),
194
+ // TODO
195
+ networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
196
+ Effect.runSync,
197
+ ),
198
+ getSyncState: syncProcessor.syncState,
199
+ sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
200
+ } satisfies ClientSessionLeaderThreadProxy
201
+
202
+ const initialSnapshot = db.export()
203
+
204
+ return { leaderThread, initialSnapshot }
205
+ }).pipe(Effect.provide(layer))
206
+ })
207
+
208
+ const makeDevtoolsOptions = ({
209
+ devtoolsEnabled,
210
+ dbReadModel,
211
+ dbMutationLog,
212
+ storeId,
213
+ clientId,
214
+ sessionId,
215
+ devtoolsWebmeshNode,
216
+ }: {
217
+ devtoolsEnabled: boolean
218
+ dbReadModel: LeaderSqliteDb
219
+ dbMutationLog: LeaderSqliteDb
220
+ storeId: string
221
+ clientId: string
222
+ sessionId: string
223
+ devtoolsWebmeshNode: MeshNode | undefined
224
+ }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
225
+ Effect.gen(function* () {
226
+ if (devtoolsEnabled === false) {
227
+ return {
228
+ enabled: false,
229
+ }
230
+ }
231
+
232
+ return {
233
+ enabled: true,
234
+ makeBootContext: Effect.gen(function* () {
235
+ return {
236
+ // devtoolsWebChannel: yield* makeExpoDevtoolsChannel({
237
+ // nodeName: `leader-${storeId}-${clientId}`,
238
+ devtoolsWebChannel: yield* makeChannelForConnectedMeshNode({
239
+ node: devtoolsWebmeshNode!,
240
+ target: `devtools-${storeId}-${clientId}-${sessionId}`,
241
+ schema: {
242
+ listen: Devtools.Leader.MessageToApp,
243
+ send: Devtools.Leader.MessageFromApp,
244
+ },
245
+ channelType: 'leader',
246
+ }),
247
+ persistenceInfo: {
248
+ readModel: dbReadModel.metadata.persistenceInfo,
249
+ mutationLog: dbMutationLog.metadata.persistenceInfo,
250
+ },
251
+ }
252
+ }),
253
+ }
254
+ })
@@ -0,0 +1,167 @@
1
+ import type { MakeSqliteDb, PersistenceInfo, PreparedStatement, SqliteDb } from '@livestore/common'
2
+ import { shouldNeverHappen } from '@livestore/utils'
3
+ import { Effect } from '@livestore/utils/effect'
4
+ import * as SQLite from 'expo-sqlite'
5
+
6
+ type Metadata = {
7
+ _tag: 'expo'
8
+ dbPointer: number
9
+ persistenceInfo: PersistenceInfo
10
+ input: ExpoDatabaseInput
11
+ }
12
+
13
+ type ExpoDatabaseInput =
14
+ | {
15
+ _tag: 'expo'
16
+ databaseName: string
17
+ directory: string
18
+ }
19
+ | {
20
+ _tag: 'in-memory'
21
+ }
22
+
23
+ export type MakeExpoSqliteDb = MakeSqliteDb<Metadata, ExpoDatabaseInput, { _tag: 'expo' } & Metadata>
24
+
25
+ export const makeSqliteDb: MakeExpoSqliteDb = (input: ExpoDatabaseInput) =>
26
+ Effect.gen(function* () {
27
+ // console.log('makeSqliteDb', input)
28
+ if (input._tag === 'in-memory') {
29
+ // const db = SQLite.openDatabaseSync(':memory:')
30
+
31
+ return makeSqliteDb_({
32
+ // db,
33
+ makeDb: () => SQLite.openDatabaseSync(':memory:'),
34
+ metadata: {
35
+ _tag: 'expo',
36
+ dbPointer: 0,
37
+ persistenceInfo: { fileName: ':memory:' },
38
+ input,
39
+ },
40
+ }) as any
41
+ }
42
+
43
+ if (input._tag === 'expo') {
44
+ // const db = SQLite.openDatabaseSync(input.databaseName, {}, input.directory)
45
+
46
+ return makeSqliteDb_({
47
+ // db,
48
+ makeDb: () => SQLite.openDatabaseSync(input.databaseName, {}, input.directory),
49
+ metadata: {
50
+ _tag: 'expo',
51
+ dbPointer: 0,
52
+ persistenceInfo: { fileName: `${input.directory}/${input.databaseName}` },
53
+ input,
54
+ },
55
+ }) as any
56
+ }
57
+ })
58
+
59
+ const makeSqliteDb_ = <TMetadata extends Metadata>({
60
+ // db,
61
+ makeDb,
62
+ metadata,
63
+ }: {
64
+ // db: SQLite.SQLiteDatabase
65
+ makeDb: () => SQLite.SQLiteDatabase
66
+ metadata: TMetadata
67
+ }): SqliteDb<TMetadata> => {
68
+ const stmts: PreparedStatement[] = []
69
+ const dbRef = { current: makeDb() }
70
+
71
+ const sqliteDb: SqliteDb<TMetadata> = {
72
+ metadata,
73
+ _tag: 'SqliteDb',
74
+ prepare: (queryStr) => {
75
+ try {
76
+ const db = dbRef.current
77
+ const dbStmt = db.prepareSync(queryStr)
78
+ const stmt = {
79
+ execute: (bindValues) => {
80
+ // console.log('execute', queryStr, bindValues)
81
+ const res = dbStmt.executeSync(bindValues ?? ([] as any))
82
+ res.resetSync()
83
+ return () => res.changes
84
+ },
85
+ select: (bindValues) => {
86
+ const res = dbStmt.executeSync(bindValues ?? ([] as any))
87
+ try {
88
+ return res.getAllSync() as any
89
+ } finally {
90
+ res.resetSync()
91
+ }
92
+ },
93
+ finalize: () => dbStmt.finalizeSync(),
94
+ sql: queryStr,
95
+ } satisfies PreparedStatement
96
+ stmts.push(stmt)
97
+ return stmt
98
+ } catch (e) {
99
+ console.error(`Error preparing statement: ${queryStr}`, e)
100
+ return shouldNeverHappen(`Error preparing statement: ${queryStr}`)
101
+ }
102
+ },
103
+ execute: (queryStr, bindValues) => {
104
+ const db = dbRef.current
105
+ const stmt = db.prepareSync(queryStr)
106
+ try {
107
+ const res = stmt.executeSync(bindValues ?? ([] as any))
108
+ return () => res.changes
109
+ } finally {
110
+ stmt.finalizeSync()
111
+ }
112
+ },
113
+ export: () => {
114
+ const db = dbRef.current
115
+ return db.serializeSync()
116
+ },
117
+ select: (queryStr, bindValues) => {
118
+ const stmt = sqliteDb.prepare(queryStr)
119
+ const res = stmt.select(bindValues)
120
+ stmt.finalize()
121
+ return res as any
122
+ },
123
+ // TODO
124
+ destroy: () => {
125
+ if (metadata.input._tag === 'expo') {
126
+ SQLite.deleteDatabaseSync(metadata.input.databaseName, metadata.input.directory)
127
+ }
128
+ },
129
+ close: () => {
130
+ const db = dbRef.current
131
+ for (const stmt of stmts) {
132
+ stmt.finalize()
133
+ }
134
+ return db.closeSync()
135
+ },
136
+ import: (data) => {
137
+ if (!(data instanceof Uint8Array)) {
138
+ throw new TypeError('importing from an existing database is not yet supported in expo')
139
+ }
140
+ if (metadata.input._tag === 'expo') {
141
+ throw new Error('not implemented')
142
+ // SQLite.importDatabaseSync(metadata.input.databaseName, metadata.input.directory, _data)
143
+ } else {
144
+ dbRef.current.closeSync()
145
+ dbRef.current = SQLite.deserializeDatabaseSync(data)
146
+ }
147
+ },
148
+ session: () => {
149
+ return {
150
+ changeset: () => new Uint8Array(),
151
+ finish: () => {},
152
+ }
153
+ },
154
+ makeChangeset: (data) => {
155
+ return {
156
+ invert: () => {
157
+ return sqliteDb.makeChangeset(data)
158
+ },
159
+ apply: () => {
160
+ // TODO
161
+ },
162
+ }
163
+ },
164
+ } satisfies SqliteDb
165
+
166
+ return sqliteDb
167
+ }
@@ -0,0 +1,10 @@
1
+ import type { ShutdownChannel } from '@livestore/common/leader-thread'
2
+ import { WebChannel } from '@livestore/utils/effect'
3
+
4
+ // TODO find an actual implementation for Expo
5
+ export const makeShutdownChannel = (storeId: string) =>
6
+ WebChannel.noopChannel<typeof ShutdownChannel.All.Type, typeof ShutdownChannel.All.Type>()
7
+ // WebChannel.broadcastChannel({
8
+ // channelName: `livestore.shutdown.${storeId}`,
9
+ // schema: ShutdownChannel.All,
10
+ // })
package/tsconfig.json CHANGED
@@ -6,5 +6,10 @@
6
6
  "tsBuildInfoFile": "./dist/.tsbuildinfo"
7
7
  },
8
8
  "include": ["./src"],
9
- "references": [{ "path": "../common" } ,{ "path": "../utils" }, { "path": "../devtools-expo-common" }]
9
+ "references": [
10
+ { "path": "../common" },
11
+ { "path": "../utils" },
12
+ { "path": "../devtools-expo-common" },
13
+ { "path": "../webmesh" }
14
+ ]
10
15
  }