@livestore/adapter-expo 0.3.0-dev.17 → 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,246 +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 { 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'
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'
18
14
  import * as SQLite from 'expo-sqlite'
19
15
 
20
- import { makeSqliteDb } from './common.js'
21
- import type { BootedDevtools } from './devtools.js'
22
- 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'
23
19
 
24
20
  export type MakeDbOptions = {
25
- fileNamePrefix?: string
26
- 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
+ }
27
31
  // syncBackend?: TODO
32
+ /** @default 'expo' */
33
+ clientId?: string
34
+ /** @default 'expo' */
35
+ sessionId?: string
28
36
  }
29
37
 
30
38
  // TODO refactor with leader-thread code from `@livestore/common/leader-thread`
31
39
  export const makeAdapter =
32
- (options?: MakeDbOptions): Adapter =>
33
- ({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled }) =>
40
+ (options: MakeDbOptions = {}): Adapter =>
41
+ ({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled, storeId, bootStatusQueue, debugInstanceId }) =>
34
42
  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)
43
+ const { storage, clientId = 'expo', sessionId = 'expo', sync: syncOptions } = options
40
44
 
41
- const dbRef = { current: { db, sqliteDb: makeSqliteDb(db) } }
45
+ const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
42
46
 
43
- const dbWasEmptyWhenOpened = dbRef.current.sqliteDb.select('SELECT 1 FROM sqlite_master').length === 0
47
+ const shutdownChannel = yield* makeShutdownChannel(storeId)
44
48
 
45
- const dbMutationLog = SQLite.openDatabaseSync(
46
- `${subDirectory ?? ''}${fileNamePrefix ?? 'livestore-'}mutationlog@${liveStoreStorageFormatVersion}.db`,
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,
47
56
  )
48
57
 
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
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)
146
79
 
147
80
  const clientSession = {
148
81
  devtools: { enabled: false },
149
82
  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
- clientId: 'expo',
189
- sessionId: 'expo',
190
- } satisfies MutationLogMetaRow
191
-
192
- try {
193
- newMutationLogStmt.execute(
194
- makeBindValues({
195
- columns: mutationLogMetaTable.sqliteDef.columns,
196
- values: mutationLogRowValues,
197
- variablePrefix: '$',
198
- }) as PreparedBindValues,
199
- )
200
- } catch (e) {
201
- console.error('Error writing to mutation_log', e, mutationLogRowValues)
202
- debugger
203
- throw e
204
- }
205
- } else {
206
- // console.debug('livestore-webworker: skipping mutation log write', mutation, statementSql, bindValues)
207
- }
208
-
209
- yield* devtools?.onMutation({ mutationEventEncoded }) ?? Effect.void
210
- }
211
- }),
212
- },
213
- initialState: {
214
- migrationsReport: {
215
- migrations: [],
216
- },
217
- leaderHead: initialMutationEventId,
218
- },
219
- export: Effect.sync(() => dbRef.current.sqliteDb.export()),
220
- getMutationLogData: Effect.sync(() => dbMutationLogRef.current.sqliteDb.export()),
221
- networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
222
- Effect.runSync,
223
- ),
224
- sendDevtoolsMessage: () => Effect.dieMessage('Not implemented'),
225
- getSyncState: Effect.dieMessage('Not implemented'),
226
- },
83
+ clientId,
84
+ sessionId,
85
+ leaderThread,
227
86
  shutdown: () => Effect.dieMessage('TODO implement shutdown'),
228
- sqliteDb: dbRef.current.sqliteDb,
87
+ sqliteDb,
229
88
  } satisfies ClientSession
230
89
 
231
90
  if (devtoolsEnabled) {
232
- devtools = yield* bootDevtools({
233
- connectDevtoolsToStore,
234
- clientSession,
235
- schema,
236
- dbRef,
237
- dbMutationLogRef,
238
- shutdown,
239
- incomingSyncMutationsQueue,
240
- }).pipe(
241
- Effect.tapCauseLogPretty,
242
- Effect.catchAll(() => Effect.succeed(undefined)),
243
- )
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)
244
100
  }
245
101
 
246
102
  return clientSession
@@ -248,3 +104,151 @@ export const makeAdapter =
248
104
  Effect.mapError((cause) => (cause._tag === 'LiveStore.UnexpectedError' ? cause : new UnexpectedError({ cause }))),
249
105
  Effect.tapCauseLogPretty,
250
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/tmp/pack.tgz ADDED
Binary file
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
  }