@livestore/livestore 0.0.12 → 0.0.14

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 (226) hide show
  1. package/README.md +25 -28
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/QueryCache.d.ts +20 -0
  4. package/dist/QueryCache.d.ts.map +1 -0
  5. package/dist/QueryCache.js +71 -0
  6. package/dist/QueryCache.js.map +1 -0
  7. package/dist/__tests__/react/fixture.d.ts +26 -0
  8. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  9. package/dist/__tests__/react/fixture.js +60 -0
  10. package/dist/__tests__/react/fixture.js.map +1 -0
  11. package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
  12. package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
  13. package/dist/__tests__/react/useComponentState.test.js +68 -0
  14. package/dist/__tests__/react/useComponentState.test.js.map +1 -0
  15. package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
  16. package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
  17. package/dist/__tests__/react/useLQuery.test.js +38 -0
  18. package/dist/__tests__/react/useLQuery.test.js.map +1 -0
  19. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  20. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  21. package/dist/__tests__/react/useLiveStoreComponent.test.js +73 -0
  22. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  23. package/dist/__tests__/react/useQuery.test.d.ts +2 -0
  24. package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
  25. package/dist/__tests__/react/useQuery.test.js +33 -0
  26. package/dist/__tests__/react/useQuery.test.js.map +1 -0
  27. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
  28. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
  29. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
  30. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
  31. package/dist/__tests__/reactive.test.d.ts +2 -0
  32. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  33. package/dist/__tests__/reactive.test.js +271 -0
  34. package/dist/__tests__/reactive.test.js.map +1 -0
  35. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  36. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  37. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  38. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  39. package/dist/bounded-collections.d.ts +34 -0
  40. package/dist/bounded-collections.d.ts.map +1 -0
  41. package/dist/bounded-collections.js +103 -0
  42. package/dist/bounded-collections.js.map +1 -0
  43. package/dist/componentKey.d.ts +20 -0
  44. package/dist/componentKey.d.ts.map +1 -0
  45. package/dist/componentKey.js +3 -0
  46. package/dist/componentKey.js.map +1 -0
  47. package/dist/effect/LiveStore.d.ts +36 -0
  48. package/dist/effect/LiveStore.d.ts.map +1 -0
  49. package/dist/effect/LiveStore.js +41 -0
  50. package/dist/effect/LiveStore.js.map +1 -0
  51. package/dist/effect/index.d.ts +2 -0
  52. package/dist/effect/index.d.ts.map +1 -0
  53. package/dist/effect/index.js +2 -0
  54. package/dist/effect/index.js.map +1 -0
  55. package/dist/events.d.ts +7 -0
  56. package/dist/events.d.ts.map +1 -0
  57. package/dist/events.js +2 -0
  58. package/dist/events.js.map +1 -0
  59. package/dist/inMemoryDatabase.d.ts +56 -0
  60. package/dist/inMemoryDatabase.d.ts.map +1 -0
  61. package/dist/inMemoryDatabase.js +223 -0
  62. package/dist/inMemoryDatabase.js.map +1 -0
  63. package/dist/index.d.ts +22 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +13 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/migrations.d.ts +16 -0
  68. package/dist/migrations.d.ts.map +1 -0
  69. package/dist/migrations.js +67 -0
  70. package/dist/migrations.js.map +1 -0
  71. package/dist/otel.d.ts +4 -0
  72. package/dist/otel.d.ts.map +1 -0
  73. package/dist/otel.js +6 -0
  74. package/dist/otel.js.map +1 -0
  75. package/dist/react/LiveStoreContext.d.ts +11 -0
  76. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  77. package/dist/react/LiveStoreContext.js +10 -0
  78. package/dist/react/LiveStoreContext.js.map +1 -0
  79. package/dist/react/LiveStoreProvider.d.ts +20 -0
  80. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  81. package/dist/react/LiveStoreProvider.js +52 -0
  82. package/dist/react/LiveStoreProvider.js.map +1 -0
  83. package/dist/react/index.d.ts +8 -0
  84. package/dist/react/index.d.ts.map +1 -0
  85. package/dist/react/index.js +6 -0
  86. package/dist/react/index.js.map +1 -0
  87. package/dist/react/useComponentState.d.ts +50 -0
  88. package/dist/react/useComponentState.d.ts.map +1 -0
  89. package/dist/react/useComponentState.js +248 -0
  90. package/dist/react/useComponentState.js.map +1 -0
  91. package/dist/react/useGlobalQuery.d.ts +3 -0
  92. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  93. package/dist/react/useGlobalQuery.js +26 -0
  94. package/dist/react/useGlobalQuery.js.map +1 -0
  95. package/dist/react/useGraphQL.d.ts +13 -0
  96. package/dist/react/useGraphQL.d.ts.map +1 -0
  97. package/dist/react/useGraphQL.js +87 -0
  98. package/dist/react/useGraphQL.js.map +1 -0
  99. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  100. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  101. package/dist/react/useLiveStoreComponent.js +361 -0
  102. package/dist/react/useLiveStoreComponent.js.map +1 -0
  103. package/dist/react/useQuery.d.ts +3 -0
  104. package/dist/react/useQuery.d.ts.map +1 -0
  105. package/dist/react/useQuery.js +42 -0
  106. package/dist/react/useQuery.js.map +1 -0
  107. package/dist/react/useTemporaryQuery.d.ts +8 -0
  108. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  109. package/dist/react/useTemporaryQuery.js +17 -0
  110. package/dist/react/useTemporaryQuery.js.map +1 -0
  111. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  112. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  113. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  114. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  115. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  116. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  117. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  118. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  119. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  120. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  121. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  122. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  123. package/dist/reactive.d.ts +134 -0
  124. package/dist/reactive.d.ts.map +1 -0
  125. package/dist/reactive.js +409 -0
  126. package/dist/reactive.js.map +1 -0
  127. package/dist/reactiveQueries/base-class.d.ts +32 -0
  128. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  129. package/dist/reactiveQueries/base-class.js +30 -0
  130. package/dist/reactiveQueries/base-class.js.map +1 -0
  131. package/dist/reactiveQueries/graph.d.ts +10 -0
  132. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  133. package/dist/reactiveQueries/graph.js +6 -0
  134. package/dist/reactiveQueries/graph.js.map +1 -0
  135. package/dist/reactiveQueries/graphql.d.ts +42 -0
  136. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  137. package/dist/reactiveQueries/graphql.js +99 -0
  138. package/dist/reactiveQueries/graphql.js.map +1 -0
  139. package/dist/reactiveQueries/js.d.ts +23 -0
  140. package/dist/reactiveQueries/js.d.ts.map +1 -0
  141. package/dist/reactiveQueries/js.js +36 -0
  142. package/dist/reactiveQueries/js.js.map +1 -0
  143. package/dist/reactiveQueries/sql.d.ts +35 -0
  144. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  145. package/dist/reactiveQueries/sql.js +97 -0
  146. package/dist/reactiveQueries/sql.js.map +1 -0
  147. package/dist/schema.d.ts +81 -0
  148. package/dist/schema.d.ts.map +1 -0
  149. package/dist/schema.js +46 -0
  150. package/dist/schema.js.map +1 -0
  151. package/dist/storage/in-memory/index.d.ts +15 -0
  152. package/dist/storage/in-memory/index.d.ts.map +1 -0
  153. package/dist/storage/in-memory/index.js +14 -0
  154. package/dist/storage/in-memory/index.js.map +1 -0
  155. package/dist/storage/index.d.ts +14 -0
  156. package/dist/storage/index.d.ts.map +1 -0
  157. package/dist/storage/index.js +9 -0
  158. package/dist/storage/index.js.map +1 -0
  159. package/dist/storage/tauri/index.d.ts +19 -0
  160. package/dist/storage/tauri/index.d.ts.map +1 -0
  161. package/dist/storage/tauri/index.js +38 -0
  162. package/dist/storage/tauri/index.js.map +1 -0
  163. package/dist/storage/utils/idb.d.ts +10 -0
  164. package/dist/storage/utils/idb.d.ts.map +1 -0
  165. package/dist/storage/utils/idb.js +58 -0
  166. package/dist/storage/utils/idb.js.map +1 -0
  167. package/dist/storage/web-worker/index.d.ts +27 -0
  168. package/dist/storage/web-worker/index.d.ts.map +1 -0
  169. package/dist/storage/web-worker/index.js +74 -0
  170. package/dist/storage/web-worker/index.js.map +1 -0
  171. package/dist/storage/web-worker/worker.d.ts +13 -0
  172. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  173. package/dist/storage/web-worker/worker.js +110 -0
  174. package/dist/storage/web-worker/worker.js.map +1 -0
  175. package/dist/store.d.ts +159 -0
  176. package/dist/store.d.ts.map +1 -0
  177. package/dist/store.js +626 -0
  178. package/dist/store.js.map +1 -0
  179. package/dist/util.d.ts +28 -0
  180. package/dist/util.d.ts.map +1 -0
  181. package/dist/util.js +55 -0
  182. package/dist/util.js.map +1 -0
  183. package/package.json +47 -19
  184. package/src/QueryCache.ts +1 -1
  185. package/src/__tests__/react/fixture.tsx +35 -39
  186. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  187. package/src/__tests__/react/useQuery.test.tsx +48 -0
  188. package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
  189. package/src/__tests__/reactive.test.ts +194 -142
  190. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  191. package/src/effect/LiveStore.ts +22 -31
  192. package/src/events.ts +1 -1
  193. package/src/inMemoryDatabase.ts +117 -142
  194. package/src/index.ts +18 -22
  195. package/src/migrations.ts +119 -0
  196. package/src/otel.ts +0 -11
  197. package/src/react/LiveStoreProvider.tsx +24 -23
  198. package/src/react/index.ts +12 -7
  199. package/src/react/useComponentState.ts +409 -0
  200. package/src/react/useQuery.ts +58 -0
  201. package/src/react/useTemporaryQuery.ts +21 -0
  202. package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
  203. package/src/reactive.ts +386 -267
  204. package/src/reactiveQueries/base-class.ts +61 -39
  205. package/src/reactiveQueries/graph.ts +15 -0
  206. package/src/reactiveQueries/graphql.ts +147 -31
  207. package/src/reactiveQueries/js.ts +54 -21
  208. package/src/reactiveQueries/sql.ts +128 -37
  209. package/src/schema.ts +69 -145
  210. package/src/storage/in-memory/index.ts +21 -0
  211. package/src/storage/index.ts +27 -0
  212. package/src/{backends/tauri.ts → storage/tauri/index.ts} +14 -28
  213. package/src/storage/web-worker/index.ts +116 -0
  214. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  215. package/src/store.ts +466 -457
  216. package/src/util.ts +13 -3
  217. package/tsconfig.json +1 -3
  218. package/src/backends/base.ts +0 -67
  219. package/src/backends/index.ts +0 -98
  220. package/src/backends/noop.ts +0 -32
  221. package/src/backends/web-in-memory.ts +0 -65
  222. package/src/backends/web.ts +0 -97
  223. package/src/react/useGlobalQuery.ts +0 -40
  224. package/src/react/useGraphQL.ts +0 -112
  225. package/src/react/useLiveStoreComponent.ts +0 -483
  226. /package/src/{backends → storage}/utils/idb.ts +0 -0
@@ -1,483 +0,0 @@
1
- import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
- import { type LiteralUnion, omit, shouldNeverHappen } from '@livestore/utils'
3
- import * as otel from '@opentelemetry/api'
4
- import { isEqual, mapValues } from 'lodash-es'
5
- import type { DependencyList } from 'react'
6
- import React from 'react'
7
- import { v4 as uuid } from 'uuid'
8
-
9
- import type { ComponentKey } from '../componentKey.js'
10
- import { labelForKey, tableNameForComponentKey } from '../componentKey.js'
11
- import type { GetAtom } from '../reactive.js'
12
- import type { LiveStoreGraphQLQuery } from '../reactiveQueries/graphql.js'
13
- import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
14
- import type { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
15
- import type { ComponentStateSchema } from '../schema.js'
16
- import type { BaseGraphQLContext, LiveStoreQuery, QueryResult, Store } from '../store.js'
17
- import type { Bindable } from '../util.js'
18
- import { sql } from '../util.js'
19
- import { useStore } from './LiveStoreContext.js'
20
- import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
21
-
22
- export interface QueryDefinitions {
23
- [queryName: string]: LiveStoreQuery
24
- }
25
- export type QueryResults<TQuery> = { [queryName in keyof TQuery]: QueryResult<TQuery[queryName]> }
26
-
27
- export type ReactiveSQL = <TResult>(
28
- genQuery: (get: GetAtom) => string,
29
- queriedTables: string[],
30
- bindValues?: Bindable | undefined,
31
- ) => LiveStoreSQLQuery<TResult>
32
- export type ReactiveGraphQL = <
33
- TResult extends Record<string, any>,
34
- TVariables extends Record<string, any>,
35
- TContext extends BaseGraphQLContext,
36
- >(
37
- query: DocumentNode<TResult, TVariables>,
38
- genVariableValues: (get: GetAtom) => TVariables,
39
- label?: string,
40
- ) => LiveStoreGraphQLQuery<TResult, TVariables, TContext>
41
-
42
- type RegisterSubscription = <TQuery extends LiveStoreQuery>(
43
- query: TQuery,
44
- onNewValue: (value: QueryResult<TQuery>) => void,
45
- onUnsubscribe?: () => void,
46
- ) => void
47
-
48
- type GenQueries<TQueries, TStateResult> = (args: {
49
- rxSQL: ReactiveSQL
50
- rxGraphQL: ReactiveGraphQL
51
- globalQueries: QueryDefinitions
52
- state$: LiveStoreJSQuery<TStateResult>
53
- /** Registers a subscription */
54
- subscribe: RegisterSubscription
55
- isTemporaryQuery: boolean
56
- }) => TQueries
57
-
58
- export type UseLiveStoreComponentProps<TQueries, TComponentState> = {
59
- stateSchema?: ComponentStateSchema<TComponentState>
60
- queries?: GenQueries<TQueries, TComponentState>
61
- reactDeps?: React.DependencyList
62
- componentKey: ComponentKeyConfig
63
- }
64
-
65
- export type ComponentKeyConfig = {
66
- /**
67
- * Name of the Component
68
- *
69
- * TODO we should eventually derive this info automatically from the component (TBD how though...)
70
- */
71
- name: string
72
- id: LiteralUnion<'singleton' | '__ephemeral__', string>
73
- }
74
-
75
- type ComponentState = {
76
- /** Equivalent to `componentKey.key` */
77
- id: string
78
- [key: string]: string | number | boolean | null
79
- }
80
-
81
- /**
82
- * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
83
- * so we need to "cache" the fact that we've already started a span for this component.
84
- * The map entry is being removed again in the `React.useEffect` call below.
85
- */
86
- const spanAlreadyStartedCache = new Map<string, { span: otel.Span; otelContext: otel.Context }>()
87
-
88
- type UseLiveStoreJsonState<TState> = <TResult>(
89
- jsonStringKey: keyof TState,
90
- parse?: (_: unknown) => TResult,
91
- ) => [value: TResult, setValue: (newVal: TResult | ((prevVal: TResult) => TResult)) => void]
92
-
93
- /**
94
- * Create reactive queries within a component.
95
- * @param config.queries A function that returns a map of named reactive queries.
96
- * @param config.componentKey A function that returns a unique key for this component.
97
- * @param config.reactDeps A list of React-level dependencies that will refresh the queries.
98
- */
99
- export const useLiveStoreComponent = <TComponentState extends ComponentState, TQueries extends QueryDefinitions>({
100
- stateSchema: stateSchema_,
101
- queries = () => ({}) as TQueries,
102
- componentKey: componentKeyConfig,
103
- reactDeps = [],
104
- }: UseLiveStoreComponentProps<TQueries, TComponentState>): {
105
- queryResults: QueryResults<TQueries>
106
- state: TComponentState
107
- setState: Setters<TComponentState>
108
- useLiveStoreJsonState: UseLiveStoreJsonState<TComponentState>
109
- } => {
110
- // TODO we should clean up the state schema handling to remove this special handling for the `id` column
111
- const stateSchema = React.useMemo(
112
- () => (stateSchema_ ? { ...stateSchema_, columns: omit(stateSchema_.columns, 'id' as any) } : undefined),
113
- [stateSchema_],
114
- )
115
-
116
- // performance.mark('useLiveStoreComponent:start')
117
- const componentKey = useComponentKey(componentKeyConfig, reactDeps)
118
- const { store, globalQueries } = useStore()
119
-
120
- const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
121
-
122
- // The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
123
- const { span, otelContext } = React.useMemo(() => {
124
- const existingSpan = spanAlreadyStartedCache.get(componentKeyLabel)
125
- if (existingSpan !== undefined) return existingSpan
126
-
127
- const span = store.otel.tracer.startSpan(
128
- `LiveStore:useLiveStoreComponent:${componentKeyLabel}`,
129
- {},
130
- store.otel.queriesSpanContext,
131
- )
132
-
133
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
134
-
135
- spanAlreadyStartedCache.set(componentKeyLabel, { span, otelContext })
136
-
137
- return { span, otelContext }
138
- }, [componentKeyLabel, store.otel.queriesSpanContext, store.otel.tracer])
139
-
140
- React.useEffect(
141
- () => () => {
142
- spanAlreadyStartedCache.delete(componentKeyLabel)
143
- span.end()
144
- },
145
- [componentKeyLabel, span],
146
- )
147
-
148
- const generateQueries = React.useCallback(
149
- ({
150
- state$,
151
- otelContext,
152
- registerSubscription,
153
- isTemporaryQuery,
154
- }: {
155
- state$: LiveStoreJSQuery<TComponentState>
156
- otelContext: otel.Context
157
- registerSubscription: RegisterSubscription
158
- isTemporaryQuery: boolean
159
- }) =>
160
- queries({
161
- rxSQL: <T>(genQuery: (get: GetAtom) => string, queriedTables: string[], bindValues?: Bindable) =>
162
- store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext }),
163
- rxGraphQL: <Result extends Record<string, any>, Variables extends Record<string, any>>(
164
- query: DocumentNode<Result, Variables>,
165
- genVariableValues: (get: GetAtom) => Variables,
166
- label?: string,
167
- ) => store.queryGraphQL(query, genVariableValues, { componentKey, label, otelContext }),
168
- globalQueries,
169
- state$,
170
- subscribe: registerSubscription,
171
- isTemporaryQuery,
172
- }),
173
-
174
- // NOTE: we don't include the queries function passed in by the user here;
175
- // the reason is that we don't want to force them to memoize that function.
176
- // Instead, we just assume that the function always has the same contents.
177
- // This makes sense for LiveStore because the component config should be static.
178
- // TODO: document this and consider whether it's the right API surface.
179
- // eslint-disable-next-line react-hooks/exhaustive-deps
180
- [store, componentKey, globalQueries],
181
- )
182
-
183
- const defaultComponentState = React.useMemo(() => {
184
- const defaultState = (
185
- stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
186
- ) as TComponentState
187
-
188
- defaultState.id = componentKeyConfig.id
189
-
190
- return defaultState
191
- }, [componentKeyConfig.id, stateSchema])
192
-
193
- // Step 1:
194
- // Synchronously create state and queries for initial render pass.
195
- // We do this in a temporary query context which cleans up after itself, making it idempotent
196
- // TODO get rid of the temporary query workaround
197
- const { initialComponentState, initialQueryResults } = React.useMemo(() => {
198
- return store.otel.tracer.startActiveSpan('LiveStore:useLiveStoreComponent:initial', {}, otelContext, (span) => {
199
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
200
-
201
- return store.inTempQueryContext(() => {
202
- try {
203
- // create state query
204
- let stateQuery: LiveStoreJSQuery<TComponentState>
205
- if (stateSchema === undefined) {
206
- // TODO don't set up a query if there's no state schema (keeps the graph more clean)
207
- stateQuery = store.queryJS(() => ({}), {
208
- componentKey,
209
- otelContext,
210
- }) as unknown as LiveStoreJSQuery<TComponentState>
211
- } else {
212
- const componentTableName = tableNameForComponentKey(componentKey)
213
- const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
214
- stateQuery = store
215
- .querySQL<TComponentState>(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
216
- queriedTables: [componentTableName],
217
- componentKey,
218
- label: `localState:query:${componentKeyLabel}`,
219
- otelContext,
220
- })
221
- .getFirstRow({ defaultValue: defaultComponentState })
222
- }
223
- const initialComponentState = stateQuery.results$.result
224
-
225
- const queries = generateQueries({
226
- state$: stateQuery,
227
- otelContext,
228
- registerSubscription: () => {},
229
- isTemporaryQuery: true,
230
- })
231
- for (const [name, query] of Object.entries(queries)) {
232
- query.label = name
233
- }
234
- const initialQueryResults = mapValues(queries, (query) => query.results$.result) as QueryResults<TQueries>
235
-
236
- return { initialComponentState, initialQueryResults }
237
- } finally {
238
- span.end()
239
- }
240
- })
241
- })
242
- }, [store, otelContext, stateSchema, generateQueries, componentKey, componentKeyLabel, defaultComponentState])
243
-
244
- // Now that we've computed the initial state synchronously,
245
- // we can set up our useState calls w/ a default value populated...
246
- const [componentStateRef, setComponentState_] = useStateRefWithReactiveInput<TComponentState>(initialComponentState)
247
-
248
- const [queryResultsRef, setQueryResults_] = useStateRefWithReactiveInput<QueryResults<TQueries>>(initialQueryResults)
249
-
250
- const setState = (
251
- stateSchema === undefined
252
- ? {}
253
- : // TODO: do we have a better type for the values that can go in SQLite?
254
- mapValues(stateSchema.columns, (_, columnName) => (value: string | number) => {
255
- // Don't update the state if it's the same as the value already seen in the component
256
- if (componentStateRef.current[columnName] === value) return
257
-
258
- if (['componentKey', 'columnNames'].includes(columnName)) {
259
- shouldNeverHappen(`Can't use reserved column name ${columnName}`)
260
- }
261
-
262
- return store.applyEvent('updateComponentState', {
263
- componentKey,
264
- columnNames: [columnName],
265
- [columnName]: value,
266
- })
267
- })
268
- ) as Setters<TComponentState>
269
-
270
- setState.setMany = (columnValues: Partial<TComponentState>) => {
271
- // TODO use hashing instead
272
- // Don't update the state if it's the same as the value already seen in the component
273
- if (Object.entries(columnValues).every(([columnName, value]) => componentStateRef.current[columnName] === value)) {
274
- return
275
- }
276
-
277
- const columnNames = Object.keys(columnValues)
278
-
279
- return store.applyEvent('updateComponentState', { componentKey, columnNames, ...columnValues })
280
- }
281
-
282
- // OK, now all the synchronous work is done;
283
- // time to set up our long-running queries in an effect
284
- React.useEffect(() => {
285
- return store.otel.tracer.startActiveSpan(
286
- 'LiveStore:useLiveStoreComponent:long-running',
287
- { attributes: {} },
288
- otelContext,
289
- (span) => {
290
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
291
- const unsubs: (() => void)[] = []
292
-
293
- // create state query
294
- let state$: LiveStoreJSQuery<TComponentState>
295
- if (stateSchema === undefined) {
296
- state$ = store.queryJS(() => ({}) as TComponentState, { componentKey, otelContext })
297
- } else {
298
- const componentTableName = tableNameForComponentKey(componentKey)
299
- insertRowForComponentInstance({ store, componentKey, stateSchema })
300
-
301
- const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
302
- state$ = store
303
- .querySQL<TComponentState>(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
304
- queriedTables: [componentTableName],
305
- componentKey,
306
- label: `localState:query:${componentKeyLabel}`,
307
- otelContext,
308
- })
309
- .getFirstRow({ defaultValue: defaultComponentState })
310
- }
311
-
312
- unsubs.push(
313
- store.subscribe(
314
- state$,
315
- (results) => {
316
- if (isEqual(results, componentStateRef.current) === false) {
317
- setComponentState_(results as TComponentState)
318
- }
319
- },
320
- undefined,
321
- { label: `useLiveStoreComponent:localState:subscribe:${state$.label}` },
322
- ),
323
- )
324
-
325
- const registerSubscription: RegisterSubscription = (query, callback, onUnsubscribe) => {
326
- unsubs.push(
327
- store.subscribe(
328
- query,
329
- (results) => {
330
- callback(results)
331
- },
332
- onUnsubscribe,
333
- { label: `useLiveStoreComponent:query:manual-subscribe:${query.label}` },
334
- ),
335
- )
336
- }
337
-
338
- const queries = generateQueries({ state$, otelContext, registerSubscription, isTemporaryQuery: false })
339
- // Use the name given to this query in the useQueries hook as its label
340
- for (const [name, query] of Object.entries(queries)) {
341
- query.label = name
342
- }
343
- for (const [key, query] of Object.entries(queries)) {
344
- unsubs.push(
345
- store.subscribe(
346
- query,
347
- (results) => {
348
- const newQueryResults = { ...queryResultsRef.current, [key]: results }
349
- if (isEqual(newQueryResults, queryResultsRef.current) === false) {
350
- setQueryResults_(newQueryResults)
351
- }
352
- },
353
- undefined,
354
- { label: `useLiveStoreComponent:query:subscribe:${query.label}` },
355
- ),
356
- )
357
- }
358
-
359
- return () => {
360
- for (const unsub of unsubs) {
361
- unsub()
362
- }
363
-
364
- span.end()
365
- }
366
- },
367
- )
368
- // NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
369
- // This should probably be improved
370
- // eslint-disable-next-line react-hooks/exhaustive-deps
371
- }, [
372
- store,
373
- componentKey,
374
- stateSchema,
375
- defaultComponentState,
376
- generateQueries,
377
- otelContext,
378
- componentStateRef,
379
- // setComponentState_,
380
- // setQueryResults_,
381
- ])
382
-
383
- // Very important: remove any queries / other resources associated w/ this component
384
- React.useEffect(() => () => store.unmountComponent(componentKey), [store, componentKey])
385
-
386
- // performance.mark('useLiveStoreComponent:end')
387
- // performance.measure(`useLiveStoreComponent:${componentKey.type}`, 'useLiveStoreComponent:start', 'useLiveStoreComponent:end')
388
-
389
- const state = componentStateRef.current
390
-
391
- const useLiveStoreJsonState = <TResult>(
392
- jsonStringKey: keyof TComponentState,
393
- parse: (_: unknown) => TResult = (_) => _ as TResult,
394
- ): [value: TResult, setValue: (newVal: TResult | ((prevVal: TResult) => TResult)) => void] => {
395
- const value = React.useMemo<TResult>(() => {
396
- return parse(JSON.parse(state[jsonStringKey] as string))
397
- // eslint-disable-next-line react-hooks/exhaustive-deps
398
- }, [state[jsonStringKey], parse])
399
-
400
- const setValue = React.useCallback(
401
- (newValOrFn: TResult | ((prev: TResult) => TResult)) => {
402
- const newVal =
403
- typeof newValOrFn === 'function'
404
- ? // NOTE we're using the ref instead of the value because we want to be sure
405
- // we're using the latest value when the setter is called
406
- (newValOrFn as any)(parse(JSON.parse(componentStateRef.current[jsonStringKey] as string)))
407
- : newValOrFn
408
- setState[jsonStringKey](JSON.stringify(newVal) as any)
409
- },
410
- [parse, jsonStringKey],
411
- )
412
-
413
- return [value, setValue]
414
- }
415
-
416
- return {
417
- queryResults: queryResultsRef.current,
418
- state,
419
- setState,
420
- useLiveStoreJsonState,
421
- }
422
- }
423
-
424
- export type Setters<TComponentState> = {
425
- [k in keyof TComponentState]: (newValue: TComponentState[k]) => void
426
- } & {
427
- setMany: (newValues: Partial<TComponentState>) => void
428
- }
429
-
430
- export const useComponentKey = ({ name, id }: ComponentKeyConfig, deps: DependencyList = []) =>
431
- React.useMemo<ComponentKey>(() => {
432
- switch (id) {
433
- case 'singleton': {
434
- return { _tag: 'singleton', componentName: name, id: 'singleton' }
435
- }
436
- case '__ephemeral__': {
437
- return { _tag: 'ephemeral', componentName: name, id: uuid() }
438
- }
439
- default: {
440
- return { _tag: 'custom', componentName: name, id }
441
- }
442
- }
443
- // eslint-disable-next-line react-hooks/exhaustive-deps
444
- }, [...deps, id, name])
445
-
446
- /**
447
- * Create a row storing the state for a component instance, if none exists yet.
448
- * Initialized with default values, and keyed on the component key.
449
- */
450
- const insertRowForComponentInstance = <T>({
451
- store,
452
- componentKey,
453
- stateSchema,
454
- }: {
455
- store: Store<BaseGraphQLContext>
456
- componentKey: ComponentKey
457
- stateSchema: ComponentStateSchema<T>
458
- }) => {
459
- const columnNames = ['id', ...Object.keys(stateSchema.columns)]
460
- const columnValues = columnNames.map((name) => `$${name}`).join(', ')
461
-
462
- const tableName = tableNameForComponentKey(componentKey)
463
- const insertQuery = sql`insert into ${tableName} (${columnNames.join(
464
- ', ',
465
- )}) select ${columnValues} where not exists(select 1 from ${tableName} where id = '${componentKey.id}')`
466
-
467
- void store.execute(
468
- insertQuery,
469
- {
470
- id: componentKey.id,
471
- ...mapValues(stateSchema.columns, (column) => prepareValueForSql(column.default ?? null)),
472
- },
473
- [tableName],
474
- )
475
- }
476
-
477
- const prepareValueForSql = (value: string | number | boolean | null) => {
478
- if (typeof value === 'string' || typeof value === 'number' || value === null) {
479
- return value
480
- } else {
481
- return value ? 1 : 0
482
- }
483
- }
File without changes