@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/dist/.tsbuildinfo +1 -1
- package/dist/common.d.ts +0 -1
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +1 -87
- package/dist/common.js.map +1 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -151
- package/dist/index.js.map +1 -1
- package/dist/make-sqlite-db.d.ts +20 -0
- package/dist/make-sqlite-db.d.ts.map +1 -0
- package/dist/make-sqlite-db.js +138 -0
- package/dist/make-sqlite-db.js.map +1 -0
- package/dist/shutdown-channel.d.ts +3 -0
- package/dist/shutdown-channel.d.ts.map +1 -0
- package/dist/shutdown-channel.js +8 -0
- package/dist/shutdown-channel.js.map +1 -0
- package/package.json +11 -6
- package/src/common.ts +2 -88
- package/src/index.ts +223 -224
- package/src/make-sqlite-db.ts +167 -0
- package/src/shutdown-channel.ts +10 -0
- package/tsconfig.json +6 -1
package/src/index.ts
CHANGED
|
@@ -1,251 +1,102 @@
|
|
|
1
|
-
import type { Adapter, ClientSession, LockStatus,
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
39
|
-
({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled }) =>
|
|
40
|
+
(options: MakeDbOptions = {}): Adapter =>
|
|
41
|
+
({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled, storeId, bootStatusQueue, debugInstanceId }) =>
|
|
40
42
|
Effect.gen(function* () {
|
|
41
|
-
const {
|
|
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')
|
|
45
|
+
const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
|
|
129
46
|
|
|
130
|
-
const
|
|
131
|
-
Effect.acquireRelease(Queue.shutdown),
|
|
132
|
-
)
|
|
47
|
+
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
133
48
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
87
|
+
sqliteDb,
|
|
234
88
|
} satisfies ClientSession
|
|
235
89
|
|
|
236
90
|
if (devtoolsEnabled) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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": [
|
|
9
|
+
"references": [
|
|
10
|
+
{ "path": "../common" },
|
|
11
|
+
{ "path": "../utils" },
|
|
12
|
+
{ "path": "../devtools-expo-common" },
|
|
13
|
+
{ "path": "../webmesh" }
|
|
14
|
+
]
|
|
10
15
|
}
|