@livestore/react 0.3.0-dev.46 → 0.3.0-dev.48

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.
@@ -79,6 +79,9 @@ export interface LiveStoreProviderProps {
79
79
  * @default undefined
80
80
  */
81
81
  syncPayload?: Schema.JsonValue
82
+ debug?: {
83
+ instanceId?: string
84
+ }
82
85
  }
83
86
 
84
87
  const defaultRenderError = (error: UnexpectedError | unknown) =>
@@ -119,6 +122,7 @@ export const LiveStoreProvider = ({
119
122
  signal,
120
123
  confirmUnsavedChanges = true,
121
124
  syncPayload,
125
+ debug,
122
126
  }: LiveStoreProviderProps & { children?: ReactNode }): React.ReactElement => {
123
127
  const storeCtx = useCreateStore({
124
128
  storeId,
@@ -131,6 +135,7 @@ export const LiveStoreProvider = ({
131
135
  signal,
132
136
  confirmUnsavedChanges,
133
137
  syncPayload,
138
+ debug,
134
139
  })
135
140
 
136
141
  if (storeCtx.stage === 'error') {
@@ -149,23 +154,11 @@ export const LiveStoreProvider = ({
149
154
  if (Object.keys(globalThis.__debugLiveStore).length === 0) {
150
155
  globalThis.__debugLiveStore['_'] = storeCtx.store
151
156
  }
152
- globalThis.__debugLiveStore[storeId] = storeCtx.store
157
+ globalThis.__debugLiveStore[debug?.instanceId ?? storeId] = storeCtx.store
153
158
 
154
159
  return <LiveStoreContext.Provider value={storeCtx as TODO}>{children}</LiveStoreContext.Provider>
155
160
  }
156
161
 
157
- type SchemaKey = string
158
- const semaphoreMap = new Map<SchemaKey, Effect.Semaphore>()
159
-
160
- const withSemaphore = (storeId: SchemaKey) => {
161
- let semaphore = semaphoreMap.get(storeId)
162
- if (!semaphore) {
163
- semaphore = Effect.makeSemaphore(1).pipe(Effect.runSync)
164
- semaphoreMap.set(storeId, semaphore)
165
- }
166
- return semaphore.withPermits(1)
167
- }
168
-
169
162
  const useCreateStore = ({
170
163
  schema,
171
164
  storeId,
@@ -179,6 +172,7 @@ const useCreateStore = ({
179
172
  params,
180
173
  confirmUnsavedChanges,
181
174
  syncPayload,
175
+ debug,
182
176
  }: CreateStoreOptions<LiveStoreSchema> & {
183
177
  signal?: AbortSignal
184
178
  otelOptions?: Partial<OtelOptions>
@@ -188,13 +182,17 @@ const useCreateStore = ({
188
182
  value: StoreContext_ | BootStatus
189
183
  componentScope: Scope.CloseableScope | undefined
190
184
  shutdownDeferred: ShutdownDeferred | undefined
185
+ /** Used to wait for the previous shutdown deferred to fully complete before creating a new one */
186
+ previousShutdownDeferred: ShutdownDeferred | undefined
191
187
  counter: number
192
188
  }>({
193
189
  value: { stage: 'loading' },
194
190
  componentScope: undefined,
195
191
  shutdownDeferred: undefined,
192
+ previousShutdownDeferred: undefined,
196
193
  counter: 0,
197
194
  })
195
+ const debugInstanceId = debug?.instanceId
198
196
 
199
197
  // console.debug(`useCreateStore (${ctxValueRef.current.counter})`, ctxValueRef.current.value.stage)
200
198
 
@@ -210,6 +208,7 @@ const useCreateStore = ({
210
208
  params,
211
209
  confirmUnsavedChanges,
212
210
  syncPayload,
211
+ debugInstanceId,
213
212
  })
214
213
 
215
214
  const interrupt = (
@@ -226,18 +225,33 @@ const useCreateStore = ({
226
225
  Effect.runFork,
227
226
  )
228
227
 
228
+ const inputPropChanges = {
229
+ schema: inputPropsCacheRef.current.schema !== schema,
230
+ otelOptions: inputPropsCacheRef.current.otelOptions !== otelOptions,
231
+ boot: inputPropsCacheRef.current.boot !== boot,
232
+ adapter: inputPropsCacheRef.current.adapter !== adapter,
233
+ batchUpdates: inputPropsCacheRef.current.batchUpdates !== batchUpdates,
234
+ disableDevtools: inputPropsCacheRef.current.disableDevtools !== disableDevtools,
235
+ signal: inputPropsCacheRef.current.signal !== signal,
236
+ context: inputPropsCacheRef.current.context !== context,
237
+ params: inputPropsCacheRef.current.params !== params,
238
+ confirmUnsavedChanges: inputPropsCacheRef.current.confirmUnsavedChanges !== confirmUnsavedChanges,
239
+ syncPayload: inputPropsCacheRef.current.syncPayload !== syncPayload,
240
+ debugInstanceId: inputPropsCacheRef.current.debugInstanceId !== debugInstanceId,
241
+ }
242
+
229
243
  if (
230
- inputPropsCacheRef.current.schema !== schema ||
231
- inputPropsCacheRef.current.otelOptions !== otelOptions ||
232
- inputPropsCacheRef.current.boot !== boot ||
233
- inputPropsCacheRef.current.adapter !== adapter ||
234
- inputPropsCacheRef.current.batchUpdates !== batchUpdates ||
235
- inputPropsCacheRef.current.disableDevtools !== disableDevtools ||
236
- inputPropsCacheRef.current.signal !== signal ||
237
- inputPropsCacheRef.current.context !== context ||
238
- inputPropsCacheRef.current.params !== params ||
239
- inputPropsCacheRef.current.confirmUnsavedChanges !== confirmUnsavedChanges ||
240
- inputPropsCacheRef.current.syncPayload !== syncPayload
244
+ inputPropChanges.schema ||
245
+ inputPropChanges.otelOptions ||
246
+ inputPropChanges.boot ||
247
+ inputPropChanges.adapter ||
248
+ inputPropChanges.batchUpdates ||
249
+ inputPropChanges.disableDevtools ||
250
+ inputPropChanges.signal ||
251
+ inputPropChanges.context ||
252
+ inputPropChanges.params ||
253
+ inputPropChanges.confirmUnsavedChanges ||
254
+ inputPropChanges.syncPayload
241
255
  ) {
242
256
  inputPropsCacheRef.current = {
243
257
  schema,
@@ -251,12 +265,17 @@ const useCreateStore = ({
251
265
  params,
252
266
  confirmUnsavedChanges,
253
267
  syncPayload,
268
+ debugInstanceId,
254
269
  }
255
270
  if (ctxValueRef.current.componentScope !== undefined && ctxValueRef.current.shutdownDeferred !== undefined) {
271
+ const changedInputProps = Object.keys(inputPropChanges).filter(
272
+ (key) => inputPropChanges[key as keyof typeof inputPropChanges],
273
+ )
274
+
256
275
  interrupt(
257
276
  ctxValueRef.current.componentScope,
258
277
  ctxValueRef.current.shutdownDeferred,
259
- new StoreInterrupted({ reason: 're-rendering due to changed input props' }),
278
+ new StoreInterrupted({ reason: `re-rendering due to changed input props: ${changedInputProps.join(', ')}` }),
260
279
  )
261
280
  ctxValueRef.current.componentScope = undefined
262
281
  ctxValueRef.current.shutdownDeferred = undefined
@@ -265,6 +284,7 @@ const useCreateStore = ({
265
284
  value: { stage: 'loading' },
266
285
  componentScope: undefined,
267
286
  shutdownDeferred: undefined,
287
+ previousShutdownDeferred: ctxValueRef.current.shutdownDeferred,
268
288
  counter: ctxValueRef.current.counter + 1,
269
289
  }
270
290
  }
@@ -295,7 +315,12 @@ const useCreateStore = ({
295
315
  })
296
316
 
297
317
  const cancel = Effect.gen(function* () {
298
- const componentScope = yield* Scope.make()
318
+ // Wait for the previous store to fully shutdown before creating a new one
319
+ if (ctxValueRef.current.previousShutdownDeferred) {
320
+ yield* Deferred.await(ctxValueRef.current.previousShutdownDeferred)
321
+ }
322
+
323
+ const componentScope = yield* Scope.make().pipe(Effect.acquireRelease(Scope.close))
299
324
  const shutdownDeferred = yield* makeShutdownDeferred
300
325
 
301
326
  ctxValueRef.current.componentScope = componentScope
@@ -319,6 +344,7 @@ const useCreateStore = ({
319
344
  // NOTE sometimes when status come in in rapid succession, only the last value will be rendered by React
320
345
  setContextValue(status)
321
346
  },
347
+ debug: { instanceId: debugInstanceId },
322
348
  }).pipe(Effect.tapErrorCause((cause) => Deferred.failCause(shutdownDeferred, cause)))
323
349
 
324
350
  setContextValue({ stage: 'running', store })
@@ -337,10 +363,6 @@ const useCreateStore = ({
337
363
  )
338
364
  }).pipe(
339
365
  Effect.scoped,
340
- // NOTE we're running the code above in a semaphore to make sure a previous store is always fully
341
- // shutdown before a new one is created - especially when shutdown logic is async. You can't trust `React.useEffect`.
342
- // Thank you to Mattia Manzati for this idea.
343
- withSemaphore(storeId),
344
366
  Effect.withSpan('@livestore/react:useCreateStore'),
345
367
  LS_DEV ? TaskTracing.withAsyncTaggingTracing((name: string) => (console as any).createTask(name)) : identity,
346
368
  provideOtel({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer }),
@@ -377,6 +399,7 @@ const useCreateStore = ({
377
399
  params,
378
400
  confirmUnsavedChanges,
379
401
  syncPayload,
402
+ debugInstanceId,
380
403
  ])
381
404
 
382
405
  return ctxValueRef.current.value
@@ -20,6 +20,28 @@ exports[`useClientDocument > otel > should update the data based on component ke
20
20
  ",
21
21
  },
22
22
  },
23
+ {
24
+ "_name": "@livestore/common:LeaderSyncProcessor:push",
25
+ "attributes": {
26
+ "batch": "undefined",
27
+ "batchSize": 1,
28
+ },
29
+ },
30
+ {
31
+ "_name": "@livestore/common:LeaderSyncProcessor:push",
32
+ "attributes": {
33
+ "batch": "undefined",
34
+ "batchSize": 1,
35
+ },
36
+ },
37
+ {
38
+ "_name": "client-session-sync-processor:pull",
39
+ "attributes": {
40
+ "code.stacktrace": "at /Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/common/src/sync/ClientSessionSyncProcessor.ts:281:14",
41
+ "span.label": "⚠︎ Interrupted",
42
+ "status.interrupted": true,
43
+ },
44
+ },
23
45
  {
24
46
  "_name": "LiveStore:sync",
25
47
  },
@@ -224,6 +246,28 @@ exports[`useClientDocument > otel > should update the data based on component ke
224
246
  ",
225
247
  },
226
248
  },
249
+ {
250
+ "_name": "@livestore/common:LeaderSyncProcessor:push",
251
+ "attributes": {
252
+ "batch": "undefined",
253
+ "batchSize": 1,
254
+ },
255
+ },
256
+ {
257
+ "_name": "@livestore/common:LeaderSyncProcessor:push",
258
+ "attributes": {
259
+ "batch": "undefined",
260
+ "batchSize": 1,
261
+ },
262
+ },
263
+ {
264
+ "_name": "client-session-sync-processor:pull",
265
+ "attributes": {
266
+ "code.stacktrace": "at /Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/common/src/sync/ClientSessionSyncProcessor.ts:281:14",
267
+ "span.label": "⚠︎ Interrupted",
268
+ "status.interrupted": true,
269
+ },
270
+ },
227
271
  {
228
272
  "_name": "LiveStore:sync",
229
273
  },
package/src/useQuery.ts CHANGED
@@ -47,7 +47,8 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
47
47
  React.useContext(LiveStoreContext)?.store ??
48
48
  shouldNeverHappen(`No store provided to useQuery`)
49
49
 
50
- const rcRefKey = `${store.storeId}_${queryDef.hash}`
50
+ // It's important to use all "aspects" of a store instance here, otherwise we get unexpected cache mappings
51
+ const rcRefKey = `${store.storeId}_${store.clientId}_${store.sessionId}_${queryDef.hash}`
51
52
 
52
53
  const stackInfo = React.useMemo(() => {
53
54
  Error.stackTraceLimit = 10