@livestore/livestore 0.0.54-dev.5 → 0.0.55-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 (118) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/MainDatabaseWrapper.d.ts +6 -5
  3. package/dist/MainDatabaseWrapper.d.ts.map +1 -1
  4. package/dist/MainDatabaseWrapper.js +3 -3
  5. package/dist/MainDatabaseWrapper.js.map +1 -1
  6. package/dist/QueryCache.d.ts +1 -1
  7. package/dist/QueryCache.d.ts.map +1 -1
  8. package/dist/QueryCache.js.map +1 -1
  9. package/dist/__tests__/react/fixture.d.ts +9 -27
  10. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  11. package/dist/__tests__/react/fixture.js +12 -10
  12. package/dist/__tests__/react/fixture.js.map +1 -1
  13. package/dist/effect/LiveStore.d.ts +20 -12
  14. package/dist/effect/LiveStore.d.ts.map +1 -1
  15. package/dist/effect/LiveStore.js +23 -22
  16. package/dist/effect/LiveStore.js.map +1 -1
  17. package/dist/effect/index.d.ts +1 -1
  18. package/dist/effect/index.d.ts.map +1 -1
  19. package/dist/effect/index.js +1 -1
  20. package/dist/effect/index.js.map +1 -1
  21. package/dist/global-state.d.ts +1 -3
  22. package/dist/global-state.d.ts.map +1 -1
  23. package/dist/global-state.js +2 -3
  24. package/dist/global-state.js.map +1 -1
  25. package/dist/index.d.ts +6 -7
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -6
  28. package/dist/index.js.map +1 -1
  29. package/dist/react/LiveStoreContext.d.ts +5 -2
  30. package/dist/react/LiveStoreContext.d.ts.map +1 -1
  31. package/dist/react/LiveStoreContext.js +3 -0
  32. package/dist/react/LiveStoreContext.js.map +1 -1
  33. package/dist/react/LiveStoreProvider.d.ts +8 -7
  34. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  35. package/dist/react/LiveStoreProvider.js +70 -43
  36. package/dist/react/LiveStoreProvider.js.map +1 -1
  37. package/dist/react/LiveStoreProvider.test.js +33 -12
  38. package/dist/react/LiveStoreProvider.test.js.map +1 -1
  39. package/dist/react/components/LiveList.d.ts.map +1 -1
  40. package/dist/react/useAtom.d.ts +1 -1
  41. package/dist/react/useAtom.d.ts.map +1 -1
  42. package/dist/react/useLocalId.d.ts.map +1 -1
  43. package/dist/react/useQuery.d.ts.map +1 -1
  44. package/dist/react/useQuery.js +2 -2
  45. package/dist/react/useQuery.js.map +1 -1
  46. package/dist/react/useRow.d.ts +2 -2
  47. package/dist/react/useRow.d.ts.map +1 -1
  48. package/dist/react/useRow.js +5 -5
  49. package/dist/react/useRow.js.map +1 -1
  50. package/dist/react/useRow.test.js +22 -22
  51. package/dist/react/useRow.test.js.map +1 -1
  52. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  53. package/dist/react/useTemporaryQuery.js +1 -1
  54. package/dist/react/useTemporaryQuery.js.map +1 -1
  55. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  56. package/dist/reactive.d.ts +1 -1
  57. package/dist/reactive.d.ts.map +1 -1
  58. package/dist/reactive.js +4 -5
  59. package/dist/reactive.js.map +1 -1
  60. package/dist/reactiveQueries/base-class.d.ts +6 -6
  61. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  62. package/dist/reactiveQueries/base-class.js +3 -3
  63. package/dist/reactiveQueries/base-class.js.map +1 -1
  64. package/dist/reactiveQueries/graphql.d.ts +8 -8
  65. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  66. package/dist/reactiveQueries/graphql.js +10 -10
  67. package/dist/reactiveQueries/graphql.js.map +1 -1
  68. package/dist/reactiveQueries/js.d.ts +6 -6
  69. package/dist/reactiveQueries/js.d.ts.map +1 -1
  70. package/dist/reactiveQueries/js.js +8 -8
  71. package/dist/reactiveQueries/js.js.map +1 -1
  72. package/dist/reactiveQueries/sql.d.ts +9 -10
  73. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  74. package/dist/reactiveQueries/sql.js +12 -12
  75. package/dist/reactiveQueries/sql.js.map +1 -1
  76. package/dist/reactiveQueries/sql.test.js +6 -6
  77. package/dist/reactiveQueries/sql.test.js.map +1 -1
  78. package/dist/row-query.d.ts +2 -2
  79. package/dist/row-query.d.ts.map +1 -1
  80. package/dist/row-query.js +4 -38
  81. package/dist/row-query.js.map +1 -1
  82. package/dist/store.d.ts +41 -24
  83. package/dist/store.d.ts.map +1 -1
  84. package/dist/store.js +336 -223
  85. package/dist/store.js.map +1 -1
  86. package/dist/utils/otel.d.ts.map +1 -1
  87. package/package.json +10 -19
  88. package/src/MainDatabaseWrapper.ts +14 -8
  89. package/src/QueryCache.ts +1 -2
  90. package/src/__tests__/react/fixture.tsx +13 -11
  91. package/src/effect/LiveStore.ts +65 -54
  92. package/src/effect/index.ts +2 -1
  93. package/src/global-state.ts +2 -6
  94. package/src/index.ts +25 -7
  95. package/src/react/LiveStoreContext.ts +7 -2
  96. package/src/react/LiveStoreProvider.test.tsx +56 -14
  97. package/src/react/LiveStoreProvider.tsx +105 -46
  98. package/src/react/useQuery.ts +2 -2
  99. package/src/react/useRow.test.tsx +22 -22
  100. package/src/react/useRow.ts +7 -10
  101. package/src/react/useTemporaryQuery.ts +2 -2
  102. package/src/reactive.ts +6 -5
  103. package/src/reactiveQueries/base-class.ts +9 -9
  104. package/src/reactiveQueries/graphql.ts +19 -15
  105. package/src/reactiveQueries/js.ts +12 -12
  106. package/src/reactiveQueries/sql.test.ts +6 -6
  107. package/src/reactiveQueries/sql.ts +19 -21
  108. package/src/row-query.ts +8 -54
  109. package/src/store.ts +533 -284
  110. package/dist/utils/bounded-collections.d.ts +0 -34
  111. package/dist/utils/bounded-collections.d.ts.map +0 -1
  112. package/dist/utils/bounded-collections.js +0 -91
  113. package/dist/utils/bounded-collections.js.map +0 -1
  114. package/dist/utils/util.d.ts +0 -14
  115. package/dist/utils/util.d.ts.map +0 -1
  116. package/dist/utils/util.js +0 -19
  117. package/dist/utils/util.js.map +0 -1
  118. package/src/utils/util.ts +0 -31
@@ -7,27 +7,25 @@ import { describe, expect, it } from 'vitest'
7
7
 
8
8
  import { parseTodos, schema } from '../__tests__/react/fixture.js'
9
9
  import { querySQL } from '../reactiveQueries/sql.js'
10
- import type { Store } from '../store.js'
11
10
  import * as LiveStoreReact from './index.js'
12
11
  import { LiveStoreProvider } from './LiveStoreProvider.js'
13
12
 
14
13
  describe('LiveStoreProvider', () => {
15
14
  it('simple', async () => {
16
- let renderCount = 0
15
+ let appRenderCount = 0
17
16
 
18
17
  const allTodos$ = querySQL(`select * from todos`, { map: parseTodos })
19
- let latestStoreCtx: { store: Store } | undefined = undefined
20
18
 
21
19
  const App = () => {
22
- renderCount++
23
-
24
- latestStoreCtx = LiveStoreReact.useStore()
20
+ appRenderCount++
25
21
 
26
22
  const todos = LiveStoreReact.useQuery(allTodos$)
27
23
 
28
24
  return <div>{JSON.stringify(todos)}</div>
29
25
  }
30
26
 
27
+ const abortController = new AbortController()
28
+
31
29
  const Root = ({ forceUpdate }: { forceUpdate: number }) => {
32
30
  const bootCb = React.useCallback(
33
31
  (db: BootDb) =>
@@ -37,7 +35,13 @@ describe('LiveStoreProvider', () => {
37
35
  // eslint-disable-next-line react-hooks/exhaustive-deps
38
36
  const adapterMemo = React.useMemo(() => makeInMemoryAdapter(), [forceUpdate])
39
37
  return (
40
- <LiveStoreProvider schema={schema} fallback={<div>Loading LiveStore</div>} adapter={adapterMemo} boot={bootCb}>
38
+ <LiveStoreProvider
39
+ schema={schema}
40
+ renderLoading={(status) => <div>Loading LiveStore: {status.stage}</div>}
41
+ adapter={adapterMemo}
42
+ boot={bootCb}
43
+ signal={abortController.signal}
44
+ >
41
45
  <App />
42
46
  </LiveStoreProvider>
43
47
  )
@@ -45,19 +49,57 @@ describe('LiveStoreProvider', () => {
45
49
 
46
50
  const { rerender } = render(<Root forceUpdate={1} />)
47
51
 
48
- expect(renderCount).toBe(0)
52
+ expect(appRenderCount).toBe(0)
49
53
 
50
- await waitForElementToBeRemoved(() => screen.getByText('Loading LiveStore'))
54
+ await waitForElementToBeRemoved(() => screen.getByText((_) => _.startsWith('Loading LiveStore')))
51
55
 
52
- expect(renderCount).toBe(1)
56
+ expect(appRenderCount).toBe(1)
53
57
 
54
58
  rerender(<Root forceUpdate={2} />)
55
59
 
56
- await waitFor(() => screen.getByText('Loading LiveStore'))
57
- await waitForElementToBeRemoved(() => screen.getByText('Loading LiveStore'))
60
+ await waitFor(() => screen.getByText('Loading LiveStore: loading'))
61
+ await waitFor(() => screen.getByText((_) => _.includes('buy milk')))
62
+
63
+ expect(appRenderCount).toBe(2)
64
+
65
+ abortController.abort()
66
+
67
+ await waitFor(() => screen.getByText('LiveStore Shutdown'))
68
+ })
69
+
70
+ it('error during boot', async () => {
71
+ let appRenderCount = 0
72
+
73
+ const App = () => {
74
+ appRenderCount++
75
+
76
+ return <div>hello world</div>
77
+ }
78
+
79
+ const Root = ({ forceUpdate }: { forceUpdate: number }) => {
80
+ const bootCb = React.useCallback(
81
+ (db: BootDb) =>
82
+ db.execute(sql`INSERT INTO todos_mising_table (id, text, completed) VALUES ('t1', 'buy milk', 0);`),
83
+ [],
84
+ )
85
+ // eslint-disable-next-line react-hooks/exhaustive-deps
86
+ const adapterMemo = React.useMemo(() => makeInMemoryAdapter(), [forceUpdate])
87
+ return (
88
+ <LiveStoreProvider
89
+ schema={schema}
90
+ renderLoading={(status) => <div>Loading LiveStore: {status.stage}</div>}
91
+ adapter={adapterMemo}
92
+ boot={bootCb}
93
+ >
94
+ <App />
95
+ </LiveStoreProvider>
96
+ )
97
+ }
98
+
99
+ render(<Root forceUpdate={1} />)
58
100
 
59
- expect(renderCount).toBe(2)
101
+ expect(appRenderCount).toBe(0)
60
102
 
61
- await latestStoreCtx!.store.destroy()
103
+ await waitFor(() => screen.getByText((_) => _.startsWith('LiveStore.UnexpectedError')))
62
104
  })
63
105
  })
@@ -1,53 +1,66 @@
1
- import type { BootDb, StoreAdapterFactory } from '@livestore/common'
1
+ import { type BootDb, type BootStatus, type StoreAdapterFactory, UnexpectedError } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
- import { shouldNeverHappen } from '@livestore/utils'
3
+ import { errorToString } from '@livestore/utils'
4
+ import { Effect, Exit, FiberSet, Logger, LogLevel, Schema, Scope } from '@livestore/utils/effect'
4
5
  import type * as otel from '@opentelemetry/api'
5
6
  import type { ReactElement, ReactNode } from 'react'
6
7
  import React from 'react'
7
8
 
8
9
  // TODO refactor so the `react` module doesn't depend on `effect` module
9
10
  import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
10
- import type { BaseGraphQLContext, GraphQLOptions, Store } from '../store.js'
11
+ import type { BaseGraphQLContext, GraphQLOptions, OtelOptions } from '../store.js'
11
12
  import { createStore } from '../store.js'
12
13
  import { LiveStoreContext } from './LiveStoreContext.js'
13
14
 
14
15
  interface LiveStoreProviderProps<GraphQLContext> {
15
16
  schema: LiveStoreSchema
16
- boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
17
+ boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
17
18
  graphQLOptions?: GraphQLOptions<GraphQLContext>
18
- otelTracer?: otel.Tracer
19
- otelRootSpanContext?: otel.Context
20
- fallback: ReactElement
19
+ otelOptions?: OtelOptions
20
+ renderLoading: (status: BootStatus) => ReactElement
21
21
  adapter: StoreAdapterFactory
22
22
  batchUpdates?: (run: () => void) => void
23
23
  disableDevtools?: boolean
24
+ signal?: AbortSignal
24
25
  }
25
26
 
26
27
  export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
27
- fallback,
28
+ renderLoading,
28
29
  graphQLOptions,
29
- otelTracer,
30
- otelRootSpanContext,
30
+ otelOptions,
31
31
  children,
32
32
  schema,
33
33
  boot,
34
34
  adapter,
35
35
  batchUpdates,
36
36
  disableDevtools,
37
+ signal,
37
38
  }: LiveStoreProviderProps<GraphQLContext> & { children?: ReactNode }): JSX.Element => {
38
39
  const storeCtx = useCreateStore({
39
40
  schema,
40
41
  graphQLOptions,
41
- otelTracer,
42
- otelRootSpanContext,
42
+ otelOptions,
43
43
  boot,
44
44
  adapter,
45
45
  batchUpdates,
46
46
  disableDevtools,
47
+ signal,
47
48
  })
48
49
 
49
- if (storeCtx === undefined) {
50
- return fallback
50
+ if (storeCtx.stage === 'error') {
51
+ return (
52
+ <div>
53
+ {Schema.is(UnexpectedError)(storeCtx.error) ? storeCtx.error.toString() : errorToString(storeCtx.error)}
54
+ </div>
55
+ )
56
+ }
57
+
58
+ if (storeCtx.stage === 'shutdown') {
59
+ return <div>LiveStore Shutdown</div>
60
+ }
61
+
62
+ if (storeCtx.stage !== 'running') {
63
+ return <div>{renderLoading(storeCtx)}</div>
51
64
  }
52
65
 
53
66
  window.__debugLiveStore = storeCtx.store
@@ -58,78 +71,124 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
58
71
  const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
59
72
  schema,
60
73
  graphQLOptions,
61
- otelTracer,
62
- otelRootSpanContext,
74
+ otelOptions,
63
75
  boot,
64
76
  adapter,
65
77
  batchUpdates,
66
78
  disableDevtools,
79
+ signal,
67
80
  }: LiveStoreCreateStoreOptions<GraphQLContext>) => {
68
81
  const [_, rerender] = React.useState(0)
69
- const ctxValueRef = React.useRef<StoreContext_ | undefined>(undefined)
82
+ const ctxValueRef = React.useRef<{
83
+ value: StoreContext_ | BootStatus
84
+ scope: Scope.CloseableScope | undefined
85
+ counter: number
86
+ }>({
87
+ value: { stage: 'loading' },
88
+ scope: undefined,
89
+ counter: 0,
90
+ })
91
+
92
+ // console.debug(`useCreateStore (${ctxValueRef.current.counter})`, ctxValueRef.current.value.stage)
93
+
70
94
  const inputPropsCacheRef = React.useRef({
71
95
  schema,
72
96
  graphQLOptions,
73
- otelTracer,
74
- otelRootSpanContext,
97
+ otelOptions,
75
98
  boot,
76
99
  adapter,
77
100
  batchUpdates,
101
+ disableDevtools,
102
+ signal,
78
103
  })
79
- const oldStoreAlreadyDestroyedRef = React.useRef(false)
80
104
 
81
105
  if (
82
106
  inputPropsCacheRef.current.schema !== schema ||
83
107
  inputPropsCacheRef.current.graphQLOptions !== graphQLOptions ||
84
- inputPropsCacheRef.current.otelTracer !== otelTracer ||
85
- inputPropsCacheRef.current.otelRootSpanContext !== otelRootSpanContext ||
108
+ inputPropsCacheRef.current.otelOptions !== otelOptions ||
86
109
  inputPropsCacheRef.current.boot !== boot ||
87
110
  inputPropsCacheRef.current.adapter !== adapter ||
88
- inputPropsCacheRef.current.batchUpdates !== batchUpdates
111
+ inputPropsCacheRef.current.batchUpdates !== batchUpdates ||
112
+ inputPropsCacheRef.current.disableDevtools !== disableDevtools ||
113
+ inputPropsCacheRef.current.signal !== signal
89
114
  ) {
90
115
  inputPropsCacheRef.current = {
91
116
  schema,
92
117
  graphQLOptions,
93
- otelTracer,
94
- otelRootSpanContext,
118
+ otelOptions,
95
119
  boot,
96
120
  adapter,
97
121
  batchUpdates,
122
+ disableDevtools,
123
+ signal,
124
+ }
125
+ if (ctxValueRef.current.scope !== undefined) {
126
+ Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
98
127
  }
99
- ctxValueRef.current?.store.destroy()
100
- oldStoreAlreadyDestroyedRef.current = true
101
- ctxValueRef.current = undefined
128
+ ctxValueRef.current = { value: { stage: 'loading' }, scope: undefined, counter: ctxValueRef.current.counter + 1 }
102
129
  }
103
130
 
104
131
  React.useEffect(() => {
105
- let store: Store | undefined
132
+ const storeScope = Scope.make().pipe(Effect.runSync)
106
133
 
107
- void (async () => {
108
- try {
109
- store = await createStore({
134
+ const counter = ctxValueRef.current.counter
135
+
136
+ const setContextValue = (value: StoreContext_ | BootStatus) => {
137
+ if (ctxValueRef.current.counter !== counter) return
138
+ ctxValueRef.current.value = value
139
+ rerender((c) => c + 1)
140
+ }
141
+
142
+ Scope.addFinalizer(
143
+ storeScope,
144
+ Effect.sync(() => setContextValue({ stage: 'shutdown' })),
145
+ ).pipe(Effect.runSync)
146
+
147
+ ctxValueRef.current.scope = storeScope
148
+
149
+ signal?.addEventListener('abort', () => {
150
+ if (ctxValueRef.current.scope !== undefined && ctxValueRef.current.counter === counter) {
151
+ Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
152
+ ctxValueRef.current.scope = undefined
153
+ }
154
+ })
155
+
156
+ FiberSet.make().pipe(
157
+ Effect.andThen((fiberSet) =>
158
+ createStore({
159
+ fiberSet,
110
160
  schema,
111
161
  graphQLOptions,
112
- otelTracer,
113
- otelRootSpanContext,
162
+ otelOptions,
114
163
  boot,
115
164
  adapter,
116
165
  batchUpdates,
117
166
  disableDevtools,
118
- })
119
- ctxValueRef.current = { store }
120
- oldStoreAlreadyDestroyedRef.current = false
121
- rerender((c) => c + 1)
122
- } catch (e) {
123
- shouldNeverHappen(`Error creating LiveStore store: ${e}`)
124
- }
125
- })()
167
+ onBootStatus: (status) => {
168
+ if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
169
+ setContextValue(status)
170
+ },
171
+ }),
172
+ ),
173
+ Effect.tapSync((store) => setContextValue({ stage: 'running', store })),
174
+ Effect.tapError((error) => Effect.sync(() => setContextValue({ stage: 'error', error }))),
175
+ Effect.tapDefect((defect) => Effect.sync(() => setContextValue({ stage: 'error', error: defect }))),
176
+ Scope.extend(storeScope),
177
+ Effect.forkIn(storeScope),
178
+ Effect.tapCauseLogPretty,
179
+ Effect.annotateLogs({ thread: 'window' }),
180
+ Effect.provide(Logger.pretty),
181
+ Logger.withMinimumLogLevel(LogLevel.Debug),
182
+ Effect.runFork,
183
+ )
126
184
 
127
185
  return () => {
128
- if (oldStoreAlreadyDestroyedRef.current === false) {
129
- store?.destroy()
186
+ if (ctxValueRef.current.scope !== undefined) {
187
+ Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
188
+ ctxValueRef.current.scope = undefined
130
189
  }
131
190
  }
132
- }, [schema, graphQLOptions, otelTracer, otelRootSpanContext, boot, adapter, batchUpdates, disableDevtools])
191
+ }, [schema, graphQLOptions, otelOptions, boot, adapter, batchUpdates, disableDevtools, signal])
133
192
 
134
- return ctxValueRef.current
193
+ return ctxValueRef.current.value
135
194
  }
@@ -1,5 +1,5 @@
1
+ import { deepEqual } from '@livestore/utils'
1
2
  import * as otel from '@opentelemetry/api'
2
- import { isEqual } from 'lodash-es'
3
3
  import React from 'react'
4
4
 
5
5
  import type { GetResult, LiveQueryAny } from '../reactiveQueries/base-class.js'
@@ -90,7 +90,7 @@ export const useQueryRef = <TQuery extends LiveQueryAny>(
90
90
  // NOTE: we return a reference to the result object within LiveStore;
91
91
  // this implies that app code must not mutate the results, or else
92
92
  // there may be weird reactivity bugs.
93
- if (isEqual(newValue, valueRef.current) === false) {
93
+ if (deepEqual(newValue, valueRef.current) === false) {
94
94
  setValue(newValue)
95
95
  }
96
96
  },
@@ -15,9 +15,9 @@ import type { StackInfo } from './utils/stack-info.js'
15
15
  // NOTE running tests concurrently doesn't work with the default global db graph
16
16
  describe.concurrent('useRow', () => {
17
17
  it('should update the data based on component key', async () => {
18
- using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
18
+ using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
19
19
 
20
- const { wrapper, AppComponentSchema, store, dbGraph, makeRenderCount } = inputs
20
+ const { wrapper, AppComponentSchema, store, reactivityGraph, makeRenderCount } = inputs
21
21
 
22
22
  const renderCount = makeRenderCount()
23
23
 
@@ -25,7 +25,7 @@ describe.concurrent('useRow', () => {
25
25
  (userId: string) => {
26
26
  renderCount.inc()
27
27
 
28
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
28
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
29
29
  return { state, setState }
30
30
  },
31
31
  { wrapper, initialProps: 'u1' },
@@ -53,9 +53,9 @@ describe.concurrent('useRow', () => {
53
53
  // TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
54
54
 
55
55
  it('should update the data reactively - via setState', async () => {
56
- using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
56
+ using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
57
57
 
58
- const { wrapper, AppComponentSchema, dbGraph, makeRenderCount } = inputs
58
+ const { wrapper, AppComponentSchema, reactivityGraph, makeRenderCount } = inputs
59
59
 
60
60
  const renderCount = makeRenderCount()
61
61
 
@@ -63,7 +63,7 @@ describe.concurrent('useRow', () => {
63
63
  (userId: string) => {
64
64
  renderCount.inc()
65
65
 
66
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
66
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
67
67
  return { state, setState }
68
68
  },
69
69
  { wrapper, initialProps: 'u1' },
@@ -81,9 +81,9 @@ describe.concurrent('useRow', () => {
81
81
  })
82
82
 
83
83
  it('should update the data reactively - via raw store mutation', async () => {
84
- using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
84
+ using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
85
85
 
86
- const { wrapper, AppComponentSchema, store, dbGraph, makeRenderCount } = inputs
86
+ const { wrapper, AppComponentSchema, store, reactivityGraph, makeRenderCount } = inputs
87
87
 
88
88
  const renderCount = makeRenderCount()
89
89
 
@@ -91,7 +91,7 @@ describe.concurrent('useRow', () => {
91
91
  (userId: string) => {
92
92
  renderCount.inc()
93
93
 
94
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
94
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
95
95
  return { state, setState }
96
96
  },
97
97
  { wrapper, initialProps: 'u1' },
@@ -115,17 +115,17 @@ describe.concurrent('useRow', () => {
115
115
  })
116
116
 
117
117
  it('should work for a larger app', async () => {
118
- using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
119
- const { wrapper, store, dbGraph, makeRenderCount, AppRouterSchema } = inputs
118
+ using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
119
+ const { wrapper, store, reactivityGraph, makeRenderCount, AppRouterSchema } = inputs
120
120
 
121
- const allTodos$ = LiveStore.querySQL<Todo[]>(`select * from todos`, { label: 'allTodos', dbGraph })
121
+ const allTodos$ = LiveStore.querySQL<Todo[]>(`select * from todos`, { label: 'allTodos', reactivityGraph })
122
122
 
123
123
  const appRouterRenderCount = makeRenderCount()
124
124
  let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
125
125
  const AppRouter: React.FC = () => {
126
126
  appRouterRenderCount.inc()
127
127
 
128
- const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { dbGraph })
128
+ const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
129
129
 
130
130
  globalSetState = setState
131
131
 
@@ -153,7 +153,7 @@ describe.concurrent('useRow', () => {
153
153
  }
154
154
 
155
155
  const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
156
- const [todo] = LiveStoreReact.useRow(todos, id, { dbGraph })
156
+ const [todo] = LiveStoreReact.useRow(todos, id, { reactivityGraph })
157
157
  return <div role="content">{JSON.stringify(todo)}</div>
158
158
  }
159
159
 
@@ -198,8 +198,8 @@ describe.concurrent('useRow', () => {
198
198
  })
199
199
 
200
200
  it('should work for a useRow query chained with a useTemporary query', async () => {
201
- using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
202
- const { store, wrapper, AppComponentSchema, dbGraph, makeRenderCount } = inputs
201
+ using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
202
+ const { store, wrapper, AppComponentSchema, reactivityGraph, makeRenderCount } = inputs
203
203
  const renderCount = makeRenderCount()
204
204
 
205
205
  store.mutate(
@@ -211,12 +211,12 @@ describe.concurrent('useRow', () => {
211
211
  (userId: string) => {
212
212
  renderCount.inc()
213
213
 
214
- const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
214
+ const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
215
215
  const todos = LiveStoreReact.useTemporaryQuery(
216
216
  () =>
217
217
  LiveStore.querySQL<any[]>(
218
218
  (get) => LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
219
- { dbGraph, label: 'todosFiltered' },
219
+ { reactivityGraph, label: 'todosFiltered' },
220
220
  ),
221
221
  userId,
222
222
  )
@@ -262,9 +262,9 @@ describe.concurrent('useRow', () => {
262
262
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
263
263
 
264
264
  it('should update the data based on component key', async () => {
265
- using inputs = await makeTodoMvc({ useGlobalDbGraph: false, otelContext, otelTracer })
265
+ using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false, otelContext, otelTracer })
266
266
 
267
- const { wrapper, AppComponentSchema, store, dbGraph, makeRenderCount, strictMode } = inputs
267
+ const { wrapper, AppComponentSchema, store, reactivityGraph, makeRenderCount, strictMode } = inputs
268
268
 
269
269
  const renderCount = makeRenderCount()
270
270
 
@@ -272,7 +272,7 @@ describe.concurrent('useRow', () => {
272
272
  (userId: string) => {
273
273
  renderCount.inc()
274
274
 
275
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
275
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
276
276
  return { state, setState }
277
277
  },
278
278
  { wrapper, initialProps: 'u1' },
@@ -297,7 +297,7 @@ describe.concurrent('useRow', () => {
297
297
  expect(renderCount.val).toBe(2)
298
298
 
299
299
  unmount()
300
- store.destroy()
300
+ await store.destroy()
301
301
  span.end()
302
302
 
303
303
  const mapAttributes = (attributes: otel.Attributes) => {
@@ -1,14 +1,11 @@
1
1
  import type { QueryInfo } from '@livestore/common'
2
- import {} from // deriveCreateMutationDef as deriveCreateMutationDef_,
3
- // updateMutationForQueryInfo as updateMutationForQueryInfo_,
4
- '@livestore/common'
5
2
  import { DbSchema } from '@livestore/common/schema'
6
3
  import { shouldNeverHappen } from '@livestore/utils'
4
+ import { ReadonlyRecord } from '@livestore/utils/effect'
7
5
  import type { SqliteDsl } from 'effect-db-schema'
8
- import { mapValues } from 'lodash-es'
9
6
  import React from 'react'
10
7
 
11
- import type { DbGraph, LiveQuery } from '../index.js'
8
+ import type { LiveQuery, ReactivityGraph } from '../index.js'
12
9
  import type { RowResult } from '../row-query.js'
13
10
  import { rowQuery } from '../row-query.js'
14
11
  import { useStore } from './LiveStoreContext.js'
@@ -26,7 +23,7 @@ export type UseRowOptionsDefaulValues<TTableDef extends DbSchema.TableDef> = {
26
23
  }
27
24
 
28
25
  export type UseRowOptionsBase = {
29
- dbGraph?: DbGraph
26
+ reactivityGraph?: ReactivityGraph
30
27
  }
31
28
 
32
29
  /**
@@ -76,7 +73,7 @@ export const useRow: {
76
73
  const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
77
74
  const options: (UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>) | undefined =
78
75
  typeof idOrOptions === 'string' ? options_ : idOrOptions
79
- const { defaultValues, dbGraph } = options ?? {}
76
+ const { defaultValues, reactivityGraph } = options ?? {}
80
77
 
81
78
  type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
82
79
 
@@ -100,11 +97,11 @@ export const useRow: {
100
97
  const { query$, otelContext } = useMakeTemporaryQuery(
101
98
  (otelContext) =>
102
99
  DbSchema.tableIsSingleton(table)
103
- ? (rowQuery(table, { otelContext, dbGraph }) as LiveQuery<RowResult<TTableDef>, QueryInfo>)
100
+ ? (rowQuery(table, { otelContext, reactivityGraph }) as LiveQuery<RowResult<TTableDef>, QueryInfo>)
104
101
  : (rowQuery(table as TTableDef & { options: { isSingleton: false } }, id!, {
105
102
  otelContext,
106
103
  defaultValues: defaultValues!,
107
- dbGraph,
104
+ reactivityGraph,
108
105
  }) as any as LiveQuery<RowResult<TTableDef>, QueryInfo>),
109
106
  [id!, tableName],
110
107
  {
@@ -133,7 +130,7 @@ export const useRow: {
133
130
  }
134
131
  } else {
135
132
  const setState = // TODO: do we have a better type for the values that can go in SQLite?
136
- mapValues(sqliteTableDef.columns, (column, columnName) => (newValueOrFn: any) => {
133
+ ReadonlyRecord.map(sqliteTableDef.columns, (column, columnName) => (newValueOrFn: any) => {
137
134
  const newValue =
138
135
  // @ts-expect-error TODO fix typing
139
136
  typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current[columnName]) : newValueOrFn
@@ -53,8 +53,8 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
53
53
  const fullKey = React.useMemo(
54
54
  // NOTE We're using the `makeQuery` function body string to make sure the key is unique across the app
55
55
  // TODO we should figure out whether this could cause some problems and/or if there's a better way to do this
56
- () => (Array.isArray(key) ? key.join('-') : key) + '-' + store.graph.id + '-' + makeQuery.toString(),
57
- [key, makeQuery, store.graph.id],
56
+ () => (Array.isArray(key) ? key.join('-') : key) + '-' + store.reactivityGraph.id + '-' + makeQuery.toString(),
57
+ [key, makeQuery, store.reactivityGraph.id],
58
58
  )
59
59
  const fullKeyRef = React.useRef<string>()
60
60
 
package/src/reactive.ts CHANGED
@@ -25,9 +25,8 @@
25
25
 
26
26
  import { BoundArray } from '@livestore/common'
27
27
  import type { PrettifyFlat } from '@livestore/utils'
28
- import { shouldNeverHappen } from '@livestore/utils'
28
+ import { deepEqual, shouldNeverHappen } from '@livestore/utils'
29
29
  import type * as otel from '@opentelemetry/api'
30
- import { isEqual } from 'lodash-es'
31
30
  // import { getDurationMsFromSpan } from './otel.js'
32
31
 
33
32
  export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
@@ -234,7 +233,7 @@ export class ReactiveGraph<
234
233
  super: new Set(),
235
234
  label: options?.label,
236
235
  meta: options?.meta,
237
- equal: options?.equal ?? isEqual,
236
+ equal: options?.equal ?? deepEqual,
238
237
  refreshes: 0,
239
238
  }
240
239
 
@@ -331,7 +330,7 @@ export class ReactiveGraph<
331
330
  recomputations: 0,
332
331
  label: options?.label,
333
332
  meta: options?.meta,
334
- equal: options?.equal ?? isEqual,
333
+ equal: options?.equal ?? deepEqual,
335
334
  __getResult: getResult,
336
335
  }
337
336
 
@@ -618,7 +617,9 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
618
617
  }
619
618
 
620
619
  const previousResult: EncodedOption<string> = includeResult
621
- ? encodedOptionSome(JSON.stringify(atom.previousResult))
620
+ ? encodedOptionSome(
621
+ atom.previousResult === NOT_REFRESHED_YET ? '"SYMBOL_NOT_REFRESHED_YET"' : JSON.stringify(atom.previousResult),
622
+ )
622
623
  : encodedOptionNone()
623
624
 
624
625
  if (atom._tag === 'ref') {
@@ -6,15 +6,15 @@ import type { StackInfo } from '../react/utils/stack-info.js'
6
6
  import { type Atom, type GetAtom, ReactiveGraph, throwContextNotSetError, type Thunk } from '../reactive.js'
7
7
  import type { QueryDebugInfo, RefreshReason, Store } from '../store.js'
8
8
 
9
- export type DbGraph = ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
9
+ export type ReactivityGraph = ReactiveGraph<RefreshReason, QueryDebugInfo, QueryContext>
10
10
 
11
- export const makeDbGraph = (): DbGraph =>
12
- new ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>({
11
+ export const makeReactivityGraph = (): ReactivityGraph =>
12
+ new ReactiveGraph<RefreshReason, QueryDebugInfo, QueryContext>({
13
13
  // TODO also find a better way to only use this effects wrapper when used in a React app
14
14
  effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
15
15
  })
16
16
 
17
- export type DbContext = {
17
+ export type QueryContext = {
18
18
  store: Store
19
19
  otelTracer: otel.Tracer
20
20
  rootOtelContext: otel.Context
@@ -37,7 +37,7 @@ export interface LiveQuery<TResult, TQueryInfo extends QueryInfo = QueryInfoNone
37
37
  '__result!': TResult
38
38
 
39
39
  /** A reactive thunk representing the query results */
40
- results$: Thunk<TResult, DbContext, RefreshReason>
40
+ results$: Thunk<TResult, QueryContext, RefreshReason>
41
41
 
42
42
  label: string
43
43
 
@@ -72,11 +72,11 @@ export abstract class LiveStoreQueryBase<TResult, TQueryInfo extends QueryInfo>
72
72
  /** Human-readable label for the query for debugging */
73
73
  abstract label: string
74
74
 
75
- abstract results$: Thunk<TResult, DbContext, RefreshReason>
75
+ abstract results$: Thunk<TResult, QueryContext, RefreshReason>
76
76
 
77
77
  activeSubscriptions: Set<StackInfo> = new Set()
78
78
 
79
- protected abstract dbGraph: DbGraph
79
+ protected abstract reactivityGraph: ReactivityGraph
80
80
 
81
81
  abstract queryInfo: TQueryInfo
82
82
 
@@ -102,8 +102,8 @@ export abstract class LiveStoreQueryBase<TResult, TQueryInfo extends QueryInfo>
102
102
  onUnsubsubscribe?: () => void,
103
103
  options?: { label?: string; otelContext?: otel.Context } | undefined,
104
104
  ): (() => void) =>
105
- this.dbGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ??
106
- throwContextNotSetError(this.dbGraph)
105
+ this.reactivityGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ??
106
+ throwContextNotSetError(this.reactivityGraph)
107
107
  }
108
108
 
109
109
  export type GetAtomResult = <T>(atom: Atom<T, any, RefreshReason> | LiveQuery<T, any>) => T