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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/adapter-expo",
3
- "version": "0.3.0-dev.18",
3
+ "version": "0.3.0-dev.21",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -12,22 +12,27 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "dependencies": {
14
14
  "@opentelemetry/api": "1.9.0",
15
- "@livestore/common": "0.3.0-dev.18",
16
- "@livestore/devtools-expo-common": "0.3.0-dev.18",
17
- "@livestore/utils": "0.3.0-dev.18"
15
+ "@livestore/common": "0.3.0-dev.21",
16
+ "@livestore/devtools-expo-common": "0.3.0-dev.21",
17
+ "@livestore/utils": "0.3.0-dev.21",
18
+ "@livestore/webmesh": "0.3.0-dev.21"
18
19
  },
19
20
  "devDependencies": {
20
21
  "expo-file-system": "*",
21
- "expo-sqlite": "^15.0.3"
22
+ "expo-sqlite": "^15.1.2"
22
23
  },
23
24
  "peerDependencies": {
24
25
  "expo-file-system": "*",
25
- "expo-sqlite": "~15.0.3"
26
+ "expo-sqlite": "~15.1.2"
26
27
  },
27
28
  "publishConfig": {
28
29
  "access": "public"
29
30
  },
31
+ "scripts_": {
32
+ "postinstall": "This is needed to avoid a Expo/PNPM related issue"
33
+ },
30
34
  "scripts": {
35
+ "postinstall": "rm -f node_modules/expo-sqlite",
31
36
  "test": "echo No tests yet"
32
37
  }
33
38
  }
package/src/index.ts CHANGED
@@ -1,251 +1,120 @@
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,
1
+ import type {
2
+ Adapter,
3
+ BootStatus,
4
+ ClientSession,
5
+ ClientSessionLeaderThreadProxy,
6
+ LockStatus,
7
+ SyncOptions,
11
8
  } 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'
9
+ import { Devtools, liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
10
+ import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
11
+ import { getClientHeadFromDb, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
12
+ import type { LiveStoreSchema } from '@livestore/common/schema'
13
+ import { MutationEvent } from '@livestore/common/schema'
14
+ import * as DevtoolsExpo from '@livestore/devtools-expo-common/web-channel'
15
+ import type { Scope } from '@livestore/utils/effect'
16
+ import { Cause, Effect, FetchHttpClient, Fiber, Layer, Queue, Stream, SubscriptionRef } from '@livestore/utils/effect'
17
+ import type { MeshNode } from '@livestore/webmesh'
24
18
  import * as SQLite from 'expo-sqlite'
25
19
 
26
- import { makeSqliteDb } from './common.js'
27
- import type { BootedDevtools } from './devtools.js'
28
- import { bootDevtools } from './devtools.js'
20
+ import type { MakeExpoSqliteDb } from './make-sqlite-db.js'
21
+ import { makeSqliteDb } from './make-sqlite-db.js'
22
+ import { makeShutdownChannel } from './shutdown-channel.js'
29
23
 
30
24
  export type MakeDbOptions = {
31
- fileNamePrefix?: string
32
- subDirectory?: string
25
+ sync?: SyncOptions
26
+ storage?: {
27
+ /**
28
+ * Relative to expo-sqlite's default directory
29
+ *
30
+ * Example of a resulting path for `subDirectory: 'my-app'`:
31
+ * `/data/Containers/Data/Application/<APP_UUID>/Documents/ExponentExperienceData/@<USERNAME>/<APPNAME>/SQLite/my-app/<STORE_ID>/livestore-mutationlog@3.db`
32
+ */
33
+ subDirectory?: string
34
+ }
33
35
  // syncBackend?: TODO
36
+ /** @default 'expo' */
37
+ clientId?: string
38
+ /** @default 'expo' */
39
+ sessionId?: string
34
40
  }
35
41
 
36
42
  // TODO refactor with leader-thread code from `@livestore/common/leader-thread`
37
43
  export const makeAdapter =
38
- (options?: MakeDbOptions): Adapter =>
39
- ({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled }) =>
44
+ (options: MakeDbOptions = {}): Adapter =>
45
+ ({ schema, connectDevtoolsToStore, shutdown, devtoolsEnabled, storeId, bootStatusQueue }) =>
40
46
  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)
47
+ const { storage, clientId = 'expo', sessionId = 'expo', sync: syncOptions } = options
46
48
 
47
- const dbRef = { current: { db, sqliteDb: makeSqliteDb(db) } }
49
+ yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
48
50
 
49
- const dbWasEmptyWhenOpened = dbRef.current.sqliteDb.select('SELECT 1 FROM sqlite_master').length === 0
51
+ const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
50
52
 
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
- )
127
-
128
- const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
53
+ const shutdownChannel = yield* makeShutdownChannel(storeId)
129
54
 
130
- const incomingSyncMutationsQueue = yield* Queue.unbounded<PullQueueItem>().pipe(
131
- Effect.acquireRelease(Queue.shutdown),
55
+ yield* shutdownChannel.listen.pipe(
56
+ Stream.flatten(),
57
+ Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
58
+ Stream.runDrain,
59
+ Effect.interruptible,
60
+ Effect.tapCauseLogPretty,
61
+ Effect.forkScoped,
132
62
  )
133
63
 
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
- ),
149
- )
150
-
151
- let devtools: BootedDevtools | undefined
64
+ const devtoolsWebmeshNode = devtoolsEnabled
65
+ ? yield* DevtoolsExpo.makeExpoDevtoolsConnectedMeshNode({
66
+ nodeName: `expo-${storeId}-${clientId}-${sessionId}`,
67
+ target: `devtools-${storeId}-${clientId}-${sessionId}`,
68
+ })
69
+ : undefined
70
+
71
+ const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
72
+ storeId,
73
+ clientId,
74
+ sessionId,
75
+ schema,
76
+ makeSqliteDb,
77
+ syncOptions,
78
+ storage: storage ?? {},
79
+ devtoolsEnabled,
80
+ devtoolsWebmeshNode,
81
+ bootStatusQueue,
82
+ })
83
+
84
+ const sqliteDb = yield* makeSqliteDb({ _tag: 'in-memory' })
85
+ sqliteDb.import(initialSnapshot)
152
86
 
153
87
  const clientSession = {
154
88
  devtools: { enabled: false },
155
89
  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
- },
90
+ clientId,
91
+ sessionId,
92
+ leaderThread,
232
93
  shutdown: () => Effect.dieMessage('TODO implement shutdown'),
233
- sqliteDb: dbRef.current.sqliteDb,
94
+ sqliteDb,
234
95
  } satisfies ClientSession
235
96
 
236
97
  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
- )
98
+ yield* Effect.gen(function* () {
99
+ const sessionInfoChannel = yield* DevtoolsExpo.makeExpoDevtoolsBroadcastChannel({
100
+ channelName: 'devtools-expo-session-info',
101
+ schema: Devtools.SessionInfo.Message,
102
+ })
103
+
104
+ yield* Devtools.SessionInfo.provideSessionInfo({
105
+ webChannel: sessionInfoChannel,
106
+ sessionInfo: Devtools.SessionInfo.SessionInfo.make({ clientId, sessionId, storeId }),
107
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
108
+
109
+ const storeDevtoolsChannel = yield* DevtoolsExpo.makeChannelForConnectedMeshNode({
110
+ target: `devtools-${storeId}-${clientId}-${sessionId}`,
111
+ node: devtoolsWebmeshNode!,
112
+ schema: { listen: Devtools.ClientSession.MessageToApp, send: Devtools.ClientSession.MessageFromApp },
113
+ channelType: 'clientSession',
114
+ })
115
+
116
+ yield* connectDevtoolsToStore(storeDevtoolsChannel)
117
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
249
118
  }
250
119
 
251
120
  return clientSession
@@ -253,3 +122,164 @@ export const makeAdapter =
253
122
  Effect.mapError((cause) => (cause._tag === 'LiveStore.UnexpectedError' ? cause : new UnexpectedError({ cause }))),
254
123
  Effect.tapCauseLogPretty,
255
124
  )
125
+
126
+ const makeLeaderThread = ({
127
+ storeId,
128
+ clientId,
129
+ sessionId,
130
+ schema,
131
+ makeSqliteDb,
132
+ syncOptions,
133
+ storage,
134
+ devtoolsEnabled,
135
+ devtoolsWebmeshNode,
136
+ bootStatusQueue: bootStatusQueueClientSession,
137
+ }: {
138
+ storeId: string
139
+ clientId: string
140
+ sessionId: string
141
+ schema: LiveStoreSchema
142
+ makeSqliteDb: MakeExpoSqliteDb
143
+ syncOptions: SyncOptions | undefined
144
+ storage: {
145
+ subDirectory?: string
146
+ }
147
+ devtoolsEnabled: boolean
148
+ devtoolsWebmeshNode: MeshNode | undefined
149
+ bootStatusQueue: Queue.Queue<BootStatus>
150
+ }) =>
151
+ Effect.gen(function* () {
152
+ const subDirectory = storage.subDirectory ? storage.subDirectory.replace(/\/$/, '') + '/' : ''
153
+ const pathJoin = (...paths: string[]) => paths.join('/').replaceAll(/\/+/g, '/')
154
+ const directory = pathJoin(SQLite.defaultDatabaseDirectory, subDirectory, storeId)
155
+
156
+ const readModelDatabaseName = `${'livestore-'}${schema.hash}@${liveStoreStorageFormatVersion}.db`
157
+ const dbMutationLogPath = `${'livestore-'}mutationlog@${liveStoreStorageFormatVersion}.db`
158
+
159
+ const dbReadModel = yield* makeSqliteDb({ _tag: 'expo', databaseName: readModelDatabaseName, directory })
160
+ const dbMutationLog = yield* makeSqliteDb({ _tag: 'expo', databaseName: dbMutationLogPath, directory })
161
+
162
+ const devtoolsOptions = yield* makeDevtoolsOptions({
163
+ devtoolsEnabled,
164
+ dbReadModel,
165
+ dbMutationLog,
166
+ storeId,
167
+ clientId,
168
+ sessionId,
169
+ devtoolsWebmeshNode,
170
+ })
171
+
172
+ const layer = yield* Layer.memoize(
173
+ makeLeaderThreadLayer({
174
+ clientId,
175
+ dbReadModel,
176
+ dbMutationLog,
177
+ devtoolsOptions,
178
+ makeSqliteDb,
179
+ schema,
180
+ // NOTE we're creating a separate channel here since you can't listen to your own channel messages
181
+ shutdownChannel: yield* makeShutdownChannel(storeId),
182
+ storeId,
183
+ syncOptions,
184
+ }).pipe(Layer.provideMerge(FetchHttpClient.layer)),
185
+ )
186
+
187
+ return yield* Effect.gen(function* () {
188
+ const {
189
+ dbReadModel: db,
190
+ dbMutationLog,
191
+ syncProcessor,
192
+ connectedClientSessionPullQueues,
193
+ extraIncomingMessagesQueue,
194
+ initialState,
195
+ bootStatusQueue,
196
+ } = yield* LeaderThreadCtx
197
+
198
+ const bootStatusFiber = yield* Queue.takeBetween(bootStatusQueue, 1, 1000).pipe(
199
+ Effect.tap((bootStatus) => Queue.offerAll(bootStatusQueueClientSession, bootStatus)),
200
+ Effect.interruptible,
201
+ Effect.tapCauseLogPretty,
202
+ Effect.forkScoped,
203
+ )
204
+
205
+ yield* Queue.awaitShutdown(bootStatusQueueClientSession).pipe(
206
+ Effect.andThen(Fiber.interrupt(bootStatusFiber)),
207
+ Effect.tapCauseLogPretty,
208
+ Effect.forkScoped,
209
+ )
210
+
211
+ const initialLeaderHead = getClientHeadFromDb(dbMutationLog)
212
+ const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(initialLeaderHead)
213
+
214
+ const leaderThread = {
215
+ mutations: {
216
+ pull: Stream.fromQueue(pullQueue),
217
+ push: (batch) =>
218
+ syncProcessor
219
+ .push(
220
+ batch.map((item) => new MutationEvent.EncodedWithMeta(item)),
221
+ { waitForProcessing: true },
222
+ )
223
+ .pipe(Effect.provide(layer), Effect.scoped),
224
+ },
225
+ initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
226
+ export: Effect.sync(() => db.export()),
227
+ getMutationLogData: Effect.sync(() => dbMutationLog.export()),
228
+ // TODO
229
+ networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
230
+ Effect.runSync,
231
+ ),
232
+ getSyncState: syncProcessor.syncState,
233
+ sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
234
+ } satisfies ClientSessionLeaderThreadProxy
235
+
236
+ const initialSnapshot = db.export()
237
+
238
+ return { leaderThread, initialSnapshot }
239
+ }).pipe(Effect.provide(layer))
240
+ })
241
+
242
+ const makeDevtoolsOptions = ({
243
+ devtoolsEnabled,
244
+ dbReadModel,
245
+ dbMutationLog,
246
+ storeId,
247
+ clientId,
248
+ sessionId,
249
+ devtoolsWebmeshNode,
250
+ }: {
251
+ devtoolsEnabled: boolean
252
+ dbReadModel: LeaderSqliteDb
253
+ dbMutationLog: LeaderSqliteDb
254
+ storeId: string
255
+ clientId: string
256
+ sessionId: string
257
+ devtoolsWebmeshNode: MeshNode | undefined
258
+ }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
259
+ Effect.gen(function* () {
260
+ if (devtoolsEnabled === false) {
261
+ return {
262
+ enabled: false,
263
+ }
264
+ }
265
+
266
+ return {
267
+ enabled: true,
268
+ makeBootContext: Effect.gen(function* () {
269
+ const devtoolsWebChannel = yield* DevtoolsExpo.makeChannelForConnectedMeshNode({
270
+ node: devtoolsWebmeshNode!,
271
+ target: `devtools-${storeId}-${clientId}-${sessionId}`,
272
+ schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
273
+ channelType: 'leader',
274
+ })
275
+
276
+ return {
277
+ devtoolsWebChannel,
278
+ persistenceInfo: {
279
+ readModel: dbReadModel.metadata.persistenceInfo,
280
+ mutationLog: dbMutationLog.metadata.persistenceInfo,
281
+ },
282
+ }
283
+ }),
284
+ }
285
+ })