@livestore/livestore 0.0.58-dev.7 → 0.0.58-dev.9

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.
@@ -1,9 +1,12 @@
1
1
  import { Effect, Schema } from '@livestore/utils/effect'
2
- import { renderHook } from '@testing-library/react'
2
+ import { render, renderHook } from '@testing-library/react'
3
+ import React from 'react'
4
+ // @ts-expect-error no types
5
+ import * as ReactWindow from 'react-window'
3
6
  import { describe, expect, it } from 'vitest'
4
7
 
5
8
  import { makeTodoMvc, tables, todos } from '../__tests__/react/fixture.js'
6
- import type * as LiveStore from '../index.js'
9
+ import * as LiveStore from '../index.js'
7
10
  import { querySQL } from '../reactiveQueries/sql.js'
8
11
  import * as LiveStoreReact from './index.js'
9
12
 
@@ -53,4 +56,43 @@ describe('useTemporaryQuery', () => {
53
56
 
54
57
  expect(queryMap.get('t2')!.runs).toBe(1)
55
58
  }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
59
+
60
+ // NOTE this test covers some special react lifecyle paths which I couldn't easily reproduce without react-window
61
+ // it basically causes a "query swap" in the `useMemo` and both a `useEffect` cleanup call.
62
+ // To handle this properly we introduced the `_tag: 'destroyed'` state in the `spanAlreadyStartedCache`.
63
+ it('should work for a list with react-window', () =>
64
+ Effect.gen(function* () {
65
+ const { wrapper } = yield* makeTodoMvc()
66
+
67
+ const ListWrapper: React.FC<{ numItems: number }> = ({ numItems }) => {
68
+ return (
69
+ <ReactWindow.FixedSizeList
70
+ height={100}
71
+ width={100}
72
+ itemSize={10}
73
+ itemCount={numItems}
74
+ itemData={Array.from({ length: numItems }, (_, i) => i).reverse()}
75
+ >
76
+ {ListItem}
77
+ </ReactWindow.FixedSizeList>
78
+ )
79
+ }
80
+
81
+ const ListItem: React.FC<{ data: ReadonlyArray<number>; index: number }> = ({ data: ids, index }) => {
82
+ const id = ids[index]!
83
+ const res = LiveStoreReact.useTemporaryQuery(
84
+ () => LiveStore.computed(() => id, { label: `ListItem.${id}` }),
85
+ id,
86
+ )
87
+ return <div role="listitem">{res}</div>
88
+ }
89
+
90
+ const renderResult = render(<ListWrapper numItems={1} />, { wrapper })
91
+
92
+ expect(renderResult.container.textContent).toBe('0')
93
+
94
+ renderResult.rerender(<ListWrapper numItems={2} />)
95
+
96
+ expect(renderResult.container.textContent).toBe('10')
97
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
56
98
  })
@@ -12,12 +12,16 @@ import { useQueryRef } from './useQuery.js'
12
12
  // Please definitely open an issue if you see or run into any problems with this approach!
13
13
  const cache = new Map<
14
14
  string,
15
- {
16
- rc: number
17
- query$: LiveQuery<any, any>
18
- span: otel.Span
19
- otelContext: otel.Context
20
- }
15
+ | {
16
+ _tag: 'active'
17
+ rc: number
18
+ query$: LiveQuery<any, any>
19
+ span: otel.Span
20
+ otelContext: otel.Context
21
+ }
22
+ | {
23
+ _tag: 'destroyed'
24
+ }
21
25
  >()
22
26
 
23
27
  export type DepKey = string | number | ReadonlyArray<string | number>
@@ -60,22 +64,24 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
60
64
 
61
65
  const { query$, otelContext } = React.useMemo(() => {
62
66
  if (fullKeyRef.current !== undefined && fullKeyRef.current !== fullKey) {
63
- // console.debug('fullKey changed, destroying previous', fullKeyRef.current.split('-')[0]!, fullKey.split('-')[0]!)
67
+ // console.debug('fullKey changed', 'prev', fullKeyRef.current.split('-')[0]!, '-> new', fullKey.split('-')[0]!)
64
68
 
65
69
  const cachedItem = cache.get(fullKeyRef.current)
66
- if (cachedItem !== undefined) {
70
+ if (cachedItem !== undefined && cachedItem._tag === 'active') {
67
71
  cachedItem.rc--
68
72
 
69
73
  if (cachedItem.rc === 0) {
74
+ // console.debug('rc=0-changed', cachedItem.query$.id, cachedItem.query$.label)
70
75
  cachedItem.query$.destroy()
71
76
  cachedItem.span.end()
72
- cache.delete(fullKeyRef.current)
77
+ cache.set(fullKeyRef.current, { _tag: 'destroyed' })
73
78
  }
74
79
  }
75
80
  }
76
81
 
77
82
  const cachedItem = cache.get(fullKey)
78
- if (cachedItem !== undefined) {
83
+ if (cachedItem !== undefined && cachedItem._tag === 'active') {
84
+ // console.debug('rc++', cachedItem.query$.id, cachedItem.query$.label)
79
85
  cachedItem.rc++
80
86
 
81
87
  return cachedItem
@@ -93,7 +99,7 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
93
99
 
94
100
  const query$ = makeQuery(otelContext)
95
101
 
96
- cache.set(fullKey, { rc: 1, query$, span, otelContext })
102
+ cache.set(fullKey, { _tag: 'active', rc: 1, query$, span, otelContext })
97
103
 
98
104
  return { query$, otelContext }
99
105
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -103,19 +109,23 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
103
109
 
104
110
  React.useEffect(() => {
105
111
  return () => {
112
+ const fullKey = fullKeyRef.current!
106
113
  const cachedItem = cache.get(fullKey)
107
114
  // NOTE in case the fullKey changed then the query was already destroyed in the useMemo above
108
- if (cachedItem === undefined) return
115
+ if (cachedItem === undefined || cachedItem._tag === 'destroyed') return
116
+
117
+ // console.debug('rc--', cachedItem.query$.id, cachedItem.query$.label)
109
118
 
110
119
  cachedItem.rc--
111
120
 
112
121
  if (cachedItem.rc === 0) {
122
+ // console.debug('rc=0', cachedItem.query$.id, cachedItem.query$.label)
113
123
  cachedItem.query$.destroy()
114
124
  cachedItem.span.end()
115
125
  cache.delete(fullKey)
116
126
  }
117
127
  }
118
- }, [fullKey])
128
+ }, [])
119
129
 
120
130
  return { query$, otelContext }
121
131
  }
@@ -56,7 +56,7 @@ export const connectDevtoolsToStore = ({
56
56
 
57
57
  const requestId = decodedMessage.requestId
58
58
 
59
- const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
59
+ const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
60
60
 
61
61
  switch (decodedMessage._tag) {
62
62
  case 'LSD.ReactivityGraphSubscribe': {
package/src/store.ts CHANGED
@@ -130,7 +130,9 @@ export type StoreMutateOptions = {
130
130
  persisted?: boolean
131
131
  }
132
132
 
133
- if (typeof window !== 'undefined') {
133
+ // eslint-disable-next-line unicorn/prefer-global-this
134
+ if (import.meta.env.DEV && typeof window !== 'undefined') {
135
+ // eslint-disable-next-line unicorn/prefer-global-this
134
136
  window.__debugDownloadBlob = downloadBlob
135
137
  }
136
138
 
@@ -308,7 +310,7 @@ export class Store<
308
310
  { attributes: { label: options?.label, queryLabel: query$.label } },
309
311
  options?.otelContext ?? this.otel.queriesSpanContext,
310
312
  (span) => {
311
- // console.log('store sub', query$.label)
313
+ // console.debug('store sub', query$.id, query$.label)
312
314
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
313
315
 
314
316
  const label = `subscribe:${options?.label}`
@@ -322,7 +324,7 @@ export class Store<
322
324
  }
323
325
 
324
326
  const unsubscribe = () => {
325
- // console.log('store unsub', query$.label)
327
+ // console.debug('store unsub', query$.id, query$.label)
326
328
  try {
327
329
  this.reactivityGraph.destroyNode(effect)
328
330
  this.activeQueries.remove(query$ as LiveQuery<TResult>)
@@ -393,7 +395,7 @@ export class Store<
393
395
  mutationsSpan.addEvent('mutate')
394
396
 
395
397
  // console.group('LiveStore.mutate', { skipRefresh, wasSyncMessage, label })
396
- // mutationsEvents.forEach((_) => console.log(_.mutation, _.id, _.args))
398
+ // mutationsEvents.forEach((_) => console.debug(_.mutation, _.id, _.args))
397
399
  // console.groupEnd()
398
400
 
399
401
  let durationMs: number
package/src/utils/dev.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable unicorn/prefer-global-this */
1
2
  export const downloadBlob = (
2
3
  data: Uint8Array | Blob | string,
3
4
  fileName: string,