@livestore/common 0.2.0-dev.2 → 0.3.0-dev.0

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 (244) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +163 -1
  3. package/dist/__tests__/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/fixture.js +3 -1
  5. package/dist/__tests__/fixture.js.map +1 -1
  6. package/dist/adapter-types.d.ts +53 -38
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +5 -7
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/bounded-collections.d.ts +2 -2
  11. package/dist/bounded-collections.d.ts.map +1 -1
  12. package/dist/debug-info.d.ts +13 -13
  13. package/dist/derived-mutations.d.ts +1 -1
  14. package/dist/derived-mutations.d.ts.map +1 -1
  15. package/dist/devtools/devtools-bridge.d.ts +2 -2
  16. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  17. package/dist/devtools/devtools-messages.d.ts +84 -196
  18. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  19. package/dist/devtools/devtools-messages.js +55 -61
  20. package/dist/devtools/devtools-messages.js.map +1 -1
  21. package/dist/devtools/index.d.ts.map +1 -1
  22. package/dist/devtools/index.js +1 -2
  23. package/dist/devtools/index.js.map +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/init-singleton-tables.d.ts +1 -1
  29. package/dist/init-singleton-tables.d.ts.map +1 -1
  30. package/dist/leader-thread/apply-mutation.d.ts +8 -0
  31. package/dist/leader-thread/apply-mutation.d.ts.map +1 -0
  32. package/dist/leader-thread/apply-mutation.js +95 -0
  33. package/dist/leader-thread/apply-mutation.js.map +1 -0
  34. package/dist/leader-thread/connection.d.ts +11 -0
  35. package/dist/leader-thread/connection.d.ts.map +1 -0
  36. package/dist/leader-thread/connection.js +44 -0
  37. package/dist/leader-thread/connection.js.map +1 -0
  38. package/dist/leader-thread/leader-sync-processor.d.ts +47 -0
  39. package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -0
  40. package/dist/leader-thread/leader-sync-processor.js +422 -0
  41. package/dist/leader-thread/leader-sync-processor.js.map +1 -0
  42. package/dist/leader-thread/leader-worker-devtools.d.ts +6 -0
  43. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -0
  44. package/dist/leader-thread/leader-worker-devtools.js +216 -0
  45. package/dist/leader-thread/leader-worker-devtools.js.map +1 -0
  46. package/dist/leader-thread/make-leader-thread-layer.d.ts +20 -0
  47. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -0
  48. package/dist/leader-thread/make-leader-thread-layer.js +106 -0
  49. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -0
  50. package/dist/leader-thread/mod.d.ts +7 -0
  51. package/dist/leader-thread/mod.d.ts.map +1 -0
  52. package/dist/leader-thread/mod.js +7 -0
  53. package/dist/leader-thread/mod.js.map +1 -0
  54. package/dist/leader-thread/mutationlog.d.ts +23 -0
  55. package/dist/leader-thread/mutationlog.d.ts.map +1 -0
  56. package/dist/leader-thread/mutationlog.js +27 -0
  57. package/dist/leader-thread/mutationlog.js.map +1 -0
  58. package/dist/leader-thread/pull-queue-set.d.ts +7 -0
  59. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -0
  60. package/dist/leader-thread/pull-queue-set.js +39 -0
  61. package/dist/leader-thread/pull-queue-set.js.map +1 -0
  62. package/dist/leader-thread/recreate-db.d.ts +7 -0
  63. package/dist/leader-thread/recreate-db.d.ts.map +1 -0
  64. package/dist/leader-thread/recreate-db.js +69 -0
  65. package/dist/leader-thread/recreate-db.js.map +1 -0
  66. package/dist/leader-thread/shutdown-channel.d.ts +15 -0
  67. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -0
  68. package/dist/leader-thread/shutdown-channel.js +7 -0
  69. package/dist/leader-thread/shutdown-channel.js.map +1 -0
  70. package/dist/leader-thread/types.d.ts +87 -0
  71. package/dist/leader-thread/types.d.ts.map +1 -0
  72. package/dist/leader-thread/types.js +11 -0
  73. package/dist/leader-thread/types.js.map +1 -0
  74. package/dist/mutation.d.ts +3 -4
  75. package/dist/mutation.d.ts.map +1 -1
  76. package/dist/mutation.js +0 -14
  77. package/dist/mutation.js.map +1 -1
  78. package/dist/otel.d.ts +7 -0
  79. package/dist/otel.d.ts.map +1 -0
  80. package/dist/otel.js +11 -0
  81. package/dist/otel.js.map +1 -0
  82. package/dist/query-builder/api.d.ts +2 -2
  83. package/dist/query-builder/api.d.ts.map +1 -1
  84. package/dist/query-builder/api.js.map +1 -1
  85. package/dist/query-builder/impl.d.ts +1 -1
  86. package/dist/query-builder/impl.d.ts.map +1 -1
  87. package/dist/query-builder/impl.js +23 -5
  88. package/dist/query-builder/impl.js.map +1 -1
  89. package/dist/query-builder/impl.test.js +30 -1
  90. package/dist/query-builder/impl.test.js.map +1 -1
  91. package/dist/query-info.d.ts +1 -1
  92. package/dist/query-info.d.ts.map +1 -1
  93. package/dist/rehydrate-from-mutationlog.d.ts +1 -1
  94. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  95. package/dist/rehydrate-from-mutationlog.js +6 -6
  96. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  97. package/dist/schema/EventId.d.ts +37 -0
  98. package/dist/schema/EventId.d.ts.map +1 -0
  99. package/dist/schema/EventId.js +30 -0
  100. package/dist/schema/EventId.js.map +1 -0
  101. package/dist/schema/MutationEvent.d.ts +191 -0
  102. package/dist/schema/MutationEvent.d.ts.map +1 -0
  103. package/dist/schema/MutationEvent.js +56 -0
  104. package/dist/schema/MutationEvent.js.map +1 -0
  105. package/dist/schema/mod.d.ts +8 -0
  106. package/dist/schema/mod.d.ts.map +1 -0
  107. package/dist/schema/mod.js +8 -0
  108. package/dist/schema/mod.js.map +1 -0
  109. package/dist/schema/mutations.d.ts +3 -123
  110. package/dist/schema/mutations.d.ts.map +1 -1
  111. package/dist/schema/mutations.js +0 -26
  112. package/dist/schema/mutations.js.map +1 -1
  113. package/dist/schema/{index.d.ts → schema.d.ts} +1 -5
  114. package/dist/schema/schema.d.ts.map +1 -0
  115. package/dist/schema/{index.js → schema.js} +1 -5
  116. package/dist/schema/schema.js.map +1 -0
  117. package/dist/schema/system-tables.d.ts +55 -29
  118. package/dist/schema/system-tables.d.ts.map +1 -1
  119. package/dist/schema/system-tables.js +10 -5
  120. package/dist/schema/system-tables.js.map +1 -1
  121. package/dist/schema-management/migrations.d.ts +1 -1
  122. package/dist/schema-management/migrations.d.ts.map +1 -1
  123. package/dist/schema-management/migrations.js +6 -1
  124. package/dist/schema-management/migrations.js.map +1 -1
  125. package/dist/schema-management/validate-mutation-defs.d.ts +1 -1
  126. package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
  127. package/dist/sync/client-session-sync-processor.d.ts +45 -0
  128. package/dist/sync/client-session-sync-processor.d.ts.map +1 -0
  129. package/dist/sync/client-session-sync-processor.js +131 -0
  130. package/dist/sync/client-session-sync-processor.js.map +1 -0
  131. package/dist/sync/index.d.ts +2 -0
  132. package/dist/sync/index.d.ts.map +1 -1
  133. package/dist/sync/index.js +2 -0
  134. package/dist/sync/index.js.map +1 -1
  135. package/dist/sync/next/compact-events.d.ts +1 -1
  136. package/dist/sync/next/compact-events.d.ts.map +1 -1
  137. package/dist/sync/next/compact-events.js +2 -1
  138. package/dist/sync/next/compact-events.js.map +1 -1
  139. package/dist/sync/next/facts.d.ts +5 -5
  140. package/dist/sync/next/facts.d.ts.map +1 -1
  141. package/dist/sync/next/facts.js +1 -1
  142. package/dist/sync/next/facts.js.map +1 -1
  143. package/dist/sync/next/history-dag-common.d.ts +30 -0
  144. package/dist/sync/next/history-dag-common.d.ts.map +1 -0
  145. package/dist/sync/next/history-dag-common.js +20 -0
  146. package/dist/sync/next/history-dag-common.js.map +1 -0
  147. package/dist/sync/next/history-dag.d.ts +4 -27
  148. package/dist/sync/next/history-dag.d.ts.map +1 -1
  149. package/dist/sync/next/history-dag.js +1 -19
  150. package/dist/sync/next/history-dag.js.map +1 -1
  151. package/dist/sync/next/mod.d.ts +1 -0
  152. package/dist/sync/next/mod.d.ts.map +1 -1
  153. package/dist/sync/next/mod.js +1 -0
  154. package/dist/sync/next/mod.js.map +1 -1
  155. package/dist/sync/next/rebase-events.d.ts +3 -2
  156. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  157. package/dist/sync/next/rebase-events.js.map +1 -1
  158. package/dist/sync/next/test/compact-events.test.d.ts.map +1 -1
  159. package/dist/sync/next/test/compact-events.test.js +2 -1
  160. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  161. package/dist/sync/next/test/mutation-fixtures.d.ts +1 -1
  162. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  163. package/dist/sync/next/test/mutation-fixtures.js +4 -3
  164. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  165. package/dist/sync/sync.d.ts +33 -12
  166. package/dist/sync/sync.d.ts.map +1 -1
  167. package/dist/sync/sync.js +10 -1
  168. package/dist/sync/sync.js.map +1 -1
  169. package/dist/sync/syncstate.d.ts +123 -0
  170. package/dist/sync/syncstate.d.ts.map +1 -0
  171. package/dist/sync/syncstate.js +248 -0
  172. package/dist/sync/syncstate.js.map +1 -0
  173. package/dist/sync/syncstate.test.d.ts +2 -0
  174. package/dist/sync/syncstate.test.d.ts.map +1 -0
  175. package/dist/sync/syncstate.test.js +399 -0
  176. package/dist/sync/syncstate.test.js.map +1 -0
  177. package/dist/sync/validate-push-payload.d.ts +5 -0
  178. package/dist/sync/validate-push-payload.d.ts.map +1 -0
  179. package/dist/sync/validate-push-payload.js +15 -0
  180. package/dist/sync/validate-push-payload.js.map +1 -0
  181. package/dist/util.d.ts +2 -2
  182. package/dist/util.d.ts.map +1 -1
  183. package/dist/version.d.ts +2 -2
  184. package/dist/version.d.ts.map +1 -1
  185. package/dist/version.js +2 -2
  186. package/dist/version.js.map +1 -1
  187. package/package.json +13 -6
  188. package/src/__tests__/fixture.ts +5 -1
  189. package/src/adapter-types.ts +60 -34
  190. package/src/derived-mutations.test.ts +1 -1
  191. package/src/derived-mutations.ts +1 -1
  192. package/src/devtools/devtools-bridge.ts +2 -2
  193. package/src/devtools/devtools-messages.ts +70 -74
  194. package/src/devtools/index.ts +1 -2
  195. package/src/index.ts +2 -1
  196. package/src/init-singleton-tables.ts +1 -1
  197. package/src/leader-thread/apply-mutation.ts +143 -0
  198. package/src/leader-thread/connection.ts +67 -0
  199. package/src/leader-thread/leader-sync-processor.ts +666 -0
  200. package/src/leader-thread/leader-worker-devtools.ts +358 -0
  201. package/src/leader-thread/make-leader-thread-layer.ts +192 -0
  202. package/src/leader-thread/mod.ts +6 -0
  203. package/src/leader-thread/mutationlog.ts +42 -0
  204. package/src/leader-thread/pull-queue-set.ts +58 -0
  205. package/src/leader-thread/recreate-db.ts +109 -0
  206. package/src/leader-thread/shutdown-channel.ts +13 -0
  207. package/src/leader-thread/types.ts +129 -0
  208. package/src/mutation.ts +3 -21
  209. package/src/otel.ts +20 -0
  210. package/src/query-builder/api.ts +3 -2
  211. package/src/query-builder/impl.test.ts +35 -1
  212. package/src/query-builder/impl.ts +23 -6
  213. package/src/query-info.ts +1 -1
  214. package/src/rehydrate-from-mutationlog.ts +7 -11
  215. package/src/schema/EventId.ts +46 -0
  216. package/src/schema/MutationEvent.ts +161 -0
  217. package/src/schema/mod.ts +7 -0
  218. package/src/schema/mutations.ts +5 -126
  219. package/src/schema/{index.ts → schema.ts} +0 -5
  220. package/src/schema/system-tables.ts +18 -5
  221. package/src/schema-management/migrations.ts +9 -2
  222. package/src/schema-management/validate-mutation-defs.ts +1 -1
  223. package/src/sync/client-session-sync-processor.ts +207 -0
  224. package/src/sync/index.ts +2 -0
  225. package/src/sync/next/compact-events.ts +3 -2
  226. package/src/sync/next/facts.ts +11 -5
  227. package/src/sync/next/history-dag-common.ts +44 -0
  228. package/src/sync/next/history-dag.ts +3 -45
  229. package/src/sync/next/mod.ts +1 -0
  230. package/src/sync/next/rebase-events.ts +6 -5
  231. package/src/sync/next/test/compact-events.test.ts +3 -2
  232. package/src/sync/next/test/mutation-fixtures.ts +7 -6
  233. package/src/sync/sync.ts +32 -12
  234. package/src/sync/syncstate.test.ts +464 -0
  235. package/src/sync/syncstate.ts +385 -0
  236. package/src/sync/validate-push-payload.ts +18 -0
  237. package/src/version.ts +2 -2
  238. package/dist/schema/index.d.ts.map +0 -1
  239. package/dist/schema/index.js.map +0 -1
  240. package/dist/sync/next-mutation-event-id-pair.d.ts +0 -14
  241. package/dist/sync/next-mutation-event-id-pair.d.ts.map +0 -1
  242. package/dist/sync/next-mutation-event-id-pair.js +0 -13
  243. package/dist/sync/next-mutation-event-id-pair.js.map +0 -1
  244. package/src/sync/next-mutation-event-id-pair.ts +0 -20
@@ -0,0 +1,358 @@
1
+ import { Effect, FiberMap, Option, PubSub, Queue, Stream, SubscriptionRef } from '@livestore/utils/effect'
2
+
3
+ import { Devtools, IntentionalShutdownCause, liveStoreVersion, UnexpectedError } from '../index.js'
4
+ import { MUTATION_LOG_META_TABLE, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE } from '../schema/mod.js'
5
+ import type { ShutdownChannel } from './shutdown-channel.js'
6
+ import type { DevtoolsOptions, PersistenceInfoPair } from './types.js'
7
+ import { LeaderThreadCtx } from './types.js'
8
+
9
+ type SendMessageToDevtools = (
10
+ message: Devtools.MessageFromAppLeader,
11
+ options?: {
12
+ /** Send message even if not connected (e.g. for initial broadcast messages) */
13
+ force: boolean
14
+ },
15
+ ) => Effect.Effect<void>
16
+
17
+ // TODO bind scope to the webchannel lifetime
18
+ export const bootDevtools = (options: DevtoolsOptions) =>
19
+ Effect.gen(function* () {
20
+ if (options.enabled === false) {
21
+ return
22
+ }
23
+
24
+ const { persistenceInfo, shutdownChannel, devtoolsWebChannel } = yield* options.makeContext
25
+
26
+ const isConnected = yield* SubscriptionRef.make(true)
27
+
28
+ const incomingMessagesPubSub = yield* PubSub.unbounded<Devtools.MessageToAppLeader>().pipe(
29
+ Effect.acquireRelease(PubSub.shutdown),
30
+ )
31
+
32
+ const incomingMessages = Stream.fromPubSub(incomingMessagesPubSub)
33
+
34
+ const outgoingMessagesQueue = yield* Queue.unbounded<Devtools.MessageFromAppLeader>().pipe(
35
+ Effect.acquireRelease(Queue.shutdown),
36
+ )
37
+
38
+ const devtoolsCoordinatorChannel = devtoolsWebChannel
39
+ // coordinatorMessagePortOrChannel instanceof MessagePort
40
+ // ? yield* WebChannel.messagePortChannel({
41
+ // port: coordinatorMessagePortOrChannel,
42
+ // schema: { send: Devtools.MessageFromAppLeader, listen: Devtools.MessageToAppLeader },
43
+ // })
44
+ // : coordinatorMessagePortOrChannel
45
+
46
+ const sendMessage: SendMessageToDevtools = (message, options) =>
47
+ Effect.gen(function* () {
48
+ if (options?.force === true || (yield* isConnected)) {
49
+ yield* devtoolsCoordinatorChannel.send(message)
50
+ } else {
51
+ yield* Queue.offer(outgoingMessagesQueue, message)
52
+ }
53
+ }).pipe(
54
+ Effect.withSpan('@livestore/common:leader-thread:devtools:sendToDevtools'),
55
+ Effect.interruptible,
56
+ Effect.ignoreLogged,
57
+ )
58
+
59
+ // broadcastCallbacks.add((message) => sendMessage(message))
60
+
61
+ const { connectedClientSessionPullQueues, syncProcessor } = yield* LeaderThreadCtx
62
+ const { localHead } = yield* syncProcessor.syncState
63
+
64
+ // TODO close queue when devtools disconnects
65
+ const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(localHead)
66
+
67
+ yield* Stream.fromQueue(pullQueue).pipe(
68
+ Stream.tap((msg) =>
69
+ Effect.gen(function* () {
70
+ if (msg.payload._tag === 'upstream-advance') {
71
+ for (const mutationEventEncoded of msg.payload.newEvents) {
72
+ yield* sendMessage(
73
+ Devtools.MutationBroadcast.make({
74
+ mutationEventEncoded,
75
+
76
+ liveStoreVersion,
77
+ }),
78
+ )
79
+ }
80
+ } else {
81
+ yield* Effect.logWarning('TODO implement rebases in devtools')
82
+ }
83
+ }),
84
+ ),
85
+ Stream.runDrain,
86
+ Effect.forkScoped,
87
+ )
88
+
89
+ yield* devtoolsCoordinatorChannel.listen.pipe(
90
+ Stream.flatten(),
91
+ // Stream.tapLogWithLabel('@livestore/common:leader-thread:devtools:onPortMessage'),
92
+ Stream.tap((msg) =>
93
+ Effect.gen(function* () {
94
+ // yield* Effect.logDebug(`[@livestore/common:leader-thread:devtools] message from port: ${msg._tag}`, msg)
95
+ // if (msg._tag === 'LSD.MessagePortForStoreRes') {
96
+ // yield* Deferred.succeed(storeMessagePortDeferred, msg.port)
97
+ // } else {
98
+ yield* PubSub.publish(incomingMessagesPubSub, msg)
99
+ // }
100
+ }),
101
+ ),
102
+ Stream.runDrain,
103
+ Effect.withSpan(`@livestore/common:leader-thread:devtools:onPortMessage`),
104
+ Effect.ignoreLogged,
105
+ Effect.forkScoped,
106
+ )
107
+
108
+ // yield* sendMessage(Devtools.AppHostReady.make({ appHostId, liveStoreVersion, isLeader }), { force: true })
109
+
110
+ // yield* sendMessage(Devtools.MessagePortForStoreReq.make({ appHostId, liveStoreVersion, requestId: nanoid() }), {
111
+ // force: true,
112
+ // })
113
+
114
+ yield* listenToDevtools({
115
+ incomingMessages,
116
+ sendMessage,
117
+ // isConnected,
118
+ // disconnect,
119
+ // storeId,
120
+ // appHostId,
121
+ // isLeader,
122
+ persistenceInfo,
123
+ shutdownChannel,
124
+ })
125
+ }).pipe(Effect.withSpan('@livestore/common:leader-thread:devtools:boot'))
126
+
127
+ const listenToDevtools = ({
128
+ incomingMessages,
129
+ sendMessage,
130
+ // isConnected,
131
+ // disconnect,
132
+ // appHostId,
133
+ // storeId,
134
+ // isLeader,
135
+ persistenceInfo,
136
+ shutdownChannel,
137
+ }: {
138
+ incomingMessages: Stream.Stream<Devtools.MessageToAppLeader>
139
+ sendMessage: SendMessageToDevtools
140
+ // isConnected: SubscriptionRef.SubscriptionRef<boolean>
141
+ // disconnect: Effect.Effect<void>
142
+ // appHostId: string
143
+ // storeId: string
144
+ // isLeader: boolean
145
+ persistenceInfo: PersistenceInfoPair
146
+ shutdownChannel: ShutdownChannel
147
+ }) =>
148
+ Effect.gen(function* () {
149
+ const innerWorkerCtx = yield* LeaderThreadCtx
150
+ const { syncBackend, makeSyncDb, db, dbLog, shutdownStateSubRef, syncProcessor } = innerWorkerCtx
151
+
152
+ type RequestId = string
153
+ const subscriptionFiberMap = yield* FiberMap.make<RequestId>()
154
+
155
+ yield* incomingMessages.pipe(
156
+ Stream.tap((decodedEvent) =>
157
+ Effect.gen(function* () {
158
+ // yield* Effect.logDebug('[@livestore/common:leader-thread:devtools] incomingMessage', decodedEvent)
159
+
160
+ if (decodedEvent._tag === 'LSD.Disconnect') {
161
+ // yield* SubscriptionRef.set(isConnected, false)
162
+
163
+ // yield* disconnect
164
+
165
+ // TODO is there a better place for this?
166
+ // yield* sendMessage(Devtools.AppHostReady.make({ appHostId, liveStoreVersion, isLeader }), {
167
+ // force: true,
168
+ // })
169
+
170
+ return
171
+ }
172
+
173
+ const { requestId } = decodedEvent
174
+ const reqPayload = { requestId, liveStoreVersion }
175
+
176
+ switch (decodedEvent._tag) {
177
+ case 'LSD.Ping': {
178
+ yield* sendMessage(Devtools.Pong.make({ ...reqPayload }))
179
+ return
180
+ }
181
+ case 'LSD.Leader.SnapshotReq': {
182
+ const snapshot = db.export()
183
+
184
+ yield* sendMessage(Devtools.SnapshotRes.make({ snapshot, ...reqPayload }))
185
+
186
+ return
187
+ }
188
+ case 'LSD.Leader.LoadDatabaseFileReq': {
189
+ const { data } = decodedEvent
190
+
191
+ let tableNames: Set<string>
192
+
193
+ try {
194
+ const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
195
+ tmpSyncDb.import(data)
196
+ const tableNameResults = tmpSyncDb.select<{ name: string }>(
197
+ `select name from sqlite_master where type = 'table'`,
198
+ )
199
+
200
+ tableNames = new Set(tableNameResults.map((_) => _.name))
201
+
202
+ tmpSyncDb.close()
203
+ } catch (e) {
204
+ yield* Effect.logError(`Error importing database file`, e)
205
+ yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-file' }))
206
+
207
+ return
208
+ }
209
+
210
+ if (tableNames.has(MUTATION_LOG_META_TABLE)) {
211
+ yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
212
+
213
+ dbLog.import(data)
214
+
215
+ db.destroy()
216
+ } else if (tableNames.has(SCHEMA_META_TABLE) && tableNames.has(SCHEMA_MUTATIONS_META_TABLE)) {
217
+ yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
218
+
219
+ db.import(data)
220
+
221
+ dbLog.destroy()
222
+ } else {
223
+ yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'unsupported-database' }))
224
+ return
225
+ }
226
+
227
+ yield* sendMessage(Devtools.LoadDatabaseFileRes.make({ ...reqPayload, status: 'ok' }))
228
+
229
+ yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-import' }))
230
+
231
+ return
232
+ }
233
+ case 'LSD.Leader.ResetAllDataReq': {
234
+ const { mode } = decodedEvent
235
+
236
+ yield* SubscriptionRef.set(shutdownStateSubRef, 'shutting-down')
237
+
238
+ db.destroy()
239
+
240
+ if (mode === 'all-data') {
241
+ dbLog.destroy()
242
+ }
243
+
244
+ yield* sendMessage(Devtools.ResetAllDataRes.make({ ...reqPayload }))
245
+
246
+ yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'devtools-reset' }))
247
+
248
+ return
249
+ }
250
+ case 'LSD.Leader.DatabaseFileInfoReq': {
251
+ const dbSizeQuery = `SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();`
252
+ const dbFileSize = db.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
253
+ const mutationLogFileSize = dbLog.select<{ size: number }>(dbSizeQuery, undefined)[0]!.size
254
+
255
+ yield* sendMessage(
256
+ Devtools.DatabaseFileInfoRes.make({
257
+ db: { fileSize: dbFileSize, persistenceInfo: persistenceInfo.db },
258
+ mutationLog: { fileSize: mutationLogFileSize, persistenceInfo: persistenceInfo.mutationLog },
259
+ ...reqPayload,
260
+ }),
261
+ )
262
+
263
+ return
264
+ }
265
+ case 'LSD.Leader.MutationLogReq': {
266
+ const mutationLog = dbLog.export()
267
+
268
+ yield* sendMessage(Devtools.MutationLogRes.make({ mutationLog, ...reqPayload }))
269
+
270
+ return
271
+ }
272
+ case 'LSD.Leader.RunMutationReq': {
273
+ yield* syncProcessor.pushPartial(decodedEvent.mutationEventEncoded)
274
+
275
+ yield* sendMessage(Devtools.RunMutationRes.make({ ...reqPayload }))
276
+
277
+ return
278
+ }
279
+ case 'LSD.Leader.SyncHistorySubscribe': {
280
+ const { requestId } = decodedEvent
281
+
282
+ if (syncBackend !== undefined) {
283
+ // TODO consider piggybacking on the existing leader-thread sync-pulling
284
+ yield* syncBackend.pull(Option.none()).pipe(
285
+ Stream.map((_) => _.batch),
286
+ Stream.flattenIterables,
287
+ Stream.tap(({ mutationEventEncoded, metadata }) =>
288
+ sendMessage(Devtools.SyncHistoryRes.make({ mutationEventEncoded, metadata, ...reqPayload })),
289
+ ),
290
+ Stream.runDrain,
291
+ Effect.acquireRelease(() => Effect.log('syncHistorySubscribe done')),
292
+ Effect.interruptible,
293
+ Effect.tapCauseLogPretty,
294
+ FiberMap.run(subscriptionFiberMap, requestId),
295
+ )
296
+ }
297
+
298
+ return
299
+ }
300
+ case 'LSD.Leader.SyncHistoryUnsubscribe': {
301
+ const { requestId } = decodedEvent
302
+ console.log('LSD.SyncHistoryUnsubscribe', requestId)
303
+
304
+ yield* FiberMap.remove(subscriptionFiberMap, requestId)
305
+
306
+ return
307
+ }
308
+ case 'LSD.Leader.SyncingInfoReq': {
309
+ const syncingInfo = Devtools.SyncingInfo.make({
310
+ enabled: syncBackend !== undefined,
311
+ metadata: {},
312
+ })
313
+
314
+ yield* sendMessage(Devtools.SyncingInfoRes.make({ syncingInfo, ...reqPayload }))
315
+
316
+ return
317
+ }
318
+ case 'LSD.Leader.NetworkStatusSubscribe': {
319
+ if (syncBackend !== undefined) {
320
+ const { requestId } = decodedEvent
321
+
322
+ // TODO investigate and fix bug. seems that when sending messages right after
323
+ // the devtools have connected get sometimes lost
324
+ // This is probably the same "flaky databrowser loading" bug as we're seeing in the playwright tests
325
+ yield* Effect.sleep(1000)
326
+
327
+ yield* syncBackend.isConnected.changes.pipe(
328
+ Stream.tap((isConnected) =>
329
+ sendMessage(
330
+ Devtools.NetworkStatusRes.make({
331
+ networkStatus: { isConnected, timestampMs: Date.now() },
332
+ ...reqPayload,
333
+ }),
334
+ ),
335
+ ),
336
+ Stream.runDrain,
337
+ Effect.interruptible,
338
+ Effect.tapCauseLogPretty,
339
+ FiberMap.run(subscriptionFiberMap, requestId),
340
+ )
341
+ }
342
+
343
+ return
344
+ }
345
+ case 'LSD.Leader.NetworkStatusUnsubscribe': {
346
+ const { requestId } = decodedEvent
347
+
348
+ yield* FiberMap.remove(subscriptionFiberMap, requestId)
349
+
350
+ return
351
+ }
352
+ }
353
+ }).pipe(Effect.withSpan(`@livestore/common:leader-thread:onDevtoolsMessage:${decodedEvent._tag}`)),
354
+ ),
355
+ UnexpectedError.mapToUnexpectedErrorStream,
356
+ Stream.runDrain,
357
+ )
358
+ })
@@ -0,0 +1,192 @@
1
+ import type { HttpClient, Scope, WebChannel } from '@livestore/utils/effect'
2
+ import { Deferred, Effect, FiberSet, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
3
+
4
+ import type { BootStatus, MakeSynchronousDatabase, SqliteError, SynchronousDatabase } from '../adapter-types.js'
5
+ import { UnexpectedError } from '../adapter-types.js'
6
+ import type { LiveStoreSchema } from '../schema/mod.js'
7
+ import { EventId, MutationEvent, mutationLogMetaTable, SYNC_STATUS_TABLE, syncStatusTable } from '../schema/mod.js'
8
+ import { migrateTable } from '../schema-management/migrations.js'
9
+ import type { InvalidPullError, IsOfflineError, SyncBackend } from '../sync/sync.js'
10
+ import { sql } from '../util.js'
11
+ import { execSql } from './connection.js'
12
+ import { makeLeaderSyncProcessor } from './leader-sync-processor.js'
13
+ import { bootDevtools } from './leader-worker-devtools.js'
14
+ import { makePullQueueSet } from './pull-queue-set.js'
15
+ import { recreateDb } from './recreate-db.js'
16
+ import type { DevtoolsOptions, InitialBlockingSyncContext, InitialSyncOptions, ShutdownState } from './types.js'
17
+ import { LeaderThreadCtx } from './types.js'
18
+
19
+ export const makeLeaderThreadLayer = ({
20
+ schema,
21
+ storeId,
22
+ originId,
23
+ makeSyncDb,
24
+ makeSyncBackend,
25
+ db,
26
+ dbLog,
27
+ devtoolsOptions,
28
+ initialSyncOptions = { _tag: 'Skip' },
29
+ }: {
30
+ storeId: string
31
+ originId: string
32
+ schema: LiveStoreSchema
33
+ makeSyncDb: MakeSynchronousDatabase
34
+ makeSyncBackend: Effect.Effect<SyncBackend, UnexpectedError, Scope.Scope> | undefined
35
+ db: SynchronousDatabase
36
+ dbLog: SynchronousDatabase
37
+ devtoolsOptions: DevtoolsOptions
38
+ initialSyncOptions: InitialSyncOptions | undefined
39
+ }): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
40
+ Effect.gen(function* () {
41
+ const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
42
+
43
+ // TODO do more validation here than just checking the count of tables
44
+ // Either happens on initial boot or if schema changes
45
+ const dbMissing = db.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
46
+
47
+ const syncBackend = makeSyncBackend === undefined ? undefined : yield* makeSyncBackend
48
+
49
+ const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({ initialSyncOptions, bootStatusQueue })
50
+
51
+ const syncProcessor = yield* makeLeaderSyncProcessor({ schema, dbMissing, dbLog, initialBlockingSyncContext })
52
+
53
+ const ctx = {
54
+ schema,
55
+ bootStatusQueue,
56
+ storeId,
57
+ originId,
58
+ db,
59
+ dbLog,
60
+ makeSyncDb,
61
+ mutationEventSchema: MutationEvent.makeMutationEventSchema(schema),
62
+ shutdownStateSubRef: yield* SubscriptionRef.make<ShutdownState>('running'),
63
+ syncBackend,
64
+ syncProcessor,
65
+ connectedClientSessionPullQueues: yield* makePullQueueSet,
66
+ } satisfies typeof LeaderThreadCtx.Service
67
+
68
+ // @ts-expect-error For debugging purposes
69
+ globalThis.__leaderThreadCtx = ctx
70
+
71
+ const layer = Layer.succeed(LeaderThreadCtx, ctx)
72
+
73
+ yield* bootLeaderThread({ dbMissing, initialBlockingSyncContext, devtoolsOptions }).pipe(Effect.provide(layer))
74
+
75
+ return layer
76
+ }).pipe(
77
+ Effect.withSpan('@livestore/common:leader-thread:boot'),
78
+ Effect.withSpanScoped('@livestore/common:leader-thread'),
79
+ UnexpectedError.mapToUnexpectedError,
80
+ Layer.unwrapScoped,
81
+ )
82
+
83
+ const makeInitialBlockingSyncContext = ({
84
+ initialSyncOptions,
85
+ bootStatusQueue,
86
+ }: {
87
+ initialSyncOptions: InitialSyncOptions
88
+ bootStatusQueue: Queue.Queue<BootStatus>
89
+ }) =>
90
+ Effect.gen(function* () {
91
+ const ctx = {
92
+ isDone: false,
93
+ processedMutations: 0,
94
+ total: -1,
95
+ }
96
+
97
+ const blockingDeferred = initialSyncOptions._tag === 'Blocking' ? yield* Deferred.make<void>() : undefined
98
+
99
+ if (blockingDeferred !== undefined && initialSyncOptions._tag === 'Blocking') {
100
+ yield* Deferred.succeed(blockingDeferred, void 0).pipe(
101
+ Effect.delay(initialSyncOptions.timeout),
102
+ Effect.forkScoped,
103
+ )
104
+ }
105
+
106
+ return {
107
+ blockingDeferred,
108
+ update: ({ processed, remaining }) =>
109
+ Effect.gen(function* () {
110
+ if (ctx.isDone === true) return
111
+
112
+ if (ctx.total === -1) {
113
+ ctx.total = remaining + processed
114
+ }
115
+
116
+ ctx.processedMutations += processed
117
+ yield* Queue.offer(bootStatusQueue, {
118
+ stage: 'syncing',
119
+ progress: { done: ctx.processedMutations, total: ctx.total },
120
+ })
121
+
122
+ if (remaining === 0 && blockingDeferred !== undefined) {
123
+ yield* Deferred.succeed(blockingDeferred, void 0)
124
+ ctx.isDone = true
125
+ }
126
+ }),
127
+ } satisfies InitialBlockingSyncContext
128
+ })
129
+
130
+ /**
131
+ * Blocks until the leader thread has finished its initial setup.
132
+ * It also starts various background processes (e.g. syncing)
133
+ */
134
+ const bootLeaderThread = ({
135
+ dbMissing,
136
+ initialBlockingSyncContext,
137
+ devtoolsOptions,
138
+ }: {
139
+ dbMissing: boolean
140
+ initialBlockingSyncContext: InitialBlockingSyncContext
141
+ devtoolsOptions: DevtoolsOptions
142
+ }): Effect.Effect<
143
+ void,
144
+ UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
145
+ LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
146
+ > =>
147
+ Effect.gen(function* () {
148
+ const { dbLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
149
+
150
+ yield* migrateTable({
151
+ db: dbLog,
152
+ behaviour: 'create-if-not-exists',
153
+ tableAst: mutationLogMetaTable.sqliteDef.ast,
154
+ skipMetaTable: true,
155
+ })
156
+
157
+ yield* migrateTable({
158
+ db: dbLog,
159
+ behaviour: 'create-if-not-exists',
160
+ tableAst: syncStatusTable.sqliteDef.ast,
161
+ skipMetaTable: true,
162
+ })
163
+
164
+ // Create sync status row if it doesn't exist
165
+ yield* execSql(
166
+ dbLog,
167
+ sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
168
+ SELECT ${EventId.ROOT.global}
169
+ WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
170
+ {},
171
+ )
172
+
173
+ const dbReady = yield* Deferred.make<void>()
174
+
175
+ // We're already starting pulling from the sync backend concurrently but wait until the db is ready before
176
+ // processing any incoming mutations
177
+ yield* syncProcessor.boot({ dbReady })
178
+
179
+ if (dbMissing) {
180
+ yield* recreateDb
181
+ }
182
+
183
+ yield* Deferred.succeed(dbReady, void 0)
184
+
185
+ if (initialBlockingSyncContext.blockingDeferred !== undefined) {
186
+ yield* initialBlockingSyncContext.blockingDeferred
187
+ }
188
+
189
+ yield* Queue.offer(bootStatusQueue, { stage: 'done' })
190
+
191
+ yield* bootDevtools(devtoolsOptions).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
192
+ })
@@ -0,0 +1,6 @@
1
+ export * from './connection.js'
2
+ export * from './types.js'
3
+ export * as ShutdownChannel from './shutdown-channel.js'
4
+ export * from './leader-worker-devtools.js'
5
+ export * from './make-leader-thread-layer.js'
6
+ export * from './mutationlog.js'
@@ -0,0 +1,42 @@
1
+ import { Effect, Schema } from '@livestore/utils/effect'
2
+
3
+ import type { SynchronousDatabase } from '../adapter-types.js'
4
+ import * as EventId from '../schema/EventId.js'
5
+ import { MUTATION_LOG_META_TABLE, mutationLogMetaTable, SYNC_STATUS_TABLE } from '../schema/system-tables.js'
6
+ import { prepareBindValues, sql } from '../util.js'
7
+ import { LeaderThreadCtx } from './types.js'
8
+
9
+ export const getMutationEventsSince = (since: EventId.EventId) =>
10
+ Effect.gen(function* () {
11
+ const { dbLog } = yield* LeaderThreadCtx
12
+
13
+ const query = mutationLogMetaTable.query.where('idGlobal', '>=', since.global).asSql()
14
+ const pendingMutationEventsRaw = dbLog.select(query.query, prepareBindValues(query.bindValues, query.query))
15
+ const pendingMutationEvents = Schema.decodeUnknownSync(mutationLogMetaTable.schema.pipe(Schema.Array))(
16
+ pendingMutationEventsRaw,
17
+ )
18
+
19
+ return pendingMutationEvents
20
+ .map((_) => ({
21
+ mutation: _.mutation,
22
+ args: _.argsJson,
23
+ id: { global: _.idGlobal, local: _.idLocal },
24
+ parentId: { global: _.parentIdGlobal, local: _.parentIdLocal },
25
+ }))
26
+ .filter((_) => EventId.compare(_.id, since) > 0)
27
+ })
28
+
29
+ export const getLocalHeadFromDb = (dbLog: SynchronousDatabase) => {
30
+ const res = dbLog.select<{ idGlobal: number; idLocal: number }>(
31
+ sql`select idGlobal, idLocal from ${MUTATION_LOG_META_TABLE} order by idGlobal DESC, idLocal DESC limit 1`,
32
+ )[0]
33
+
34
+ return res ? { global: res.idGlobal, local: res.idLocal } : EventId.ROOT
35
+ }
36
+
37
+ export const getBackendHeadFromDb = (dbLog: SynchronousDatabase) =>
38
+ dbLog.select<{ head: number }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ?? EventId.ROOT.global
39
+
40
+ // TODO use prepared statements
41
+ export const updateBackendHead = (dbLog: SynchronousDatabase, head: EventId.EventId) =>
42
+ dbLog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
@@ -0,0 +1,58 @@
1
+ import { Effect, Queue } from '@livestore/utils/effect'
2
+
3
+ import * as MutationEvent from '../schema/MutationEvent.js'
4
+ import { getMutationEventsSince } from './mutationlog.js'
5
+ import { type PullQueueItem, type PullQueueSet } from './types.js'
6
+
7
+ export const makePullQueueSet = Effect.gen(function* () {
8
+ const set = new Set<Queue.Queue<PullQueueItem>>()
9
+
10
+ yield* Effect.addFinalizer(() =>
11
+ Effect.gen(function* () {
12
+ for (const queue of set) {
13
+ yield* Queue.shutdown(queue)
14
+ }
15
+
16
+ set.clear()
17
+ }),
18
+ )
19
+
20
+ const makeQueue: PullQueueSet['makeQueue'] = (since) =>
21
+ Effect.gen(function* () {
22
+ const queue = yield* Queue.unbounded<PullQueueItem>().pipe(Effect.acquireRelease(Queue.shutdown))
23
+
24
+ yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
25
+
26
+ const mutationEvents = yield* getMutationEventsSince(since)
27
+
28
+ if (mutationEvents.length > 0) {
29
+ const newEvents = mutationEvents.map((mutationEvent) => new MutationEvent.EncodedWithMeta(mutationEvent))
30
+ yield* queue.offer({ payload: { _tag: 'upstream-advance', newEvents }, remaining: 0 })
31
+ }
32
+
33
+ set.add(queue)
34
+
35
+ return queue
36
+ })
37
+
38
+ const offer: PullQueueSet['offer'] = (item) =>
39
+ Effect.gen(function* () {
40
+ // Short-circuit if the payload is an empty upstream advance
41
+ if (
42
+ item.payload._tag === 'upstream-advance' &&
43
+ item.payload.newEvents.length === 0 &&
44
+ item.payload.trimRollbackUntil === undefined
45
+ ) {
46
+ return
47
+ }
48
+
49
+ for (const queue of set) {
50
+ yield* Queue.offer(queue, item)
51
+ }
52
+ })
53
+
54
+ return {
55
+ makeQueue,
56
+ offer,
57
+ }
58
+ })