@livestore/livestore 0.0.12 → 0.0.15

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 (173) hide show
  1. package/README.md +7 -7
  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 +25 -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/useLiveStoreComponent.test.d.ts +2 -0
  12. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  13. package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
  14. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  15. package/dist/__tests__/reactive.test.d.ts +2 -0
  16. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  17. package/dist/__tests__/reactive.test.js +197 -0
  18. package/dist/__tests__/reactive.test.js.map +1 -0
  19. package/dist/bounded-collections.d.ts +34 -0
  20. package/dist/bounded-collections.d.ts.map +1 -0
  21. package/dist/bounded-collections.js +103 -0
  22. package/dist/bounded-collections.js.map +1 -0
  23. package/dist/componentKey.d.ts +20 -0
  24. package/dist/componentKey.d.ts.map +1 -0
  25. package/dist/componentKey.js +3 -0
  26. package/dist/componentKey.js.map +1 -0
  27. package/dist/effect/LiveStore.d.ts +36 -0
  28. package/dist/effect/LiveStore.d.ts.map +1 -0
  29. package/dist/effect/LiveStore.js +41 -0
  30. package/dist/effect/LiveStore.js.map +1 -0
  31. package/dist/effect/index.d.ts +2 -0
  32. package/dist/effect/index.d.ts.map +1 -0
  33. package/dist/effect/index.js +2 -0
  34. package/dist/effect/index.js.map +1 -0
  35. package/dist/events.d.ts +7 -0
  36. package/dist/events.d.ts.map +1 -0
  37. package/dist/events.js +2 -0
  38. package/dist/events.js.map +1 -0
  39. package/dist/inMemoryDatabase.d.ts +56 -0
  40. package/dist/inMemoryDatabase.d.ts.map +1 -0
  41. package/dist/inMemoryDatabase.js +223 -0
  42. package/dist/inMemoryDatabase.js.map +1 -0
  43. package/dist/index.d.ts +20 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +9 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/migrations.d.ts +16 -0
  48. package/dist/migrations.d.ts.map +1 -0
  49. package/dist/migrations.js +67 -0
  50. package/dist/migrations.js.map +1 -0
  51. package/dist/otel.d.ts +4 -0
  52. package/dist/otel.d.ts.map +1 -0
  53. package/dist/otel.js +6 -0
  54. package/dist/otel.js.map +1 -0
  55. package/dist/react/LiveStoreContext.d.ts +11 -0
  56. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  57. package/dist/react/LiveStoreContext.js +10 -0
  58. package/dist/react/LiveStoreContext.js.map +1 -0
  59. package/dist/react/LiveStoreProvider.d.ts +20 -0
  60. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  61. package/dist/react/LiveStoreProvider.js +52 -0
  62. package/dist/react/LiveStoreProvider.js.map +1 -0
  63. package/dist/react/index.d.ts +8 -0
  64. package/dist/react/index.d.ts.map +1 -0
  65. package/dist/react/index.js +6 -0
  66. package/dist/react/index.js.map +1 -0
  67. package/dist/react/useGraphQL.d.ts +13 -0
  68. package/dist/react/useGraphQL.d.ts.map +1 -0
  69. package/dist/react/useGraphQL.js +85 -0
  70. package/dist/react/useGraphQL.js.map +1 -0
  71. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  72. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  73. package/dist/react/useLiveStoreComponent.js +317 -0
  74. package/dist/react/useLiveStoreComponent.js.map +1 -0
  75. package/dist/react/useQuery.d.ts +3 -0
  76. package/dist/react/useQuery.d.ts.map +1 -0
  77. package/dist/react/useQuery.js +38 -0
  78. package/dist/react/useQuery.js.map +1 -0
  79. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  80. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  81. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  82. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  83. package/dist/reactive.d.ts +140 -0
  84. package/dist/reactive.d.ts.map +1 -0
  85. package/dist/reactive.js +302 -0
  86. package/dist/reactive.js.map +1 -0
  87. package/dist/reactiveQueries/base-class.d.ts +27 -0
  88. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  89. package/dist/reactiveQueries/base-class.js +23 -0
  90. package/dist/reactiveQueries/base-class.js.map +1 -0
  91. package/dist/reactiveQueries/graphql.d.ts +25 -0
  92. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  93. package/dist/reactiveQueries/graphql.js +18 -0
  94. package/dist/reactiveQueries/graphql.js.map +1 -0
  95. package/dist/reactiveQueries/js.d.ts +19 -0
  96. package/dist/reactiveQueries/js.d.ts.map +1 -0
  97. package/dist/reactiveQueries/js.js +13 -0
  98. package/dist/reactiveQueries/js.js.map +1 -0
  99. package/dist/reactiveQueries/sql.d.ts +31 -0
  100. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  101. package/dist/reactiveQueries/sql.js +32 -0
  102. package/dist/reactiveQueries/sql.js.map +1 -0
  103. package/dist/schema.d.ts +81 -0
  104. package/dist/schema.d.ts.map +1 -0
  105. package/dist/schema.js +46 -0
  106. package/dist/schema.js.map +1 -0
  107. package/dist/storage/in-memory/index.d.ts +15 -0
  108. package/dist/storage/in-memory/index.d.ts.map +1 -0
  109. package/dist/storage/in-memory/index.js +14 -0
  110. package/dist/storage/in-memory/index.js.map +1 -0
  111. package/dist/storage/index.d.ts +14 -0
  112. package/dist/storage/index.d.ts.map +1 -0
  113. package/dist/storage/index.js +9 -0
  114. package/dist/storage/index.js.map +1 -0
  115. package/dist/storage/tauri/index.d.ts +19 -0
  116. package/dist/storage/tauri/index.d.ts.map +1 -0
  117. package/dist/storage/tauri/index.js +38 -0
  118. package/dist/storage/tauri/index.js.map +1 -0
  119. package/dist/storage/utils/idb.d.ts +10 -0
  120. package/dist/storage/utils/idb.d.ts.map +1 -0
  121. package/dist/storage/utils/idb.js +58 -0
  122. package/dist/storage/utils/idb.js.map +1 -0
  123. package/dist/storage/web-worker/index.d.ts +27 -0
  124. package/dist/storage/web-worker/index.d.ts.map +1 -0
  125. package/dist/storage/web-worker/index.js +74 -0
  126. package/dist/storage/web-worker/index.js.map +1 -0
  127. package/dist/storage/web-worker/worker.d.ts +13 -0
  128. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  129. package/dist/storage/web-worker/worker.js +110 -0
  130. package/dist/storage/web-worker/worker.js.map +1 -0
  131. package/dist/store.d.ts +199 -0
  132. package/dist/store.d.ts.map +1 -0
  133. package/dist/store.js +603 -0
  134. package/dist/store.js.map +1 -0
  135. package/dist/util.d.ts +28 -0
  136. package/dist/util.d.ts.map +1 -0
  137. package/dist/util.js +55 -0
  138. package/dist/util.js.map +1 -0
  139. package/package.json +46 -19
  140. package/src/__tests__/react/fixture.tsx +23 -32
  141. package/src/__tests__/reactive.test.ts +3 -4
  142. package/src/effect/LiveStore.ts +22 -31
  143. package/src/events.ts +1 -1
  144. package/src/inMemoryDatabase.ts +115 -140
  145. package/src/index.ts +20 -20
  146. package/src/migrations.ts +119 -0
  147. package/src/otel.ts +0 -11
  148. package/src/react/LiveStoreProvider.tsx +24 -23
  149. package/src/react/index.ts +10 -1
  150. package/src/react/useGraphQL.ts +28 -2
  151. package/src/react/useLiveStoreComponent.ts +134 -50
  152. package/src/react/useQuery.ts +56 -0
  153. package/src/reactive.ts +6 -4
  154. package/src/reactiveQueries/base-class.ts +9 -3
  155. package/src/reactiveQueries/graphql.ts +4 -4
  156. package/src/reactiveQueries/js.ts +2 -2
  157. package/src/reactiveQueries/sql.ts +6 -6
  158. package/src/schema.ts +69 -145
  159. package/src/storage/in-memory/index.ts +21 -0
  160. package/src/storage/index.ts +27 -0
  161. package/src/{backends/tauri.ts → storage/tauri/index.ts} +14 -28
  162. package/src/storage/web-worker/index.ts +116 -0
  163. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  164. package/src/store.ts +171 -98
  165. package/src/util.ts +13 -3
  166. package/tsconfig.json +1 -3
  167. package/src/backends/base.ts +0 -67
  168. package/src/backends/index.ts +0 -98
  169. package/src/backends/noop.ts +0 -32
  170. package/src/backends/web-in-memory.ts +0 -65
  171. package/src/backends/web.ts +0 -97
  172. package/src/react/useGlobalQuery.ts +0 -40
  173. /package/src/{backends → storage}/utils/idb.ts +0 -0
@@ -15,6 +15,12 @@ export type UseLiveStoreComponentProps<TResult extends Record<string, any>, TVar
15
15
  reactDeps?: React.DependencyList
16
16
  }
17
17
 
18
+ type Variables = Record<string, any>
19
+
20
+ // TODO get rid of the query cache in favour of the new side-effect-free query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
21
+ // NOTE we're using a nested map here since we need to resolve 2 levels of object identities (query + variables)
22
+ // const queryCache = new Map<DocumentNode<any, any>, Map<Variables, LiveStoreGraphQLQuery<any, any, any>>>()
23
+
18
24
  /**
19
25
  * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
20
26
  * so we need to "cache" the fact that we've already started a span for this component.
@@ -24,7 +30,7 @@ const spanAlreadyStartedCache = new Map<string, { span: otel.Span; otelContext:
24
30
 
25
31
  // TODO 1) figure out a way to make `variables` optional if the query doesn't have any variables (probably requires positional args)
26
32
  // TODO 2) allow `.pipe` on the resulting query (possibly as a separate optional prop)
27
- export const useGraphQL = <TResult extends Record<string, any>, TVariables extends Record<string, any> = {}>({
33
+ export const useGraphQL = <TResult extends Record<string, any>, TVariables extends Variables = {}>({
28
34
  query,
29
35
  variables,
30
36
  componentKey: componentKeyConfig,
@@ -62,7 +68,27 @@ export const useGraphQL = <TResult extends Record<string, any>, TVariables exten
62
68
  )
63
69
 
64
70
  const makeLiveStoreQuery = React.useCallback(
65
- () => store.queryGraphQL(query, () => variables ?? ({} as TVariables), { componentKey, otelContext }),
71
+ () => {
72
+ return store.queryGraphQL(query, () => variables ?? ({} as TVariables), { componentKey, otelContext })
73
+
74
+ // NOTE I had to disable the caching below as still led to many problems
75
+ // We should just implement the new query definition approach instead
76
+
77
+ // const queryCacheForQuery = queryCache.get(query)
78
+ // if (queryCacheForQuery && queryCacheForQuery.has(variables)) {
79
+ // return queryCacheForQuery.get(variables)!
80
+ // }
81
+
82
+ // const newQuery = store.queryGraphQL(query, () => variables ?? ({} as TVariables), { componentKey, otelContext })
83
+
84
+ // if (queryCacheForQuery) {
85
+ // queryCacheForQuery.set(variables, newQuery)
86
+ // } else {
87
+ // queryCache.set(query, new Map([[variables, newQuery]]))
88
+ // }
89
+
90
+ // return newQuery
91
+ },
66
92
  // NOTE: we don't include the queries function passed in by the user here;
67
93
  // the reason is that we don't want to force them to memoize that function.
68
94
  // Instead, we just assume that the function always has the same contents.
@@ -1,6 +1,9 @@
1
1
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
- import { type LiteralUnion, omit, shouldNeverHappen } from '@livestore/utils'
2
+ import type { LiteralUnion, PrettifyFlat } from '@livestore/utils'
3
+ import { omit, shouldNeverHappen } from '@livestore/utils'
4
+ import { Schema } from '@livestore/utils/effect'
3
5
  import * as otel from '@opentelemetry/api'
6
+ import { SqliteAst, SqliteDsl } from 'effect-db-schema'
4
7
  import { isEqual, mapValues } from 'lodash-es'
5
8
  import type { DependencyList } from 'react'
6
9
  import React from 'react'
@@ -8,12 +11,12 @@ import { v4 as uuid } from 'uuid'
8
11
 
9
12
  import type { ComponentKey } from '../componentKey.js'
10
13
  import { labelForKey, tableNameForComponentKey } from '../componentKey.js'
11
- import type { GetAtom } from '../reactive.js'
14
+ import { migrateTable } from '../migrations.js'
12
15
  import type { LiveStoreGraphQLQuery } from '../reactiveQueries/graphql.js'
13
16
  import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
14
17
  import type { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
15
- import type { ComponentStateSchema } from '../schema.js'
16
- import type { BaseGraphQLContext, LiveStoreQuery, QueryResult, Store } from '../store.js'
18
+ import { SCHEMA_META_TABLE } from '../schema.js'
19
+ import type { BaseGraphQLContext, GetAtomResult, LiveStoreQuery, QueryResult, Store } from '../store.js'
17
20
  import type { Bindable } from '../util.js'
18
21
  import { sql } from '../util.js'
19
22
  import { useStore } from './LiveStoreContext.js'
@@ -22,20 +25,24 @@ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInp
22
25
  export interface QueryDefinitions {
23
26
  [queryName: string]: LiveStoreQuery
24
27
  }
25
- export type QueryResults<TQuery> = { [queryName in keyof TQuery]: QueryResult<TQuery[queryName]> }
28
+
29
+ export type QueryResults<TQuery> = { [queryName in keyof TQuery]: PrettifyFlat<QueryResult<TQuery[queryName]>> }
26
30
 
27
31
  export type ReactiveSQL = <TResult>(
28
- genQuery: (get: GetAtom) => string,
32
+ query: string | ((get: GetAtomResult) => string),
29
33
  queriedTables: string[],
30
34
  bindValues?: Bindable | undefined,
31
35
  ) => LiveStoreSQLQuery<TResult>
36
+
37
+ export type ReactiveJS = <TResult>(query: (get: GetAtomResult) => TResult) => LiveStoreJSQuery<TResult>
38
+
32
39
  export type ReactiveGraphQL = <
33
40
  TResult extends Record<string, any>,
34
41
  TVariables extends Record<string, any>,
35
42
  TContext extends BaseGraphQLContext,
36
43
  >(
37
44
  query: DocumentNode<TResult, TVariables>,
38
- genVariableValues: (get: GetAtom) => TVariables,
45
+ variableValues: TVariables | ((get: GetAtomResult) => TVariables),
39
46
  label?: string,
40
47
  ) => LiveStoreGraphQLQuery<TResult, TVariables, TContext>
41
48
 
@@ -48,16 +55,20 @@ type RegisterSubscription = <TQuery extends LiveStoreQuery>(
48
55
  type GenQueries<TQueries, TStateResult> = (args: {
49
56
  rxSQL: ReactiveSQL
50
57
  rxGraphQL: ReactiveGraphQL
51
- globalQueries: QueryDefinitions
58
+ rxJS: ReactiveJS
52
59
  state$: LiveStoreJSQuery<TStateResult>
53
- /** Registers a subscription */
60
+ /**
61
+ * Registers a subscription.
62
+ *
63
+ * Passed down for some manual subscribing. Use carefully.
64
+ */
54
65
  subscribe: RegisterSubscription
55
66
  isTemporaryQuery: boolean
56
67
  }) => TQueries
57
68
 
58
- export type UseLiveStoreComponentProps<TQueries, TComponentState> = {
59
- stateSchema?: ComponentStateSchema<TComponentState>
60
- queries?: GenQueries<TQueries, TComponentState>
69
+ export type UseLiveStoreComponentProps<TQueries, TStateColumns extends ComponentColumns> = {
70
+ stateSchema?: SqliteDsl.TableDefinition<string, TStateColumns>
71
+ queries?: GenQueries<TQueries, SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
61
72
  reactDeps?: React.DependencyList
62
73
  componentKey: ComponentKeyConfig
63
74
  }
@@ -72,12 +83,17 @@ export type ComponentKeyConfig = {
72
83
  id: LiteralUnion<'singleton' | '__ephemeral__', string>
73
84
  }
74
85
 
75
- type ComponentState = {
76
- /** Equivalent to `componentKey.key` */
77
- id: string
78
- [key: string]: string | number | boolean | null
86
+ // TODO enforce columns are non-nullable or have a default
87
+ export interface ComponentColumns extends SqliteDsl.Columns {
88
+ id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false>
79
89
  }
80
90
 
91
+ // type ComponentState = {
92
+ // /** Equivalent to `componentKey.key` */
93
+ // id: string
94
+ // [key: string]: string | number | boolean | null
95
+ // }
96
+
81
97
  /**
82
98
  * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
83
99
  * so we need to "cache" the fact that we've already started a span for this component.
@@ -90,23 +106,33 @@ type UseLiveStoreJsonState<TState> = <TResult>(
90
106
  parse?: (_: unknown) => TResult,
91
107
  ) => [value: TResult, setValue: (newVal: TResult | ((prevVal: TResult) => TResult)) => void]
92
108
 
109
+ export type GetStateType<TTableDef extends SqliteDsl.TableDefinition<any, any>> = SqliteDsl.FromColumns.RowDecoded<
110
+ TTableDef['columns']
111
+ >
112
+
113
+ export type GetStateTypeEncoded<TTableDef extends SqliteDsl.TableDefinition<any, any>> =
114
+ SqliteDsl.FromColumns.RowEncoded<TTableDef['columns']>
115
+
93
116
  /**
94
117
  * Create reactive queries within a component.
95
118
  * @param config.queries A function that returns a map of named reactive queries.
96
119
  * @param config.componentKey A function that returns a unique key for this component.
97
120
  * @param config.reactDeps A list of React-level dependencies that will refresh the queries.
98
121
  */
99
- export const useLiveStoreComponent = <TComponentState extends ComponentState, TQueries extends QueryDefinitions>({
122
+ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQueries extends QueryDefinitions>({
100
123
  stateSchema: stateSchema_,
101
124
  queries = () => ({}) as TQueries,
102
125
  componentKey: componentKeyConfig,
103
126
  reactDeps = [],
104
- }: UseLiveStoreComponentProps<TQueries, TComponentState>): {
127
+ }: UseLiveStoreComponentProps<TQueries, TStateColumns>): {
105
128
  queryResults: QueryResults<TQueries>
106
- state: TComponentState
107
- setState: Setters<TComponentState>
108
- useLiveStoreJsonState: UseLiveStoreJsonState<TComponentState>
129
+ state: SqliteDsl.FromColumns.RowDecoded<TStateColumns>
130
+ setState: Setters<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
131
+ useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
109
132
  } => {
133
+ type TComponentState = SqliteDsl.FromColumns.RowDecoded<TStateColumns>
134
+
135
+ // TODO validate schema to make sure each column has a default value
110
136
  // TODO we should clean up the state schema handling to remove this special handling for the `id` column
111
137
  const stateSchema = React.useMemo(
112
138
  () => (stateSchema_ ? { ...stateSchema_, columns: omit(stateSchema_.columns, 'id' as any) } : undefined),
@@ -115,7 +141,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
115
141
 
116
142
  // performance.mark('useLiveStoreComponent:start')
117
143
  const componentKey = useComponentKey(componentKeyConfig, reactDeps)
118
- const { store, globalQueries } = useStore()
144
+ const { store } = useStore()
119
145
 
120
146
  const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
121
147
 
@@ -158,14 +184,17 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
158
184
  isTemporaryQuery: boolean
159
185
  }) =>
160
186
  queries({
161
- rxSQL: <T>(genQuery: (get: GetAtom) => string, queriedTables: string[], bindValues?: Bindable) =>
162
- store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext }),
187
+ rxSQL: <T>(
188
+ genQuery: string | ((get: GetAtomResult) => string),
189
+ queriedTables: string[],
190
+ bindValues?: Bindable,
191
+ ) => store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext, componentKey }),
163
192
  rxGraphQL: <Result extends Record<string, any>, Variables extends Record<string, any>>(
164
193
  query: DocumentNode<Result, Variables>,
165
- genVariableValues: (get: GetAtom) => Variables,
194
+ genVariableValues: Variables | ((get: GetAtomResult) => Variables),
166
195
  label?: string,
167
196
  ) => store.queryGraphQL(query, genVariableValues, { componentKey, label, otelContext }),
168
- globalQueries,
197
+ rxJS: <T>(genQuery: (get: GetAtomResult) => T) => store.queryJS(genQuery, { componentKey, otelContext }),
169
198
  state$,
170
199
  subscribe: registerSubscription,
171
200
  isTemporaryQuery,
@@ -177,7 +206,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
177
206
  // This makes sense for LiveStore because the component config should be static.
178
207
  // TODO: document this and consider whether it's the right API surface.
179
208
  // eslint-disable-next-line react-hooks/exhaustive-deps
180
- [store, componentKey, globalQueries],
209
+ [store, componentKey],
181
210
  )
182
211
 
183
212
  const defaultComponentState = React.useMemo(() => {
@@ -185,11 +214,17 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
185
214
  stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
186
215
  ) as TComponentState
187
216
 
217
+ // @ts-expect-error TODO fix typing
188
218
  defaultState.id = componentKeyConfig.id
189
219
 
190
220
  return defaultState
191
221
  }, [componentKeyConfig.id, stateSchema])
192
222
 
223
+ const componentStateEffectSchema = React.useMemo(
224
+ () => (stateSchema ? SqliteDsl.structSchemaForTable(stateSchema) : Schema.any),
225
+ [stateSchema],
226
+ )
227
+
193
228
  // Step 1:
194
229
  // Synchronously create state and queries for initial render pass.
195
230
  // We do this in a temporary query context which cleans up after itself, making it idempotent
@@ -198,32 +233,56 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
198
233
  return store.otel.tracer.startActiveSpan('LiveStore:useLiveStoreComponent:initial', {}, otelContext, (span) => {
199
234
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
200
235
 
236
+ // NOTE `inTempQueryContext` automatically destroys the queries once the callback is done
201
237
  return store.inTempQueryContext(() => {
202
238
  try {
203
239
  // create state query
204
- let stateQuery: LiveStoreJSQuery<TComponentState>
240
+ let state$: LiveStoreJSQuery<TComponentState>
205
241
  if (stateSchema === undefined) {
206
242
  // TODO don't set up a query if there's no state schema (keeps the graph more clean)
207
- stateQuery = store.queryJS(() => ({}), {
243
+ state$ = store.queryJS(() => ({}), {
208
244
  componentKey,
209
245
  otelContext,
210
246
  }) as unknown as LiveStoreJSQuery<TComponentState>
211
247
  } else {
212
248
  const componentTableName = tableNameForComponentKey(componentKey)
213
249
  const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
214
- stateQuery = store
215
- .querySQL<TComponentState>(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
250
+
251
+ // TODO find a better solution for this
252
+ if (store.tableRefs[componentTableName] === undefined) {
253
+ const schemaHash = SqliteAst.hash(stateSchema.ast)
254
+ const res = store.inMemoryDB.select<{ schemaHash: number }>(
255
+ sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
256
+ )
257
+ if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
258
+ migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
259
+ }
260
+
261
+ store.tableRefs[componentTableName] = store.graph.makeRef(null, {
262
+ equal: () => false,
263
+ label: componentTableName,
264
+ meta: { liveStoreRefType: 'table' },
265
+ })
266
+ }
267
+
268
+ state$ = store
269
+ .querySQL(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
216
270
  queriedTables: [componentTableName],
217
271
  componentKey,
218
272
  label: `localState:query:${componentKeyLabel}`,
219
273
  otelContext,
220
274
  })
221
- .getFirstRow({ defaultValue: defaultComponentState })
275
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
276
+ .pipe<TComponentState>((results) =>
277
+ results.length === 1
278
+ ? Schema.parseSync(componentStateEffectSchema)(results[0]!)
279
+ : defaultComponentState,
280
+ )
222
281
  }
223
- const initialComponentState = stateQuery.results$.result
282
+ const initialComponentState = state$.results$.result
224
283
 
225
284
  const queries = generateQueries({
226
- state$: stateQuery,
285
+ state$: state$,
227
286
  otelContext,
228
287
  registerSubscription: () => {},
229
288
  isTemporaryQuery: true,
@@ -231,7 +290,11 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
231
290
  for (const [name, query] of Object.entries(queries)) {
232
291
  query.label = name
233
292
  }
234
- const initialQueryResults = mapValues(queries, (query) => query.results$.result) as QueryResults<TQueries>
293
+ const initialQueryResults = mapValues(
294
+ queries,
295
+ (query) => query.results$.result,
296
+ // TODO improve typing
297
+ ) as unknown as QueryResults<TQueries>
235
298
 
236
299
  return { initialComponentState, initialQueryResults }
237
300
  } finally {
@@ -239,7 +302,16 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
239
302
  }
240
303
  })
241
304
  })
242
- }, [store, otelContext, stateSchema, generateQueries, componentKey, componentKeyLabel, defaultComponentState])
305
+ }, [
306
+ store,
307
+ otelContext,
308
+ stateSchema,
309
+ generateQueries,
310
+ componentKey,
311
+ componentKeyLabel,
312
+ componentStateEffectSchema,
313
+ defaultComponentState,
314
+ ])
243
315
 
244
316
  // Now that we've computed the initial state synchronously,
245
317
  // we can set up our useState calls w/ a default value populated...
@@ -251,10 +323,13 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
251
323
  stateSchema === undefined
252
324
  ? {}
253
325
  : // TODO: do we have a better type for the values that can go in SQLite?
254
- mapValues(stateSchema.columns, (_, columnName) => (value: string | number) => {
326
+ mapValues(stateSchema.columns, (column, columnName) => (value: string | number) => {
255
327
  // Don't update the state if it's the same as the value already seen in the component
328
+ // @ts-expect-error TODO fix typing
256
329
  if (componentStateRef.current[columnName] === value) return
257
330
 
331
+ const encodedValue = Schema.encodeSync(column.type.codec)(value)
332
+
258
333
  if (['componentKey', 'columnNames'].includes(columnName)) {
259
334
  shouldNeverHappen(`Can't use reserved column name ${columnName}`)
260
335
  }
@@ -262,7 +337,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
262
337
  return store.applyEvent('updateComponentState', {
263
338
  componentKey,
264
339
  columnNames: [columnName],
265
- [columnName]: value,
340
+ [columnName]: encodedValue,
266
341
  })
267
342
  })
268
343
  ) as Setters<TComponentState>
@@ -270,6 +345,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
270
345
  setState.setMany = (columnValues: Partial<TComponentState>) => {
271
346
  // TODO use hashing instead
272
347
  // Don't update the state if it's the same as the value already seen in the component
348
+ // @ts-expect-error TODO fix typing
273
349
  if (Object.entries(columnValues).every(([columnName, value]) => componentStateRef.current[columnName] === value)) {
274
350
  return
275
351
  }
@@ -293,7 +369,12 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
293
369
  // create state query
294
370
  let state$: LiveStoreJSQuery<TComponentState>
295
371
  if (stateSchema === undefined) {
296
- state$ = store.queryJS(() => ({}) as TComponentState, { componentKey, otelContext })
372
+ // TODO remove this query
373
+ state$ = store.queryJS(() => ({}) as TComponentState, {
374
+ componentKey,
375
+ otelContext,
376
+ label: 'empty-component-state',
377
+ })
297
378
  } else {
298
379
  const componentTableName = tableNameForComponentKey(componentKey)
299
380
  insertRowForComponentInstance({ store, componentKey, stateSchema })
@@ -306,7 +387,10 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
306
387
  label: `localState:query:${componentKeyLabel}`,
307
388
  otelContext,
308
389
  })
309
- .getFirstRow({ defaultValue: defaultComponentState })
390
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
391
+ .pipe<TComponentState>((results) =>
392
+ results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
393
+ )
310
394
  }
311
395
 
312
396
  unsubs.push(
@@ -322,25 +406,25 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
322
406
  ),
323
407
  )
324
408
 
325
- const registerSubscription: RegisterSubscription = (query, callback, onUnsubscribe) => {
409
+ const registerSubscription: RegisterSubscription = (query$, callback, onUnsubscribe) => {
326
410
  unsubs.push(
327
411
  store.subscribe(
328
- query,
412
+ query$,
329
413
  (results) => {
330
414
  callback(results)
331
415
  },
332
416
  onUnsubscribe,
333
- { label: `useLiveStoreComponent:query:manual-subscribe:${query.label}` },
417
+ { label: `useLiveStoreComponent:query:manual-subscribe:${query$.label}` },
334
418
  ),
335
419
  )
336
420
  }
337
421
 
338
422
  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
- }
423
+
343
424
  for (const [key, query] of Object.entries(queries)) {
425
+ // Use the field name given to this query in the useQueries hook as its label
426
+ query.label = key
427
+
344
428
  unsubs.push(
345
429
  store.subscribe(
346
430
  query,
@@ -447,14 +531,14 @@ export const useComponentKey = ({ name, id }: ComponentKeyConfig, deps: Dependen
447
531
  * Create a row storing the state for a component instance, if none exists yet.
448
532
  * Initialized with default values, and keyed on the component key.
449
533
  */
450
- const insertRowForComponentInstance = <T>({
534
+ const insertRowForComponentInstance = ({
451
535
  store,
452
536
  componentKey,
453
537
  stateSchema,
454
538
  }: {
455
539
  store: Store<BaseGraphQLContext>
456
540
  componentKey: ComponentKey
457
- stateSchema: ComponentStateSchema<T>
541
+ stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
458
542
  }) => {
459
543
  const columnNames = ['id', ...Object.keys(stateSchema.columns)]
460
544
  const columnValues = columnNames.map((name) => `$${name}`).join(', ')
@@ -467,8 +551,8 @@ const insertRowForComponentInstance = <T>({
467
551
  void store.execute(
468
552
  insertQuery,
469
553
  {
470
- id: componentKey.id,
471
554
  ...mapValues(stateSchema.columns, (column) => prepareValueForSql(column.default ?? null)),
555
+ id: componentKey.id,
472
556
  },
473
557
  [tableName],
474
558
  )
@@ -0,0 +1,56 @@
1
+ import React from 'react'
2
+
3
+ import { labelForKey } from '../componentKey.js'
4
+ import type { QueryDefinition } from '../effect/LiveStore.js'
5
+ import type { LiveStoreQuery, QueryResult, Store } from '../store.js'
6
+ import { useStore } from './LiveStoreContext.js'
7
+
8
+ // TODO get rid of the query cache in favour of the new side-effect-free query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
9
+ const queryCache = new Map<QueryDefinition, LiveStoreQuery>()
10
+
11
+ export const useQuery = <Q extends LiveStoreQuery>(queryDef: (store: Store) => Q): QueryResult<Q> => {
12
+ const { store } = useStore()
13
+ const query = React.useMemo(() => {
14
+ if (queryCache.has(queryDef)) return queryCache.get(queryDef) as Q
15
+
16
+ const query = queryDef(store)
17
+ queryCache.set(queryDef, query)
18
+ return query
19
+ }, [store, queryDef])
20
+
21
+ // We know the query has a result by the time we use it; so we can synchronously populate a default state
22
+ const [value, setValue] = React.useState<QueryResult<Q>>(query.results$.result)
23
+
24
+ // Subscribe to future updates for this query
25
+ React.useEffect(() => {
26
+ return store.otel.tracer.startActiveSpan(
27
+ `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
28
+ { attributes: { label: query.label } },
29
+ query.otelContext,
30
+ (span) => {
31
+ const cancel = store.subscribe(
32
+ query,
33
+ (v) => {
34
+ // NOTE: we return a reference to the result object within LiveStore;
35
+ // this implies that app code must not mutate the results, or else
36
+ // there may be weird reactivity bugs.
37
+ return setValue(v)
38
+ },
39
+ undefined,
40
+ { label: query.label },
41
+ )
42
+ return () => {
43
+ // // NOTE destroying the whole query will also unsubscribe it
44
+ // query.destroy()
45
+
46
+ // TODO for now we'll still `cancel` manually, but we should remove this once we have some kind of
47
+ // ARC-based system
48
+ cancel()
49
+ span.end()
50
+ }
51
+ },
52
+ )
53
+ }, [query, store])
54
+
55
+ return value
56
+ }
package/src/reactive.ts CHANGED
@@ -30,6 +30,9 @@ import { isEqual, max, uniqueId } from 'lodash-es'
30
30
 
31
31
  import { BoundArray } from './bounded-collections.js'
32
32
 
33
+ const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
34
+ type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
35
+
33
36
  export type GetAtom = <T>(atom: Atom<T>) => T
34
37
 
35
38
  export type Ref<T> = {
@@ -59,7 +62,7 @@ type BaseThunk<T> = {
59
62
  equal: (a: T, b: T) => boolean
60
63
  }
61
64
 
62
- type UnevaluatedThunk<T> = BaseThunk<T> & { result: undefined }
65
+ type UnevaluatedThunk<T> = BaseThunk<T> & { result: NOT_REFRESHED_YET }
63
66
  export type Thunk<T> = BaseThunk<T> & { result: T }
64
67
 
65
68
  export type Atom<T> = Ref<T> | Thunk<T>
@@ -201,7 +204,7 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
201
204
  const thunk: UnevaluatedThunk<T> = {
202
205
  _tag: 'thunk',
203
206
  id: uniqueNodeId(),
204
- result: undefined,
207
+ result: NOT_REFRESHED_YET,
205
208
  height: 0,
206
209
  getResult,
207
210
  sub: new Set(),
@@ -342,7 +345,7 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
342
345
  this.addEdge(context, atom)
343
346
 
344
347
  const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
345
- const dependencyNotRefreshedYet = atom.result === undefined
348
+ const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
346
349
 
347
350
  if (dependencyMightBeStale || dependencyNotRefreshedYet) {
348
351
  throw new DependencyNotReadyError(
@@ -350,7 +353,6 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
350
353
  )
351
354
  }
352
355
 
353
- // TODO handle case when `atom.result` is undefined
354
356
  return atom.result
355
357
  }
356
358
 
@@ -5,13 +5,13 @@ import type { Store } from '../store.js'
5
5
 
6
6
  export type UnsubscribeQuery = () => void
7
7
 
8
- export abstract class LiveStoreQueryBase {
8
+ export abstract class LiveStoreQueryBase<TResult> {
9
9
  /** The key for the associated component */
10
10
  componentKey: ComponentKey
11
11
  /** Human-readable label for the query for debugging */
12
12
  label: string
13
13
  /** A pointer back to the store containing this query */
14
- store: Store<any>
14
+ store: Store
15
15
  /** Otel Span is started in LiveStore store but ended in this query */
16
16
  otelContext: otel.Context
17
17
 
@@ -26,7 +26,7 @@ export abstract class LiveStoreQueryBase {
26
26
  }: {
27
27
  componentKey: ComponentKey
28
28
  label: string
29
- store: Store<any>
29
+ store: Store
30
30
  otelContext: otel.Context
31
31
  }) {
32
32
  this.componentKey = componentKey
@@ -46,4 +46,10 @@ export abstract class LiveStoreQueryBase {
46
46
  unsubscribe()
47
47
  }
48
48
  }
49
+
50
+ subscribe = (
51
+ onNewValue: (value: TResult) => void,
52
+ onSubsubscribe?: () => void,
53
+ options?: { label?: string } | undefined,
54
+ ): (() => void) => this.store.subscribe(this as any, onNewValue as any, onSubsubscribe, options)
49
55
  }
@@ -2,8 +2,8 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
2
2
  import type * as otel from '@opentelemetry/api'
3
3
 
4
4
  import type { ComponentKey } from '../componentKey.js'
5
- import type { GetAtom, Thunk } from '../reactive.js'
6
- import type { BaseGraphQLContext, Store } from '../store.js'
5
+ import type { Thunk } from '../reactive.js'
6
+ import type { BaseGraphQLContext, GetAtomResult, Store } from '../store.js'
7
7
  import { LiveStoreQueryBase } from './base-class.js'
8
8
  import type { LiveStoreJSQuery } from './js.js'
9
9
 
@@ -11,7 +11,7 @@ export class LiveStoreGraphQLQuery<
11
11
  TResult extends Record<string, any>,
12
12
  VariableValues extends Record<string, any>,
13
13
  TContext extends BaseGraphQLContext,
14
- > extends LiveStoreQueryBase {
14
+ > extends LiveStoreQueryBase<TResult> {
15
15
  _tag: 'graphql' = 'graphql'
16
16
 
17
17
  /** The abstract GraphQL query */
@@ -39,7 +39,7 @@ export class LiveStoreGraphQLQuery<
39
39
  this.results$ = results$
40
40
  }
41
41
 
42
- pipe = <U>(f: (x: TResult, get: GetAtom) => U): LiveStoreJSQuery<U> =>
42
+ pipe = <U>(f: (x: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
43
43
  this.store.queryJS(
44
44
  (get) => {
45
45
  const results = get(this.results$)
@@ -5,7 +5,7 @@ import type { GetAtom, Thunk } from '../reactive.js'
5
5
  import type { Store } from '../store.js'
6
6
  import { LiveStoreQueryBase } from './base-class.js'
7
7
 
8
- export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase {
8
+ export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase<TResult> {
9
9
  _tag: 'js' = 'js'
10
10
  /** A reactive thunk representing the query results */
11
11
  results$: Thunk<TResult>
@@ -17,7 +17,7 @@ export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase {
17
17
  results$: Thunk<TResult>
18
18
  componentKey: ComponentKey
19
19
  label: string
20
- store: Store<any>
20
+ store: Store
21
21
  otelContext: otel.Context
22
22
  }) {
23
23
  super(baseProps)