@livestore/livestore 0.0.24 → 0.0.25

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 (106) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts +2 -2
  3. package/dist/QueryCache.d.ts.map +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/__tests__/react/fixture.d.ts +2 -2
  6. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  7. package/dist/__tests__/react/fixture.js +2 -2
  8. package/dist/__tests__/react/fixture.js.map +1 -1
  9. package/dist/__tests__/react/useComponentState.test.js +78 -10
  10. package/dist/__tests__/react/useComponentState.test.js.map +1 -1
  11. package/dist/__tests__/react/useQuery.test.js +35 -10
  12. package/dist/__tests__/react/useQuery.test.js.map +1 -1
  13. package/dist/__tests__/reactive.test.js +51 -0
  14. package/dist/__tests__/reactive.test.js.map +1 -1
  15. package/dist/__tests__/reactiveQueries/sql.test.js +2 -9
  16. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
  17. package/dist/effect/LiveStore.js +1 -1
  18. package/dist/effect/LiveStore.js.map +1 -1
  19. package/dist/inMemoryDatabase.d.ts +3 -3
  20. package/dist/inMemoryDatabase.d.ts.map +1 -1
  21. package/dist/inMemoryDatabase.js +3 -3
  22. package/dist/inMemoryDatabase.js.map +1 -1
  23. package/dist/index.d.ts +4 -4
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/migrations.js.map +1 -1
  28. package/dist/react/useComponentState.d.ts +3 -3
  29. package/dist/react/useComponentState.d.ts.map +1 -1
  30. package/dist/react/useComponentState.js +43 -57
  31. package/dist/react/useComponentState.js.map +1 -1
  32. package/dist/react/useQuery.js +2 -2
  33. package/dist/react/useQuery.js.map +1 -1
  34. package/dist/react/utils/stack-info.d.ts.map +1 -1
  35. package/dist/react/utils/stack-info.js +0 -1
  36. package/dist/react/utils/stack-info.js.map +1 -1
  37. package/dist/reactive.d.ts +37 -28
  38. package/dist/reactive.d.ts.map +1 -1
  39. package/dist/reactive.js +44 -19
  40. package/dist/reactive.js.map +1 -1
  41. package/dist/reactiveQueries/base-class.d.ts +4 -4
  42. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  43. package/dist/reactiveQueries/base-class.js +2 -1
  44. package/dist/reactiveQueries/base-class.js.map +1 -1
  45. package/dist/reactiveQueries/js.d.ts +6 -1
  46. package/dist/reactiveQueries/js.d.ts.map +1 -1
  47. package/dist/reactiveQueries/js.js.map +1 -1
  48. package/dist/reactiveQueries/sql.d.ts +2 -2
  49. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  50. package/dist/reactiveQueries/sql.js +1 -1
  51. package/dist/reactiveQueries/sql.js.map +1 -1
  52. package/dist/store.d.ts +2 -11
  53. package/dist/store.d.ts.map +1 -1
  54. package/dist/store.js +6 -11
  55. package/dist/store.js.map +1 -1
  56. package/package.json +16 -13
  57. package/src/QueryCache.ts +2 -2
  58. package/src/__tests__/react/fixture.tsx +3 -3
  59. package/src/__tests__/react/useComponentState.test.tsx +116 -10
  60. package/src/__tests__/react/useQuery.test.tsx +54 -12
  61. package/src/__tests__/reactive.test.ts +71 -0
  62. package/src/__tests__/reactiveQueries/sql.test.ts +2 -9
  63. package/src/effect/LiveStore.ts +1 -1
  64. package/src/inMemoryDatabase.ts +8 -8
  65. package/src/index.ts +4 -12
  66. package/src/migrations.ts +2 -2
  67. package/src/react/useComponentState.ts +53 -72
  68. package/src/react/useQuery.ts +2 -2
  69. package/src/react/utils/stack-info.ts +0 -1
  70. package/src/reactive.ts +80 -64
  71. package/src/reactiveQueries/base-class.ts +6 -8
  72. package/src/reactiveQueries/js.ts +6 -1
  73. package/src/reactiveQueries/sql.ts +3 -3
  74. package/src/store.ts +12 -24
  75. package/dist/__tests__/react/useLQuery.test.d.ts +0 -2
  76. package/dist/__tests__/react/useLQuery.test.d.ts.map +0 -1
  77. package/dist/__tests__/react/useLQuery.test.js +0 -38
  78. package/dist/__tests__/react/useLQuery.test.js.map +0 -1
  79. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +0 -2
  80. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +0 -1
  81. package/dist/__tests__/react/useLiveStoreComponent.test.js +0 -73
  82. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +0 -1
  83. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +0 -2
  84. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +0 -1
  85. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +0 -38
  86. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +0 -1
  87. package/dist/react/useGlobalQuery.d.ts +0 -3
  88. package/dist/react/useGlobalQuery.d.ts.map +0 -1
  89. package/dist/react/useGlobalQuery.js +0 -26
  90. package/dist/react/useGlobalQuery.js.map +0 -1
  91. package/dist/react/useGraphQL.d.ts +0 -13
  92. package/dist/react/useGraphQL.d.ts.map +0 -1
  93. package/dist/react/useGraphQL.js +0 -87
  94. package/dist/react/useGraphQL.js.map +0 -1
  95. package/dist/react/useLiveStoreComponent.d.ts +0 -75
  96. package/dist/react/useLiveStoreComponent.d.ts.map +0 -1
  97. package/dist/react/useLiveStoreComponent.js +0 -361
  98. package/dist/react/useLiveStoreComponent.js.map +0 -1
  99. package/dist/react/utils/extractNamesFromStackTrace.d.ts +0 -3
  100. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +0 -1
  101. package/dist/react/utils/extractNamesFromStackTrace.js +0 -40
  102. package/dist/react/utils/extractNamesFromStackTrace.js.map +0 -1
  103. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +0 -7
  104. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +0 -1
  105. package/dist/react/utils/extractStackInfoFromStackTrace.js +0 -40
  106. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +0 -1
@@ -11,7 +11,7 @@ import { v4 as uuid } from 'uuid'
11
11
  import type { ComponentKey } from '../componentKey.js'
12
12
  import { labelForKey, tableNameForComponentKey } from '../componentKey.js'
13
13
  import { migrateTable } from '../migrations.js'
14
- import { LiveStoreJSQuery } from '../reactiveQueries/js.js'
14
+ import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
15
15
  import { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
16
16
  import { SCHEMA_META_TABLE } from '../schema.js'
17
17
  import type { BaseGraphQLContext, LiveStoreQuery, Store } from '../store.js'
@@ -25,9 +25,9 @@ export interface QueryDefinitions {
25
25
  }
26
26
 
27
27
  export type UseComponentStateProps<TStateColumns extends ComponentColumns> = {
28
- schema?: SqliteDsl.TableDefinition<string, TStateColumns>
29
- reactDeps?: React.DependencyList
28
+ schema: SqliteDsl.TableDefinition<string, TStateColumns>
30
29
  componentKey: ComponentKeyConfig
30
+ reactDeps?: React.DependencyList
31
31
  }
32
32
 
33
33
  export type ComponentKeyConfig = {
@@ -91,7 +91,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
91
91
  // TODO validate schema to make sure each column has a default value
92
92
  // TODO we should clean up the state schema handling to remove this special handling for the `id` column
93
93
  const stateSchema = React.useMemo(
94
- () => (stateSchema_ ? { ...stateSchema_, columns: omit(stateSchema_.columns, 'id' as any) } : undefined),
94
+ () => ({ ...stateSchema_, columns: omit(stateSchema_.columns, 'id' as any) }),
95
95
  [stateSchema_],
96
96
  )
97
97
 
@@ -135,9 +135,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
135
135
  )
136
136
 
137
137
  const defaultComponentState = React.useMemo(() => {
138
- const defaultState = (
139
- stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
140
- ) as TComponentState
138
+ const defaultState = mapValues(stateSchema.columns, (c) => c.default) as TComponentState
141
139
 
142
140
  // @ts-expect-error TODO fix typing
143
141
  defaultState.id = componentKeyConfig.id
@@ -145,54 +143,43 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
145
143
  return defaultState
146
144
  }, [componentKeyConfig.id, stateSchema])
147
145
 
148
- const componentStateEffectSchema = React.useMemo(
149
- () => (stateSchema ? SqliteDsl.structSchemaForTable(stateSchema) : Schema.any),
150
- [stateSchema],
151
- )
146
+ const componentStateEffectSchema = React.useMemo(() => SqliteDsl.structSchemaForTable(stateSchema), [stateSchema])
152
147
 
148
+ // create state query
153
149
  const state$ = React.useMemo(() => {
154
- // create state query
155
- if (stateSchema === undefined) {
156
- // TODO don't set up a query if there's no state schema (keeps the graph more clean)
157
- return new LiveStoreJSQuery({
158
- fn: () => ({}) as TComponentState,
159
- label: 'empty-component-state',
160
- // otelContext,
161
- // otelTracer: store.otel.tracer,
162
- })
163
- } else {
164
- const componentTableName = tableNameForComponentKey(componentKey)
165
- const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
166
-
167
- // TODO find a better solution for this
168
- if (store.tableRefs[componentTableName] === undefined) {
169
- const schemaHash = SqliteAst.hash(stateSchema.ast)
170
- const res = store.inMemoryDB.select<{ schemaHash: number }>(
171
- sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
172
- )
173
- if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
174
- migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
175
- }
176
-
177
- store.tableRefs[componentTableName] = store.graph.makeRef(null, {
178
- equal: () => false,
179
- label: componentTableName,
180
- meta: { liveStoreRefType: 'table' },
181
- })
150
+ const componentTableName = tableNameForComponentKey(componentKey)
151
+ const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
152
+
153
+ // TODO find a better solution for this
154
+ if (store.tableRefs[componentTableName] === undefined) {
155
+ const schemaHash = SqliteAst.hash(stateSchema.ast)
156
+ const res = store.inMemoryDB.select<{ schemaHash: number }>(
157
+ sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
158
+ )
159
+ if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
160
+ migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
182
161
  }
183
162
 
184
- return (
185
- new LiveStoreSQLQuery({
186
- label: `localState:query:${componentKeyLabel}`,
187
- genQueryString: () => sql`select * from ${componentTableName} ${whereClause} limit 1`,
188
- queriedTables: [componentTableName],
189
- })
190
- // TODO consider to instead of just returning the default value, to write the default component state to the DB
191
- .pipe<TComponentState>((results) =>
192
- results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
193
- )
194
- )
163
+ store.tableRefs[componentTableName] = store.graph.makeRef(null, {
164
+ equal: () => false,
165
+ label: componentTableName,
166
+ meta: { liveStoreRefType: 'table' },
167
+ })
195
168
  }
169
+
170
+ return (
171
+ new LiveStoreSQLQuery({
172
+ label: `localState:query:${componentKeyLabel}`,
173
+ genQueryString: () => sql`select * from ${componentTableName} ${whereClause} limit 1`,
174
+ queriedTables: new Set([componentTableName]),
175
+ })
176
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
177
+ .pipe<TComponentState>((results) =>
178
+ results.length === 1
179
+ ? (Schema.parseSync(componentStateEffectSchema)(results[0]!) as TComponentState)
180
+ : defaultComponentState,
181
+ )
182
+ )
196
183
  }, [
197
184
  componentKey,
198
185
  componentKeyLabel,
@@ -214,28 +201,24 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
214
201
  // we can set up our useState calls w/ a default value populated...
215
202
  const [componentStateRef, setComponentState_] = useStateRefWithReactiveInput<TComponentState>(initialComponentState)
216
203
 
217
- const setState = (
218
- stateSchema === undefined
219
- ? {}
220
- : // TODO: do we have a better type for the values that can go in SQLite?
221
- mapValues(stateSchema.columns, (column, columnName) => (value: string | number) => {
222
- // Don't update the state if it's the same as the value already seen in the component
223
- // @ts-expect-error TODO fix typing
224
- if (componentStateRef.current[columnName] === value) return
204
+ const setState = // TODO: do we have a better type for the values that can go in SQLite?
205
+ mapValues(stateSchema.columns, (column, columnName) => (value: string | number) => {
206
+ // Don't update the state if it's the same as the value already seen in the component
207
+ // @ts-expect-error TODO fix typing
208
+ if (componentStateRef.current[columnName] === value) return
225
209
 
226
- const encodedValue = Schema.encodeSync(column.type.codec)(value)
210
+ const encodedValue = Schema.encodeSync(column.type.codec)(value)
227
211
 
228
- if (['componentKey', 'columnNames'].includes(columnName)) {
229
- shouldNeverHappen(`Can't use reserved column name ${columnName}`)
230
- }
212
+ if (['componentKey', 'columnNames'].includes(columnName)) {
213
+ shouldNeverHappen(`Can't use reserved column name ${columnName}`)
214
+ }
231
215
 
232
- return store.applyEvent('updateComponentState', {
233
- componentKey,
234
- columnNames: [columnName],
235
- [columnName]: encodedValue,
236
- })
237
- })
238
- ) as Setters<TComponentState>
216
+ return store.applyEvent('updateComponentState', {
217
+ componentKey,
218
+ columnNames: [columnName],
219
+ [columnName]: encodedValue,
220
+ })
221
+ }) as Setters<TComponentState>
239
222
 
240
223
  setState.setMany = (columnValues: Partial<TComponentState>) => {
241
224
  // TODO use hashing instead
@@ -261,9 +244,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
261
244
  const unsubs: (() => void)[] = []
262
245
 
263
246
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
264
- if (stateSchema !== undefined) {
265
- insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
266
- }
247
+ insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
267
248
 
268
249
  state$.activeSubscriptions.add(stackInfo)
269
250
 
@@ -67,7 +67,7 @@ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
67
67
  // Subscribe to future updates for this query
68
68
  React.useEffect(() => {
69
69
  query.activeSubscriptions.add(stackInfo)
70
- const unsub = store.subscribe(
70
+ const unsubFromStore = store.subscribe(
71
71
  query,
72
72
  (newValue) => {
73
73
  // NOTE: we return a reference to the result object within LiveStore;
@@ -82,7 +82,7 @@ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
82
82
  )
83
83
  return () => {
84
84
  query.activeSubscriptions.delete(stackInfo)
85
- unsub()
85
+ unsubFromStore()
86
86
  }
87
87
  }, [stackInfo, query, setValue, store, valueRef, otelContext, span])
88
88
 
@@ -57,7 +57,6 @@ export const useStackInfo = (): StackInfo =>
57
57
  Error.stackTraceLimit = 10
58
58
  // eslint-disable-next-line unicorn/error-message
59
59
  const stack = new Error().stack!
60
- console.log('stack', stack)
61
60
  Error.stackTraceLimit = originalStackLimit
62
61
  return extractStackInfoFromStackTrace(stack)
63
62
  }, [])
package/src/reactive.ts CHANGED
@@ -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,7 +1,7 @@
1
1
  import type * as otel from '@opentelemetry/api'
2
2
 
3
3
  import type { StackInfo } from '../react/utils/stack-info.js'
4
- import type { Atom, GetAtom, RefreshReasonWithGenericReasons, Thunk } from '../reactive.js'
4
+ import { type Atom, type GetAtom, throwContextNotSetError, type Thunk } from '../reactive.js'
5
5
  import type { RefreshReason } from '../store.js'
6
6
  import { type DbContext, dbGraph } from './graph.js'
7
7
  import type { LiveStoreJSQuery } from './js.js'
@@ -18,7 +18,7 @@ export interface ILiveStoreQuery<TResult> {
18
18
 
19
19
  label: string
20
20
 
21
- run: (otelContext?: otel.Context, debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>) => TResult
21
+ run: (otelContext?: otel.Context, debugRefreshReason?: RefreshReason) => TResult
22
22
 
23
23
  destroy(): void
24
24
 
@@ -41,13 +41,10 @@ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TRe
41
41
 
42
42
  abstract destroy: () => void
43
43
 
44
- run = (otelContext?: otel.Context, debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>): TResult =>
44
+ run = (otelContext?: otel.Context, debugRefreshReason?: RefreshReason): TResult =>
45
45
  this.results$.computeResult(otelContext, debugRefreshReason)
46
46
 
47
- runAndDestroy = (
48
- otelContext?: otel.Context,
49
- debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>,
50
- ): TResult => {
47
+ runAndDestroy = (otelContext?: otel.Context, debugRefreshReason?: RefreshReason): TResult => {
51
48
  const result = this.run(otelContext, debugRefreshReason)
52
49
  this.destroy()
53
50
  return result
@@ -57,7 +54,8 @@ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TRe
57
54
  onNewValue: (value: TResult) => void,
58
55
  onUnsubsubscribe?: () => void,
59
56
  options?: { label?: string; otelContext?: otel.Context } | undefined,
60
- ): (() => void) => dbGraph.context!.store.subscribe(this, onNewValue, onUnsubsubscribe, options)
57
+ ): (() => void) =>
58
+ dbGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ?? throwContextNotSetError()
61
59
  }
62
60
 
63
61
  export type GetAtomResult = <T>(atom: Atom<T, any, RefreshReason> | LiveStoreJSQuery<T>) => T
@@ -18,7 +18,12 @@ export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase<TResult> {
18
18
 
19
19
  label: string
20
20
 
21
- /** Currently only used for "nested destruction" of piped queries */
21
+ /**
22
+ * Currently only used for "nested destruction" of piped queries
23
+ *
24
+ * i.e. when doing something like `const q = querySQL(...).pipe(...)`
25
+ * we need to also destory the SQL query when the JS query `q` is destroyed
26
+ */
22
27
  private onDestroy: (() => void) | undefined
23
28
 
24
29
  constructor({
@@ -19,7 +19,7 @@ export const querySQL = <Row>(
19
19
  *
20
20
  * NOTE In the future we want to do this automatically at build time
21
21
  */
22
- queriedTables?: ReadonlyArray<string>
22
+ queriedTables?: Set<string>
23
23
  bindValues?: Bindable
24
24
  label?: string
25
25
  },
@@ -51,7 +51,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
51
51
  }: {
52
52
  label?: string
53
53
  genQueryString: string | ((get: GetAtomResult) => string)
54
- queriedTables?: ReadonlyArray<string>
54
+ queriedTables?: Set<string>
55
55
  bindValues?: Bindable
56
56
  }) {
57
57
  super()
@@ -150,7 +150,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
150
150
  if (results.length === 0 && args?.defaultValue === undefined) {
151
151
  // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
152
152
  const queryLabel = this.label
153
- throw new Error(`Expected query ${queryLabel} to return at least one result`)
153
+ return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
154
154
  }
155
155
  return results[0] ?? args!.defaultValue!
156
156
  },