@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
@@ -0,0 +1,409 @@
1
+ import type { LiteralUnion } from '@livestore/utils'
2
+ import { omit, shouldNeverHappen } from '@livestore/utils'
3
+ import { Schema } from '@livestore/utils/effect'
4
+ import * as otel from '@opentelemetry/api'
5
+ import { SqliteAst, SqliteDsl } from 'effect-db-schema'
6
+ import { isEqual, mapValues } from 'lodash-es'
7
+ import type { DependencyList } from 'react'
8
+ import React from 'react'
9
+ import { v4 as uuid } from 'uuid'
10
+
11
+ import type { ComponentKey } from '../componentKey.js'
12
+ import { labelForKey, tableNameForComponentKey } from '../componentKey.js'
13
+ import { migrateTable } from '../migrations.js'
14
+ import { LiveStoreJSQuery } from '../reactiveQueries/js.js'
15
+ import { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
16
+ import { SCHEMA_META_TABLE } from '../schema.js'
17
+ import type { BaseGraphQLContext, LiveStoreQuery, Store } from '../store.js'
18
+ import { sql } from '../util.js'
19
+ import { useStore } from './LiveStoreContext.js'
20
+ import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/extractStackInfoFromStackTrace.js'
21
+ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
22
+
23
+ export interface QueryDefinitions {
24
+ [queryName: string]: LiveStoreQuery
25
+ }
26
+
27
+ export type UseComponentStateProps<TStateColumns extends ComponentColumns> = {
28
+ schema?: SqliteDsl.TableDefinition<string, TStateColumns>
29
+ reactDeps?: React.DependencyList
30
+ componentKey: ComponentKeyConfig
31
+ }
32
+
33
+ export type ComponentKeyConfig = {
34
+ /**
35
+ * Name of the Component
36
+ *
37
+ * TODO we should eventually derive this info automatically from the component (TBD how though...)
38
+ */
39
+ name: string
40
+ id: LiteralUnion<'singleton' | '__ephemeral__', string>
41
+ }
42
+
43
+ // TODO enforce columns are non-nullable or have a default
44
+ export interface ComponentColumns extends SqliteDsl.Columns {
45
+ id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false>
46
+ }
47
+
48
+ // type ComponentState = {
49
+ // /** Equivalent to `componentKey.key` */
50
+ // id: string
51
+ // [key: string]: string | number | boolean | null
52
+ // }
53
+
54
+ /**
55
+ * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
56
+ * so we need to "cache" the fact that we've already started a span for this component.
57
+ * The map entry is being removed again in the `React.useEffect` call below.
58
+ */
59
+ const spanAlreadyStartedCache = new Map<string, { span: otel.Span; otelContext: otel.Context }>()
60
+
61
+ type UseLiveStoreJsonState<TState> = <TResult>(
62
+ jsonStringKey: keyof TState,
63
+ parse?: (_: unknown) => TResult,
64
+ ) => [value: TResult, setValue: (newVal: TResult | ((prevVal: TResult) => TResult)) => void]
65
+
66
+ export type GetStateType<TTableDef extends SqliteDsl.TableDefinition<any, any>> = SqliteDsl.FromColumns.RowDecoded<
67
+ TTableDef['columns']
68
+ >
69
+
70
+ export type GetStateTypeEncoded<TTableDef extends SqliteDsl.TableDefinition<any, any>> =
71
+ SqliteDsl.FromColumns.RowEncoded<TTableDef['columns']>
72
+
73
+ /**
74
+ * Create reactive queries within a component.
75
+ * @param config.queries A function that returns a map of named reactive queries.
76
+ * @param config.componentKey A function that returns a unique key for this component.
77
+ * @param config.reactDeps A list of React-level dependencies that will refresh the queries.
78
+ */
79
+ export const useComponentState = <TStateColumns extends ComponentColumns>({
80
+ schema: stateSchema_,
81
+ componentKey: componentKeyConfig,
82
+ reactDeps = [],
83
+ }: UseComponentStateProps<TStateColumns>): {
84
+ state$: LiveStoreJSQuery<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
85
+ state: SqliteDsl.FromColumns.RowDecoded<TStateColumns>
86
+ setState: Setters<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
87
+ useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
88
+ } => {
89
+ type TComponentState = SqliteDsl.FromColumns.RowDecoded<TStateColumns>
90
+
91
+ // TODO validate schema to make sure each column has a default value
92
+ // TODO we should clean up the state schema handling to remove this special handling for the `id` column
93
+ const stateSchema = React.useMemo(
94
+ () => (stateSchema_ ? { ...stateSchema_, columns: omit(stateSchema_.columns, 'id' as any) } : undefined),
95
+ [stateSchema_],
96
+ )
97
+
98
+ const componentKey = useComponentKey(componentKeyConfig, reactDeps)
99
+ const { store } = useStore()
100
+
101
+ const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
102
+
103
+ // The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
104
+ const { span, otelContext } = React.useMemo(() => {
105
+ const existingSpan = spanAlreadyStartedCache.get(componentKeyLabel)
106
+ if (existingSpan !== undefined) return existingSpan
107
+
108
+ const span = store.otel.tracer.startSpan(
109
+ `LiveStore:useComponentState:${componentKeyLabel}`,
110
+ {},
111
+ store.otel.queriesSpanContext,
112
+ )
113
+
114
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
115
+
116
+ spanAlreadyStartedCache.set(componentKeyLabel, { span, otelContext })
117
+
118
+ return { span, otelContext }
119
+ }, [componentKeyLabel, store.otel.queriesSpanContext, store.otel.tracer])
120
+
121
+ React.useEffect(
122
+ () => () => {
123
+ spanAlreadyStartedCache.delete(componentKeyLabel)
124
+ span.end()
125
+ },
126
+ [componentKeyLabel, span],
127
+ )
128
+
129
+ const defaultComponentState = React.useMemo(() => {
130
+ const defaultState = (
131
+ stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
132
+ ) as TComponentState
133
+
134
+ // @ts-expect-error TODO fix typing
135
+ defaultState.id = componentKeyConfig.id
136
+
137
+ return defaultState
138
+ }, [componentKeyConfig.id, stateSchema])
139
+
140
+ const componentStateEffectSchema = React.useMemo(
141
+ () => (stateSchema ? SqliteDsl.structSchemaForTable(stateSchema) : Schema.any),
142
+ [stateSchema],
143
+ )
144
+
145
+ const state$ = React.useMemo(() => {
146
+ console.log('useComponentState make state$', labelForKey(componentKey))
147
+ // create state query
148
+ if (stateSchema === undefined) {
149
+ // TODO don't set up a query if there's no state schema (keeps the graph more clean)
150
+ return new LiveStoreJSQuery({
151
+ fn: () => ({}) as TComponentState,
152
+ label: 'empty-component-state',
153
+ // otelContext,
154
+ // otelTracer: store.otel.tracer,
155
+ })
156
+ } else {
157
+ const componentTableName = tableNameForComponentKey(componentKey)
158
+ const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
159
+
160
+ // TODO find a better solution for this
161
+ if (store.tableRefs[componentTableName] === undefined) {
162
+ const schemaHash = SqliteAst.hash(stateSchema.ast)
163
+ const res = store.inMemoryDB.select<{ schemaHash: number }>(
164
+ sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
165
+ )
166
+ if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
167
+ migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
168
+ }
169
+
170
+ store.tableRefs[componentTableName] = store.graph.makeRef(null, {
171
+ equal: () => false,
172
+ label: componentTableName,
173
+ meta: { liveStoreRefType: 'table' },
174
+ })
175
+ }
176
+
177
+ return (
178
+ new LiveStoreSQLQuery({
179
+ label: `localState:query:${componentKeyLabel}`,
180
+ genQueryString: () => sql`select * from ${componentTableName} ${whereClause} limit 1`,
181
+ queriedTables: [componentTableName],
182
+ })
183
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
184
+ .pipe<TComponentState>((results) =>
185
+ results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
186
+ )
187
+ )
188
+ }
189
+ }, [
190
+ componentKey,
191
+ componentKeyLabel,
192
+ componentStateEffectSchema,
193
+ defaultComponentState,
194
+ otelContext,
195
+ stateSchema,
196
+ store,
197
+ ])
198
+
199
+ // Step 1:
200
+ // Synchronously create state and queries for initial render pass.
201
+ const initialComponentState = React.useMemo(() => state$.run(otelContext), [otelContext, state$])
202
+
203
+ // Now that we've computed the initial state synchronously,
204
+ // we can set up our useState calls w/ a default value populated...
205
+ const [componentStateRef, setComponentState_] = useStateRefWithReactiveInput<TComponentState>(initialComponentState)
206
+
207
+ const setState = (
208
+ stateSchema === undefined
209
+ ? {}
210
+ : // TODO: do we have a better type for the values that can go in SQLite?
211
+ mapValues(stateSchema.columns, (column, columnName) => (value: string | number) => {
212
+ // Don't update the state if it's the same as the value already seen in the component
213
+ // @ts-expect-error TODO fix typing
214
+ if (componentStateRef.current[columnName] === value) return
215
+
216
+ const encodedValue = Schema.encodeSync(column.type.codec)(value)
217
+
218
+ if (['componentKey', 'columnNames'].includes(columnName)) {
219
+ shouldNeverHappen(`Can't use reserved column name ${columnName}`)
220
+ }
221
+
222
+ return store.applyEvent('updateComponentState', {
223
+ componentKey,
224
+ columnNames: [columnName],
225
+ [columnName]: encodedValue,
226
+ })
227
+ })
228
+ ) as Setters<TComponentState>
229
+
230
+ setState.setMany = (columnValues: Partial<TComponentState>) => {
231
+ // TODO use hashing instead
232
+ // Don't update the state if it's the same as the value already seen in the component
233
+ // @ts-expect-error TODO fix typing
234
+ if (Object.entries(columnValues).every(([columnName, value]) => componentStateRef.current[columnName] === value)) {
235
+ return
236
+ }
237
+
238
+ const columnNames = Object.keys(columnValues)
239
+
240
+ return store.applyEvent('updateComponentState', { componentKey, columnNames, ...columnValues })
241
+ }
242
+
243
+ const subscriptionInfo = React.useMemo(() => {
244
+ Error.stackTraceLimit = 10
245
+ // eslint-disable-next-line unicorn/error-message
246
+ const stack = new Error().stack!
247
+ Error.stackTraceLimit = originalStackLimit
248
+ return { stack: extractStackInfoFromStackTrace(stack) }
249
+ }, [])
250
+
251
+ // OK, now all the synchronous work is done;
252
+ // time to set up our long-running queries in an effect
253
+ React.useEffect(() => {
254
+ return store.otel.tracer.startActiveSpan(
255
+ 'LiveStore:useComponentState:long-running',
256
+ { attributes: {} },
257
+ otelContext,
258
+ (span) => {
259
+ const unsubs: (() => void)[] = []
260
+
261
+ if (stateSchema !== undefined) {
262
+ insertRowForComponentInstance({ store, componentKey, stateSchema })
263
+ }
264
+
265
+ state$.activeSubscriptions.add(subscriptionInfo)
266
+
267
+ unsubs.push(
268
+ store.subscribe(
269
+ state$,
270
+ (results) => {
271
+ if (isEqual(results, componentStateRef.current) === false) {
272
+ setComponentState_(results as TComponentState)
273
+ }
274
+ },
275
+ undefined,
276
+ { label: `useComponentState:localState:subscribe:${state$.label}` },
277
+ ),
278
+ () => state$.activeSubscriptions.delete(subscriptionInfo),
279
+ )
280
+
281
+ return () => {
282
+ for (const unsub of unsubs) {
283
+ unsub()
284
+ }
285
+
286
+ span.end()
287
+ }
288
+ },
289
+ )
290
+ // NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
291
+ // This should probably be improved
292
+ // TODO is this still true?
293
+ // // eslint-disable-next-line react-hooks/exhaustive-deps
294
+ }, [
295
+ store,
296
+ subscriptionInfo,
297
+ stateSchema,
298
+ defaultComponentState,
299
+ otelContext,
300
+ componentStateRef,
301
+ state$,
302
+ setComponentState_,
303
+ componentKey,
304
+ ])
305
+
306
+ // Very important: remove any queries / other resources associated w/ this component
307
+ React.useEffect(
308
+ () => () => {
309
+ console.log('useComponentState destroy', labelForKey(componentKey))
310
+ return state$.destroy()
311
+ },
312
+ [state$],
313
+ )
314
+
315
+ const state = componentStateRef.current
316
+
317
+ const useLiveStoreJsonState = <TResult>(
318
+ jsonStringKey: keyof TComponentState,
319
+ parse: (_: unknown) => TResult = (_) => _ as TResult,
320
+ ): [value: TResult, setValue: (newVal: TResult | ((prevVal: TResult) => TResult)) => void] => {
321
+ const value = React.useMemo<TResult>(() => {
322
+ return parse(JSON.parse(state[jsonStringKey] as string))
323
+ // eslint-disable-next-line react-hooks/exhaustive-deps
324
+ }, [state[jsonStringKey], parse])
325
+
326
+ const setValue = React.useCallback(
327
+ (newValOrFn: TResult | ((prev: TResult) => TResult)) => {
328
+ const newVal =
329
+ typeof newValOrFn === 'function'
330
+ ? // NOTE we're using the ref instead of the value because we want to be sure
331
+ // we're using the latest value when the setter is called
332
+ (newValOrFn as any)(parse(JSON.parse(componentStateRef.current[jsonStringKey] as string)))
333
+ : newValOrFn
334
+ setState[jsonStringKey](JSON.stringify(newVal) as any)
335
+ },
336
+ [parse, jsonStringKey],
337
+ )
338
+
339
+ return [value, setValue]
340
+ }
341
+
342
+ return {
343
+ state$,
344
+ state,
345
+ setState,
346
+ useLiveStoreJsonState,
347
+ }
348
+ }
349
+
350
+ export type Setters<TComponentState> = {
351
+ [k in keyof TComponentState]: (newValue: TComponentState[k]) => void
352
+ } & {
353
+ setMany: (newValues: Partial<TComponentState>) => void
354
+ }
355
+
356
+ export const useComponentKey = ({ name, id }: ComponentKeyConfig, deps: DependencyList = []) =>
357
+ React.useMemo<ComponentKey>(() => {
358
+ switch (id) {
359
+ case 'singleton': {
360
+ return { _tag: 'singleton', componentName: name, id: 'singleton' }
361
+ }
362
+ case '__ephemeral__': {
363
+ return { _tag: 'ephemeral', componentName: name, id: uuid() }
364
+ }
365
+ default: {
366
+ return { _tag: 'custom', componentName: name, id }
367
+ }
368
+ }
369
+ // eslint-disable-next-line react-hooks/exhaustive-deps
370
+ }, [...deps, id, name])
371
+
372
+ /**
373
+ * Create a row storing the state for a component instance, if none exists yet.
374
+ * Initialized with default values, and keyed on the component key.
375
+ */
376
+ const insertRowForComponentInstance = ({
377
+ store,
378
+ componentKey,
379
+ stateSchema,
380
+ }: {
381
+ store: Store<BaseGraphQLContext>
382
+ componentKey: ComponentKey
383
+ stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
384
+ }) => {
385
+ const columnNames = ['id', ...Object.keys(stateSchema.columns)]
386
+ const columnValues = columnNames.map((name) => `$${name}`).join(', ')
387
+
388
+ const tableName = tableNameForComponentKey(componentKey)
389
+ const insertQuery = sql`insert into ${tableName} (${columnNames.join(
390
+ ', ',
391
+ )}) select ${columnValues} where not exists(select 1 from ${tableName} where id = '${componentKey.id}')`
392
+
393
+ void store.execute(
394
+ insertQuery,
395
+ {
396
+ ...mapValues(stateSchema.columns, (column) => prepareValueForSql(column.default ?? null)),
397
+ id: componentKey.id,
398
+ },
399
+ [tableName],
400
+ )
401
+ }
402
+
403
+ const prepareValueForSql = (value: string | number | boolean | null) => {
404
+ if (typeof value === 'string' || typeof value === 'number' || value === null) {
405
+ return value
406
+ } else {
407
+ return value ? 1 : 0
408
+ }
409
+ }
@@ -0,0 +1,58 @@
1
+ import { isEqual } from 'lodash-es'
2
+ import React from 'react'
3
+
4
+ import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
5
+ import { useStore } from './LiveStoreContext.js'
6
+ import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/extractStackInfoFromStackTrace.js'
7
+ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
8
+
9
+ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
10
+ const { store } = useStore()
11
+
12
+ // TODO proper otel context
13
+ const initialResult = React.useMemo(() => query.run(), [query])
14
+
15
+ // We know the query has a result by the time we use it; so we can synchronously populate a default state
16
+ const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
17
+
18
+ const subscriptionInfo = React.useMemo(() => {
19
+ Error.stackTraceLimit = 10
20
+ // eslint-disable-next-line unicorn/error-message
21
+ const stack = new Error().stack!
22
+ Error.stackTraceLimit = originalStackLimit
23
+ return { stack: extractStackInfoFromStackTrace(stack) }
24
+ }, [])
25
+
26
+ // Subscribe to future updates for this query
27
+ React.useEffect(() => {
28
+ return store.otel.tracer.startActiveSpan(
29
+ `LiveStore:useQuery:${query.label}`,
30
+ // `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
31
+ { attributes: { label: query.label } },
32
+ store.otel.queriesSpanContext,
33
+ (span) => {
34
+ query.activeSubscriptions.add(subscriptionInfo)
35
+ const unsub = store.subscribe(
36
+ query,
37
+ (v) => {
38
+ // NOTE: we return a reference to the result object within LiveStore;
39
+ // this implies that app code must not mutate the results, or else
40
+ // there may be weird reactivity bugs.
41
+ if (isEqual(v, valueRef.current) === false) {
42
+ setValue(v)
43
+ }
44
+ },
45
+ undefined,
46
+ { label: query.label },
47
+ )
48
+ return () => {
49
+ query.activeSubscriptions.delete(subscriptionInfo)
50
+ unsub()
51
+ span.end()
52
+ }
53
+ },
54
+ )
55
+ }, [subscriptionInfo, query, setValue, store, valueRef])
56
+
57
+ return valueRef.current
58
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react'
2
+
3
+ import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
4
+ import { useQuery } from './useQuery.js'
5
+
6
+ /**
7
+ * Creates a query, subscribes and destroys it when the component unmounts.
8
+ *
9
+ * Make sure `makeQuery` is a memoized function.
10
+ */
11
+ export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult => {
12
+ const query = React.useMemo(() => makeQuery(), [makeQuery])
13
+
14
+ React.useEffect(() => {
15
+ return () => {
16
+ query.destroy()
17
+ }
18
+ }, [query])
19
+
20
+ return useQuery(query)
21
+ }
@@ -0,0 +1,47 @@
1
+ export const originalStackLimit = Error.stackTraceLimit
2
+
3
+ export type StackInfo = {
4
+ name: string
5
+ filePath: string
6
+ }
7
+
8
+ /*
9
+ Example stack trace:
10
+
11
+ Error
12
+ at https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1699550216884:18:23
13
+ at mountMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:12817:27)
14
+ at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:13141:24)
15
+ at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-4WADDZ2G.js?v=3eb66ed6:1094:29)
16
+ at useQuery (https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1699550216884:13:33)
17
+ at useAppState (https://localhost:8081/src/db/AppState.ts?t=1699550216884:17:34)
18
+ at useRoute (https://localhost:8081/src/db/AppState.ts?t=1699550216884:74:22)
19
+ at RouteLink (https://localhost:8081/src/components/Link.tsx?t=1699550216884:36:7)
20
+ at renderWithHooks (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:12171:26)
21
+ at mountIndeterminateComponent (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:14921:21)
22
+
23
+ Approach:
24
+ - Start filtering at `at useQuery` (including)
25
+ - Stop filtering at `at renderWithHooks` (excluding)
26
+ */
27
+ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo[] => {
28
+ const namePattern = /at (\S+) \((.+)\)/g
29
+ let match: RegExpExecArray | null
30
+ const stackInfoArr: StackInfo[] = []
31
+ let hasReachedStart = false
32
+
33
+ while ((match = namePattern.exec(stackTrace)) !== null) {
34
+ const [, name, filePath] = match as any as [string, string, string]
35
+ if (name.startsWith('use')) {
36
+ hasReachedStart = true
37
+
38
+ stackInfoArr.unshift({ name, filePath })
39
+ } else if (hasReachedStart) {
40
+ // We've reached the end of the `use*` functions, so we're adding the component name and stop
41
+ stackInfoArr.unshift({ name, filePath })
42
+ break
43
+ }
44
+ }
45
+
46
+ return stackInfoArr
47
+ }