@livestore/common 0.3.0-dev.11 → 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 (88) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +41 -18
  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/devtools-bridge.d.ts +10 -7
  7. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  8. package/dist/devtools/devtools-messages-client-session.d.ts +101 -28
  9. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
  10. package/dist/devtools/devtools-messages-client-session.js +19 -3
  11. package/dist/devtools/devtools-messages-client-session.js.map +1 -1
  12. package/dist/devtools/devtools-messages-common.d.ts +24 -32
  13. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  14. package/dist/devtools/devtools-messages-common.js +19 -10
  15. package/dist/devtools/devtools-messages-common.js.map +1 -1
  16. package/dist/devtools/devtools-messages-leader.d.ts +210 -34
  17. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  18. package/dist/devtools/devtools-messages-leader.js +53 -6
  19. package/dist/devtools/devtools-messages-leader.js.map +1 -1
  20. package/dist/devtools/devtools-messages.d.ts +2 -2
  21. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  22. package/dist/devtools/devtools-messages.js +2 -2
  23. package/dist/devtools/devtools-messages.js.map +1 -1
  24. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  25. package/dist/leader-thread/LeaderSyncProcessor.js +26 -13
  26. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  27. package/dist/leader-thread/connection.d.ts +31 -3
  28. package/dist/leader-thread/connection.d.ts.map +1 -1
  29. package/dist/leader-thread/connection.js +18 -3
  30. package/dist/leader-thread/connection.js.map +1 -1
  31. package/dist/leader-thread/leader-worker-devtools.js +51 -20
  32. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  33. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  34. package/dist/leader-thread/make-leader-thread-layer.js +17 -5
  35. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  36. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  37. package/dist/leader-thread/recreate-db.d.ts +4 -2
  38. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  39. package/dist/leader-thread/recreate-db.js +13 -8
  40. package/dist/leader-thread/recreate-db.js.map +1 -1
  41. package/dist/leader-thread/types.d.ts +15 -12
  42. package/dist/leader-thread/types.d.ts.map +1 -1
  43. package/dist/leader-thread/types.js +0 -2
  44. package/dist/leader-thread/types.js.map +1 -1
  45. package/dist/query-builder/api.d.ts +2 -2
  46. package/dist/query-builder/api.d.ts.map +1 -1
  47. package/dist/query-builder/impl.js.map +1 -1
  48. package/dist/query-builder/impl.test.js +16 -1
  49. package/dist/query-builder/impl.test.js.map +1 -1
  50. package/dist/query-info.d.ts +3 -3
  51. package/dist/query-info.d.ts.map +1 -1
  52. package/dist/schema/EventId.d.ts +1 -0
  53. package/dist/schema/EventId.d.ts.map +1 -1
  54. package/dist/schema/EventId.js +3 -0
  55. package/dist/schema/EventId.js.map +1 -1
  56. package/dist/schema/mutations.d.ts +1 -1
  57. package/dist/schema/system-tables.d.ts +1 -1
  58. package/dist/schema-management/migrations.d.ts +2 -2
  59. package/dist/schema-management/migrations.d.ts.map +1 -1
  60. package/dist/schema-management/migrations.js +6 -1
  61. package/dist/schema-management/migrations.js.map +1 -1
  62. package/dist/sync/ClientSessionSyncProcessor.d.ts +3 -5
  63. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  64. package/dist/sync/ClientSessionSyncProcessor.js +20 -9
  65. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +3 -3
  69. package/src/adapter-types.ts +35 -17
  70. package/src/devtools/devtools-bridge.ts +10 -7
  71. package/src/devtools/devtools-messages-client-session.ts +26 -10
  72. package/src/devtools/devtools-messages-common.ts +37 -8
  73. package/src/devtools/devtools-messages-leader.ts +73 -12
  74. package/src/devtools/devtools-messages.ts +2 -2
  75. package/src/leader-thread/LeaderSyncProcessor.ts +31 -16
  76. package/src/leader-thread/connection.ts +48 -3
  77. package/src/leader-thread/leader-worker-devtools.ts +80 -23
  78. package/src/leader-thread/make-leader-thread-layer.ts +20 -8
  79. package/src/leader-thread/recreate-db.ts +19 -10
  80. package/src/leader-thread/types.ts +16 -13
  81. package/src/query-builder/api.ts +3 -3
  82. package/src/query-builder/impl.test.ts +22 -1
  83. package/src/query-builder/impl.ts +2 -2
  84. package/src/query-info.ts +3 -3
  85. package/src/schema/EventId.ts +4 -0
  86. package/src/schema-management/migrations.ts +9 -5
  87. package/src/sync/ClientSessionSyncProcessor.ts +22 -11
  88. package/src/version.ts +1 -1
@@ -1,15 +1,20 @@
1
1
  import { Schema, Transferable } from '@livestore/utils/effect'
2
2
 
3
- import { NetworkStatus } from '../adapter-types.js'
3
+ import { NetworkStatus, UnexpectedError } from '../adapter-types.js'
4
+ import { EventId } from '../schema/mod.js'
4
5
  import * as MutationEvent from '../schema/MutationEvent.js'
5
- import { Disconnect, LSDMessage, LSDReqResMessage, Ping, Pong } from './devtools-messages-common.js'
6
+ import {
7
+ LeaderReqResMessage,
8
+ liveStoreVersion,
9
+ LSDMessage,
10
+ LSDReqResMessage,
11
+ requestId,
12
+ } from './devtools-messages-common.js'
6
13
 
7
14
  export class ResetAllDataReq extends LSDReqResMessage('LSD.Leader.ResetAllDataReq', {
8
15
  mode: Schema.Literal('all-data', 'only-app-db'),
9
16
  }) {}
10
17
 
11
- export class ResetAllDataRes extends LSDReqResMessage('LSD.Leader.ResetAllDataRes', {}) {}
12
-
13
18
  export class DatabaseFileInfoReq extends LSDReqResMessage('LSD.Leader.DatabaseFileInfoReq', {}) {}
14
19
 
15
20
  export class DatabaseFileInfo extends Schema.Struct({
@@ -47,6 +52,13 @@ export class SyncHistoryRes extends LSDReqResMessage('LSD.Leader.SyncHistoryRes'
47
52
  metadata: Schema.Option(Schema.JsonValue),
48
53
  }) {}
49
54
 
55
+ export class SyncHeadSubscribe extends LSDReqResMessage('LSD.Leader.SyncHeadSubscribe', {}) {}
56
+ export class SyncHeadUnsubscribe extends LSDReqResMessage('LSD.Leader.SyncHeadUnsubscribe', {}) {}
57
+ export class SyncHeadRes extends LSDReqResMessage('LSD.Leader.SyncHeadRes', {
58
+ local: EventId.EventId,
59
+ upstream: EventId.EventId,
60
+ }) {}
61
+
50
62
  export class SnapshotReq extends LSDReqResMessage('LSD.Leader.SnapshotReq', {}) {}
51
63
 
52
64
  export class SnapshotRes extends LSDReqResMessage('LSD.Leader.SnapshotRes', {
@@ -79,11 +91,55 @@ export class MutationLogRes extends LSDReqResMessage('LSD.Leader.MutationLogRes'
79
91
  mutationLog: Transferable.Uint8Array,
80
92
  }) {}
81
93
 
82
- export const MessageToAppLeader = Schema.Union(
94
+ export class Ping extends LSDReqResMessage('LSD.Leader.Ping', {}) {}
95
+
96
+ export class Pong extends LSDReqResMessage('LSD.Leader.Pong', {}) {}
97
+
98
+ export class Disconnect extends LSDReqResMessage('LSD.Leader.Disconnect', {}) {}
99
+
100
+ export const SetSyncLatch = LeaderReqResMessage('LSD.Leader.SetSyncLatch', {
101
+ payload: {
102
+ closeLatch: Schema.Boolean,
103
+ },
104
+ success: {},
105
+ })
106
+
107
+ export const ResetAllData = LeaderReqResMessage('LSD.Leader.ResetAllData', {
108
+ payload: {
109
+ mode: Schema.Literal('all-data', 'only-app-db'),
110
+ },
111
+ success: {},
112
+ })
113
+
114
+ // TODO move to `Schema.TaggedRequest` once new RPC is ready https://github.com/Effect-TS/effect/pull/4362
115
+ export class DatabaseFileInfo_ extends Schema.TaggedRequest<DatabaseFileInfo_>()('LSD.Leader.DatabaseFileInfo', {
116
+ payload: {
117
+ requestId,
118
+ liveStoreVersion,
119
+ },
120
+ success: DatabaseFileInfo,
121
+ failure: UnexpectedError,
122
+ }) {}
123
+
124
+ export class NetworkStatus_ extends Schema.TaggedRequest<NetworkStatus_>()('LSD.Leader.NetworkStatus', {
125
+ payload: {
126
+ requestId,
127
+ liveStoreVersion,
128
+ },
129
+ success: NetworkStatus,
130
+ failure: UnexpectedError,
131
+ }) {}
132
+
133
+ export const MessageToApp_ = Schema.Union(DatabaseFileInfo_, NetworkStatus_)
134
+
135
+ export type MessageToApp_ = typeof MessageToApp_.Type
136
+ //
137
+
138
+ export const MessageToApp = Schema.Union(
83
139
  SnapshotReq,
84
140
  LoadDatabaseFileReq,
85
141
  MutationLogReq,
86
- ResetAllDataReq,
142
+ ResetAllData.Request,
87
143
  NetworkStatusSubscribe,
88
144
  NetworkStatusUnsubscribe,
89
145
  Disconnect,
@@ -93,15 +149,17 @@ export const MessageToAppLeader = Schema.Union(
93
149
  SyncHistorySubscribe,
94
150
  SyncHistoryUnsubscribe,
95
151
  SyncingInfoReq,
96
- ).annotations({ identifier: 'LSD.MessageToAppLeader' })
152
+ SyncHeadSubscribe,
153
+ SyncHeadUnsubscribe,
154
+ SetSyncLatch.Request,
155
+ ).annotations({ identifier: 'LSD.Leader.MessageToApp' })
97
156
 
98
- export type MessageToAppLeader = typeof MessageToAppLeader.Type
157
+ export type MessageToApp = typeof MessageToApp.Type
99
158
 
100
- export const MessageFromAppLeader = Schema.Union(
159
+ export const MessageFromApp = Schema.Union(
101
160
  SnapshotRes,
102
161
  LoadDatabaseFileRes,
103
162
  MutationLogRes,
104
- ResetAllDataRes,
105
163
  Disconnect,
106
164
  MutationBroadcast,
107
165
  NetworkStatusRes,
@@ -110,6 +168,9 @@ export const MessageFromAppLeader = Schema.Union(
110
168
  DatabaseFileInfoRes,
111
169
  SyncHistoryRes,
112
170
  SyncingInfoRes,
113
- ).annotations({ identifier: 'LSD.MessageFromAppLeader' })
171
+ SyncHeadRes,
172
+ ResetAllData.Response,
173
+ SetSyncLatch.Response,
174
+ ).annotations({ identifier: 'LSD.Leader.MessageFromApp' })
114
175
 
115
- export type MessageFromAppLeader = typeof MessageFromAppLeader.Type
176
+ export type MessageFromApp = typeof MessageFromApp.Type
@@ -1,3 +1,3 @@
1
- export * from './devtools-messages-client-session.js'
2
- export * from './devtools-messages-leader.js'
1
+ export * as ClientSession from './devtools-messages-client-session.js'
2
+ export * as Leader from './devtools-messages-leader.js'
3
3
  export * from './devtools-messages-common.js'
@@ -1,4 +1,4 @@
1
- import { isNotUndefined, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
1
+ import { isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
2
2
  import type { HttpClient, Scope, Tracer } from '@livestore/utils/effect'
3
3
  import {
4
4
  BucketQueue,
@@ -93,8 +93,7 @@ export const makeLeaderSyncProcessor = ({
93
93
  | {
94
94
  otelSpan: otel.Span | undefined
95
95
  span: Tracer.Span
96
- devtoolsPullLatch: Effect.Latch | undefined
97
- devtoolsPushLatch: Effect.Latch | undefined
96
+ devtoolsLatch: Effect.Latch | undefined
98
97
  },
99
98
  }
100
99
 
@@ -107,10 +106,6 @@ export const makeLeaderSyncProcessor = ({
107
106
  // TODO validate batch
108
107
  if (newEvents.length === 0) return
109
108
 
110
- if (ctxRef.current?.devtoolsPushLatch !== undefined) {
111
- yield* ctxRef.current.devtoolsPushLatch.await
112
- }
113
-
114
109
  const waitForProcessing = options?.waitForProcessing ?? false
115
110
 
116
111
  if (waitForProcessing) {
@@ -164,8 +159,7 @@ export const makeLeaderSyncProcessor = ({
164
159
  ctxRef.current = {
165
160
  otelSpan,
166
161
  span,
167
- devtoolsPullLatch: devtools.enabled ? devtools.syncBackendPullLatch : undefined,
168
- devtoolsPushLatch: devtools.enabled ? devtools.syncBackendPushLatch : undefined,
162
+ devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
169
163
  }
170
164
 
171
165
  const initialBackendHead = dbMissing ? EventId.ROOT.global : getBackendHeadFromDb(dbMutationLog)
@@ -220,7 +214,12 @@ export const makeLeaderSyncProcessor = ({
220
214
 
221
215
  yield* FiberHandle.run(
222
216
  backendPushingFiberHandle,
223
- backgroundBackendPushing({ dbReady, syncBackendQueue, otelSpan }).pipe(Effect.tapCauseLogPretty),
217
+ backgroundBackendPushing({
218
+ dbReady,
219
+ syncBackendQueue,
220
+ otelSpan,
221
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
222
+ }).pipe(Effect.tapCauseLogPretty),
224
223
  )
225
224
 
226
225
  yield* backgroundBackendPulling({
@@ -239,7 +238,12 @@ export const makeLeaderSyncProcessor = ({
239
238
  // Restart pushing fiber
240
239
  yield* FiberHandle.run(
241
240
  backendPushingFiberHandle,
242
- backgroundBackendPushing({ dbReady, syncBackendQueue, otelSpan }).pipe(Effect.tapCauseLogPretty),
241
+ backgroundBackendPushing({
242
+ dbReady,
243
+ syncBackendQueue,
244
+ otelSpan,
245
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
246
+ }).pipe(Effect.tapCauseLogPretty),
243
247
  )
244
248
  }),
245
249
  syncStateSref,
@@ -247,8 +251,10 @@ export const makeLeaderSyncProcessor = ({
247
251
  pullLatch,
248
252
  otelSpan,
249
253
  initialBlockingSyncContext,
250
- devtoolsPullLatch: ctxRef.current?.devtoolsPullLatch,
254
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
251
255
  }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
256
+
257
+ return { initialLeaderHead: initialLocalHead }
252
258
  }).pipe(Effect.withSpanScoped('@livestore/common:leader-thread:syncing'))
253
259
 
254
260
  return {
@@ -431,7 +437,7 @@ const backgroundBackendPulling = ({
431
437
  syncStateSref,
432
438
  localPushesLatch,
433
439
  pullLatch,
434
- devtoolsPullLatch,
440
+ devtoolsLatch,
435
441
  initialBlockingSyncContext,
436
442
  }: {
437
443
  dbReady: Deferred.Deferred<void>
@@ -444,7 +450,7 @@ const backgroundBackendPulling = ({
444
450
  syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
445
451
  localPushesLatch: Effect.Latch
446
452
  pullLatch: Effect.Latch
447
- devtoolsPullLatch: Effect.Latch | undefined
453
+ devtoolsLatch: Effect.Latch | undefined
448
454
  initialBlockingSyncContext: InitialBlockingSyncContext
449
455
  }) =>
450
456
  Effect.gen(function* () {
@@ -466,8 +472,8 @@ const backgroundBackendPulling = ({
466
472
  Effect.gen(function* () {
467
473
  if (newEvents.length === 0) return
468
474
 
469
- if (devtoolsPullLatch !== undefined) {
470
- yield* devtoolsPullLatch.await
475
+ if (devtoolsLatch !== undefined) {
476
+ yield* devtoolsLatch.await
471
477
  }
472
478
 
473
479
  // Prevent more local pushes from being processed until this pull is finished
@@ -646,10 +652,12 @@ const backgroundBackendPushing = ({
646
652
  dbReady,
647
653
  syncBackendQueue,
648
654
  otelSpan,
655
+ devtoolsLatch,
649
656
  }: {
650
657
  dbReady: Deferred.Deferred<void>
651
658
  syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
652
659
  otelSpan: otel.Span | undefined
660
+ devtoolsLatch: Effect.Latch | undefined
653
661
  }) =>
654
662
  Effect.gen(function* () {
655
663
  const { syncBackend, dbMutationLog } = yield* LeaderThreadCtx
@@ -665,6 +673,10 @@ const backgroundBackendPushing = ({
665
673
 
666
674
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
667
675
 
676
+ if (devtoolsLatch !== undefined) {
677
+ yield* devtoolsLatch.await
678
+ }
679
+
668
680
  otelSpan?.addEvent('backend-push', {
669
681
  batchSize: queueItems.length,
670
682
  batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
@@ -674,6 +686,9 @@ const backgroundBackendPushing = ({
674
686
  const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
675
687
 
676
688
  if (pushResult._tag === 'Left') {
689
+ if (LS_DEV) {
690
+ yield* Effect.logDebug('backend-push-error', { error: pushResult.left.toString() })
691
+ }
677
692
  otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() })
678
693
  // wait for interrupt caused by background pulling which will then restart pushing
679
694
  return yield* Effect.never
@@ -12,13 +12,58 @@ namespace WaSqlite {
12
12
  export type SQLiteError = any
13
13
  }
14
14
 
15
- export const configureConnection = (sqliteDb: SqliteDb, { fkEnabled }: { fkEnabled: boolean }) =>
15
+ type ConnectionOptions = {
16
+ /**
17
+ * The database connection locking mode.
18
+ *
19
+ * @remarks
20
+ *
21
+ * This **option is ignored** when used on an **in-memory database** as they can only operate in exclusive locking mode.
22
+ * In-memory databases can’t share state between connections (unless using a
23
+ * {@link https://www.sqlite.org/sharedcache.html#shared_cache_and_in_memory_databases|shared cache}),
24
+ * making concurrent access impossible. This is functionally equivalent to exclusive locking.
25
+ *
26
+ * @defaultValue
27
+ * The default is `"NORMAL"` unless it was unless overridden at compile-time using `SQLITE_DEFAULT_LOCKING_MODE`.
28
+ *
29
+ * @see {@link https://www.sqlite.org/pragma.html#pragma_locking_mode|`locking_mode` pragma}
30
+ */
31
+ lockingMode?: 'NORMAL' | 'EXCLUSIVE'
32
+
33
+ /**
34
+ * Whether to enforce foreign key constraints.
35
+ *
36
+ * @privateRemarks
37
+ *
38
+ * We require a value for this option to minimize future problems, as the default value might change in future
39
+ * versions of SQLite.
40
+ *
41
+ * @see {@link https://www.sqlite.org/pragma.html#pragma_foreign_keys|`foreign_keys` pragma}
42
+ */
43
+ foreignKeys: boolean
44
+ }
45
+
46
+ export const configureConnection = (sqliteDb: SqliteDb, { foreignKeys, lockingMode }: ConnectionOptions) =>
16
47
  execSql(
17
48
  sqliteDb,
49
+ // We use the WAL journal mode is significantly faster in most scenarios than the traditional rollback journal mode.
50
+ // It specifically significantly improves write performance. However, when using the WAL journal mode, transactions
51
+ // that involve changes against multiple ATTACHed databases are atomic for each database but are not atomic
52
+ // across all databases as a set. Additionally, it is not possible to change the page size after entering WAL mode,
53
+ // whether on an empty database or by using VACUUM or the backup API. To change the page size, we must switch to the
54
+ // rollback journal mode.
55
+ //
56
+ // When connected to an in-memory database, the WAL journal mode option is ignored because an in-memory database can
57
+ // only be in either the MEMORY or OFF options. By default, an in-memory database is in the MEMORY option, which
58
+ // means that it stores the rollback journal in volatile RAM. This saves disk I/O but at the expense of safety and
59
+ // integrity. If the thread using SQLite crashes in the middle of a transaction, then the database file will very
60
+ // likely go corrupt.
18
61
  sql`
62
+ -- disable WAL until we have it working properly
63
+ -- PRAGMA journal_mode=WAL;
19
64
  PRAGMA page_size=8192;
20
- PRAGMA journal_mode=MEMORY;
21
- ${fkEnabled ? sql`PRAGMA foreign_keys='ON';` : sql`PRAGMA foreign_keys='OFF';`}
65
+ PRAGMA foreign_keys=${foreignKeys ? 'ON' : 'OFF'};
66
+ ${lockingMode === undefined ? '' : sql`PRAGMA locking_mode=${lockingMode};`}
22
67
  `,
23
68
  {},
24
69
  )
@@ -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) =>
@@ -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,7 +66,7 @@ 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
  }) =>
@@ -79,6 +79,8 @@ const listenToDevtools = ({
79
79
  shutdownStateSubRef,
80
80
  shutdownChannel,
81
81
  syncProcessor,
82
+ clientId,
83
+ devtools,
82
84
  } = yield* LeaderThreadCtx
83
85
 
84
86
  type RequestId = string
@@ -89,22 +91,22 @@ const listenToDevtools = ({
89
91
  Effect.gen(function* () {
90
92
  // yield* Effect.logDebug('[@livestore/common:leader-thread:devtools] incomingMessage', decodedEvent)
91
93
 
92
- if (decodedEvent._tag === 'LSD.Disconnect') {
94
+ if (decodedEvent._tag === 'LSD.Leader.Disconnect') {
93
95
  return
94
96
  }
95
97
 
96
98
  const { requestId } = decodedEvent
97
- const reqPayload = { requestId, liveStoreVersion }
99
+ const reqPayload = { requestId, liveStoreVersion, clientId }
98
100
 
99
101
  switch (decodedEvent._tag) {
100
- case 'LSD.Ping': {
101
- yield* sendMessage(Devtools.Pong.make({ ...reqPayload }))
102
+ case 'LSD.Leader.Ping': {
103
+ yield* sendMessage(Devtools.Leader.Pong.make({ ...reqPayload }))
102
104
  return
103
105
  }
104
106
  case 'LSD.Leader.SnapshotReq': {
105
107
  const snapshot = dbReadModel.export()
106
108
 
107
- yield* sendMessage(Devtools.SnapshotRes.make({ snapshot, ...reqPayload }))
109
+ yield* sendMessage(Devtools.Leader.SnapshotRes.make({ snapshot, ...reqPayload }))
108
110
 
109
111
  return
110
112
  }
@@ -125,7 +127,9 @@ const listenToDevtools = ({
125
127
  tmpDb.close()
126
128
  } catch (e) {
127
129
  yield* Effect.logError(`Error importing database file`, e)
128
- yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-file' }))
130
+ yield* sendMessage(
131
+ Devtools.Leader.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-file' }),
132
+ )
129
133
 
130
134
  return
131
135
  }
@@ -143,17 +147,19 @@ const listenToDevtools = ({
143
147
 
144
148
  dbMutationLog.destroy()
145
149
  } else {
146
- yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-database' }))
150
+ yield* sendMessage(
151
+ Devtools.Leader.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-database' }),
152
+ )
147
153
  return
148
154
  }
149
155
 
150
- yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'ok' }))
156
+ yield* sendMessage(Devtools.Leader.LoadDatabaseFileRes.make({ ...reqPayload, status: 'ok' }))
151
157
 
152
158
  yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-import' })) ?? Effect.void
153
159
 
154
160
  return
155
161
  }
156
- case 'LSD.Leader.ResetAllDataReq': {
162
+ case 'LSD.Leader.ResetAllData.Request': {
157
163
  const { mode } = decodedEvent
158
164
 
159
165
  yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
@@ -164,7 +170,7 @@ const listenToDevtools = ({
164
170
  dbMutationLog.destroy()
165
171
  }
166
172
 
167
- yield* sendMessage(Devtools.ResetAllDataRes.make({ ...reqPayload }))
173
+ yield* sendMessage(Devtools.Leader.ResetAllData.Response.make({ ...reqPayload }))
168
174
 
169
175
  yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-reset' })) ?? Effect.void
170
176
 
@@ -181,7 +187,7 @@ const listenToDevtools = ({
181
187
  const mutationLogFileSize = dbMutationLog.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
182
188
 
183
189
  yield* sendMessage(
184
- Devtools.DatabaseFileInfoRes.make({
190
+ Devtools.Leader.DatabaseFileInfoRes.make({
185
191
  readModel: { fileSize: dbFileSize, persistenceInfo: persistenceInfo.readModel },
186
192
  mutationLog: { fileSize: mutationLogFileSize, persistenceInfo: persistenceInfo.mutationLog },
187
193
  ...reqPayload,
@@ -193,14 +199,14 @@ const listenToDevtools = ({
193
199
  case 'LSD.Leader.MutationLogReq': {
194
200
  const mutationLog = dbMutationLog.export()
195
201
 
196
- yield* sendMessage(Devtools.MutationLogRes.make({ mutationLog, ...reqPayload }))
202
+ yield* sendMessage(Devtools.Leader.MutationLogRes.make({ mutationLog, ...reqPayload }))
197
203
 
198
204
  return
199
205
  }
200
206
  case 'LSD.Leader.RunMutationReq': {
201
207
  yield* syncProcessor.pushPartial(decodedEvent.mutationEventEncoded)
202
208
 
203
- yield* sendMessage(Devtools.RunMutationRes.make({ ...reqPayload }))
209
+ yield* sendMessage(Devtools.Leader.RunMutationRes.make({ ...reqPayload }))
204
210
 
205
211
  return
206
212
  }
@@ -213,7 +219,7 @@ const listenToDevtools = ({
213
219
  Stream.map((_) => _.batch),
214
220
  Stream.flattenIterables,
215
221
  Stream.tap(({ mutationEventEncoded, metadata }) =>
216
- sendMessage(Devtools.SyncHistoryRes.make({ mutationEventEncoded, metadata, ...reqPayload })),
222
+ sendMessage(Devtools.Leader.SyncHistoryRes.make({ mutationEventEncoded, metadata, ...reqPayload })),
217
223
  ),
218
224
  Stream.runDrain,
219
225
  Effect.acquireRelease(() => Effect.log('syncHistorySubscribe done')),
@@ -234,12 +240,12 @@ const listenToDevtools = ({
234
240
  return
235
241
  }
236
242
  case 'LSD.Leader.SyncingInfoReq': {
237
- const syncingInfo = Devtools.SyncingInfo.make({
243
+ const syncingInfo = Devtools.Leader.SyncingInfo.make({
238
244
  enabled: syncBackend !== undefined,
239
245
  metadata: {},
240
246
  })
241
247
 
242
- yield* sendMessage(Devtools.SyncingInfoRes.make({ syncingInfo, ...reqPayload }))
248
+ yield* sendMessage(Devtools.Leader.SyncingInfoRes.make({ syncingInfo, ...reqPayload }))
243
249
 
244
250
  return
245
251
  }
@@ -252,11 +258,14 @@ const listenToDevtools = ({
252
258
  // This is probably the same "flaky databrowser loading" bug as we're seeing in the playwright tests
253
259
  yield* Effect.sleep(1000)
254
260
 
255
- yield* syncBackend.isConnected.changes.pipe(
256
- 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 }]) =>
257
266
  sendMessage(
258
- Devtools.NetworkStatusRes.make({
259
- networkStatus: { isConnected, timestampMs: Date.now() },
267
+ Devtools.Leader.NetworkStatusRes.make({
268
+ networkStatus: { isConnected, timestampMs: Date.now(), latchClosed },
260
269
  ...reqPayload,
261
270
  }),
262
271
  ),
@@ -277,6 +286,54 @@ const listenToDevtools = ({
277
286
 
278
287
  return
279
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
+ }
280
337
  }
281
338
  }).pipe(Effect.withSpan(`@livestore/common:leader-thread:onDevtoolsMessage:${decodedEvent._tag}`)),
282
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, MakeSqliteDb, SqliteError } 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'
@@ -67,15 +67,15 @@ export const makeLeaderThreadLayer = ({
67
67
  initialBlockingSyncContext,
68
68
  })
69
69
 
70
- const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.MessageToAppLeader>().pipe(
70
+ const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.Leader.MessageToApp>().pipe(
71
71
  Effect.acquireRelease(Queue.shutdown),
72
72
  )
73
73
 
74
74
  const devtoolsContext = devtoolsOptions.enabled
75
75
  ? {
76
76
  enabled: true as const,
77
- syncBackendPullLatch: yield* Effect.makeLatch(true),
78
- syncBackendPushLatch: yield* Effect.makeLatch(true),
77
+ syncBackendLatch: yield* Effect.makeLatch(true),
78
+ syncBackendLatchState: yield* SubscriptionRef.make<{ latchClosed: boolean }>({ latchClosed: false }),
79
79
  }
80
80
  : { enabled: false as const }
81
81
 
@@ -95,6 +95,8 @@ export const makeLeaderThreadLayer = ({
95
95
  connectedClientSessionPullQueues: yield* makePullQueueSet,
96
96
  extraIncomingMessagesQueue,
97
97
  devtools: devtoolsContext,
98
+ // State will be set during `bootLeaderThread`
99
+ initialState: {} as any as LeaderThreadCtx['Type']['initialState'],
98
100
  } satisfies typeof LeaderThreadCtx.Service
99
101
 
100
102
  // @ts-expect-error For debugging purposes
@@ -102,7 +104,11 @@ export const makeLeaderThreadLayer = ({
102
104
 
103
105
  const layer = Layer.succeed(LeaderThreadCtx, ctx)
104
106
 
105
- 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))
106
112
 
107
113
  return layer
108
114
  }).pipe(
@@ -172,7 +178,7 @@ const bootLeaderThread = ({
172
178
  initialBlockingSyncContext: InitialBlockingSyncContext
173
179
  devtoolsOptions: DevtoolsOptions
174
180
  }): Effect.Effect<
175
- void,
181
+ LeaderThreadCtx['Type']['initialState'],
176
182
  UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
177
183
  LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
178
184
  > =>
@@ -206,10 +212,14 @@ const bootLeaderThread = ({
206
212
 
207
213
  // We're already starting pulling from the sync backend concurrently but wait until the db is ready before
208
214
  // processing any incoming mutations
209
- yield* syncProcessor.boot({ dbReady })
215
+ const { initialLeaderHead } = yield* syncProcessor.boot({ dbReady })
210
216
 
217
+ let migrationsReport: MigrationsReport
211
218
  if (dbMissing) {
212
- yield* recreateDb
219
+ const recreateResult = yield* recreateDb
220
+ migrationsReport = recreateResult.migrationsReport
221
+ } else {
222
+ migrationsReport = { migrations: [] }
213
223
  }
214
224
 
215
225
  yield* Deferred.succeed(dbReady, void 0)
@@ -221,4 +231,6 @@ const bootLeaderThread = ({
221
231
  yield* Queue.offer(bootStatusQueue, { stage: 'done' })
222
232
 
223
233
  yield* bootDevtools(devtoolsOptions).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
234
+
235
+ return { migrationsReport, leaderHead: initialLeaderHead }
224
236
  })