@livestore/livestore 0.4.0-dev.9 → 0.5.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 (132) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +15 -13
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +24 -6
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +134 -6
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +190 -11
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +41 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +129 -9
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +30 -4
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +4 -3
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts +56 -0
  32. package/dist/live-queries/computed.d.ts.map +1 -1
  33. package/dist/live-queries/computed.js +62 -6
  34. package/dist/live-queries/computed.js.map +1 -1
  35. package/dist/live-queries/db-query.d.ts +2 -2
  36. package/dist/live-queries/db-query.d.ts.map +1 -1
  37. package/dist/live-queries/db-query.js +41 -30
  38. package/dist/live-queries/db-query.js.map +1 -1
  39. package/dist/live-queries/db-query.test.js +112 -29
  40. package/dist/live-queries/db-query.test.js.map +1 -1
  41. package/dist/live-queries/signal.d.ts +49 -0
  42. package/dist/live-queries/signal.d.ts.map +1 -1
  43. package/dist/live-queries/signal.js +51 -2
  44. package/dist/live-queries/signal.js.map +1 -1
  45. package/dist/live-queries/signal.test.js +4 -4
  46. package/dist/live-queries/signal.test.js.map +1 -1
  47. package/dist/mod.d.ts +3 -3
  48. package/dist/mod.d.ts.map +1 -1
  49. package/dist/mod.js +3 -2
  50. package/dist/mod.js.map +1 -1
  51. package/dist/reactive.d.ts +10 -10
  52. package/dist/reactive.d.ts.map +1 -1
  53. package/dist/reactive.js +29 -27
  54. package/dist/reactive.js.map +1 -1
  55. package/dist/reactive.test.js +2 -2
  56. package/dist/reactive.test.js.map +1 -1
  57. package/dist/store/StoreRegistry.d.ts +215 -0
  58. package/dist/store/StoreRegistry.d.ts.map +1 -0
  59. package/dist/store/StoreRegistry.js +267 -0
  60. package/dist/store/StoreRegistry.js.map +1 -0
  61. package/dist/store/StoreRegistry.test.d.ts +2 -0
  62. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  63. package/dist/store/StoreRegistry.test.js +384 -0
  64. package/dist/store/StoreRegistry.test.js.map +1 -0
  65. package/dist/store/create-store.d.ts +99 -22
  66. package/dist/store/create-store.d.ts.map +1 -1
  67. package/dist/store/create-store.js +85 -37
  68. package/dist/store/create-store.js.map +1 -1
  69. package/dist/store/devtools.d.ts +6 -18
  70. package/dist/store/devtools.d.ts.map +1 -1
  71. package/dist/store/devtools.js +61 -19
  72. package/dist/store/devtools.js.map +1 -1
  73. package/dist/store/store-eventstream.test.d.ts +2 -0
  74. package/dist/store/store-eventstream.test.d.ts.map +1 -0
  75. package/dist/store/store-eventstream.test.js +65 -0
  76. package/dist/store/store-eventstream.test.js.map +1 -0
  77. package/dist/store/store-types.d.ts +271 -31
  78. package/dist/store/store-types.d.ts.map +1 -1
  79. package/dist/store/store-types.js +41 -1
  80. package/dist/store/store-types.js.map +1 -1
  81. package/dist/store/store-types.test.d.ts +2 -0
  82. package/dist/store/store-types.test.d.ts.map +1 -0
  83. package/dist/store/store-types.test.js +39 -0
  84. package/dist/store/store-types.test.js.map +1 -0
  85. package/dist/store/store.d.ts +255 -68
  86. package/dist/store/store.d.ts.map +1 -1
  87. package/dist/store/store.js +533 -186
  88. package/dist/store/store.js.map +1 -1
  89. package/dist/utils/dev.d.ts.map +1 -1
  90. package/dist/utils/dev.js +1 -1
  91. package/dist/utils/dev.js.map +1 -1
  92. package/dist/utils/stack-info.d.ts.map +1 -1
  93. package/dist/utils/stack-info.js +3 -2
  94. package/dist/utils/stack-info.js.map +1 -1
  95. package/dist/utils/tests/fixture.d.ts +46 -39
  96. package/dist/utils/tests/fixture.d.ts.map +1 -1
  97. package/dist/utils/tests/fixture.js +7 -0
  98. package/dist/utils/tests/fixture.js.map +1 -1
  99. package/dist/utils/tests/otel.d.ts.map +1 -1
  100. package/dist/utils/tests/otel.js +5 -5
  101. package/dist/utils/tests/otel.js.map +1 -1
  102. package/package.json +50 -18
  103. package/src/QueryCache.ts +1 -1
  104. package/src/SqliteDbWrapper.test.ts +30 -6
  105. package/src/SqliteDbWrapper.ts +21 -17
  106. package/src/ambient.d.ts +0 -7
  107. package/src/effect/LiveStore.test.ts +61 -0
  108. package/src/effect/LiveStore.ts +426 -22
  109. package/src/effect/mod.ts +13 -1
  110. package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
  111. package/src/live-queries/base-class.ts +162 -32
  112. package/src/live-queries/client-document-get-query.ts +6 -4
  113. package/src/live-queries/computed.ts +65 -8
  114. package/src/live-queries/db-query.test.ts +168 -30
  115. package/src/live-queries/db-query.ts +58 -43
  116. package/src/live-queries/signal.test.ts +5 -4
  117. package/src/live-queries/signal.ts +52 -3
  118. package/src/mod.ts +19 -2
  119. package/src/reactive.test.ts +3 -2
  120. package/src/reactive.ts +58 -49
  121. package/src/store/StoreRegistry.test.ts +543 -0
  122. package/src/store/StoreRegistry.ts +429 -0
  123. package/src/store/create-store.ts +227 -74
  124. package/src/store/devtools.ts +305 -261
  125. package/src/store/store-eventstream.test.ts +123 -0
  126. package/src/store/store-types.test.ts +52 -0
  127. package/src/store/store-types.ts +318 -44
  128. package/src/store/store.ts +695 -283
  129. package/src/utils/dev.ts +3 -4
  130. package/src/utils/stack-info.ts +5 -2
  131. package/src/utils/tests/fixture.ts +9 -1
  132. package/src/utils/tests/otel.ts +8 -7
@@ -1,23 +1,20 @@
1
- import type { ClientSession, ClientSessionSyncProcessor, DebugInfo, SyncState } from '@livestore/common'
2
- import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
1
+ import type { DebugInfo, SyncState } from '@livestore/common'
2
+ import {
3
+ Devtools,
4
+ devtoolsProtocolVersion,
5
+ isDevtoolsProtocolVersionSupported,
6
+ liveStoreVersion,
7
+ resolveDevtoolsProtocolVersion,
8
+ UnknownError,
9
+ } from '@livestore/common'
3
10
  import { throttle } from '@livestore/utils'
4
- import type { WebChannel } from '@livestore/utils/effect'
5
- import { Effect, Stream } from '@livestore/utils/effect'
11
+ import { type WebChannel, Effect, Stream } from '@livestore/utils/effect'
6
12
  import { nanoid } from '@livestore/utils/nanoid'
7
13
 
8
- import type { LiveQuery, ReactivityGraph } from '../live-queries/base-class.ts'
9
14
  import { NOT_REFRESHED_YET } from '../reactive.ts'
10
- import type { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
11
15
  import { emptyDebugInfo as makeEmptyDebugInfo } from '../SqliteDbWrapper.ts'
12
- import type { ReferenceCountedSet } from '../utils/data-structures.ts'
13
-
14
- type IStore = {
15
- clientSession: ClientSession
16
- reactivityGraph: ReactivityGraph
17
- sqliteDbWrapper: SqliteDbWrapper
18
- activeQueries: ReferenceCountedSet<LiveQuery<any>>
19
- syncProcessor: ClientSessionSyncProcessor
20
- }
16
+ import { StoreInternalsSymbol } from './store-types.ts'
17
+ import type { Store } from './store.ts'
21
18
 
22
19
  type Unsub = () => void
23
20
  type RequestId = string
@@ -32,7 +29,7 @@ const requestNextTick: (cb: () => void) => number =
32
29
  const cancelTick: (id: number) => void =
33
30
  globalThis.cancelAnimationFrame === undefined ? (id: number) => clearTimeout(id) : globalThis.cancelAnimationFrame
34
31
 
35
- export const connectDevtoolsToStore = ({
32
+ export const connectDevtoolsToStore = Effect.fn('LSD.devtools.connectStoreToDevtools')(function* ({
36
33
  storeDevtoolsChannel,
37
34
  store,
38
35
  }: {
@@ -40,236 +37,139 @@ export const connectDevtoolsToStore = ({
40
37
  Devtools.ClientSession.MessageToApp,
41
38
  Devtools.ClientSession.MessageFromApp
42
39
  >
43
- store: IStore
44
- }) =>
45
- Effect.gen(function* () {
46
- const reactivityGraphSubcriptions: SubMap = new Map()
47
- const liveQueriesSubscriptions: SubMap = new Map()
48
- const debugInfoHistorySubscriptions: SubMap = new Map()
49
- const syncHeadClientSessionSubscriptions: SubMap = new Map()
50
-
51
- const { clientId, sessionId } = store.clientSession
52
-
53
- yield* Effect.addFinalizer(() =>
54
- Effect.sync(() => {
55
- for (const unsub of reactivityGraphSubcriptions.values()) unsub()
56
- for (const unsub of liveQueriesSubscriptions.values()) unsub()
57
- for (const unsub of debugInfoHistorySubscriptions.values()) unsub()
58
- for (const unsub of syncHeadClientSessionSubscriptions.values()) unsub()
59
- }),
60
- )
61
-
62
- const handledRequestIds = new Set<RequestId>()
63
-
64
- const sendToDevtools = (message: Devtools.ClientSession.MessageFromApp) =>
65
- storeDevtoolsChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runFork)
66
-
67
- const onMessage = (decodedMessage: typeof Devtools.ClientSession.MessageToApp.Type) => {
68
- // console.debug('@livestore/livestore:store:devtools:onMessage', decodedMessage)
69
-
70
- if (decodedMessage.clientId !== clientId || decodedMessage.sessionId !== sessionId) {
71
- // console.log(`Unknown message`, event)
72
- return
73
- }
74
-
75
- if (decodedMessage._tag === 'LSD.ClientSession.Disconnect') {
76
- // console.error('TODO handle disconnect properly in store')
77
- return
78
- }
40
+ store: Store
41
+ }) {
42
+ const reactivityGraphSubcriptions: SubMap = new Map()
43
+ const liveQueriesSubscriptions: SubMap = new Map()
44
+ const debugInfoHistorySubscriptions: SubMap = new Map()
45
+ const syncHeadClientSessionSubscriptions: SubMap = new Map()
46
+ const services = yield* Effect.context()
47
+
48
+ const { clientId, sessionId } = store[StoreInternalsSymbol].clientSession
49
+
50
+ yield* Effect.addFinalizer(() =>
51
+ Effect.sync(() => {
52
+ for (const unsub of reactivityGraphSubcriptions.values()) unsub()
53
+ for (const unsub of liveQueriesSubscriptions.values()) unsub()
54
+ for (const unsub of debugInfoHistorySubscriptions.values()) unsub()
55
+ for (const unsub of syncHeadClientSessionSubscriptions.values()) unsub()
56
+ }),
57
+ )
58
+
59
+ const handledRequestIds = new Set<RequestId>()
60
+
61
+ const sendToDevtools = (message: Devtools.ClientSession.MessageFromApp) =>
62
+ storeDevtoolsChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runFork)
63
+
64
+ const onMessage = (decodedMessage: Devtools.ClientSession.MessageToApp) => {
65
+ // console.debug('@livestore/livestore:store:devtools:onMessage', decodedMessage)
66
+
67
+ if (decodedMessage.clientId !== clientId || decodedMessage.sessionId !== sessionId) {
68
+ // console.log(`Unknown message`, event)
69
+ return
70
+ }
79
71
 
80
- const requestId = decodedMessage.requestId
72
+ if (decodedMessage._tag === 'LSD.ClientSession.Disconnect') {
73
+ // Gracefully tear down all DevTools subscriptions and close the channel.
74
+ // This prevents background fibers from lingering after DevTools closes
75
+ // (e.g. when a window is closed without sending explicit unsubs).
76
+ for (const unsub of reactivityGraphSubcriptions.values()) unsub()
77
+ reactivityGraphSubcriptions.clear()
78
+ for (const unsub of liveQueriesSubscriptions.values()) unsub()
79
+ liveQueriesSubscriptions.clear()
80
+ for (const unsub of debugInfoHistorySubscriptions.values()) unsub()
81
+ debugInfoHistorySubscriptions.clear()
82
+ for (const unsub of syncHeadClientSessionSubscriptions.values()) unsub()
83
+ syncHeadClientSessionSubscriptions.clear()
84
+
85
+ // Signal the WebChannel to shut down; this causes the `listen` stream
86
+ // to complete and allows the surrounding scoped fiber to exit.
87
+ storeDevtoolsChannel.shutdown.pipe(Effect.runFork)
88
+ return
89
+ }
81
90
 
82
- // TODO we should try to move the duplicate message handling on the webmesh layer
83
- // So far I could only observe this problem with webmesh proxy channels (e.g. for Expo)
84
- // Proof: https://share.cleanshot.com/V9G87B0B
85
- // Also see `leader-worker-devtools.ts` for same problem
86
- if (handledRequestIds.has(requestId)) {
87
- return
88
- }
91
+ const requestId = decodedMessage.requestId
89
92
 
90
- handledRequestIds.add(requestId)
91
-
92
- const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
93
-
94
- switch (decodedMessage._tag) {
95
- case 'LSD.ClientSession.ReactivityGraphSubscribe': {
96
- const includeResults = decodedMessage.includeResults
97
- const { subscriptionId } = decodedMessage
98
-
99
- const send = () =>
100
- // In order to not add more work to the current tick, we use requestIdleCallback
101
- // to send the reactivity graph updates to the devtools
102
- requestIdleCallback(
103
- () =>
104
- sendToDevtools(
105
- Devtools.ClientSession.ReactivityGraphRes.make({
106
- reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
107
- requestId: nanoid(10),
108
- clientId,
109
- sessionId,
110
- liveStoreVersion,
111
- subscriptionId,
112
- }),
113
- ),
114
- { timeout: 500 },
115
- )
93
+ // TODO we should try to move the duplicate message handling on the webmesh layer
94
+ // So far I could only observe this problem with webmesh proxy channels (e.g. for Expo)
95
+ // Proof: https://share.cleanshot.com/V9G87B0B
96
+ // Also see `leader-worker-devtools.ts` for same problem
97
+ if (handledRequestIds.has(requestId) === true) {
98
+ return
99
+ }
116
100
 
117
- send()
101
+ handledRequestIds.add(requestId)
118
102
 
119
- // In some cases, there can be A LOT of reactivity graph updates in a short period of time
120
- // so we throttle the updates to avoid sending too much data
121
- // This might need to be tweaked further and possibly be exposed to the user in some way.
122
- const throttledSend = throttle(send, 20)
103
+ const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
123
104
 
124
- reactivityGraphSubcriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
105
+ switch (decodedMessage._tag) {
106
+ case 'LSD.ClientSession.ReactivityGraphSubscribe': {
107
+ const includeResults = decodedMessage.includeResults
108
+ const { subscriptionId } = decodedMessage
125
109
 
126
- break
127
- }
128
- case 'LSD.ClientSession.DebugInfoReq': {
129
- sendToDevtools(
130
- Devtools.ClientSession.DebugInfoRes.make({
131
- debugInfo: store.sqliteDbWrapper.debugInfo,
132
- requestId,
133
- clientId,
134
- sessionId,
135
- liveStoreVersion,
136
- }),
137
- )
138
- break
139
- }
140
- case 'LSD.ClientSession.DebugInfoHistorySubscribe': {
141
- const { subscriptionId } = decodedMessage
142
- const buffer: DebugInfo[] = []
143
- let hasStopped = false
144
- let tickHandle: number | undefined
145
-
146
- const tick = () => {
147
- buffer.push(store.sqliteDbWrapper.debugInfo)
148
-
149
- // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
150
- // will get the empty debug info
151
- // TODO We need to come up with a more graceful way to do store. Probably via a single global
152
- // `requestAnimationFrame` loop that is passed in somehow.
153
- store.sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
154
-
155
- if (buffer.length > 10) {
110
+ const send = () =>
111
+ // In order to not add more work to the current tick, we use requestIdleCallback
112
+ // to send the reactivity graph updates to the devtools
113
+ requestIdleCallback(
114
+ () =>
156
115
  sendToDevtools(
157
- Devtools.ClientSession.DebugInfoHistoryRes.make({
158
- debugInfoHistory: buffer,
116
+ Devtools.ClientSession.ReactivityGraphRes.make({
117
+ reactivityGraph: store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults }),
159
118
  requestId: nanoid(10),
160
119
  clientId,
161
120
  sessionId,
162
121
  liveStoreVersion,
163
122
  subscriptionId,
164
123
  }),
165
- )
166
- buffer.length = 0
167
- }
168
-
169
- if (hasStopped === false) {
170
- tickHandle = requestNextTick(tick)
171
- }
172
- }
173
-
174
- tickHandle = requestNextTick(tick)
175
-
176
- const unsub = () => {
177
- hasStopped = true
178
- if (tickHandle !== undefined) {
179
- cancelTick(tickHandle)
180
- tickHandle = undefined
181
- }
182
- }
183
-
184
- debugInfoHistorySubscriptions.set(subscriptionId, unsub)
185
-
186
- break
187
- }
188
- case 'LSD.ClientSession.DebugInfoHistoryUnsubscribe': {
189
- const { subscriptionId } = decodedMessage
190
- // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
191
- // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
192
- debugInfoHistorySubscriptions.get(subscriptionId)?.()
193
- debugInfoHistorySubscriptions.delete(subscriptionId)
194
- break
195
- }
196
- case 'LSD.ClientSession.DebugInfoResetReq': {
197
- store.sqliteDbWrapper.debugInfo.slowQueries.clear()
198
- sendToDevtools(
199
- Devtools.ClientSession.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
200
- )
201
- break
202
- }
203
- case 'LSD.ClientSession.DebugInfoRerunQueryReq': {
204
- const { queryStr, bindValues, queriedTables } = decodedMessage
205
- store.sqliteDbWrapper.cachedSelect(queryStr, bindValues, { queriedTables, skipCache: true })
206
- sendToDevtools(
207
- Devtools.ClientSession.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
124
+ ),
125
+ { timeout: 500 },
208
126
  )
209
- break
210
- }
211
- case 'LSD.ClientSession.ReactivityGraphUnsubscribe': {
212
- const { subscriptionId } = decodedMessage
213
- // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
214
- // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
215
- reactivityGraphSubcriptions.get(subscriptionId)?.()
216
- reactivityGraphSubcriptions.delete(subscriptionId)
217
- break
218
- }
219
- case 'LSD.ClientSession.LiveQueriesSubscribe': {
220
- const { subscriptionId } = decodedMessage
221
- const send = () =>
222
- requestIdleCallback(
223
- () =>
224
- sendToDevtools(
225
- Devtools.ClientSession.LiveQueriesRes.make({
226
- liveQueries: [...store.activeQueries].map((q) => ({
227
- _tag: q._tag,
228
- id: q.id,
229
- label: q.label,
230
- hash: q.def.hash,
231
- runs: q.runs,
232
- executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
233
- lastestResult:
234
- q.results$.previousResult === NOT_REFRESHED_YET
235
- ? 'SYMBOL_NOT_REFRESHED_YET'
236
- : q.results$.previousResult,
237
- activeSubscriptions: Array.from(q.activeSubscriptions),
238
- })),
239
- requestId: nanoid(10),
240
- liveStoreVersion,
241
- clientId,
242
- sessionId,
243
- subscriptionId,
244
- }),
245
- ),
246
- { timeout: 500 },
247
- )
248
127
 
249
- send()
128
+ send()
250
129
 
251
- // Same as in the reactivity graph subscription case above, we need to throttle the updates
252
- const throttledSend = throttle(send, 20)
130
+ // In some cases, there can be A LOT of reactivity graph updates in a short period of time
131
+ // so we throttle the updates to avoid sending too much data
132
+ // This might need to be tweaked further and possibly be exposed to the user in some way.
133
+ const throttledSend = throttle(send, 20)
253
134
 
254
- liveQueriesSubscriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
135
+ reactivityGraphSubcriptions.set(
136
+ subscriptionId,
137
+ store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
138
+ )
255
139
 
256
- break
257
- }
258
- case 'LSD.ClientSession.LiveQueriesUnsubscribe': {
259
- const { subscriptionId } = decodedMessage
260
- // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
261
- // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
262
- liveQueriesSubscriptions.get(subscriptionId)?.()
263
- liveQueriesSubscriptions.delete(subscriptionId)
264
- break
265
- }
266
- case 'LSD.ClientSession.SyncHeadSubscribe': {
267
- const { subscriptionId } = decodedMessage
268
- const send = (syncState: SyncState.SyncState) =>
140
+ break
141
+ }
142
+ case 'LSD.ClientSession.DebugInfoReq': {
143
+ sendToDevtools(
144
+ Devtools.ClientSession.DebugInfoRes.make({
145
+ debugInfo: store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo,
146
+ requestId,
147
+ clientId,
148
+ sessionId,
149
+ liveStoreVersion,
150
+ }),
151
+ )
152
+ break
153
+ }
154
+ case 'LSD.ClientSession.DebugInfoHistorySubscribe': {
155
+ const { subscriptionId } = decodedMessage
156
+ const buffer: DebugInfo[] = []
157
+ let hasStopped = false
158
+ let tickHandle: number | undefined
159
+
160
+ const tick = () => {
161
+ buffer.push(store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo)
162
+
163
+ // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
164
+ // will get the empty debug info
165
+ // TODO We need to come up with a more graceful way to do store. Probably via a single global
166
+ // `requestAnimationFrame` loop that is passed in somehow.
167
+ store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
168
+
169
+ if (buffer.length > 10) {
269
170
  sendToDevtools(
270
- Devtools.ClientSession.SyncHeadRes.make({
271
- local: syncState.localHead,
272
- upstream: syncState.upstreamHead,
171
+ Devtools.ClientSession.DebugInfoHistoryRes.make({
172
+ debugInfoHistory: buffer,
273
173
  requestId: nanoid(10),
274
174
  clientId,
275
175
  sessionId,
@@ -277,45 +177,189 @@ export const connectDevtoolsToStore = ({
277
177
  subscriptionId,
278
178
  }),
279
179
  )
180
+ buffer.length = 0
181
+ }
280
182
 
281
- send(store.syncProcessor.syncState.pipe(Effect.runSync))
282
-
283
- syncHeadClientSessionSubscriptions.set(
284
- subscriptionId,
285
- store.syncProcessor.syncState.changes.pipe(
286
- Stream.tap((syncState) => send(syncState)),
287
- Stream.runDrain,
288
- Effect.interruptible,
289
- Effect.tapCauseLogPretty,
290
- Effect.runCallback,
291
- ),
292
- )
293
-
294
- break
183
+ if (hasStopped === false) {
184
+ tickHandle = requestNextTick(tick)
185
+ }
295
186
  }
296
- case 'LSD.ClientSession.SyncHeadUnsubscribe': {
297
- const { subscriptionId } = decodedMessage
298
- // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
299
- // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
300
- syncHeadClientSessionSubscriptions.get(subscriptionId)?.()
301
- syncHeadClientSessionSubscriptions.delete(subscriptionId)
302
- break
187
+
188
+ tickHandle = requestNextTick(tick)
189
+
190
+ const unsub = () => {
191
+ hasStopped = true
192
+ if (tickHandle !== undefined) {
193
+ cancelTick(tickHandle)
194
+ tickHandle = undefined
195
+ }
303
196
  }
304
- case 'LSD.ClientSession.Ping': {
305
- sendToDevtools(Devtools.ClientSession.Pong.make({ requestId, clientId, sessionId, liveStoreVersion }))
197
+
198
+ debugInfoHistorySubscriptions.set(subscriptionId, unsub)
199
+
200
+ break
201
+ }
202
+ case 'LSD.ClientSession.DebugInfoHistoryUnsubscribe': {
203
+ const { subscriptionId } = decodedMessage
204
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
205
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
206
+ debugInfoHistorySubscriptions.get(subscriptionId)?.()
207
+ debugInfoHistorySubscriptions.delete(subscriptionId)
208
+ break
209
+ }
210
+ case 'LSD.ClientSession.DebugInfoResetReq': {
211
+ store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo.slowQueries.clear()
212
+ sendToDevtools(
213
+ Devtools.ClientSession.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
214
+ )
215
+ break
216
+ }
217
+ case 'LSD.ClientSession.DebugInfoRerunQueryReq': {
218
+ const { queryStr, bindValues, queriedTables } = decodedMessage
219
+ store[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(queryStr, bindValues, {
220
+ queriedTables,
221
+ skipCache: true,
222
+ })
223
+ sendToDevtools(
224
+ Devtools.ClientSession.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
225
+ )
226
+ break
227
+ }
228
+ case 'LSD.ClientSession.ReactivityGraphUnsubscribe': {
229
+ const { subscriptionId } = decodedMessage
230
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
231
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
232
+ reactivityGraphSubcriptions.get(subscriptionId)?.()
233
+ reactivityGraphSubcriptions.delete(subscriptionId)
234
+ break
235
+ }
236
+ case 'LSD.ClientSession.LiveQueriesSubscribe': {
237
+ const { subscriptionId } = decodedMessage
238
+ const send = () =>
239
+ requestIdleCallback(
240
+ () =>
241
+ sendToDevtools(
242
+ Devtools.ClientSession.LiveQueriesRes.make({
243
+ liveQueries: [...store[StoreInternalsSymbol].activeQueries].map((q) => ({
244
+ /** TODO: include schema metadata for schema-aware rendering in devtools (e.g., schema AST/hash/identifier or table+columns for QueryBuilder-derived queries). */
245
+ _tag: q._tag,
246
+ id: q.id,
247
+ label: q.label,
248
+ hash: q.def.hash,
249
+ runs: q.runs,
250
+ executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
251
+ lastestResult:
252
+ q.results$.previousResult === NOT_REFRESHED_YET
253
+ ? 'SYMBOL_NOT_REFRESHED_YET'
254
+ : q.results$.previousResult,
255
+ activeSubscriptions: Array.from(q.activeSubscriptions),
256
+ })),
257
+ requestId: nanoid(10),
258
+ liveStoreVersion,
259
+ clientId,
260
+ sessionId,
261
+ subscriptionId,
262
+ }),
263
+ ),
264
+ { timeout: 500 },
265
+ )
266
+
267
+ send()
268
+
269
+ // Same as in the reactivity graph subscription case above, we need to throttle the updates
270
+ const throttledSend = throttle(send, 20)
271
+
272
+ liveQueriesSubscriptions.set(
273
+ subscriptionId,
274
+ store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
275
+ )
276
+
277
+ break
278
+ }
279
+ case 'LSD.ClientSession.LiveQueriesUnsubscribe': {
280
+ const { subscriptionId } = decodedMessage
281
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
282
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
283
+ liveQueriesSubscriptions.get(subscriptionId)?.()
284
+ liveQueriesSubscriptions.delete(subscriptionId)
285
+ break
286
+ }
287
+ case 'LSD.ClientSession.SyncHeadSubscribe': {
288
+ const { subscriptionId } = decodedMessage
289
+ const send = (syncState: SyncState.SyncState) =>
290
+ sendToDevtools(
291
+ Devtools.ClientSession.SyncHeadRes.make({
292
+ local: syncState.localHead,
293
+ upstream: syncState.upstreamHead,
294
+ requestId: nanoid(10),
295
+ clientId,
296
+ sessionId,
297
+ liveStoreVersion,
298
+ subscriptionId,
299
+ }),
300
+ )
301
+
302
+ send(store[StoreInternalsSymbol].syncProcessor.syncState.pipe(Effect.runSync))
303
+
304
+ syncHeadClientSessionSubscriptions.set(
305
+ subscriptionId,
306
+ store[StoreInternalsSymbol].syncProcessor.syncState.changes.pipe(
307
+ Stream.tap((syncState) => Effect.sync(() => send(syncState))),
308
+ Stream.runDrain,
309
+ Effect.interruptible,
310
+ Effect.tapCauseLogPretty,
311
+ Effect.runCallbackWith(services),
312
+ ),
313
+ )
314
+
315
+ break
316
+ }
317
+ case 'LSD.ClientSession.SyncHeadUnsubscribe': {
318
+ const { subscriptionId } = decodedMessage
319
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
320
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
321
+ syncHeadClientSessionSubscriptions.get(subscriptionId)?.()
322
+ syncHeadClientSessionSubscriptions.delete(subscriptionId)
323
+ break
324
+ }
325
+ case 'LSD.ClientSession.Ping': {
326
+ if (isDevtoolsProtocolVersionSupported(decodedMessage.devtoolsProtocolVersion) === false) {
327
+ sendToDevtools(
328
+ Devtools.ClientSession.VersionMismatch.make({
329
+ requestId,
330
+ clientId,
331
+ sessionId,
332
+ liveStoreVersion,
333
+ appVersion: liveStoreVersion,
334
+ receivedVersion: decodedMessage.liveStoreVersion,
335
+ appDevtoolsProtocolVersion: devtoolsProtocolVersion,
336
+ receivedDevtoolsProtocolVersion: resolveDevtoolsProtocolVersion(decodedMessage.devtoolsProtocolVersion),
337
+ }),
338
+ )
306
339
  break
307
340
  }
308
- default: {
309
- console.warn(`[LSD.ClientSession] Unknown message`, decodedMessage)
310
- }
341
+ sendToDevtools(
342
+ Devtools.ClientSession.Pong.make({
343
+ requestId,
344
+ clientId,
345
+ sessionId,
346
+ liveStoreVersion,
347
+ devtoolsProtocolVersion,
348
+ }),
349
+ )
350
+ break
351
+ }
352
+ default: {
353
+ console.warn(`[LSD.ClientSession] Unknown message`, decodedMessage)
311
354
  }
312
355
  }
313
-
314
- yield* storeDevtoolsChannel.listen.pipe(
315
- // Stream.tapLogWithLabel('@livestore/livestore:store:devtools:onMessage'),
316
- Stream.flatten(),
317
- Stream.tapSync((message) => onMessage(message)),
318
- Stream.runDrain,
319
- Effect.withSpan('LSD.devtools.onMessage'),
320
- )
321
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('LSD.devtools.connectStoreToDevtools'))
356
+ }
357
+
358
+ yield* storeDevtoolsChannel.listen.pipe(
359
+ // Stream.tapLogWithLabel('@livestore/livestore:store:devtools:onMessage'),
360
+ Stream.mapEffect(Effect.fromResult),
361
+ Stream.tapSync((message) => onMessage(message)),
362
+ Stream.runDrain,
363
+ Effect.withSpan('LSD.devtools.onMessage'),
364
+ )
365
+ }, UnknownError.mapToUnknownError)