@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.d.ts +2 -2
- package/dist/QueryCache.d.ts.map +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/__tests__/react/fixture.d.ts +2 -2
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +2 -2
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/useComponentState.test.js +78 -10
- package/dist/__tests__/react/useComponentState.test.js.map +1 -1
- package/dist/__tests__/react/useQuery.test.js +35 -10
- package/dist/__tests__/react/useQuery.test.js.map +1 -1
- package/dist/__tests__/reactive.test.js +51 -0
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/__tests__/reactiveQueries/sql.test.js +2 -9
- package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
- package/dist/effect/LiveStore.js +1 -1
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/inMemoryDatabase.d.ts +3 -3
- package/dist/inMemoryDatabase.d.ts.map +1 -1
- package/dist/inMemoryDatabase.js +3 -3
- package/dist/inMemoryDatabase.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/migrations.js.map +1 -1
- package/dist/react/useComponentState.d.ts +3 -3
- package/dist/react/useComponentState.d.ts.map +1 -1
- package/dist/react/useComponentState.js +43 -57
- package/dist/react/useComponentState.js.map +1 -1
- package/dist/react/useQuery.js +2 -2
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/utils/stack-info.d.ts.map +1 -1
- package/dist/react/utils/stack-info.js +0 -1
- package/dist/react/utils/stack-info.js.map +1 -1
- package/dist/reactive.d.ts +37 -28
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +44 -19
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +4 -4
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +2 -1
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +6 -1
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +2 -2
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +1 -1
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/store.d.ts +2 -11
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -11
- package/dist/store.js.map +1 -1
- package/package.json +16 -13
- package/src/QueryCache.ts +2 -2
- package/src/__tests__/react/fixture.tsx +3 -3
- package/src/__tests__/react/useComponentState.test.tsx +116 -10
- package/src/__tests__/react/useQuery.test.tsx +54 -12
- package/src/__tests__/reactive.test.ts +71 -0
- package/src/__tests__/reactiveQueries/sql.test.ts +2 -9
- package/src/effect/LiveStore.ts +1 -1
- package/src/inMemoryDatabase.ts +8 -8
- package/src/index.ts +4 -12
- package/src/migrations.ts +2 -2
- package/src/react/useComponentState.ts +53 -72
- package/src/react/useQuery.ts +2 -2
- package/src/react/utils/stack-info.ts +0 -1
- package/src/reactive.ts +80 -64
- package/src/reactiveQueries/base-class.ts +6 -8
- package/src/reactiveQueries/js.ts +6 -1
- package/src/reactiveQueries/sql.ts +3 -3
- package/src/store.ts +12 -24
- package/dist/__tests__/react/useLQuery.test.d.ts +0 -2
- package/dist/__tests__/react/useLQuery.test.d.ts.map +0 -1
- package/dist/__tests__/react/useLQuery.test.js +0 -38
- package/dist/__tests__/react/useLQuery.test.js.map +0 -1
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +0 -2
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +0 -1
- package/dist/__tests__/react/useLiveStoreComponent.test.js +0 -73
- package/dist/__tests__/react/useLiveStoreComponent.test.js.map +0 -1
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +0 -2
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +0 -1
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +0 -38
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +0 -1
- package/dist/react/useGlobalQuery.d.ts +0 -3
- package/dist/react/useGlobalQuery.d.ts.map +0 -1
- package/dist/react/useGlobalQuery.js +0 -26
- package/dist/react/useGlobalQuery.js.map +0 -1
- package/dist/react/useGraphQL.d.ts +0 -13
- package/dist/react/useGraphQL.d.ts.map +0 -1
- package/dist/react/useGraphQL.js +0 -87
- package/dist/react/useGraphQL.js.map +0 -1
- package/dist/react/useLiveStoreComponent.d.ts +0 -75
- package/dist/react/useLiveStoreComponent.d.ts.map +0 -1
- package/dist/react/useLiveStoreComponent.js +0 -361
- package/dist/react/useLiveStoreComponent.js.map +0 -1
- package/dist/react/utils/extractNamesFromStackTrace.d.ts +0 -3
- package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +0 -1
- package/dist/react/utils/extractNamesFromStackTrace.js +0 -40
- package/dist/react/utils/extractNamesFromStackTrace.js.map +0 -1
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +0 -7
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +0 -1
- package/dist/react/utils/extractStackInfoFromStackTrace.js +0 -40
- 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
|
|
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
|
-
() => (
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
210
|
+
const encodedValue = Schema.encodeSync(column.type.codec)(value)
|
|
227
211
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
212
|
+
if (['componentKey', 'columnNames'].includes(columnName)) {
|
|
213
|
+
shouldNeverHappen(`Can't use reserved column name ${columnName}`)
|
|
214
|
+
}
|
|
231
215
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
265
|
-
insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
|
|
266
|
-
}
|
|
247
|
+
insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
|
|
267
248
|
|
|
268
249
|
state$.activeSubscriptions.add(stackInfo)
|
|
269
250
|
|
package/src/react/useQuery.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
161
|
+
debugRefreshInfos: BoundArray<RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo>> = new BoundArray(5000)
|
|
162
|
+
|
|
163
|
+
private currentDebugRefresh:
|
|
164
|
+
| { refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]; startMs: DOMHighResTimeStamp }
|
|
165
|
+
| undefined
|
|
170
166
|
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
|
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
|
-
|
|
450
|
+
getSnapshot = (): ReactiveGraphSnapshot => ({
|
|
430
451
|
atoms: Array.from(this.atoms).map(serializeAtom),
|
|
431
|
-
|
|
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
|
|
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?:
|
|
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?:
|
|
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) =>
|
|
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
|
-
/**
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
153
|
+
return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
|
|
154
154
|
}
|
|
155
155
|
return results[0] ?? args!.defaultValue!
|
|
156
156
|
},
|