@livestore/adapter-expo 0.0.0-snapshot-2411da6706c12365b5aa4533b551b9b6554d4617 → 0.0.0-snapshot-97ca7eac46b6a583b22d40189126d06a377ec1b0

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