@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/effect/LiveStore.js +1 -1
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/react/LiveStoreProvider.js +2 -2
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/useLocalId.d.ts.map +1 -1
- package/dist/react/useLocalId.js +1 -0
- package/dist/react/useLocalId.js.map +1 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +1 -0
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js +12 -7
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/react/useTemporaryQuery.test.js +23 -1
- package/dist/react/useTemporaryQuery.test.js.map +1 -1
- package/dist/store-devtools.js +1 -1
- package/dist/store-devtools.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -4
- package/dist/store.js.map +1 -1
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js +1 -0
- package/dist/utils/dev.js.map +1 -1
- package/package.json +11 -10
- package/src/ambient.d.ts +3 -0
- package/src/effect/LiveStore.ts +1 -1
- package/src/react/LiveStoreProvider.tsx +2 -2
- package/src/react/useLocalId.ts +1 -0
- package/src/react/useQuery.ts +1 -0
- package/src/react/useTemporaryQuery.test.tsx +44 -2
- package/src/react/useTemporaryQuery.ts +23 -13
- package/src/store-devtools.ts +1 -1
- package/src/store.ts +6 -4
- package/src/utils/dev.ts +1 -0
|
@@ -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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|
|
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.
|
|
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
|
-
}, [
|
|
128
|
+
}, [])
|
|
119
129
|
|
|
120
130
|
return { query$, otelContext }
|
|
121
131
|
}
|
package/src/store-devtools.ts
CHANGED
|
@@ -56,7 +56,7 @@ export const connectDevtoolsToStore = ({
|
|
|
56
56
|
|
|
57
57
|
const requestId = decodedMessage.requestId
|
|
58
58
|
|
|
59
|
-
const requestIdleCallback =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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