@livestore/common 0.3.0-dev.10 → 0.3.0-dev.12

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.
Files changed (116) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +62 -30
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js +12 -0
  5. package/dist/adapter-types.js.map +1 -1
  6. package/dist/devtools/devtool-message-leader.d.ts +2 -0
  7. package/dist/devtools/devtool-message-leader.d.ts.map +1 -0
  8. package/dist/devtools/devtool-message-leader.js +2 -0
  9. package/dist/devtools/devtool-message-leader.js.map +1 -0
  10. package/dist/devtools/devtools-bridge.d.ts +10 -7
  11. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  12. package/dist/devtools/devtools-messages-client-session.d.ts +370 -0
  13. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -0
  14. package/dist/devtools/devtools-messages-client-session.js +77 -0
  15. package/dist/devtools/devtools-messages-client-session.js.map +1 -0
  16. package/dist/devtools/devtools-messages-common.d.ts +57 -0
  17. package/dist/devtools/devtools-messages-common.d.ts.map +1 -0
  18. package/dist/devtools/devtools-messages-common.js +44 -0
  19. package/dist/devtools/devtools-messages-common.js.map +1 -0
  20. package/dist/devtools/devtools-messages-leader.d.ts +437 -0
  21. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -0
  22. package/dist/devtools/devtools-messages-leader.js +132 -0
  23. package/dist/devtools/devtools-messages-leader.js.map +1 -0
  24. package/dist/devtools/devtools-messages.d.ts +3 -580
  25. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  26. package/dist/devtools/devtools-messages.js +3 -174
  27. package/dist/devtools/devtools-messages.js.map +1 -1
  28. package/dist/init-singleton-tables.d.ts +2 -2
  29. package/dist/init-singleton-tables.d.ts.map +1 -1
  30. package/dist/init-singleton-tables.js.map +1 -1
  31. package/dist/leader-thread/LeaderSyncProcessor.d.ts +4 -4
  32. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  33. package/dist/leader-thread/LeaderSyncProcessor.js +64 -36
  34. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  35. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  36. package/dist/leader-thread/apply-mutation.js +4 -4
  37. package/dist/leader-thread/apply-mutation.js.map +1 -1
  38. package/dist/leader-thread/connection.d.ts +34 -6
  39. package/dist/leader-thread/connection.d.ts.map +1 -1
  40. package/dist/leader-thread/connection.js +22 -7
  41. package/dist/leader-thread/connection.js.map +1 -1
  42. package/dist/leader-thread/leader-worker-devtools.js +67 -36
  43. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  44. package/dist/leader-thread/make-leader-thread-layer.d.ts +6 -6
  45. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  46. package/dist/leader-thread/make-leader-thread-layer.js +38 -13
  47. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  48. package/dist/leader-thread/mutationlog.d.ts +4 -4
  49. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  50. package/dist/leader-thread/mutationlog.js +6 -6
  51. package/dist/leader-thread/mutationlog.js.map +1 -1
  52. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  53. package/dist/leader-thread/recreate-db.d.ts +4 -2
  54. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  55. package/dist/leader-thread/recreate-db.js +27 -22
  56. package/dist/leader-thread/recreate-db.js.map +1 -1
  57. package/dist/leader-thread/types.d.ts +32 -17
  58. package/dist/leader-thread/types.d.ts.map +1 -1
  59. package/dist/leader-thread/types.js +0 -2
  60. package/dist/leader-thread/types.js.map +1 -1
  61. package/dist/query-builder/api.d.ts +2 -2
  62. package/dist/query-builder/api.d.ts.map +1 -1
  63. package/dist/query-builder/impl.js.map +1 -1
  64. package/dist/query-builder/impl.test.js +16 -1
  65. package/dist/query-builder/impl.test.js.map +1 -1
  66. package/dist/query-info.d.ts +3 -3
  67. package/dist/query-info.d.ts.map +1 -1
  68. package/dist/rehydrate-from-mutationlog.d.ts +3 -3
  69. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  70. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  71. package/dist/schema/EventId.d.ts +1 -0
  72. package/dist/schema/EventId.d.ts.map +1 -1
  73. package/dist/schema/EventId.js +3 -0
  74. package/dist/schema/EventId.js.map +1 -1
  75. package/dist/schema/mutations.d.ts +1 -1
  76. package/dist/schema/system-tables.d.ts +1 -1
  77. package/dist/schema-management/common.d.ts +3 -3
  78. package/dist/schema-management/common.d.ts.map +1 -1
  79. package/dist/schema-management/common.js.map +1 -1
  80. package/dist/schema-management/migrations.d.ts +5 -5
  81. package/dist/schema-management/migrations.d.ts.map +1 -1
  82. package/dist/schema-management/migrations.js +6 -1
  83. package/dist/schema-management/migrations.js.map +1 -1
  84. package/dist/sync/ClientSessionSyncProcessor.d.ts +8 -12
  85. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  86. package/dist/sync/ClientSessionSyncProcessor.js +31 -13
  87. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  88. package/dist/sync/next/test/mutation-fixtures.d.ts +7 -7
  89. package/dist/version.d.ts +1 -1
  90. package/dist/version.js +1 -1
  91. package/package.json +3 -3
  92. package/src/adapter-types.ts +52 -33
  93. package/src/devtools/devtools-bridge.ts +10 -7
  94. package/src/devtools/devtools-messages-client-session.ts +125 -0
  95. package/src/devtools/devtools-messages-common.ts +81 -0
  96. package/src/devtools/devtools-messages-leader.ts +176 -0
  97. package/src/devtools/devtools-messages.ts +3 -246
  98. package/src/init-singleton-tables.ts +2 -2
  99. package/src/leader-thread/LeaderSyncProcessor.ts +94 -46
  100. package/src/leader-thread/apply-mutation.ts +5 -5
  101. package/src/leader-thread/connection.ts +54 -9
  102. package/src/leader-thread/leader-worker-devtools.ts +105 -41
  103. package/src/leader-thread/make-leader-thread-layer.ts +55 -22
  104. package/src/leader-thread/mutationlog.ts +9 -9
  105. package/src/leader-thread/recreate-db.ts +33 -24
  106. package/src/leader-thread/types.ts +38 -21
  107. package/src/query-builder/api.ts +3 -3
  108. package/src/query-builder/impl.test.ts +22 -1
  109. package/src/query-builder/impl.ts +2 -2
  110. package/src/query-info.ts +3 -3
  111. package/src/rehydrate-from-mutationlog.ts +3 -3
  112. package/src/schema/EventId.ts +4 -0
  113. package/src/schema-management/common.ts +3 -3
  114. package/src/schema-management/migrations.ts +12 -8
  115. package/src/sync/ClientSessionSyncProcessor.ts +38 -22
  116. package/src/version.ts +1 -1
@@ -5,7 +5,7 @@ import { MUTATION_LOG_META_TABLE, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE
5
5
  import type { DevtoolsOptions, PersistenceInfoPair } from './types.js'
6
6
  import { LeaderThreadCtx } from './types.js'
7
7
 
8
- type SendMessageToDevtools = (message: Devtools.MessageFromAppLeader) => Effect.Effect<void>
8
+ type SendMessageToDevtools = (message: Devtools.Leader.MessageFromApp) => Effect.Effect<void>
9
9
 
10
10
  // TODO bind scope to the webchannel lifetime
11
11
  export const bootDevtools = (options: DevtoolsOptions) =>
@@ -21,7 +21,7 @@ export const bootDevtools = (options: DevtoolsOptions) =>
21
21
  sendMessage: () => Effect.void,
22
22
  }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
23
23
 
24
- const { persistenceInfo, devtoolsWebChannel } = yield* options.makeContext
24
+ const { persistenceInfo, devtoolsWebChannel } = yield* options.makeBootContext
25
25
 
26
26
  const sendMessage: SendMessageToDevtools = (message) =>
27
27
  devtoolsWebChannel
@@ -43,7 +43,7 @@ export const bootDevtools = (options: DevtoolsOptions) =>
43
43
  if (msg.payload._tag === 'upstream-advance') {
44
44
  for (const mutationEventEncoded of msg.payload.newEvents) {
45
45
  // TODO refactor with push semantics
46
- yield* sendMessage(Devtools.MutationBroadcast.make({ mutationEventEncoded, liveStoreVersion }))
46
+ yield* sendMessage(Devtools.Leader.MutationBroadcast.make({ mutationEventEncoded, liveStoreVersion }))
47
47
  }
48
48
  } else {
49
49
  yield* Effect.logWarning('TODO implement rebases in devtools')
@@ -66,13 +66,22 @@ const listenToDevtools = ({
66
66
  sendMessage,
67
67
  persistenceInfo,
68
68
  }: {
69
- incomingMessages: Stream.Stream<Devtools.MessageToAppLeader>
69
+ incomingMessages: Stream.Stream<Devtools.Leader.MessageToApp>
70
70
  sendMessage: SendMessageToDevtools
71
71
  persistenceInfo?: PersistenceInfoPair
72
72
  }) =>
73
73
  Effect.gen(function* () {
74
- const { syncBackend, makeSyncDb, db, dbLog, shutdownStateSubRef, shutdownChannel, syncProcessor } =
75
- yield* LeaderThreadCtx
74
+ const {
75
+ syncBackend,
76
+ makeSqliteDb,
77
+ dbReadModel,
78
+ dbMutationLog,
79
+ shutdownStateSubRef,
80
+ shutdownChannel,
81
+ syncProcessor,
82
+ clientId,
83
+ devtools,
84
+ } = yield* LeaderThreadCtx
76
85
 
77
86
  type RequestId = string
78
87
  const subscriptionFiberMap = yield* FiberMap.make<RequestId>()
@@ -82,22 +91,22 @@ const listenToDevtools = ({
82
91
  Effect.gen(function* () {
83
92
  // yield* Effect.logDebug('[@livestore/common:leader-thread:devtools] incomingMessage', decodedEvent)
84
93
 
85
- if (decodedEvent._tag === 'LSD.Disconnect') {
94
+ if (decodedEvent._tag === 'LSD.Leader.Disconnect') {
86
95
  return
87
96
  }
88
97
 
89
98
  const { requestId } = decodedEvent
90
- const reqPayload = { requestId, liveStoreVersion }
99
+ const reqPayload = { requestId, liveStoreVersion, clientId }
91
100
 
92
101
  switch (decodedEvent._tag) {
93
- case 'LSD.Ping': {
94
- yield* sendMessage(Devtools.Pong.make({ ...reqPayload }))
102
+ case 'LSD.Leader.Ping': {
103
+ yield* sendMessage(Devtools.Leader.Pong.make({ ...reqPayload }))
95
104
  return
96
105
  }
97
106
  case 'LSD.Leader.SnapshotReq': {
98
- const snapshot = db.export()
107
+ const snapshot = dbReadModel.export()
99
108
 
100
- yield* sendMessage(Devtools.SnapshotRes.make({ snapshot, ...reqPayload }))
109
+ yield* sendMessage(Devtools.Leader.SnapshotRes.make({ snapshot, ...reqPayload }))
101
110
 
102
111
  return
103
112
  }
@@ -107,18 +116,20 @@ const listenToDevtools = ({
107
116
  let tableNames: Set<string>
108
117
 
109
118
  try {
110
- const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
111
- tmpSyncDb.import(data)
112
- const tableNameResults = tmpSyncDb.select<{ name: string }>(
119
+ const tmpDb = yield* makeSqliteDb({ _tag: 'in-memory' })
120
+ tmpDb.import(data)
121
+ const tableNameResults = tmpDb.select<{ name: string }>(
113
122
  `select name from sqlite_master where type = 'table'`,
114
123
  )
115
124
 
116
125
  tableNames = new Set(tableNameResults.map((_) => _.name))
117
126
 
118
- tmpSyncDb.close()
127
+ tmpDb.close()
119
128
  } catch (e) {
120
129
  yield* Effect.logError(`Error importing database file`, e)
121
- yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-file' }))
130
+ yield* sendMessage(
131
+ Devtools.Leader.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-file' }),
132
+ )
122
133
 
123
134
  return
124
135
  }
@@ -126,38 +137,40 @@ const listenToDevtools = ({
126
137
  if (tableNames.has(MUTATION_LOG_META_TABLE)) {
127
138
  yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
128
139
 
129
- dbLog.import(data)
140
+ dbMutationLog.import(data)
130
141
 
131
- db.destroy()
142
+ dbReadModel.destroy()
132
143
  } else if (tableNames.has(SCHEMA_META_TABLE) && tableNames.has(SCHEMA_MUTATIONS_META_TABLE)) {
133
144
  yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
134
145
 
135
- db.import(data)
146
+ dbReadModel.import(data)
136
147
 
137
- dbLog.destroy()
148
+ dbMutationLog.destroy()
138
149
  } else {
139
- yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-database' }))
150
+ yield* sendMessage(
151
+ Devtools.Leader.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-database' }),
152
+ )
140
153
  return
141
154
  }
142
155
 
143
- yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'ok' }))
156
+ yield* sendMessage(Devtools.Leader.LoadDatabaseFileRes.make({ ...reqPayload, status: 'ok' }))
144
157
 
145
158
  yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-import' })) ?? Effect.void
146
159
 
147
160
  return
148
161
  }
149
- case 'LSD.Leader.ResetAllDataReq': {
162
+ case 'LSD.Leader.ResetAllData.Request': {
150
163
  const { mode } = decodedEvent
151
164
 
152
165
  yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
153
166
 
154
- db.destroy()
167
+ dbReadModel.destroy()
155
168
 
156
169
  if (mode === 'all-data') {
157
- dbLog.destroy()
170
+ dbMutationLog.destroy()
158
171
  }
159
172
 
160
- yield* sendMessage(Devtools.ResetAllDataRes.make({ ...reqPayload }))
173
+ yield* sendMessage(Devtools.Leader.ResetAllData.Response.make({ ...reqPayload }))
161
174
 
162
175
  yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-reset' })) ?? Effect.void
163
176
 
@@ -170,12 +183,12 @@ const listenToDevtools = ({
170
183
  }
171
184
 
172
185
  const dbSizeQuery = `SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();`
173
- const dbFileSize = db.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
174
- const mutationLogFileSize = dbLog.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
186
+ const dbFileSize = dbReadModel.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
187
+ const mutationLogFileSize = dbMutationLog.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
175
188
 
176
189
  yield* sendMessage(
177
- Devtools.DatabaseFileInfoRes.make({
178
- db: { fileSize: dbFileSize, persistenceInfo: persistenceInfo.db },
190
+ Devtools.Leader.DatabaseFileInfoRes.make({
191
+ readModel: { fileSize: dbFileSize, persistenceInfo: persistenceInfo.readModel },
179
192
  mutationLog: { fileSize: mutationLogFileSize, persistenceInfo: persistenceInfo.mutationLog },
180
193
  ...reqPayload,
181
194
  }),
@@ -184,16 +197,16 @@ const listenToDevtools = ({
184
197
  return
185
198
  }
186
199
  case 'LSD.Leader.MutationLogReq': {
187
- const mutationLog = dbLog.export()
200
+ const mutationLog = dbMutationLog.export()
188
201
 
189
- yield* sendMessage(Devtools.MutationLogRes.make({ mutationLog, ...reqPayload }))
202
+ yield* sendMessage(Devtools.Leader.MutationLogRes.make({ mutationLog, ...reqPayload }))
190
203
 
191
204
  return
192
205
  }
193
206
  case 'LSD.Leader.RunMutationReq': {
194
207
  yield* syncProcessor.pushPartial(decodedEvent.mutationEventEncoded)
195
208
 
196
- yield* sendMessage(Devtools.RunMutationRes.make({ ...reqPayload }))
209
+ yield* sendMessage(Devtools.Leader.RunMutationRes.make({ ...reqPayload }))
197
210
 
198
211
  return
199
212
  }
@@ -206,7 +219,7 @@ const listenToDevtools = ({
206
219
  Stream.map((_) => _.batch),
207
220
  Stream.flattenIterables,
208
221
  Stream.tap(({ mutationEventEncoded, metadata }) =>
209
- sendMessage(Devtools.SyncHistoryRes.make({ mutationEventEncoded, metadata, ...reqPayload })),
222
+ sendMessage(Devtools.Leader.SyncHistoryRes.make({ mutationEventEncoded, metadata, ...reqPayload })),
210
223
  ),
211
224
  Stream.runDrain,
212
225
  Effect.acquireRelease(() => Effect.log('syncHistorySubscribe done')),
@@ -227,12 +240,12 @@ const listenToDevtools = ({
227
240
  return
228
241
  }
229
242
  case 'LSD.Leader.SyncingInfoReq': {
230
- const syncingInfo = Devtools.SyncingInfo.make({
243
+ const syncingInfo = Devtools.Leader.SyncingInfo.make({
231
244
  enabled: syncBackend !== undefined,
232
245
  metadata: {},
233
246
  })
234
247
 
235
- yield* sendMessage(Devtools.SyncingInfoRes.make({ syncingInfo, ...reqPayload }))
248
+ yield* sendMessage(Devtools.Leader.SyncingInfoRes.make({ syncingInfo, ...reqPayload }))
236
249
 
237
250
  return
238
251
  }
@@ -245,11 +258,14 @@ const listenToDevtools = ({
245
258
  // This is probably the same "flaky databrowser loading" bug as we're seeing in the playwright tests
246
259
  yield* Effect.sleep(1000)
247
260
 
248
- yield* syncBackend.isConnected.changes.pipe(
249
- Stream.tap((isConnected) =>
261
+ yield* Stream.zipLatest(
262
+ syncBackend.isConnected.changes,
263
+ devtools.enabled ? devtools.syncBackendLatchState.changes : Stream.make({ latchClosed: false }),
264
+ ).pipe(
265
+ Stream.tap(([isConnected, { latchClosed }]) =>
250
266
  sendMessage(
251
- Devtools.NetworkStatusRes.make({
252
- networkStatus: { isConnected, timestampMs: Date.now() },
267
+ Devtools.Leader.NetworkStatusRes.make({
268
+ networkStatus: { isConnected, timestampMs: Date.now(), latchClosed },
253
269
  ...reqPayload,
254
270
  }),
255
271
  ),
@@ -270,6 +286,54 @@ const listenToDevtools = ({
270
286
 
271
287
  return
272
288
  }
289
+ case 'LSD.Leader.SyncHeadSubscribe': {
290
+ const { requestId } = decodedEvent
291
+
292
+ yield* syncProcessor.syncState.changes.pipe(
293
+ Stream.tap((syncState) =>
294
+ sendMessage(
295
+ Devtools.Leader.SyncHeadRes.make({
296
+ local: syncState.localHead,
297
+ upstream: syncState.upstreamHead,
298
+ ...reqPayload,
299
+ }),
300
+ ),
301
+ ),
302
+ Stream.runDrain,
303
+ Effect.interruptible,
304
+ Effect.tapCauseLogPretty,
305
+ FiberMap.run(subscriptionFiberMap, requestId),
306
+ )
307
+
308
+ return
309
+ }
310
+ case 'LSD.Leader.SyncHeadUnsubscribe': {
311
+ const { requestId } = decodedEvent
312
+
313
+ yield* FiberMap.remove(subscriptionFiberMap, requestId)
314
+
315
+ return
316
+ }
317
+ case 'LSD.Leader.SetSyncLatch.Request': {
318
+ const { closeLatch } = decodedEvent
319
+
320
+ if (devtools.enabled === false) return
321
+
322
+ if (closeLatch === true) {
323
+ yield* devtools.syncBackendLatch.close
324
+ } else {
325
+ yield* devtools.syncBackendLatch.open
326
+ }
327
+
328
+ yield* SubscriptionRef.set(devtools.syncBackendLatchState, { latchClosed: closeLatch })
329
+
330
+ yield* sendMessage(Devtools.Leader.SetSyncLatch.Response.make({ ...reqPayload }))
331
+
332
+ return
333
+ }
334
+ default: {
335
+ yield* Effect.logWarning(`TODO implement devtools message`, decodedEvent)
336
+ }
273
337
  }
274
338
  }).pipe(Effect.withSpan(`@livestore/common:leader-thread:onDevtoolsMessage:${decodedEvent._tag}`)),
275
339
  ),
@@ -1,7 +1,7 @@
1
1
  import type { HttpClient, Scope } from '@livestore/utils/effect'
2
2
  import { Deferred, Effect, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
3
3
 
4
- import type { BootStatus, MakeSynchronousDatabase, SqliteError, SynchronousDatabase } from '../adapter-types.js'
4
+ import type { BootStatus, MakeSqliteDb, MigrationsReport, SqliteError } from '../adapter-types.js'
5
5
  import { UnexpectedError } from '../adapter-types.js'
6
6
  import type * as Devtools from '../devtools/index.js'
7
7
  import type { LiveStoreSchema } from '../schema/mod.js'
@@ -15,27 +15,33 @@ import { makeLeaderSyncProcessor } from './LeaderSyncProcessor.js'
15
15
  import { makePullQueueSet } from './pull-queue-set.js'
16
16
  import { recreateDb } from './recreate-db.js'
17
17
  import type { ShutdownChannel } from './shutdown-channel.js'
18
- import type { DevtoolsOptions, InitialBlockingSyncContext, InitialSyncOptions, ShutdownState } from './types.js'
18
+ import type {
19
+ DevtoolsOptions,
20
+ InitialBlockingSyncContext,
21
+ InitialSyncOptions,
22
+ LeaderSqliteDb,
23
+ ShutdownState,
24
+ } from './types.js'
19
25
  import { LeaderThreadCtx } from './types.js'
20
26
 
21
27
  export const makeLeaderThreadLayer = ({
22
28
  schema,
23
29
  storeId,
24
30
  clientId,
25
- makeSyncDb,
31
+ makeSqliteDb,
26
32
  syncOptions,
27
- db,
28
- dbLog,
33
+ dbReadModel,
34
+ dbMutationLog,
29
35
  devtoolsOptions,
30
36
  shutdownChannel,
31
37
  }: {
32
38
  storeId: string
33
39
  clientId: string
34
40
  schema: LiveStoreSchema
35
- makeSyncDb: MakeSynchronousDatabase
41
+ makeSqliteDb: MakeSqliteDb
36
42
  syncOptions: SyncOptions | undefined
37
- db: SynchronousDatabase
38
- dbLog: SynchronousDatabase
43
+ dbReadModel: LeaderSqliteDb
44
+ dbMutationLog: LeaderSqliteDb
39
45
  devtoolsOptions: DevtoolsOptions
40
46
  shutdownChannel: ShutdownChannel
41
47
  }): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
@@ -44,7 +50,8 @@ export const makeLeaderThreadLayer = ({
44
50
 
45
51
  // TODO do more validation here than just checking the count of tables
46
52
  // Either happens on initial boot or if schema changes
47
- const dbMissing = db.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
53
+ const dbMissing =
54
+ dbReadModel.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
48
55
 
49
56
  const syncBackend = syncOptions === undefined ? undefined : yield* syncOptions.makeBackend({ storeId, clientId })
50
57
 
@@ -53,20 +60,33 @@ export const makeLeaderThreadLayer = ({
53
60
  bootStatusQueue,
54
61
  })
55
62
 
56
- const syncProcessor = yield* makeLeaderSyncProcessor({ schema, dbMissing, dbLog, initialBlockingSyncContext })
63
+ const syncProcessor = yield* makeLeaderSyncProcessor({
64
+ schema,
65
+ dbMissing,
66
+ dbMutationLog,
67
+ initialBlockingSyncContext,
68
+ })
57
69
 
58
- const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.MessageToAppLeader>().pipe(
70
+ const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.Leader.MessageToApp>().pipe(
59
71
  Effect.acquireRelease(Queue.shutdown),
60
72
  )
61
73
 
74
+ const devtoolsContext = devtoolsOptions.enabled
75
+ ? {
76
+ enabled: true as const,
77
+ syncBackendLatch: yield* Effect.makeLatch(true),
78
+ syncBackendLatchState: yield* SubscriptionRef.make<{ latchClosed: boolean }>({ latchClosed: false }),
79
+ }
80
+ : { enabled: false as const }
81
+
62
82
  const ctx = {
63
83
  schema,
64
84
  bootStatusQueue,
65
85
  storeId,
66
86
  clientId,
67
- db,
68
- dbLog,
69
- makeSyncDb,
87
+ dbReadModel,
88
+ dbMutationLog,
89
+ makeSqliteDb,
70
90
  mutationEventSchema: MutationEvent.makeMutationEventSchema(schema),
71
91
  shutdownStateSubRef: yield* SubscriptionRef.make<ShutdownState>('running'),
72
92
  shutdownChannel,
@@ -74,6 +94,9 @@ export const makeLeaderThreadLayer = ({
74
94
  syncProcessor,
75
95
  connectedClientSessionPullQueues: yield* makePullQueueSet,
76
96
  extraIncomingMessagesQueue,
97
+ devtools: devtoolsContext,
98
+ // State will be set during `bootLeaderThread`
99
+ initialState: {} as any as LeaderThreadCtx['Type']['initialState'],
77
100
  } satisfies typeof LeaderThreadCtx.Service
78
101
 
79
102
  // @ts-expect-error For debugging purposes
@@ -81,7 +104,11 @@ export const makeLeaderThreadLayer = ({
81
104
 
82
105
  const layer = Layer.succeed(LeaderThreadCtx, ctx)
83
106
 
84
- yield* bootLeaderThread({ dbMissing, initialBlockingSyncContext, devtoolsOptions }).pipe(Effect.provide(layer))
107
+ ctx.initialState = yield* bootLeaderThread({
108
+ dbMissing,
109
+ initialBlockingSyncContext,
110
+ devtoolsOptions,
111
+ }).pipe(Effect.provide(layer))
85
112
 
86
113
  return layer
87
114
  }).pipe(
@@ -151,22 +178,22 @@ const bootLeaderThread = ({
151
178
  initialBlockingSyncContext: InitialBlockingSyncContext
152
179
  devtoolsOptions: DevtoolsOptions
153
180
  }): Effect.Effect<
154
- void,
181
+ LeaderThreadCtx['Type']['initialState'],
155
182
  UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
156
183
  LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
157
184
  > =>
158
185
  Effect.gen(function* () {
159
- const { dbLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
186
+ const { dbMutationLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
160
187
 
161
188
  yield* migrateTable({
162
- db: dbLog,
189
+ db: dbMutationLog,
163
190
  behaviour: 'create-if-not-exists',
164
191
  tableAst: mutationLogMetaTable.sqliteDef.ast,
165
192
  skipMetaTable: true,
166
193
  })
167
194
 
168
195
  yield* migrateTable({
169
- db: dbLog,
196
+ db: dbMutationLog,
170
197
  behaviour: 'create-if-not-exists',
171
198
  tableAst: syncStatusTable.sqliteDef.ast,
172
199
  skipMetaTable: true,
@@ -174,7 +201,7 @@ const bootLeaderThread = ({
174
201
 
175
202
  // Create sync status row if it doesn't exist
176
203
  yield* execSql(
177
- dbLog,
204
+ dbMutationLog,
178
205
  sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
179
206
  SELECT ${EventId.ROOT.global}
180
207
  WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
@@ -185,10 +212,14 @@ const bootLeaderThread = ({
185
212
 
186
213
  // We're already starting pulling from the sync backend concurrently but wait until the db is ready before
187
214
  // processing any incoming mutations
188
- yield* syncProcessor.boot({ dbReady })
215
+ const { initialLeaderHead } = yield* syncProcessor.boot({ dbReady })
189
216
 
217
+ let migrationsReport: MigrationsReport
190
218
  if (dbMissing) {
191
- yield* recreateDb
219
+ const recreateResult = yield* recreateDb
220
+ migrationsReport = recreateResult.migrationsReport
221
+ } else {
222
+ migrationsReport = { migrations: [] }
192
223
  }
193
224
 
194
225
  yield* Deferred.succeed(dbReady, void 0)
@@ -200,4 +231,6 @@ const bootLeaderThread = ({
200
231
  yield* Queue.offer(bootStatusQueue, { stage: 'done' })
201
232
 
202
233
  yield* bootDevtools(devtoolsOptions).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
234
+
235
+ return { migrationsReport, leaderHead: initialLeaderHead }
203
236
  })
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from '@livestore/utils/effect'
2
2
 
3
- import type { SynchronousDatabase } from '../adapter-types.js'
3
+ import type { SqliteDb } from '../adapter-types.js'
4
4
  import * as EventId from '../schema/EventId.js'
5
5
  import type * as MutationEvent from '../schema/MutationEvent.js'
6
6
  import { MUTATION_LOG_META_TABLE, mutationLogMetaTable, SYNC_STATUS_TABLE } from '../schema/system-tables.js'
@@ -11,10 +11,10 @@ export const getMutationEventsSince = (
11
11
  since: EventId.EventId,
12
12
  ): Effect.Effect<ReadonlyArray<MutationEvent.AnyEncoded>, never, LeaderThreadCtx> =>
13
13
  Effect.gen(function* () {
14
- const { dbLog } = yield* LeaderThreadCtx
14
+ const { dbMutationLog } = yield* LeaderThreadCtx
15
15
 
16
16
  const query = mutationLogMetaTable.query.where('idGlobal', '>=', since.global).asSql()
17
- const pendingMutationEventsRaw = dbLog.select(query.query, prepareBindValues(query.bindValues, query.query))
17
+ const pendingMutationEventsRaw = dbMutationLog.select(query.query, prepareBindValues(query.bindValues, query.query))
18
18
  const pendingMutationEvents = Schema.decodeUnknownSync(mutationLogMetaTable.schema.pipe(Schema.Array))(
19
19
  pendingMutationEventsRaw,
20
20
  )
@@ -29,18 +29,18 @@ export const getMutationEventsSince = (
29
29
  .filter((_) => EventId.compare(_.id, since) > 0)
30
30
  })
31
31
 
32
- export const getLocalHeadFromDb = (dbLog: SynchronousDatabase): EventId.EventId => {
33
- const res = dbLog.select<{ idGlobal: EventId.GlobalEventId; idLocal: EventId.LocalEventId }>(
32
+ export const getLocalHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
33
+ const res = dbMutationLog.select<{ idGlobal: EventId.GlobalEventId; idLocal: EventId.LocalEventId }>(
34
34
  sql`select idGlobal, idLocal from ${MUTATION_LOG_META_TABLE} order by idGlobal DESC, idLocal DESC limit 1`,
35
35
  )[0]
36
36
 
37
37
  return res ? { global: res.idGlobal, local: res.idLocal } : EventId.ROOT
38
38
  }
39
39
 
40
- export const getBackendHeadFromDb = (dbLog: SynchronousDatabase): EventId.GlobalEventId =>
41
- dbLog.select<{ head: EventId.GlobalEventId }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ??
40
+ export const getBackendHeadFromDb = (dbMutationLog: SqliteDb): EventId.GlobalEventId =>
41
+ dbMutationLog.select<{ head: EventId.GlobalEventId }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ??
42
42
  EventId.ROOT.global
43
43
 
44
44
  // TODO use prepared statements
45
- export const updateBackendHead = (dbLog: SynchronousDatabase, head: EventId.EventId) =>
46
- dbLog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
45
+ export const updateBackendHead = (dbMutationLog: SqliteDb, head: EventId.EventId) =>
46
+ dbMutationLog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
@@ -2,87 +2,94 @@ import { casesHandled } from '@livestore/utils'
2
2
  import type { HttpClient } from '@livestore/utils/effect'
3
3
  import { Effect, Queue } from '@livestore/utils/effect'
4
4
 
5
- import type { InvalidPullError, IsOfflineError, MigrationHooks, SqliteError } from '../index.js'
5
+ import type { InvalidPullError, IsOfflineError, MigrationHooks, MigrationsReport, SqliteError } from '../index.js'
6
6
  import { initializeSingletonTables, migrateDb, rehydrateFromMutationLog, UnexpectedError } from '../index.js'
7
7
  import { configureConnection } from './connection.js'
8
8
  import { LeaderThreadCtx } from './types.js'
9
9
 
10
10
  export const recreateDb: Effect.Effect<
11
- void,
11
+ { migrationsReport: MigrationsReport },
12
12
  UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
13
13
  LeaderThreadCtx | HttpClient.HttpClient
14
14
  > = Effect.gen(function* () {
15
- const { db, dbLog, makeSyncDb, schema, bootStatusQueue } = yield* LeaderThreadCtx
15
+ const { dbReadModel, dbMutationLog, schema, bootStatusQueue } = yield* LeaderThreadCtx
16
16
 
17
17
  const migrationOptions = schema.migrationOptions
18
+ let migrationsReport: MigrationsReport
18
19
 
19
20
  yield* Effect.addFinalizer(
20
21
  Effect.fn('recreateDb:finalizer')(function* (ex) {
21
- if (ex._tag === 'Failure') db.destroy()
22
+ if (ex._tag === 'Failure') dbReadModel.destroy()
22
23
  }),
23
24
  )
24
25
 
25
26
  // NOTE to speed up the operations below, we're creating a temporary in-memory database
26
27
  // and later we'll overwrite the persisted database with the new data
27
28
  // TODO bring back this optimization
28
- // const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
29
- const tmpSyncDb = db
30
- yield* configureConnection(tmpSyncDb, { fkEnabled: true })
29
+ // const tmpDb = yield* makeSqliteDb({ _tag: 'in-memory' })
30
+ const tmpDb = dbReadModel
31
+ yield* configureConnection(tmpDb, { foreignKeys: true })
31
32
 
32
33
  const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
33
34
  Effect.gen(function* () {
34
- yield* Effect.tryAll(() => hooks?.init?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
35
+ yield* Effect.tryAll(() => hooks?.init?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
35
36
 
36
- yield* migrateDb({
37
- db: tmpSyncDb,
37
+ const migrationsReport = yield* migrateDb({
38
+ db: tmpDb,
38
39
  schema,
39
40
  onProgress: ({ done, total }) =>
40
41
  Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
41
42
  })
42
43
 
43
- initializeSingletonTables(schema, tmpSyncDb)
44
+ initializeSingletonTables(schema, tmpDb)
44
45
 
45
- yield* Effect.tryAll(() => hooks?.pre?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
46
+ yield* Effect.tryAll(() => hooks?.pre?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
46
47
 
47
- return tmpSyncDb
48
+ return { migrationsReport, tmpDb }
48
49
  })
49
50
 
50
51
  switch (migrationOptions.strategy) {
51
52
  case 'from-mutation-log': {
52
53
  const hooks = migrationOptions.hooks
53
- const tmpSyncDb = yield* initDb(hooks)
54
+ const initResult = yield* initDb(hooks)
55
+
56
+ migrationsReport = initResult.migrationsReport
54
57
 
55
58
  yield* rehydrateFromMutationLog({
56
- db: tmpSyncDb,
57
- logDb: dbLog,
59
+ db: initResult.tmpDb,
60
+ logDb: dbMutationLog,
58
61
  schema,
59
62
  migrationOptions,
60
63
  onProgress: ({ done, total }) =>
61
64
  Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
62
65
  })
63
66
 
64
- yield* Effect.tryAll(() => hooks?.post?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
67
+ yield* Effect.tryAll(() => hooks?.post?.(initResult.tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
65
68
 
66
69
  break
67
70
  }
68
71
  case 'hard-reset': {
69
72
  const hooks = migrationOptions.hooks
70
- const tmpInMemoryDb = yield* initDb(hooks)
73
+ const initResult = yield* initDb(hooks)
74
+
75
+ migrationsReport = initResult.migrationsReport
71
76
 
72
77
  // The database is migrated but empty now, so nothing else to do
73
78
 
74
- yield* Effect.tryAll(() => hooks?.post?.(tmpInMemoryDb)).pipe(UnexpectedError.mapToUnexpectedError)
79
+ yield* Effect.tryAll(() => hooks?.post?.(initResult.tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
75
80
 
76
81
  break
77
82
  }
78
83
  case 'manual': {
79
- const oldDbData = db.export()
84
+ const oldDbData = dbReadModel.export()
85
+
86
+ migrationsReport = { migrations: [] }
80
87
 
81
88
  const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
82
89
  UnexpectedError.mapToUnexpectedError,
83
90
  )
84
91
 
85
- tmpSyncDb.import(newDbData)
92
+ tmpDb.import(newDbData)
86
93
 
87
94
  // TODO validate schema
88
95
 
@@ -95,17 +102,19 @@ export const recreateDb: Effect.Effect<
95
102
 
96
103
  // TODO bring back
97
104
  // Import the temporary in-memory database into the persistent database
98
- // yield* Effect.sync(() => db.import(tmpSyncDb)).pipe(
105
+ // yield* Effect.sync(() => db.import(tmpDb)).pipe(
99
106
  // Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
100
107
  // )
101
108
 
102
109
  // TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
103
110
  // We've disabled this for now as it made the code too complex, as we often run syncing right after
104
111
  // so the snapshot is no longer up to date
105
- // const snapshotFromTmpDb = tmpSyncDb.export()
112
+ // const snapshotFromTmpDb = tmpDb.export()
106
113
 
107
114
  // TODO bring back
108
- // tmpSyncDb.close()
115
+ // tmpDb.close()
116
+
117
+ return { migrationsReport }
109
118
  }).pipe(
110
119
  Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
111
120
  Effect.withSpan('@livestore/common:leader-thread:recreateDb'),