@livestore/livestore 0.0.0-snapshot-ee8e0fc3b894cf3159269c9c8969a8fc4b398dca → 0.0.0-snapshot-fec375f0f61a7bc75278adc60d1a55f96a9c292a

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.
@@ -3,6 +3,7 @@ import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
3
3
  import { throttle } from '@livestore/utils'
4
4
  import type { WebChannel } from '@livestore/utils/effect'
5
5
  import { Effect, Stream } from '@livestore/utils/effect'
6
+ import { nanoid } from '@livestore/utils/nanoid'
6
7
 
7
8
  import type { LiveQuery, ReactivityGraph } from '../live-queries/base-class.js'
8
9
  import { NOT_REFRESHED_YET } from '../reactive.js'
@@ -58,6 +59,8 @@ export const connectDevtoolsToStore = ({
58
59
  }),
59
60
  )
60
61
 
62
+ const handledRequestIds = new Set<RequestId>()
63
+
61
64
  const sendToDevtools = (message: Devtools.ClientSession.MessageFromApp) =>
62
65
  storeDevtoolsChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runFork)
63
66
 
@@ -76,11 +79,22 @@ export const connectDevtoolsToStore = ({
76
79
 
77
80
  const requestId = decodedMessage.requestId
78
81
 
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
+ }
89
+
90
+ handledRequestIds.add(requestId)
91
+
79
92
  const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
80
93
 
81
94
  switch (decodedMessage._tag) {
82
95
  case 'LSD.ClientSession.ReactivityGraphSubscribe': {
83
96
  const includeResults = decodedMessage.includeResults
97
+ const { subscriptionId } = decodedMessage
84
98
 
85
99
  const send = () =>
86
100
  // In order to not add more work to the current tick, we use requestIdleCallback
@@ -90,10 +104,11 @@ export const connectDevtoolsToStore = ({
90
104
  sendToDevtools(
91
105
  Devtools.ClientSession.ReactivityGraphRes.make({
92
106
  reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
93
- requestId,
107
+ requestId: nanoid(10),
94
108
  clientId,
95
109
  sessionId,
96
110
  liveStoreVersion,
111
+ subscriptionId,
97
112
  }),
98
113
  ),
99
114
  { timeout: 500 },
@@ -106,7 +121,7 @@ export const connectDevtoolsToStore = ({
106
121
  // This might need to be tweaked further and possibly be exposed to the user in some way.
107
122
  const throttledSend = throttle(send, 20)
108
123
 
109
- reactivityGraphSubcriptions.set(requestId, store.reactivityGraph.subscribeToRefresh(throttledSend))
124
+ reactivityGraphSubcriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
110
125
 
111
126
  break
112
127
  }
@@ -123,6 +138,7 @@ export const connectDevtoolsToStore = ({
123
138
  break
124
139
  }
125
140
  case 'LSD.ClientSession.DebugInfoHistorySubscribe': {
141
+ const { subscriptionId } = decodedMessage
126
142
  const buffer: DebugInfo[] = []
127
143
  let hasStopped = false
128
144
  let tickHandle: number | undefined
@@ -140,10 +156,11 @@ export const connectDevtoolsToStore = ({
140
156
  sendToDevtools(
141
157
  Devtools.ClientSession.DebugInfoHistoryRes.make({
142
158
  debugInfoHistory: buffer,
143
- requestId,
159
+ requestId: nanoid(10),
144
160
  clientId,
145
161
  sessionId,
146
162
  liveStoreVersion,
163
+ subscriptionId,
147
164
  }),
148
165
  )
149
166
  buffer.length = 0
@@ -164,15 +181,16 @@ export const connectDevtoolsToStore = ({
164
181
  }
165
182
  }
166
183
 
167
- debugInfoHistorySubscriptions.set(requestId, unsub)
184
+ debugInfoHistorySubscriptions.set(subscriptionId, unsub)
168
185
 
169
186
  break
170
187
  }
171
188
  case 'LSD.ClientSession.DebugInfoHistoryUnsubscribe': {
189
+ const { subscriptionId } = decodedMessage
172
190
  // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
173
191
  // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
174
- debugInfoHistorySubscriptions.get(requestId)?.()
175
- debugInfoHistorySubscriptions.delete(requestId)
192
+ debugInfoHistorySubscriptions.get(subscriptionId)?.()
193
+ debugInfoHistorySubscriptions.delete(subscriptionId)
176
194
  break
177
195
  }
178
196
  case 'LSD.ClientSession.DebugInfoResetReq': {
@@ -191,12 +209,15 @@ export const connectDevtoolsToStore = ({
191
209
  break
192
210
  }
193
211
  case 'LSD.ClientSession.ReactivityGraphUnsubscribe': {
212
+ const { subscriptionId } = decodedMessage
194
213
  // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
195
214
  // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
196
- reactivityGraphSubcriptions.get(requestId)?.()
215
+ reactivityGraphSubcriptions.get(subscriptionId)?.()
216
+ reactivityGraphSubcriptions.delete(subscriptionId)
197
217
  break
198
218
  }
199
219
  case 'LSD.ClientSession.LiveQueriesSubscribe': {
220
+ const { subscriptionId } = decodedMessage
200
221
  const send = () =>
201
222
  requestIdleCallback(
202
223
  () =>
@@ -214,10 +235,11 @@ export const connectDevtoolsToStore = ({
214
235
  : q.results$.previousResult,
215
236
  activeSubscriptions: Array.from(q.activeSubscriptions),
216
237
  })),
217
- requestId,
238
+ requestId: nanoid(10),
218
239
  liveStoreVersion,
219
240
  clientId,
220
241
  sessionId,
242
+ subscriptionId,
221
243
  }),
222
244
  ),
223
245
  { timeout: 500 },
@@ -228,34 +250,37 @@ export const connectDevtoolsToStore = ({
228
250
  // Same as in the reactivity graph subscription case above, we need to throttle the updates
229
251
  const throttledSend = throttle(send, 20)
230
252
 
231
- liveQueriesSubscriptions.set(requestId, store.reactivityGraph.subscribeToRefresh(throttledSend))
253
+ liveQueriesSubscriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
232
254
 
233
255
  break
234
256
  }
235
257
  case 'LSD.ClientSession.LiveQueriesUnsubscribe': {
258
+ const { subscriptionId } = decodedMessage
236
259
  // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
237
260
  // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
238
- liveQueriesSubscriptions.get(requestId)?.()
239
- liveQueriesSubscriptions.delete(requestId)
261
+ liveQueriesSubscriptions.get(subscriptionId)?.()
262
+ liveQueriesSubscriptions.delete(subscriptionId)
240
263
  break
241
264
  }
242
265
  case 'LSD.ClientSession.SyncHeadSubscribe': {
266
+ const { subscriptionId } = decodedMessage
243
267
  const send = (syncState: SyncState.SyncState) =>
244
268
  sendToDevtools(
245
269
  Devtools.ClientSession.SyncHeadRes.make({
246
270
  local: syncState.localHead,
247
271
  upstream: syncState.upstreamHead,
248
- requestId,
272
+ requestId: nanoid(10),
249
273
  clientId,
250
274
  sessionId,
251
275
  liveStoreVersion,
276
+ subscriptionId,
252
277
  }),
253
278
  )
254
279
 
255
280
  send(store.syncProcessor.syncState.pipe(Effect.runSync))
256
281
 
257
282
  syncHeadClientSessionSubscriptions.set(
258
- requestId,
283
+ subscriptionId,
259
284
  store.syncProcessor.syncState.changes.pipe(
260
285
  Stream.tap((syncState) => send(syncState)),
261
286
  Stream.runDrain,
@@ -268,8 +293,11 @@ export const connectDevtoolsToStore = ({
268
293
  break
269
294
  }
270
295
  case 'LSD.ClientSession.SyncHeadUnsubscribe': {
271
- syncHeadClientSessionSubscriptions.get(requestId)?.()
272
- syncHeadClientSessionSubscriptions.delete(requestId)
296
+ const { subscriptionId } = decodedMessage
297
+ // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
298
+ // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
299
+ syncHeadClientSessionSubscriptions.get(subscriptionId)?.()
300
+ syncHeadClientSessionSubscriptions.delete(subscriptionId)
273
301
  break
274
302
  }
275
303
  default: {
@@ -39,6 +39,7 @@ export type StoreOptions<TSchema extends LiveStoreSchema = LiveStoreSchema, TCon
39
39
  disableDevtools?: boolean
40
40
  lifetimeScope: Scope.Scope
41
41
  runtime: Runtime.Runtime<Scope.Scope>
42
+ confirmUnsavedChanges: boolean
42
43
  batchUpdates: (runUpdates: () => void) => void
43
44
  // TODO validate whether we still need this
44
45
  unsyncedMutationEvents: MutableHashMap.MutableHashMap<EventId.EventId, MutationEvent.ForSchema<TSchema>>
@@ -103,6 +103,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
103
103
  lifetimeScope,
104
104
  runtime,
105
105
  params,
106
+ confirmUnsavedChanges,
106
107
  }: StoreOptions<TSchema, TContext>) {
107
108
  super()
108
109
 
@@ -173,6 +174,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
173
174
  params: {
174
175
  leaderPushBatchSize: params.leaderPushBatchSize,
175
176
  },
177
+ confirmUnsavedChanges,
176
178
  })
177
179
 
178
180
  this.__mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)