@livestore/livestore 0.0.24 → 0.0.27

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 (243) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts +3 -3
  3. package/dist/QueryCache.d.ts.map +1 -1
  4. package/dist/QueryCache.js +50 -60
  5. package/dist/QueryCache.js.map +1 -1
  6. package/dist/__tests__/react/fixture.d.ts +22 -7
  7. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  8. package/dist/__tests__/react/fixture.js +14 -15
  9. package/dist/__tests__/react/fixture.js.map +1 -1
  10. package/dist/__tests__/react/useQuery.test.js +37 -12
  11. package/dist/__tests__/react/useQuery.test.js.map +1 -1
  12. package/dist/__tests__/react/useRow.test.d.ts +2 -0
  13. package/dist/__tests__/react/useRow.test.d.ts.map +1 -0
  14. package/dist/__tests__/react/useRow.test.js +131 -0
  15. package/dist/__tests__/react/useRow.test.js.map +1 -0
  16. package/dist/__tests__/react/utils/stack-info.test.js +32 -0
  17. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -1
  18. package/dist/__tests__/reactive.test.js +51 -0
  19. package/dist/__tests__/reactive.test.js.map +1 -1
  20. package/dist/__tests__/reactiveQueries/sql.test.js +6 -13
  21. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
  22. package/dist/effect/LiveStore.d.ts +3 -3
  23. package/dist/effect/LiveStore.d.ts.map +1 -1
  24. package/dist/effect/LiveStore.js +2 -2
  25. package/dist/effect/LiveStore.js.map +1 -1
  26. package/dist/global-state.d.ts +19 -0
  27. package/dist/global-state.d.ts.map +1 -0
  28. package/dist/global-state.js +20 -0
  29. package/dist/global-state.js.map +1 -0
  30. package/dist/inMemoryDatabase.d.ts +6 -6
  31. package/dist/inMemoryDatabase.d.ts.map +1 -1
  32. package/dist/inMemoryDatabase.js +16 -10
  33. package/dist/inMemoryDatabase.js.map +1 -1
  34. package/dist/index.d.ts +9 -13
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +7 -8
  37. package/dist/index.js.map +1 -1
  38. package/dist/migrations.d.ts +4 -4
  39. package/dist/migrations.d.ts.map +1 -1
  40. package/dist/migrations.js +34 -28
  41. package/dist/migrations.js.map +1 -1
  42. package/dist/react/LiveStoreContext.js.map +1 -1
  43. package/dist/react/LiveStoreProvider.d.ts +2 -2
  44. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  45. package/dist/react/LiveStoreProvider.js.map +1 -1
  46. package/dist/react/index.d.ts +1 -2
  47. package/dist/react/index.d.ts.map +1 -1
  48. package/dist/react/index.js +1 -1
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/react/useQuery.d.ts +3 -0
  51. package/dist/react/useQuery.d.ts.map +1 -1
  52. package/dist/react/useQuery.js +7 -6
  53. package/dist/react/useQuery.js.map +1 -1
  54. package/dist/react/useRow.d.ts +33 -0
  55. package/dist/react/useRow.d.ts.map +1 -0
  56. package/dist/react/useRow.js +136 -0
  57. package/dist/react/useRow.js.map +1 -0
  58. package/dist/react/useTemporaryQuery.d.ts +2 -0
  59. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  60. package/dist/react/useTemporaryQuery.js +28 -11
  61. package/dist/react/useTemporaryQuery.js.map +1 -1
  62. package/dist/react/utils/stack-info.d.ts.map +1 -1
  63. package/dist/react/utils/stack-info.js +3 -3
  64. package/dist/react/utils/stack-info.js.map +1 -1
  65. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -1
  66. package/dist/reactive.d.ts +38 -29
  67. package/dist/reactive.d.ts.map +1 -1
  68. package/dist/reactive.js +73 -45
  69. package/dist/reactive.js.map +1 -1
  70. package/dist/reactiveQueries/base-class.d.ts +10 -6
  71. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  72. package/dist/reactiveQueries/base-class.js +11 -12
  73. package/dist/reactiveQueries/base-class.js.map +1 -1
  74. package/dist/reactiveQueries/graphql.d.ts +2 -2
  75. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  76. package/dist/reactiveQueries/graphql.js +56 -50
  77. package/dist/reactiveQueries/graphql.js.map +1 -1
  78. package/dist/reactiveQueries/js.d.ts +7 -3
  79. package/dist/reactiveQueries/js.d.ts.map +1 -1
  80. package/dist/reactiveQueries/js.js +25 -15
  81. package/dist/reactiveQueries/js.js.map +1 -1
  82. package/dist/reactiveQueries/sql.d.ts +5 -5
  83. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  84. package/dist/reactiveQueries/sql.js +39 -34
  85. package/dist/reactiveQueries/sql.js.map +1 -1
  86. package/dist/row-query.d.ts +21 -0
  87. package/dist/row-query.d.ts.map +1 -0
  88. package/dist/row-query.js +77 -0
  89. package/dist/row-query.js.map +1 -0
  90. package/dist/schema/action.d.ts +30 -0
  91. package/dist/schema/action.d.ts.map +1 -0
  92. package/dist/schema/action.js +3 -0
  93. package/dist/schema/action.js.map +1 -0
  94. package/dist/schema/index.d.ts +28 -0
  95. package/dist/schema/index.d.ts.map +1 -0
  96. package/dist/schema/index.js +26 -0
  97. package/dist/schema/index.js.map +1 -0
  98. package/dist/schema/system-tables.d.ts +24 -0
  99. package/dist/schema/system-tables.d.ts.map +1 -0
  100. package/dist/schema/system-tables.js +11 -0
  101. package/dist/schema/system-tables.js.map +1 -0
  102. package/dist/schema/table-def.d.ts +161 -0
  103. package/dist/schema/table-def.d.ts.map +1 -0
  104. package/dist/schema/table-def.js +53 -0
  105. package/dist/schema/table-def.js.map +1 -0
  106. package/dist/storage/in-memory/index.d.ts +1 -1
  107. package/dist/storage/in-memory/index.d.ts.map +1 -1
  108. package/dist/storage/in-memory/index.js +6 -7
  109. package/dist/storage/in-memory/index.js.map +1 -1
  110. package/dist/storage/index.d.ts +1 -1
  111. package/dist/storage/index.d.ts.map +1 -1
  112. package/dist/storage/tauri/index.d.ts +1 -1
  113. package/dist/storage/tauri/index.d.ts.map +1 -1
  114. package/dist/storage/tauri/index.js +25 -23
  115. package/dist/storage/tauri/index.js.map +1 -1
  116. package/dist/storage/utils/idb.js +3 -1
  117. package/dist/storage/utils/idb.js.map +1 -1
  118. package/dist/storage/web-worker/index.d.ts +1 -1
  119. package/dist/storage/web-worker/index.d.ts.map +1 -1
  120. package/dist/storage/web-worker/index.js +38 -34
  121. package/dist/storage/web-worker/index.js.map +1 -1
  122. package/dist/storage/web-worker/worker.d.ts +1 -1
  123. package/dist/storage/web-worker/worker.d.ts.map +1 -1
  124. package/dist/storage/web-worker/worker.js +1 -1
  125. package/dist/storage/web-worker/worker.js.map +1 -1
  126. package/dist/store.d.ts +11 -21
  127. package/dist/store.d.ts.map +1 -1
  128. package/dist/store.js +284 -272
  129. package/dist/store.js.map +1 -1
  130. package/dist/utils/bounded-collections.d.ts.map +1 -0
  131. package/dist/utils/bounded-collections.js +90 -0
  132. package/dist/utils/bounded-collections.js.map +1 -0
  133. package/dist/utils/otel.d.ts.map +1 -0
  134. package/dist/{otel.js → utils/otel.js} +1 -1
  135. package/dist/utils/otel.js.map +1 -0
  136. package/dist/utils/util.d.ts.map +1 -0
  137. package/dist/utils/util.js.map +1 -0
  138. package/package.json +21 -18
  139. package/src/QueryCache.ts +4 -4
  140. package/src/__tests__/react/fixture.tsx +17 -17
  141. package/src/__tests__/react/useQuery.test.tsx +56 -14
  142. package/src/__tests__/react/useRow.test.tsx +205 -0
  143. package/src/__tests__/react/utils/stack-info.test.ts +34 -0
  144. package/src/__tests__/reactive.test.ts +71 -0
  145. package/src/__tests__/reactiveQueries/sql.test.ts +6 -13
  146. package/src/effect/LiveStore.ts +7 -7
  147. package/src/global-state.ts +26 -0
  148. package/src/inMemoryDatabase.ts +14 -12
  149. package/src/index.ts +22 -29
  150. package/src/migrations.ts +41 -35
  151. package/src/react/LiveStoreProvider.tsx +2 -2
  152. package/src/react/index.ts +7 -9
  153. package/src/react/useQuery.ts +12 -6
  154. package/src/react/useRow.ts +221 -0
  155. package/src/react/useTemporaryQuery.ts +43 -11
  156. package/src/react/utils/stack-info.ts +4 -3
  157. package/src/reactive.ts +81 -65
  158. package/src/reactiveQueries/base-class.ts +14 -10
  159. package/src/reactiveQueries/graphql.ts +4 -3
  160. package/src/reactiveQueries/js.ts +9 -5
  161. package/src/reactiveQueries/sql.ts +9 -9
  162. package/src/row-query.ts +142 -0
  163. package/src/schema/action.ts +41 -0
  164. package/src/schema/index.ts +63 -0
  165. package/src/schema/system-tables.ts +21 -0
  166. package/src/schema/table-def.ts +199 -0
  167. package/src/storage/in-memory/index.ts +1 -1
  168. package/src/storage/index.ts +2 -1
  169. package/src/storage/tauri/index.ts +2 -2
  170. package/src/storage/web-worker/index.ts +1 -1
  171. package/src/storage/web-worker/worker.ts +2 -2
  172. package/src/store.ts +51 -51
  173. package/dist/__tests__/react/useComponentState.test.d.ts +0 -2
  174. package/dist/__tests__/react/useComponentState.test.d.ts.map +0 -1
  175. package/dist/__tests__/react/useComponentState.test.js +0 -68
  176. package/dist/__tests__/react/useComponentState.test.js.map +0 -1
  177. package/dist/__tests__/react/useLQuery.test.d.ts +0 -2
  178. package/dist/__tests__/react/useLQuery.test.d.ts.map +0 -1
  179. package/dist/__tests__/react/useLQuery.test.js +0 -38
  180. package/dist/__tests__/react/useLQuery.test.js.map +0 -1
  181. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +0 -2
  182. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +0 -1
  183. package/dist/__tests__/react/useLiveStoreComponent.test.js +0 -73
  184. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +0 -1
  185. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +0 -2
  186. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +0 -1
  187. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +0 -38
  188. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +0 -1
  189. package/dist/bounded-collections.d.ts.map +0 -1
  190. package/dist/bounded-collections.js +0 -103
  191. package/dist/bounded-collections.js.map +0 -1
  192. package/dist/componentKey.d.ts +0 -20
  193. package/dist/componentKey.d.ts.map +0 -1
  194. package/dist/componentKey.js +0 -3
  195. package/dist/componentKey.js.map +0 -1
  196. package/dist/otel.d.ts.map +0 -1
  197. package/dist/otel.js.map +0 -1
  198. package/dist/react/useComponentState.d.ts +0 -50
  199. package/dist/react/useComponentState.d.ts.map +0 -1
  200. package/dist/react/useComponentState.js +0 -240
  201. package/dist/react/useComponentState.js.map +0 -1
  202. package/dist/react/useGlobalQuery.d.ts +0 -3
  203. package/dist/react/useGlobalQuery.d.ts.map +0 -1
  204. package/dist/react/useGlobalQuery.js +0 -26
  205. package/dist/react/useGlobalQuery.js.map +0 -1
  206. package/dist/react/useGraphQL.d.ts +0 -13
  207. package/dist/react/useGraphQL.d.ts.map +0 -1
  208. package/dist/react/useGraphQL.js +0 -87
  209. package/dist/react/useGraphQL.js.map +0 -1
  210. package/dist/react/useLiveStoreComponent.d.ts +0 -75
  211. package/dist/react/useLiveStoreComponent.d.ts.map +0 -1
  212. package/dist/react/useLiveStoreComponent.js +0 -361
  213. package/dist/react/useLiveStoreComponent.js.map +0 -1
  214. package/dist/react/utils/extractNamesFromStackTrace.d.ts +0 -3
  215. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +0 -1
  216. package/dist/react/utils/extractNamesFromStackTrace.js +0 -40
  217. package/dist/react/utils/extractNamesFromStackTrace.js.map +0 -1
  218. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +0 -7
  219. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +0 -1
  220. package/dist/react/utils/extractStackInfoFromStackTrace.js +0 -40
  221. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +0 -1
  222. package/dist/reactiveQueries/graph.d.ts +0 -10
  223. package/dist/reactiveQueries/graph.d.ts.map +0 -1
  224. package/dist/reactiveQueries/graph.js +0 -6
  225. package/dist/reactiveQueries/graph.js.map +0 -1
  226. package/dist/schema.d.ts +0 -81
  227. package/dist/schema.d.ts.map +0 -1
  228. package/dist/schema.js +0 -46
  229. package/dist/schema.js.map +0 -1
  230. package/dist/util.d.ts.map +0 -1
  231. package/dist/util.js.map +0 -1
  232. package/src/__tests__/react/useComponentState.test.tsx +0 -100
  233. package/src/componentKey.ts +0 -9
  234. package/src/react/useComponentState.ts +0 -404
  235. package/src/reactiveQueries/graph.ts +0 -15
  236. package/src/schema.ts +0 -143
  237. /package/dist/{bounded-collections.d.ts → utils/bounded-collections.d.ts} +0 -0
  238. /package/dist/{otel.d.ts → utils/otel.d.ts} +0 -0
  239. /package/dist/{util.d.ts → utils/util.d.ts} +0 -0
  240. /package/dist/{util.js → utils/util.js} +0 -0
  241. /package/src/{bounded-collections.ts → utils/bounded-collections.ts} +0 -0
  242. /package/src/{otel.ts → utils/otel.ts} +0 -0
  243. /package/src/{util.ts → utils/util.ts} +0 -0
@@ -0,0 +1,221 @@
1
+ import { Schema } from '@livestore/utils/effect'
2
+ import * as otel from '@opentelemetry/api'
3
+ import type { SqliteDsl } from 'effect-db-schema'
4
+ import { mapValues } from 'lodash-es'
5
+ import React from 'react'
6
+
7
+ import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
8
+ import type { RowQueryArgs, RowResult } from '../row-query.js'
9
+ import { rowQuery } from '../row-query.js'
10
+ import type { DefaultSqliteTableDef, TableDef, TableOptions } from '../schema/table-def.js'
11
+ import { useStore } from './LiveStoreContext.js'
12
+ import { useQueryRef } from './useQuery.js'
13
+
14
+ export type UseRowResult<TTableDef extends TableDef> = [
15
+ row: RowResult<TTableDef>,
16
+ setRow: StateSetters<TTableDef>,
17
+ query$: LiveStoreJSQuery<RowResult<TTableDef>>,
18
+ ]
19
+
20
+ /**
21
+ * Similar to `React.useState` but returns a tuple of `[row, setRow, query$]` for a given table where ...
22
+ *
23
+ * - `row` is the current value of the row (fully decoded according to the table schema)
24
+ * - `setRow` is a function that can be used to update the row (values will be encoded according to the table schema)
25
+ * - `query$` is a `LiveStoreJSQuery` that e.g. can be used to subscribe to changes to the row
26
+ *
27
+ * If the table is a singleton table, `useRow` can be called without an `id` argument. Otherwise, the `id` argument is required.
28
+ */
29
+ export const useRow: {
30
+ <TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: true }>>(
31
+ table: TTableDef,
32
+ ): UseRowResult<TTableDef>
33
+ <TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: false }>>(
34
+ table: TTableDef,
35
+ // TODO adjust so it works with arbitrary primary keys or unique constraints
36
+ id: string,
37
+ ): UseRowResult<TTableDef>
38
+ } = <TTableDef extends TableDef>(table: TTableDef, id?: string): UseRowResult<TTableDef> => {
39
+ const sqliteTableDef = table.schema
40
+ type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['schema']['columns']>
41
+
42
+ const { store } = useStore()
43
+
44
+ const reactId = React.useId()
45
+
46
+ const { query$, otelContext } = React.useMemo(() => {
47
+ const cachedItem = rcCache.get(table, id ?? 'singleton')
48
+ if (cachedItem !== undefined) {
49
+ cachedItem.reactIds.add(reactId)
50
+ cachedItem.span.addEvent('new-subscriber', { reactId })
51
+
52
+ return {
53
+ query$: cachedItem.query$ as LiveStoreJSQuery<RowResult<TTableDef>>,
54
+ otelContext: cachedItem.otelContext,
55
+ }
56
+ }
57
+
58
+ const span = store.otel.tracer.startSpan(
59
+ `LiveStore:useState:${table.schema.name}${id === undefined ? '' : `:${id}`}`,
60
+ { attributes: { id } },
61
+ store.otel.queriesSpanContext,
62
+ )
63
+
64
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
65
+
66
+ const query$ = table.options.isSingleton
67
+ ? rowQuery({ table, store, otelContext } as RowQueryArgs<TTableDef>)
68
+ : rowQuery({ table, store, id, otelContext } as RowQueryArgs<TTableDef>)
69
+
70
+ rcCache.set(table, id ?? 'singleton', query$, reactId, otelContext, span)
71
+
72
+ return { query$, otelContext }
73
+ }, [table, id, reactId, store])
74
+
75
+ React.useEffect(
76
+ () => () => {
77
+ const cachedItem = rcCache.get(table, id ?? 'singleton')!
78
+
79
+ cachedItem.reactIds.delete(reactId)
80
+ if (cachedItem.reactIds.size === 0) {
81
+ rcCache.delete(cachedItem.query$)
82
+ cachedItem.query$.destroy()
83
+ cachedItem.span.end()
84
+ }
85
+ },
86
+ [table, id, reactId],
87
+ )
88
+
89
+ const query$Ref = useQueryRef(query$, otelContext)
90
+
91
+ const setState = React.useMemo<StateSetters<TTableDef>>(() => {
92
+ if (table.isSingleColumn) {
93
+ return (newValue: RowResult<TTableDef>) => {
94
+ if (query$Ref.current === newValue) return
95
+
96
+ const encodedValue = Schema.encodeSync(sqliteTableDef.columns['value']!.type.codec)(newValue)
97
+
98
+ store.applyEvent('livestore.UpdateComponentState', {
99
+ tableName: sqliteTableDef.name,
100
+ columnNames: ['value'],
101
+ id,
102
+ bindValues: { ['value']: encodedValue },
103
+ })
104
+ }
105
+ } else {
106
+ const setState = // TODO: do we have a better type for the values that can go in SQLite?
107
+ mapValues(sqliteTableDef.columns, (column, columnName) => (newValue: string | number) => {
108
+ // Don't update the state if it's the same as the value already seen in the component
109
+ // @ts-expect-error TODO fix typing
110
+ if (query$Ref.current[columnName] === newValue) return
111
+
112
+ const encodedValue = Schema.encodeSync(column.type.codec)(newValue)
113
+
114
+ store.applyEvent('livestore.UpdateComponentState', {
115
+ tableName: sqliteTableDef.name,
116
+ columnNames: [columnName],
117
+ id,
118
+ bindValues: { [columnName]: encodedValue },
119
+ })
120
+ })
121
+
122
+ // @ts-expect-error TODO fix typing
123
+ setState.setMany = (columnValues: Partial<TComponentState>) => {
124
+ // TODO use hashing instead
125
+ // Don't update the state if it's the same as the value already seen in the component
126
+ if (
127
+ // @ts-expect-error TODO fix typing
128
+ Object.entries(columnValues).every(([columnName, value]) => query$Ref.current[columnName] === value)
129
+ ) {
130
+ return
131
+ }
132
+
133
+ const columnNames = Object.keys(columnValues)
134
+ const bindValues = mapValues(columnValues, (value, columnName) =>
135
+ Schema.encodeSync(sqliteTableDef.columns[columnName]!.type.codec)(value),
136
+ )
137
+
138
+ store.applyEvent('livestore.UpdateComponentState', {
139
+ tableName: sqliteTableDef.name,
140
+ columnNames,
141
+ id,
142
+ bindValues,
143
+ })
144
+ }
145
+
146
+ return setState as any
147
+ }
148
+ }, [table.isSingleColumn, id, sqliteTableDef.columns, sqliteTableDef.name, store, query$Ref])
149
+
150
+ return [query$Ref.current, setState, query$]
151
+ }
152
+
153
+ export type Dispatch<A> = (action: A) => void
154
+ export type SetStateAction<S> = S | ((previousValue: S) => S)
155
+
156
+ export type StateSetters<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
157
+ ? Dispatch<SetStateAction<RowResult<TTableDef>>>
158
+ : {
159
+ [K in keyof RowResult<TTableDef>]: Dispatch<SetStateAction<RowResult<TTableDef>[K]>>
160
+ } & {
161
+ setMany: Dispatch<SetStateAction<Partial<RowResult<TTableDef>>>>
162
+ }
163
+
164
+ /** Reference counted cache for `query$` and otel context */
165
+ class RCCache {
166
+ private readonly cache = new Map<
167
+ TableDef,
168
+ Map<
169
+ string,
170
+ {
171
+ reactIds: Set<string>
172
+ span: otel.Span
173
+ otelContext: otel.Context
174
+ query$: LiveStoreJSQuery<any>
175
+ }
176
+ >
177
+ >()
178
+ private reverseCache = new Map<LiveStoreJSQuery<any>, [TableDef, string]>()
179
+
180
+ get = (table: TableDef, id: string) => {
181
+ const queries = this.cache.get(table)
182
+ if (queries === undefined) return undefined
183
+ return queries.get(id)
184
+ }
185
+
186
+ set = (
187
+ table: TableDef,
188
+ id: string,
189
+ query$: LiveStoreJSQuery<any>,
190
+ reactId: string,
191
+ otelContext: otel.Context,
192
+ span: otel.Span,
193
+ ) => {
194
+ let queries = this.cache.get(table)
195
+ if (queries === undefined) {
196
+ queries = new Map()
197
+ this.cache.set(table, queries)
198
+ }
199
+ queries.set(id, { query$, otelContext, span, reactIds: new Set([reactId]) })
200
+ this.reverseCache.set(query$, [table, id])
201
+ }
202
+
203
+ delete = (query$: LiveStoreJSQuery<any>) => {
204
+ const item = this.reverseCache.get(query$)
205
+ if (item === undefined) return
206
+
207
+ const [table, id] = item
208
+ const queries = this.cache.get(table)
209
+ if (queries === undefined) return
210
+
211
+ queries.delete(id)
212
+
213
+ if (queries.size === 0) {
214
+ this.cache.delete(table)
215
+ }
216
+
217
+ this.reverseCache.delete(query$)
218
+ }
219
+ }
220
+
221
+ const rcCache = new RCCache()
@@ -1,23 +1,55 @@
1
1
  import React from 'react'
2
2
 
3
3
  import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
4
- import { useQuery } from './useQuery.js'
4
+ import { useQueryRef } from './useQuery.js'
5
+
6
+ /**
7
+ * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷.
8
+ * The map entry is being removed again in the `React.useEffect` call below.
9
+ */
10
+ const queryCache = new Map<() => ILiveStoreQuery<any>, { reactIds: Set<string>; query$: ILiveStoreQuery<any> }>()
5
11
 
6
12
  /**
7
13
  * Creates a query, subscribes and destroys it when the component unmounts.
8
14
  *
9
15
  * Make sure `makeQuery` is a memoized function.
10
16
  */
11
- export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult => {
12
- // TODO cache the query outside of the `useMemo` since `useMemo` might be called multiple times
13
- // also need to update the `useEffect` below https://stackoverflow.com/questions/66446642/react-usememo-memory-clean/77457605#77457605
14
- const query = React.useMemo(() => makeQuery(), [makeQuery])
15
-
16
- React.useEffect(() => {
17
- return () => {
18
- query.destroy()
17
+ export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult =>
18
+ useTemporaryQueryRef(makeQuery).current
19
+
20
+ export const useTemporaryQueryRef = <TResult>(
21
+ makeQuery: () => ILiveStoreQuery<TResult>,
22
+ ): React.MutableRefObject<TResult> => {
23
+ const reactId = React.useId()
24
+
25
+ const query$ = React.useMemo(() => {
26
+ const cachedItem = queryCache.get(makeQuery)
27
+ if (cachedItem !== undefined) {
28
+ cachedItem.reactIds.add(reactId)
29
+
30
+ return cachedItem.query$
19
31
  }
20
- }, [query])
21
32
 
22
- return useQuery(query)
33
+ const query$ = makeQuery()
34
+
35
+ queryCache.set(makeQuery, { reactIds: new Set([reactId]), query$ })
36
+
37
+ return query$
38
+ }, [reactId, makeQuery])
39
+
40
+ React.useEffect(
41
+ () => () => {
42
+ const cachedItem = queryCache.get(makeQuery)!
43
+
44
+ cachedItem.reactIds.delete(reactId)
45
+
46
+ if (cachedItem.reactIds.size === 0) {
47
+ cachedItem.query$.destroy()
48
+ queryCache.delete(makeQuery)
49
+ }
50
+ },
51
+ [makeQuery, reactId],
52
+ )
53
+
54
+ return useQueryRef(query$)
23
55
  }
@@ -38,10 +38,12 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
38
38
 
39
39
  while ((match = namePattern.exec(stackTrace)) !== null) {
40
40
  const [, name, filePath] = match as any as [string, string, string]
41
- if (name.startsWith('use')) {
41
+
42
+ // NOTE No idea where this `Module.` comes from - possibly a Vite thing?
43
+ if ((name.startsWith('use') || name.startsWith('Module.use')) && name.endsWith('QueryRef') === false) {
42
44
  hasReachedStart = true
43
45
 
44
- frames.unshift({ name, filePath })
46
+ frames.unshift({ name: name.replace(/^Module\./, ''), filePath })
45
47
  } else if (hasReachedStart) {
46
48
  // We've reached the end of the `use*` functions, so we're adding the component name and stop
47
49
  frames.unshift({ name, filePath })
@@ -57,7 +59,6 @@ export const useStackInfo = (): StackInfo =>
57
59
  Error.stackTraceLimit = 10
58
60
  // eslint-disable-next-line unicorn/error-message
59
61
  const stack = new Error().stack!
60
- console.log('stack', stack)
61
62
  Error.stackTraceLimit = originalStackLimit
62
63
  return extractStackInfoFromStackTrace(stack)
63
64
  }, [])
package/src/reactive.ts CHANGED
@@ -28,7 +28,7 @@ import { pick } from '@livestore/utils'
28
28
  import type * as otel from '@opentelemetry/api'
29
29
  import { isEqual, uniqueId } from 'lodash-es'
30
30
 
31
- import { BoundArray } from './bounded-collections.js'
31
+ import { BoundArray } from './utils/bounded-collections.js'
32
32
  // import { getDurationMsFromSpan } from './otel.js'
33
33
 
34
34
  export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
@@ -36,7 +36,7 @@ export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
36
36
 
37
37
  export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context) => T
38
38
 
39
- export type Ref<T, TContext, TDebugRefreshReason extends Taggable> = {
39
+ export type Ref<T, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
40
40
  _tag: 'ref'
41
41
  id: string
42
42
  isDirty: false
@@ -50,14 +50,11 @@ export type Ref<T, TContext, TDebugRefreshReason extends Taggable> = {
50
50
  equal: (a: T, b: T) => boolean
51
51
  }
52
52
 
53
- export type Thunk<TResult, TContext, TDebugRefreshReason extends Taggable> = {
53
+ export type Thunk<TResult, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
54
54
  _tag: 'thunk'
55
55
  id: string
56
56
  isDirty: boolean
57
- computeResult: (
58
- otelContext?: otel.Context,
59
- debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>,
60
- ) => TResult
57
+ computeResult: (otelContext?: otel.Context, debugRefreshReason?: TDebugRefreshReason) => TResult
61
58
  previousResult: TResult | NOT_REFRESHED_YET
62
59
  sub: Set<Atom<any, TContext, TDebugRefreshReason>>
63
60
  super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
@@ -70,7 +67,7 @@ export type Thunk<TResult, TContext, TDebugRefreshReason extends Taggable> = {
70
67
  __getResult: any
71
68
  }
72
69
 
73
- export type Atom<T, TContext, TDebugRefreshReason extends Taggable> =
70
+ export type Atom<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
74
71
  | Ref<T, TContext, TDebugRefreshReason>
75
72
  | Thunk<T, TContext, TDebugRefreshReason>
76
73
 
@@ -82,13 +79,23 @@ export type Effect = {
82
79
  label?: string
83
80
  }
84
81
 
85
- export type Taggable<T extends string = string> = { _tag: T }
86
-
87
82
  export type DebugThunkInfo<T extends string = string> = {
88
83
  _tag: T
89
84
  durationMs: number
90
85
  }
91
86
 
87
+ export type DebugRefreshReasonBase =
88
+ /** Usually in response to some `applyEvent`/`applyEvents` with `skipRefresh: true` */
89
+ | {
90
+ _tag: 'runDeferredEffects'
91
+ originalRefreshReasons?: ReadonlyArray<DebugRefreshReasonBase>
92
+ manualRefreshReason?: DebugRefreshReasonBase
93
+ }
94
+ | { _tag: 'makeThunk'; label?: string }
95
+ | { _tag: 'unknown' }
96
+
97
+ export type DebugRefreshReason<T extends string = string> = DebugRefreshReasonBase | { _tag: T }
98
+
92
99
  export type ReactiveGraphOptions = {
93
100
  effectsWrapper?: (runEffects: () => void) => void
94
101
  }
@@ -100,7 +107,7 @@ export type AtomDebugInfo<TDebugThunkInfo extends DebugThunkInfo> = {
100
107
  }
101
108
 
102
109
  // TODO possibly find a better name for "refresh"
103
- export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends DebugThunkInfo> = {
110
+ export type RefreshDebugInfo<TDebugRefreshReason extends DebugRefreshReason, TDebugThunkInfo extends DebugThunkInfo> = {
104
111
  /** Currently only used for easier handling in React (e.g. as key) */
105
112
  id: string
106
113
  reason: TDebugRefreshReason
@@ -112,15 +119,7 @@ export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkIn
112
119
  graphSnapshot: ReactiveGraphSnapshot
113
120
  }
114
121
 
115
- export type RefreshReasonWithGenericReasons<T extends Taggable> =
116
- | T
117
- | {
118
- _tag: 'makeThunk'
119
- label?: string
120
- }
121
- | { _tag: 'unknown' }
122
-
123
- export const unknownRefreshReason = () => {
122
+ const unknownRefreshReason = () => {
124
123
  // debugger
125
124
  return { _tag: 'unknown' as const }
126
125
  }
@@ -128,34 +127,29 @@ export const unknownRefreshReason = () => {
128
127
  export type SerializedAtom = Readonly<
129
128
  PrettifyFlat<
130
129
  Pick<Atom<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta'> & {
131
- sub: string[]
132
- super: string[]
130
+ sub: ReadonlyArray<string>
131
+ super: ReadonlyArray<string>
133
132
  }
134
133
  >
135
134
  >
136
135
 
137
- export type SerializedEffect = Readonly<PrettifyFlat<Pick<Effect, '_tag' | 'id'>>>
138
-
139
136
  type ReactiveGraphSnapshot = {
140
- readonly atoms: SerializedAtom[]
141
- // readonly effects: SerializedEffect[]
142
- /** IDs of atoms and effects that are dirty */
143
- // readonly dirtyNodes: string[]
137
+ readonly atoms: ReadonlyArray<SerializedAtom>
138
+ /** IDs of deferred effects */
139
+ readonly deferredEffects: ReadonlyArray<string>
144
140
  }
145
141
 
146
142
  const uniqueNodeId = () => uniqueId('node-')
147
143
  const uniqueRefreshInfoId = () => uniqueId('refresh-info-')
148
144
 
149
145
  const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
150
- ...pick(atom, ['_tag', 'id', 'label', 'meta']),
146
+ ...pick(atom, ['_tag', 'id', 'label', 'meta', 'isDirty']),
151
147
  sub: Array.from(atom.sub).map((a) => a.id),
152
148
  super: Array.from(atom.super).map((a) => a.id),
153
149
  })
154
150
 
155
- // const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
156
-
157
151
  export class ReactiveGraph<
158
- TDebugRefreshReason extends Taggable,
152
+ TDebugRefreshReason extends DebugRefreshReason,
159
153
  TDebugThunkInfo extends DebugThunkInfo,
160
154
  TContext = {},
161
155
  > {
@@ -164,11 +158,13 @@ export class ReactiveGraph<
164
158
 
165
159
  context: TContext | undefined
166
160
 
167
- debugRefreshInfos: BoundArray<
168
- RefreshDebugInfo<RefreshReasonWithGenericReasons<TDebugRefreshReason>, TDebugThunkInfo>
169
- > = new BoundArray(5000)
161
+ debugRefreshInfos: BoundArray<RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo>> = new BoundArray(5000)
162
+
163
+ private currentDebugRefresh:
164
+ | { refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]; startMs: DOMHighResTimeStamp }
165
+ | undefined
170
166
 
171
- currentDebugRefresh: { refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]; startMs: DOMHighResTimeStamp } | undefined
167
+ private deferredEffects: Map<Effect, Set<TDebugRefreshReason>> = new Map()
172
168
 
173
169
  constructor(options: ReactiveGraphOptions) {
174
170
  this.effectsWrapper = options?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
@@ -208,8 +204,6 @@ export class ReactiveGraph<
208
204
  label?: string
209
205
  meta?: any
210
206
  equal?: (a: T, b: T) => boolean
211
- /** Debug info for initializing the thunk (i.e. running it the first time) */
212
- // debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
213
207
  }
214
208
  | undefined,
215
209
  ): Thunk<T, TContext, TDebugRefreshReason> {
@@ -264,16 +258,15 @@ export class ReactiveGraph<
264
258
  const durationMs = performance.now() - this.currentDebugRefresh!.startMs
265
259
  this.currentDebugRefresh = undefined
266
260
 
267
- const refreshDebugInfo = {
261
+ this.debugRefreshInfos.push({
268
262
  id: uniqueRefreshInfoId(),
269
- reason: debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label },
263
+ reason: debugRefreshReason ?? ({ _tag: 'makeThunk', label: options?.label } as TDebugRefreshReason),
270
264
  skippedRefresh: false,
271
265
  refreshedAtoms,
272
266
  durationMs,
273
267
  completedTimestamp: Date.now(),
274
268
  graphSnapshot: this.getSnapshot(),
275
- }
276
- this.debugRefreshInfos.push(refreshDebugInfo)
269
+ })
277
270
  }
278
271
 
279
272
  return result
@@ -308,7 +301,9 @@ export class ReactiveGraph<
308
301
  this.removeEdge(node, subComp)
309
302
  }
310
303
 
311
- if (node._tag !== 'effect') {
304
+ if (node._tag === 'effect') {
305
+ this.deferredEffects.delete(node)
306
+ } else {
312
307
  this.atoms.delete(node)
313
308
  }
314
309
  }
@@ -345,23 +340,20 @@ export class ReactiveGraph<
345
340
  val: T,
346
341
  options?:
347
342
  | {
343
+ skipRefresh?: boolean
348
344
  debugRefreshReason?: TDebugRefreshReason
349
345
  otelContext?: otel.Context
350
346
  }
351
347
  | undefined,
352
348
  ) {
353
- ref.previousResult = val
354
-
355
- const effectsToRefresh = new Set<Effect>()
356
- markSuperCompDirtyRec(ref, effectsToRefresh)
357
-
358
- this.runEffects(effectsToRefresh, options)
349
+ this.setRefs([[ref, val]], options)
359
350
  }
360
351
 
361
352
  setRefs<T>(
362
353
  refs: [Ref<T, TContext, TDebugRefreshReason>, T][],
363
354
  options?:
364
355
  | {
356
+ skipRefresh?: boolean
365
357
  debugRefreshReason?: TDebugRefreshReason
366
358
  otelContext?: otel.Context
367
359
  }
@@ -374,17 +366,30 @@ export class ReactiveGraph<
374
366
  markSuperCompDirtyRec(ref, effectsToRefresh)
375
367
  }
376
368
 
377
- this.runEffects(effectsToRefresh, options)
369
+ if (options?.skipRefresh) {
370
+ for (const effect of effectsToRefresh) {
371
+ if (this.deferredEffects.has(effect) === false) {
372
+ this.deferredEffects.set(effect, new Set())
373
+ }
374
+
375
+ if (options?.debugRefreshReason !== undefined) {
376
+ this.deferredEffects.get(effect)!.add(options.debugRefreshReason)
377
+ }
378
+ }
379
+ } else {
380
+ this.runEffects(effectsToRefresh, {
381
+ debugRefreshReason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
382
+ otelContext: options?.otelContext,
383
+ })
384
+ }
378
385
  }
379
386
 
380
387
  private runEffects = (
381
388
  effectsToRefresh: Set<Effect>,
382
- options?:
383
- | {
384
- debugRefreshReason?: TDebugRefreshReason
385
- otelContext?: otel.Context
386
- }
387
- | undefined,
389
+ options: {
390
+ debugRefreshReason: TDebugRefreshReason
391
+ otelContext?: otel.Context
392
+ },
388
393
  ) => {
389
394
  this.effectsWrapper(() => {
390
395
  this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
@@ -399,7 +404,7 @@ export class ReactiveGraph<
399
404
 
400
405
  const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
401
406
  id: uniqueRefreshInfoId(),
402
- reason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
407
+ reason: options.debugRefreshReason,
403
408
  skippedRefresh: false,
404
409
  refreshedAtoms,
405
410
  durationMs,
@@ -410,6 +415,22 @@ export class ReactiveGraph<
410
415
  })
411
416
  }
412
417
 
418
+ runDeferredEffects = (options?: { debugRefreshReason?: TDebugRefreshReason; otelContext?: otel.Context }) => {
419
+ // TODO improve how refresh reasons are propagated for deferred effect execution
420
+ // TODO also improve "batching" of running deferred effects (i.e. in a single `this.runEffects` call)
421
+ // but need to be careful to not overwhelm the main thread
422
+ for (const [effect, debugRefreshReasons] of this.deferredEffects) {
423
+ this.runEffects(new Set([effect]), {
424
+ debugRefreshReason: {
425
+ _tag: 'runDeferredEffects',
426
+ originalRefreshReasons: Array.from(debugRefreshReasons) as ReadonlyArray<DebugRefreshReasonBase>,
427
+ manualRefreshReason: options?.debugRefreshReason,
428
+ } as TDebugRefreshReason,
429
+ otelContext: options?.otelContext,
430
+ })
431
+ }
432
+ }
433
+
413
434
  addEdge(
414
435
  superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
415
436
  subComp: Atom<any, TContext, TDebugRefreshReason>,
@@ -426,17 +447,12 @@ export class ReactiveGraph<
426
447
  subComp.super.delete(superComp)
427
448
  }
428
449
 
429
- private getSnapshot = (): ReactiveGraphSnapshot => ({
450
+ getSnapshot = (): ReactiveGraphSnapshot => ({
430
451
  atoms: Array.from(this.atoms).map(serializeAtom),
431
- // effects: Array.from(this.effects).map(serializeEffect),
432
- // dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
452
+ deferredEffects: Array.from(this.deferredEffects.keys()).map((_) => _.id),
433
453
  })
434
454
  }
435
455
 
436
- // const isAtom = <T, TContext>(a: Atom<T, TContext> | Effect): a is Atom<T, TContext> =>
437
- // a._tag === 'ref' || a._tag === 'thunk'
438
- // const isEffect = <T, TContext>(a: Atom<T, TContext> | Effect): a is Effect => a._tag === 'effect'
439
-
440
456
  const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
441
457
  // const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
442
458
  if (atom.isDirty) {
@@ -462,6 +478,6 @@ const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh:
462
478
  }
463
479
  }
464
480
 
465
- const throwContextNotSetError = (): never => {
481
+ export const throwContextNotSetError = (): never => {
466
482
  throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph`)
467
483
  }
@@ -1,11 +1,17 @@
1
1
  import type * as otel from '@opentelemetry/api'
2
2
 
3
+ import { dbGraph } from '../global-state.js'
3
4
  import type { StackInfo } from '../react/utils/stack-info.js'
4
- import type { Atom, GetAtom, RefreshReasonWithGenericReasons, Thunk } from '../reactive.js'
5
- import type { RefreshReason } from '../store.js'
6
- import { type DbContext, dbGraph } from './graph.js'
5
+ import { type Atom, type GetAtom, throwContextNotSetError, type Thunk } from '../reactive.js'
6
+ import type { RefreshReason, Store } from '../store.js'
7
7
  import type { LiveStoreJSQuery } from './js.js'
8
8
 
9
+ export type DbContext = {
10
+ store: Store
11
+ otelTracer: otel.Tracer
12
+ rootOtelContext: otel.Context
13
+ }
14
+
9
15
  export type UnsubscribeQuery = () => void
10
16
 
11
17
  let queryIdCounter = 0
@@ -18,7 +24,7 @@ export interface ILiveStoreQuery<TResult> {
18
24
 
19
25
  label: string
20
26
 
21
- run: (otelContext?: otel.Context, debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>) => TResult
27
+ run: (otelContext?: otel.Context, debugRefreshReason?: RefreshReason) => TResult
22
28
 
23
29
  destroy(): void
24
30
 
@@ -41,13 +47,10 @@ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TRe
41
47
 
42
48
  abstract destroy: () => void
43
49
 
44
- run = (otelContext?: otel.Context, debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>): TResult =>
50
+ run = (otelContext?: otel.Context, debugRefreshReason?: RefreshReason): TResult =>
45
51
  this.results$.computeResult(otelContext, debugRefreshReason)
46
52
 
47
- runAndDestroy = (
48
- otelContext?: otel.Context,
49
- debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>,
50
- ): TResult => {
53
+ runAndDestroy = (otelContext?: otel.Context, debugRefreshReason?: RefreshReason): TResult => {
51
54
  const result = this.run(otelContext, debugRefreshReason)
52
55
  this.destroy()
53
56
  return result
@@ -57,7 +60,8 @@ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TRe
57
60
  onNewValue: (value: TResult) => void,
58
61
  onUnsubsubscribe?: () => void,
59
62
  options?: { label?: string; otelContext?: otel.Context } | undefined,
60
- ): (() => void) => dbGraph.context!.store.subscribe(this, onNewValue, onUnsubsubscribe, options)
63
+ ): (() => void) =>
64
+ dbGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ?? throwContextNotSetError()
61
65
  }
62
66
 
63
67
  export type GetAtomResult = <T>(atom: Atom<T, any, RefreshReason> | LiveStoreJSQuery<T>) => T