@livestore/livestore 0.4.0 → 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 (89) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  3. package/dist/SqliteDbWrapper.js +7 -5
  4. package/dist/SqliteDbWrapper.js.map +1 -1
  5. package/dist/SqliteDbWrapper.test.js +20 -3
  6. package/dist/SqliteDbWrapper.test.js.map +1 -1
  7. package/dist/effect/LiveStore.d.ts +15 -15
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +17 -17
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/effect/LiveStore.test.js +5 -6
  12. package/dist/effect/LiveStore.test.js.map +1 -1
  13. package/dist/live-queries/base-class.d.ts +19 -2
  14. package/dist/live-queries/base-class.d.ts.map +1 -1
  15. package/dist/live-queries/base-class.js +28 -2
  16. package/dist/live-queries/base-class.js.map +1 -1
  17. package/dist/live-queries/computed.d.ts.map +1 -1
  18. package/dist/live-queries/computed.js +4 -4
  19. package/dist/live-queries/computed.js.map +1 -1
  20. package/dist/live-queries/db-query.d.ts +2 -2
  21. package/dist/live-queries/db-query.d.ts.map +1 -1
  22. package/dist/live-queries/db-query.js +26 -17
  23. package/dist/live-queries/db-query.js.map +1 -1
  24. package/dist/live-queries/db-query.test.js +9 -9
  25. package/dist/live-queries/db-query.test.js.map +1 -1
  26. package/dist/live-queries/signal.d.ts.map +1 -1
  27. package/dist/live-queries/signal.js +2 -2
  28. package/dist/live-queries/signal.js.map +1 -1
  29. package/dist/live-queries/signal.test.js +2 -2
  30. package/dist/live-queries/signal.test.js.map +1 -1
  31. package/dist/reactive.d.ts +1 -1
  32. package/dist/reactive.d.ts.map +1 -1
  33. package/dist/reactive.js +20 -1
  34. package/dist/reactive.js.map +1 -1
  35. package/dist/store/StoreRegistry.d.ts +12 -12
  36. package/dist/store/StoreRegistry.d.ts.map +1 -1
  37. package/dist/store/StoreRegistry.js +24 -24
  38. package/dist/store/StoreRegistry.js.map +1 -1
  39. package/dist/store/StoreRegistry.test.js +62 -59
  40. package/dist/store/StoreRegistry.test.js.map +1 -1
  41. package/dist/store/create-store.d.ts +12 -15
  42. package/dist/store/create-store.d.ts.map +1 -1
  43. package/dist/store/create-store.js +43 -24
  44. package/dist/store/create-store.js.map +1 -1
  45. package/dist/store/devtools.d.ts +1 -2
  46. package/dist/store/devtools.d.ts.map +1 -1
  47. package/dist/store/devtools.js +3 -2
  48. package/dist/store/devtools.js.map +1 -1
  49. package/dist/store/store-eventstream.test.js +5 -5
  50. package/dist/store/store-eventstream.test.js.map +1 -1
  51. package/dist/store/store-types.d.ts +10 -28
  52. package/dist/store/store-types.d.ts.map +1 -1
  53. package/dist/store/store-types.js +2 -38
  54. package/dist/store/store-types.js.map +1 -1
  55. package/dist/store/store-types.test.js +1 -1
  56. package/dist/store/store.d.ts +2 -2
  57. package/dist/store/store.d.ts.map +1 -1
  58. package/dist/store/store.js +114 -56
  59. package/dist/store/store.js.map +1 -1
  60. package/dist/utils/dev.js +1 -1
  61. package/dist/utils/dev.js.map +1 -1
  62. package/dist/utils/stack-info.d.ts.map +1 -1
  63. package/dist/utils/stack-info.js +2 -1
  64. package/dist/utils/stack-info.js.map +1 -1
  65. package/dist/utils/tests/fixture.d.ts +26 -34
  66. package/dist/utils/tests/fixture.d.ts.map +1 -1
  67. package/package.json +14 -24
  68. package/src/SqliteDbWrapper.test.ts +25 -3
  69. package/src/SqliteDbWrapper.ts +12 -9
  70. package/src/effect/LiveStore.test.ts +16 -16
  71. package/src/effect/LiveStore.ts +79 -50
  72. package/src/live-queries/__snapshots__/db-query.test.ts.snap +273 -49
  73. package/src/live-queries/base-class.ts +38 -6
  74. package/src/live-queries/computed.ts +5 -5
  75. package/src/live-queries/db-query.test.ts +9 -9
  76. package/src/live-queries/db-query.ts +42 -30
  77. package/src/live-queries/signal.test.ts +2 -2
  78. package/src/live-queries/signal.ts +3 -3
  79. package/src/reactive.ts +45 -35
  80. package/src/store/StoreRegistry.test.ts +62 -59
  81. package/src/store/StoreRegistry.ts +49 -38
  82. package/src/store/create-store.ts +85 -51
  83. package/src/store/devtools.ts +283 -283
  84. package/src/store/store-eventstream.test.ts +16 -7
  85. package/src/store/store-types.test.ts +1 -1
  86. package/src/store/store-types.ts +10 -56
  87. package/src/store/store.ts +148 -71
  88. package/src/utils/dev.ts +1 -1
  89. package/src/utils/stack-info.ts +4 -1
@@ -8,8 +8,7 @@ import {
8
8
  UnknownError,
9
9
  } from '@livestore/common'
10
10
  import { throttle } from '@livestore/utils'
11
- import type { WebChannel } from '@livestore/utils/effect'
12
- import { Effect, Stream } from '@livestore/utils/effect'
11
+ import { type WebChannel, Effect, Stream } from '@livestore/utils/effect'
13
12
  import { nanoid } from '@livestore/utils/nanoid'
14
13
 
15
14
  import { NOT_REFRESHED_YET } from '../reactive.ts'
@@ -40,257 +39,137 @@ export const connectDevtoolsToStore = Effect.fn('LSD.devtools.connectStoreToDevt
40
39
  >
41
40
  store: Store
42
41
  }) {
43
- const reactivityGraphSubcriptions: SubMap = new Map()
44
- const liveQueriesSubscriptions: SubMap = new Map()
45
- const debugInfoHistorySubscriptions: SubMap = new Map()
46
- const syncHeadClientSessionSubscriptions: SubMap = new Map()
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: typeof Devtools.ClientSession.MessageToApp.Type) => {
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
- }
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
+ }
71
71
 
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
- }
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
+ }
90
90
 
91
- const requestId = decodedMessage.requestId
91
+ const requestId = decodedMessage.requestId
92
92
 
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
- }
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
+ }
100
100
 
101
- handledRequestIds.add(requestId)
102
-
103
- const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
104
-
105
- switch (decodedMessage._tag) {
106
- case 'LSD.ClientSession.ReactivityGraphSubscribe': {
107
- const includeResults = decodedMessage.includeResults
108
- const { subscriptionId } = decodedMessage
109
-
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
- () =>
115
- sendToDevtools(
116
- Devtools.ClientSession.ReactivityGraphRes.make({
117
- reactivityGraph: store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults }),
118
- requestId: nanoid(10),
119
- clientId,
120
- sessionId,
121
- liveStoreVersion,
122
- subscriptionId,
123
- }),
124
- ),
125
- { timeout: 500 },
126
- )
101
+ handledRequestIds.add(requestId)
127
102
 
128
- send()
103
+ const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
129
104
 
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)
105
+ switch (decodedMessage._tag) {
106
+ case 'LSD.ClientSession.ReactivityGraphSubscribe': {
107
+ const includeResults = decodedMessage.includeResults
108
+ const { subscriptionId } = decodedMessage
134
109
 
135
- reactivityGraphSubcriptions.set(
136
- subscriptionId,
137
- store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
138
- )
139
-
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) {
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
+ () =>
170
115
  sendToDevtools(
171
- Devtools.ClientSession.DebugInfoHistoryRes.make({
172
- debugInfoHistory: buffer,
116
+ Devtools.ClientSession.ReactivityGraphRes.make({
117
+ reactivityGraph: store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults }),
173
118
  requestId: nanoid(10),
174
119
  clientId,
175
120
  sessionId,
176
121
  liveStoreVersion,
177
122
  subscriptionId,
178
123
  }),
179
- )
180
- buffer.length = 0
181
- }
182
-
183
- if (hasStopped === false) {
184
- tickHandle = requestNextTick(tick)
185
- }
186
- }
187
-
188
- tickHandle = requestNextTick(tick)
189
-
190
- const unsub = () => {
191
- hasStopped = true
192
- if (tickHandle !== undefined) {
193
- cancelTick(tickHandle)
194
- tickHandle = undefined
195
- }
196
- }
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 }),
124
+ ),
125
+ { timeout: 500 },
225
126
  )
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
127
 
267
- send()
128
+ send()
268
129
 
269
- // Same as in the reactivity graph subscription case above, we need to throttle the updates
270
- 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)
271
134
 
272
- liveQueriesSubscriptions.set(
273
- subscriptionId,
274
- store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
275
- )
135
+ reactivityGraphSubcriptions.set(
136
+ subscriptionId,
137
+ store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
138
+ )
276
139
 
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) =>
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) {
290
170
  sendToDevtools(
291
- Devtools.ClientSession.SyncHeadRes.make({
292
- local: syncState.localHead,
293
- upstream: syncState.upstreamHead,
171
+ Devtools.ClientSession.DebugInfoHistoryRes.make({
172
+ debugInfoHistory: buffer,
294
173
  requestId: nanoid(10),
295
174
  clientId,
296
175
  sessionId,
@@ -298,68 +177,189 @@ export const connectDevtoolsToStore = Effect.fn('LSD.devtools.connectStoreToDevt
298
177
  subscriptionId,
299
178
  }),
300
179
  )
180
+ buffer.length = 0
181
+ }
301
182
 
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) => send(syncState)),
308
- Stream.runDrain,
309
- Effect.interruptible,
310
- Effect.tapCauseLogPretty,
311
- Effect.runCallback,
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
183
+ if (hasStopped === false) {
184
+ tickHandle = requestNextTick(tick)
185
+ }
324
186
  }
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
- )
339
- break
187
+
188
+ tickHandle = requestNextTick(tick)
189
+
190
+ const unsub = () => {
191
+ hasStopped = true
192
+ if (tickHandle !== undefined) {
193
+ cancelTick(tickHandle)
194
+ tickHandle = undefined
340
195
  }
196
+ }
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) {
341
327
  sendToDevtools(
342
- Devtools.ClientSession.Pong.make({
328
+ Devtools.ClientSession.VersionMismatch.make({
343
329
  requestId,
344
330
  clientId,
345
331
  sessionId,
346
332
  liveStoreVersion,
347
- devtoolsProtocolVersion,
333
+ appVersion: liveStoreVersion,
334
+ receivedVersion: decodedMessage.liveStoreVersion,
335
+ appDevtoolsProtocolVersion: devtoolsProtocolVersion,
336
+ receivedDevtoolsProtocolVersion: resolveDevtoolsProtocolVersion(decodedMessage.devtoolsProtocolVersion),
348
337
  }),
349
338
  )
350
339
  break
351
340
  }
352
- default: {
353
- console.warn(`[LSD.ClientSession] Unknown message`, decodedMessage)
354
- }
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)
355
354
  }
356
355
  }
357
-
358
- yield* storeDevtoolsChannel.listen.pipe(
359
- // Stream.tapLogWithLabel('@livestore/livestore:store:devtools:onMessage'),
360
- Stream.flatten(),
361
- Stream.tapSync((message) => onMessage(message)),
362
- Stream.runDrain,
363
- Effect.withSpan('LSD.devtools.onMessage'),
364
- )
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
365
  }, UnknownError.mapToUnknownError)