@livestore/livestore 0.0.21 → 0.0.22
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/README.md +14 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +0 -2
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/useQuery.test.js +1 -1
- package/dist/__tests__/react/useQuery.test.js.map +1 -1
- package/dist/__tests__/react/utils/stack-info.test.d.ts +2 -0
- package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
- package/dist/__tests__/react/utils/stack-info.test.js +43 -0
- package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
- package/dist/__tests__/reactive.test.js +13 -1
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/__tests__/reactiveQueries/sql.test.js +3 -3
- package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
- package/dist/inMemoryDatabase.d.ts +2 -1
- package/dist/inMemoryDatabase.d.ts.map +1 -1
- package/dist/inMemoryDatabase.js +3 -2
- package/dist/inMemoryDatabase.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/useComponentState.d.ts.map +1 -1
- package/dist/react/useComponentState.js +19 -27
- package/dist/react/useComponentState.js.map +1 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +46 -26
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js +2 -0
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/react/utils/stack-info.d.ts +11 -0
- package/dist/react/utils/stack-info.d.ts.map +1 -0
- package/dist/react/utils/stack-info.js +49 -0
- package/dist/react/utils/stack-info.js.map +1 -0
- package/dist/reactive.d.ts +33 -43
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +66 -255
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +15 -13
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +5 -8
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +4 -3
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +29 -34
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +2 -1
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +8 -9
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +11 -5
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +31 -34
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/store.d.ts +26 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +41 -255
- package/dist/store.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/react/fixture.tsx +0 -3
- package/src/__tests__/react/useQuery.test.tsx +1 -1
- package/src/__tests__/react/utils/{extractStackInfoFromStackTrace.test.ts → stack-info.test.ts} +25 -20
- package/src/__tests__/reactive.test.ts +20 -1
- package/src/__tests__/reactiveQueries/sql.test.ts +3 -3
- package/src/inMemoryDatabase.ts +9 -6
- package/src/react/index.ts +1 -0
- package/src/react/useComponentState.ts +25 -30
- package/src/react/useQuery.ts +66 -34
- package/src/react/useTemporaryQuery.ts +2 -0
- package/src/react/utils/{extractStackInfoFromStackTrace.ts → stack-info.ts} +21 -5
- package/src/reactive.ts +148 -339
- package/src/reactiveQueries/base-class.ts +23 -22
- package/src/reactiveQueries/graphql.ts +34 -36
- package/src/reactiveQueries/js.ts +14 -10
- package/src/reactiveQueries/sql.ts +55 -48
- package/src/store.ts +70 -305
package/src/reactive.ts
CHANGED
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
25
25
|
|
|
26
26
|
import type { PrettifyFlat } from '@livestore/utils'
|
|
27
|
-
import { pick
|
|
27
|
+
import { pick } from '@livestore/utils'
|
|
28
28
|
import type * as otel from '@opentelemetry/api'
|
|
29
|
-
import { isEqual,
|
|
29
|
+
import { isEqual, uniqueId } from 'lodash-es'
|
|
30
30
|
|
|
31
31
|
import { BoundArray } from './bounded-collections.js'
|
|
32
32
|
// import { getDurationMsFromSpan } from './otel.js'
|
|
@@ -34,32 +34,33 @@ import { BoundArray } from './bounded-collections.js'
|
|
|
34
34
|
export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
|
|
35
35
|
export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
|
|
36
36
|
|
|
37
|
-
export type GetAtom = <T>(atom: Atom<T, any>, otelContext?: otel.Context) => T
|
|
37
|
+
export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context) => T
|
|
38
38
|
|
|
39
|
-
export type Ref<T> = {
|
|
39
|
+
export type Ref<T, TContext, TDebugRefreshReason extends Taggable> = {
|
|
40
40
|
_tag: 'ref'
|
|
41
41
|
id: string
|
|
42
42
|
isDirty: false
|
|
43
43
|
previousResult: T
|
|
44
|
-
height: 0
|
|
45
44
|
computeResult: () => T
|
|
46
|
-
sub: Set<Atom<any,
|
|
47
|
-
super: Set<Atom<any,
|
|
45
|
+
sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
|
|
46
|
+
super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
|
|
48
47
|
label?: string
|
|
49
48
|
/** Container for meta information (e.g. the LiveStore Store) */
|
|
50
49
|
meta?: any
|
|
51
50
|
equal: (a: T, b: T) => boolean
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
type
|
|
53
|
+
export type Thunk<TResult, TContext, TDebugRefreshReason extends Taggable> = {
|
|
55
54
|
_tag: 'thunk'
|
|
56
55
|
id: string
|
|
57
56
|
isDirty: boolean
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
computeResult: (
|
|
58
|
+
otelContext?: otel.Context,
|
|
59
|
+
debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>,
|
|
60
|
+
) => TResult
|
|
60
61
|
previousResult: TResult | NOT_REFRESHED_YET
|
|
61
|
-
sub: Set<Atom<any, TContext>>
|
|
62
|
-
super: Set<Atom<any, TContext> | Effect>
|
|
62
|
+
sub: Set<Atom<any, TContext, TDebugRefreshReason>>
|
|
63
|
+
super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
|
|
63
64
|
label?: string
|
|
64
65
|
/** Container for meta information (e.g. the LiveStore Store) */
|
|
65
66
|
meta?: any
|
|
@@ -69,34 +70,37 @@ type BaseThunk<TResult, TContext> = {
|
|
|
69
70
|
__getResult: any
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
type
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// & { result: T }
|
|
76
|
-
|
|
77
|
-
export type Atom<T, TContext> = Ref<T> | Thunk<T, TContext>
|
|
73
|
+
export type Atom<T, TContext, TDebugRefreshReason extends Taggable> =
|
|
74
|
+
| Ref<T, TContext, TDebugRefreshReason>
|
|
75
|
+
| Thunk<T, TContext, TDebugRefreshReason>
|
|
78
76
|
|
|
79
77
|
export type Effect = {
|
|
80
78
|
_tag: 'effect'
|
|
81
79
|
id: string
|
|
82
80
|
doEffect: (otelContext?: otel.Context) => void
|
|
83
|
-
sub: Set<Atom<any, TODO>>
|
|
81
|
+
sub: Set<Atom<any, TODO, TODO>>
|
|
82
|
+
label?: string
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
export type Taggable<T extends string = string> = { _tag: T }
|
|
87
86
|
|
|
87
|
+
export type DebugThunkInfo<T extends string = string> = {
|
|
88
|
+
_tag: T
|
|
89
|
+
durationMs: number
|
|
90
|
+
}
|
|
91
|
+
|
|
88
92
|
export type ReactiveGraphOptions = {
|
|
89
93
|
effectsWrapper?: (runEffects: () => void) => void
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
export type AtomDebugInfo<TDebugThunkInfo extends
|
|
96
|
+
export type AtomDebugInfo<TDebugThunkInfo extends DebugThunkInfo> = {
|
|
93
97
|
atom: SerializedAtom
|
|
94
98
|
resultChanged: boolean
|
|
95
|
-
durationMs: number
|
|
96
99
|
debugInfo: TDebugThunkInfo
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
// TODO possibly find a better name for "refresh"
|
|
103
|
+
export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends DebugThunkInfo> = {
|
|
100
104
|
/** Currently only used for easier handling in React (e.g. as key) */
|
|
101
105
|
id: string
|
|
102
106
|
reason: TDebugRefreshReason
|
|
@@ -114,10 +118,6 @@ export type RefreshReasonWithGenericReasons<T extends Taggable> =
|
|
|
114
118
|
_tag: 'makeThunk'
|
|
115
119
|
label?: string
|
|
116
120
|
}
|
|
117
|
-
| {
|
|
118
|
-
_tag: 'makeEffect'
|
|
119
|
-
label?: string
|
|
120
|
-
}
|
|
121
121
|
| { _tag: 'unknown' }
|
|
122
122
|
|
|
123
123
|
export const unknownRefreshReason = () => {
|
|
@@ -127,7 +127,7 @@ export const unknownRefreshReason = () => {
|
|
|
127
127
|
|
|
128
128
|
export type SerializedAtom = Readonly<
|
|
129
129
|
PrettifyFlat<
|
|
130
|
-
Pick<Atom<unknown,
|
|
130
|
+
Pick<Atom<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta'> & {
|
|
131
131
|
sub: string[]
|
|
132
132
|
super: string[]
|
|
133
133
|
}
|
|
@@ -146,16 +146,20 @@ type ReactiveGraphSnapshot = {
|
|
|
146
146
|
const uniqueNodeId = () => uniqueId('node-')
|
|
147
147
|
const uniqueRefreshInfoId = () => uniqueId('refresh-info-')
|
|
148
148
|
|
|
149
|
-
const serializeAtom = (atom: Atom<any,
|
|
150
|
-
...pick(atom, ['_tag', '
|
|
149
|
+
const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
|
|
150
|
+
...pick(atom, ['_tag', 'id', 'label', 'meta']),
|
|
151
151
|
sub: Array.from(atom.sub).map((a) => a.id),
|
|
152
152
|
super: Array.from(atom.super).map((a) => a.id),
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
// const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
|
|
156
156
|
|
|
157
|
-
export class ReactiveGraph<
|
|
158
|
-
|
|
157
|
+
export class ReactiveGraph<
|
|
158
|
+
TDebugRefreshReason extends Taggable,
|
|
159
|
+
TDebugThunkInfo extends DebugThunkInfo,
|
|
160
|
+
TContext = {},
|
|
161
|
+
> {
|
|
162
|
+
readonly atoms: Set<Atom<any, TContext, TDebugRefreshReason>> = new Set()
|
|
159
163
|
effectsWrapper: (runEffects: () => void) => void
|
|
160
164
|
|
|
161
165
|
context: TContext | undefined
|
|
@@ -164,17 +168,21 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
164
168
|
RefreshDebugInfo<RefreshReasonWithGenericReasons<TDebugRefreshReason>, TDebugThunkInfo>
|
|
165
169
|
> = new BoundArray(5000)
|
|
166
170
|
|
|
171
|
+
currentDebugRefresh: { refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]; startMs: DOMHighResTimeStamp } | undefined
|
|
172
|
+
|
|
167
173
|
constructor(options: ReactiveGraphOptions) {
|
|
168
174
|
this.effectsWrapper = options?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
makeRef<T>(
|
|
172
|
-
|
|
177
|
+
makeRef<T>(
|
|
178
|
+
val: T,
|
|
179
|
+
options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean },
|
|
180
|
+
): Ref<T, TContext, TDebugRefreshReason> {
|
|
181
|
+
const ref: Ref<T, TContext, TDebugRefreshReason> = {
|
|
173
182
|
_tag: 'ref',
|
|
174
183
|
id: uniqueNodeId(),
|
|
175
184
|
isDirty: false,
|
|
176
185
|
previousResult: val,
|
|
177
|
-
height: 0,
|
|
178
186
|
computeResult: () => ref.previousResult,
|
|
179
187
|
sub: new Set(),
|
|
180
188
|
super: new Set(),
|
|
@@ -189,9 +197,9 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
makeThunk<T>(
|
|
192
|
-
|
|
200
|
+
getResult: (
|
|
193
201
|
get: GetAtom,
|
|
194
|
-
|
|
202
|
+
setDebugInfo: (debugInfo: TDebugThunkInfo) => void,
|
|
195
203
|
ctx: TContext,
|
|
196
204
|
otelContext: otel.Context | undefined,
|
|
197
205
|
) => T,
|
|
@@ -201,67 +209,73 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
201
209
|
meta?: any
|
|
202
210
|
equal?: (a: T, b: T) => boolean
|
|
203
211
|
/** Debug info for initializing the thunk (i.e. running it the first time) */
|
|
204
|
-
debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
212
|
+
// debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
205
213
|
}
|
|
206
214
|
| undefined,
|
|
207
|
-
): Thunk<T, TContext> {
|
|
208
|
-
|
|
209
|
-
// const getAtom = (atom: Atom<T, any>): T => {
|
|
210
|
-
// const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
|
|
211
|
-
// if (atom.isDirty) {
|
|
212
|
-
// console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
|
|
213
|
-
// const result = atom.computeResult()
|
|
214
|
-
// atom.isDirty = false
|
|
215
|
-
// atom.previousResult = result
|
|
216
|
-
// return result
|
|
217
|
-
// } else {
|
|
218
|
-
// console.log('atom is clean', atom.id, atom.label ?? '', atom._tag, __getResult)
|
|
219
|
-
// return atom.previousResult as T
|
|
220
|
-
// }
|
|
221
|
-
// }
|
|
222
|
-
|
|
223
|
-
// let resultChanged = false
|
|
224
|
-
// const debugInfoForAtom = {
|
|
225
|
-
// atom: serializeAtom(null as TODO),
|
|
226
|
-
// resultChanged,
|
|
227
|
-
// // debugInfo: unknownRefreshReason() as TDebugThunkInfo,
|
|
228
|
-
// debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
|
|
229
|
-
// durationMs: 0,
|
|
230
|
-
// } satisfies AtomDebugInfo<TDebugThunkInfo>
|
|
231
|
-
|
|
232
|
-
const addDebugInfo = (_debugInfo: TDebugThunkInfo) => {
|
|
233
|
-
// debugInfoForAtom.debugInfo = debugInfo
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// debugInfoForRefreshedAtoms.push(debugInfoForAtom)
|
|
237
|
-
|
|
238
|
-
// return getResult_(getAtom as GetAtom, addDebugInfo, this.context!)
|
|
239
|
-
// }
|
|
240
|
-
|
|
241
|
-
const thunk: UnevaluatedThunk<T, TContext> = {
|
|
215
|
+
): Thunk<T, TContext, TDebugRefreshReason> {
|
|
216
|
+
const thunk: Thunk<T, TContext, TDebugRefreshReason> = {
|
|
242
217
|
_tag: 'thunk',
|
|
243
218
|
id: uniqueNodeId(),
|
|
244
219
|
previousResult: NOT_REFRESHED_YET,
|
|
245
220
|
isDirty: true,
|
|
246
|
-
|
|
247
|
-
computeResult: (otelContext) => {
|
|
221
|
+
computeResult: (otelContext, debugRefreshReason) => {
|
|
248
222
|
if (thunk.isDirty) {
|
|
223
|
+
const neededCurrentRefresh = this.currentDebugRefresh === undefined
|
|
224
|
+
if (neededCurrentRefresh) {
|
|
225
|
+
this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
|
|
226
|
+
}
|
|
227
|
+
|
|
249
228
|
// Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
|
|
250
229
|
thunk.sub = new Set()
|
|
251
230
|
|
|
252
|
-
const
|
|
231
|
+
const getAtom = (atom: Atom<T, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
|
|
253
232
|
this.addEdge(thunk, atom)
|
|
254
233
|
return compute(atom, otelContext)
|
|
255
234
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
235
|
+
|
|
236
|
+
let debugInfo: TDebugThunkInfo | undefined = undefined
|
|
237
|
+
const setDebugInfo = (debugInfo_: TDebugThunkInfo) => {
|
|
238
|
+
debugInfo = debugInfo_
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result = getResult(
|
|
242
|
+
getAtom as GetAtom,
|
|
243
|
+
setDebugInfo,
|
|
244
|
+
this.context ?? throwContextNotSetError(),
|
|
260
245
|
otelContext,
|
|
261
246
|
)
|
|
247
|
+
|
|
248
|
+
const resultChanged = thunk.equal(thunk.previousResult as T, result) === false
|
|
249
|
+
|
|
250
|
+
const debugInfoForAtom = {
|
|
251
|
+
atom: serializeAtom(thunk),
|
|
252
|
+
resultChanged,
|
|
253
|
+
debugInfo: debugInfo ?? (unknownRefreshReason() as TDebugThunkInfo),
|
|
254
|
+
} satisfies AtomDebugInfo<TDebugThunkInfo>
|
|
255
|
+
|
|
256
|
+
this.currentDebugRefresh!.refreshedAtoms.push(debugInfoForAtom)
|
|
257
|
+
|
|
262
258
|
thunk.isDirty = false
|
|
263
259
|
thunk.previousResult = result
|
|
264
260
|
thunk.recomputations++
|
|
261
|
+
|
|
262
|
+
if (neededCurrentRefresh) {
|
|
263
|
+
const refreshedAtoms = this.currentDebugRefresh!.refreshedAtoms
|
|
264
|
+
const durationMs = performance.now() - this.currentDebugRefresh!.startMs
|
|
265
|
+
this.currentDebugRefresh = undefined
|
|
266
|
+
|
|
267
|
+
const refreshDebugInfo = {
|
|
268
|
+
id: uniqueRefreshInfoId(),
|
|
269
|
+
reason: debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label },
|
|
270
|
+
skippedRefresh: false,
|
|
271
|
+
refreshedAtoms,
|
|
272
|
+
durationMs,
|
|
273
|
+
completedTimestamp: Date.now(),
|
|
274
|
+
graphSnapshot: this.getSnapshot(),
|
|
275
|
+
}
|
|
276
|
+
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
277
|
+
}
|
|
278
|
+
|
|
265
279
|
return result
|
|
266
280
|
} else {
|
|
267
281
|
return thunk.previousResult as T
|
|
@@ -273,29 +287,15 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
273
287
|
label: options?.label,
|
|
274
288
|
meta: options?.meta,
|
|
275
289
|
equal: options?.equal ?? isEqual,
|
|
276
|
-
__getResult:
|
|
290
|
+
__getResult: getResult,
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
this.atoms.add(thunk)
|
|
280
|
-
// this.dirtyNodes.add(thunk)
|
|
281
|
-
|
|
282
|
-
const debugRefreshReason = options?.debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label }
|
|
283
|
-
|
|
284
|
-
const refreshDebugInfo = {
|
|
285
|
-
id: uniqueRefreshInfoId(),
|
|
286
|
-
reason: debugRefreshReason,
|
|
287
|
-
skippedRefresh: true,
|
|
288
|
-
refreshedAtoms: [],
|
|
289
|
-
durationMs: 0,
|
|
290
|
-
completedTimestamp: Date.now(),
|
|
291
|
-
graphSnapshot: this.getSnapshot(),
|
|
292
|
-
}
|
|
293
|
-
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
294
294
|
|
|
295
|
-
return thunk
|
|
295
|
+
return thunk
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
destroy(node: Atom<any, TContext> | Effect) {
|
|
298
|
+
destroy(node: Atom<any, TContext, TDebugRefreshReason> | Effect) {
|
|
299
299
|
// Recursively destroy any supercomputations
|
|
300
300
|
if (node._tag === 'ref' || node._tag === 'thunk') {
|
|
301
301
|
for (const superComp of node.super) {
|
|
@@ -315,50 +315,33 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
315
315
|
|
|
316
316
|
makeEffect(
|
|
317
317
|
doEffect: (get: GetAtom, otelContext?: otel.Context) => void,
|
|
318
|
-
options?:
|
|
319
|
-
| {
|
|
320
|
-
label?: string
|
|
321
|
-
debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
322
|
-
}
|
|
323
|
-
| undefined,
|
|
318
|
+
options?: { label?: string } | undefined,
|
|
324
319
|
): Effect {
|
|
325
320
|
const effect: Effect = {
|
|
326
321
|
_tag: 'effect',
|
|
327
322
|
id: uniqueNodeId(),
|
|
328
323
|
doEffect: (otelContext) => {
|
|
324
|
+
// NOTE we're not tracking any debug refresh info for effects as they're tracked by the thunks they depend on
|
|
325
|
+
|
|
329
326
|
// Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
|
|
330
327
|
effect.sub = new Set()
|
|
331
328
|
|
|
332
|
-
const getAtom = (atom: Atom<any,
|
|
329
|
+
const getAtom = (atom: Atom<any, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
|
|
333
330
|
this.addEdge(effect, atom)
|
|
334
331
|
return compute(atom, otelContext)
|
|
335
332
|
}
|
|
333
|
+
|
|
336
334
|
doEffect(getAtom as GetAtom, otelContext)
|
|
337
335
|
},
|
|
338
336
|
sub: new Set(),
|
|
337
|
+
label: options?.label,
|
|
339
338
|
}
|
|
340
339
|
|
|
341
|
-
// this.effects.add(effect)
|
|
342
|
-
// this.dirtyNodes.add(effect)
|
|
343
|
-
|
|
344
|
-
const debugRefreshReason = options?.debugRefreshReason ?? { _tag: 'makeEffect', label: options?.label }
|
|
345
|
-
|
|
346
|
-
const refreshDebugInfo = {
|
|
347
|
-
id: uniqueRefreshInfoId(),
|
|
348
|
-
reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
349
|
-
skippedRefresh: true,
|
|
350
|
-
refreshedAtoms: [],
|
|
351
|
-
durationMs: 0,
|
|
352
|
-
completedTimestamp: Date.now(),
|
|
353
|
-
graphSnapshot: this.getSnapshot(),
|
|
354
|
-
}
|
|
355
|
-
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
356
|
-
|
|
357
340
|
return effect
|
|
358
341
|
}
|
|
359
342
|
|
|
360
343
|
setRef<T>(
|
|
361
|
-
ref: Ref<T>,
|
|
344
|
+
ref: Ref<T, TContext, TDebugRefreshReason>,
|
|
362
345
|
val: T,
|
|
363
346
|
options?:
|
|
364
347
|
| {
|
|
@@ -367,32 +350,16 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
367
350
|
}
|
|
368
351
|
| undefined,
|
|
369
352
|
) {
|
|
370
|
-
const { debugRefreshReason } = options ?? {}
|
|
371
353
|
ref.previousResult = val
|
|
372
354
|
|
|
373
355
|
const effectsToRefresh = new Set<Effect>()
|
|
374
356
|
markSuperCompDirtyRec(ref, effectsToRefresh)
|
|
375
357
|
|
|
376
|
-
this.
|
|
377
|
-
for (const effect of effectsToRefresh) {
|
|
378
|
-
effect.doEffect(options?.otelContext)
|
|
379
|
-
}
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
|
|
383
|
-
id: uniqueRefreshInfoId(),
|
|
384
|
-
reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
385
|
-
skippedRefresh: true,
|
|
386
|
-
refreshedAtoms: [],
|
|
387
|
-
durationMs: 0,
|
|
388
|
-
completedTimestamp: Date.now(),
|
|
389
|
-
graphSnapshot: this.getSnapshot(),
|
|
390
|
-
}
|
|
391
|
-
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
358
|
+
this.runEffects(effectsToRefresh, options)
|
|
392
359
|
}
|
|
393
360
|
|
|
394
361
|
setRefs<T>(
|
|
395
|
-
refs: [Ref<T>, T][],
|
|
362
|
+
refs: [Ref<T, TContext, TDebugRefreshReason>, T][],
|
|
396
363
|
options?:
|
|
397
364
|
| {
|
|
398
365
|
debugRefreshReason?: TDebugRefreshReason
|
|
@@ -400,7 +367,6 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
400
367
|
}
|
|
401
368
|
| undefined,
|
|
402
369
|
) {
|
|
403
|
-
const debugRefreshReason = options?.debugRefreshReason
|
|
404
370
|
const effectsToRefresh = new Set<Effect>()
|
|
405
371
|
for (const [ref, val] of refs) {
|
|
406
372
|
ref.previousResult = val
|
|
@@ -408,213 +374,56 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
408
374
|
markSuperCompDirtyRec(ref, effectsToRefresh)
|
|
409
375
|
}
|
|
410
376
|
|
|
377
|
+
this.runEffects(effectsToRefresh, options)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private runEffects = (
|
|
381
|
+
effectsToRefresh: Set<Effect>,
|
|
382
|
+
options?:
|
|
383
|
+
| {
|
|
384
|
+
debugRefreshReason?: TDebugRefreshReason
|
|
385
|
+
otelContext?: otel.Context
|
|
386
|
+
}
|
|
387
|
+
| undefined,
|
|
388
|
+
) => {
|
|
411
389
|
this.effectsWrapper(() => {
|
|
390
|
+
this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
|
|
391
|
+
|
|
412
392
|
for (const effect of effectsToRefresh) {
|
|
413
393
|
effect.doEffect(options?.otelContext)
|
|
414
394
|
}
|
|
415
|
-
})
|
|
416
395
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// this.addEdge(context, atom)
|
|
433
|
-
|
|
434
|
-
// const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
|
|
435
|
-
// const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
|
|
436
|
-
|
|
437
|
-
// if (dependencyMightBeStale || dependencyNotRefreshedYet) {
|
|
438
|
-
// throw new DependencyNotReadyError(
|
|
439
|
-
// `${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
|
|
440
|
-
// )
|
|
441
|
-
// }
|
|
442
|
-
|
|
443
|
-
// return atom.result
|
|
444
|
-
// }
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Update the graph to be consistent with the current values of the root atoms.
|
|
448
|
-
* Generally we run this after a ref is updated.
|
|
449
|
-
* At the end of the refresh, we run any effects that were scheduled.
|
|
450
|
-
*
|
|
451
|
-
* @param roots Root atoms to start the refresh from
|
|
452
|
-
*/
|
|
453
|
-
// refresh(
|
|
454
|
-
// options?:
|
|
455
|
-
// | {
|
|
456
|
-
// otelHint?: string
|
|
457
|
-
// debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
458
|
-
// }
|
|
459
|
-
// | undefined,
|
|
460
|
-
// otelContext: otel.Context = otel.context.active(),
|
|
461
|
-
// ): void {
|
|
462
|
-
// const otelHint = options?.otelHint ?? ''
|
|
463
|
-
// const debugRefreshReason = options?.debugRefreshReason
|
|
464
|
-
|
|
465
|
-
// const roots = [...this.dirtyNodes]
|
|
466
|
-
|
|
467
|
-
// const debugInfoForRefreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[] = []
|
|
468
|
-
|
|
469
|
-
// // if (otelHint.includes('tableName')) {
|
|
470
|
-
// // console.log('refresh', otelHint, { shouldTrace })
|
|
471
|
-
// // }
|
|
472
|
-
|
|
473
|
-
// this.otelTracer.startActiveSpan(`LiveStore.refresh:${otelHint}`, {}, otelContext, (span) => {
|
|
474
|
-
// const atomsToRefresh = roots.filter(isAtom)
|
|
475
|
-
// const effectsToRun = new Set(roots.filter(isEffect))
|
|
476
|
-
|
|
477
|
-
// span.setAttribute('livestore.hint', otelHint)
|
|
478
|
-
// span.setAttribute('livestore.rootsCount', roots.length)
|
|
479
|
-
// // span.setAttribute('sstack', new Error().stack!)
|
|
480
|
-
|
|
481
|
-
// // Sort in topological order, starting with minimum height
|
|
482
|
-
// while (atomsToRefresh.length > 0) {
|
|
483
|
-
// atomsToRefresh.sort((a, b) => a.height - b.height)
|
|
484
|
-
// const atomToRefresh = atomsToRefresh.shift()!
|
|
485
|
-
|
|
486
|
-
// // Recompute the value
|
|
487
|
-
// let resultChanged = false
|
|
488
|
-
// const debugInfoForAtom = {
|
|
489
|
-
// atom: serializeAtom(atomToRefresh),
|
|
490
|
-
// resultChanged,
|
|
491
|
-
// // debugInfo: unknownRefreshReason() as TDebugThunkInfo,
|
|
492
|
-
// debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
|
|
493
|
-
// durationMs: 0,
|
|
494
|
-
// } satisfies AtomDebugInfo<TDebugThunkInfo>
|
|
495
|
-
// try {
|
|
496
|
-
// atomToRefresh.sub = new Set()
|
|
497
|
-
// const beforeTimestamp = performance.now()
|
|
498
|
-
// const newResult = atomToRefresh.getResult(
|
|
499
|
-
// (atom) => this.get(atom, atomToRefresh),
|
|
500
|
-
// (debugInfo) => {
|
|
501
|
-
// debugInfoForAtom.debugInfo = debugInfo
|
|
502
|
-
// },
|
|
503
|
-
// this.context ?? shouldNeverHappen(`No context provided yet for ReactiveGraph`),
|
|
504
|
-
// )
|
|
505
|
-
// const afterTimestamp = performance.now()
|
|
506
|
-
// debugInfoForAtom.durationMs = afterTimestamp - beforeTimestamp
|
|
507
|
-
|
|
508
|
-
// // Determine if the result changed to do early cutoff and avoid further unnecessary updates.
|
|
509
|
-
// // Refs never depend on anything, so if a ref is being refreshed it definitely changed.
|
|
510
|
-
// // For thunks, we use a deep equality check.
|
|
511
|
-
// resultChanged =
|
|
512
|
-
// atomToRefresh._tag === 'ref' ||
|
|
513
|
-
// (atomToRefresh._tag === 'thunk' && !atomToRefresh.equal(atomToRefresh.result, newResult))
|
|
514
|
-
|
|
515
|
-
// if (resultChanged) {
|
|
516
|
-
// atomToRefresh.result = newResult
|
|
517
|
-
// }
|
|
518
|
-
|
|
519
|
-
// this.dirtyNodes.delete(atomToRefresh)
|
|
520
|
-
// } catch (e) {
|
|
521
|
-
// if (e instanceof DependencyNotReadyError) {
|
|
522
|
-
// // If we hit a dependency that wasn't ready yet,
|
|
523
|
-
// // abort this recomputation and try again later.
|
|
524
|
-
// if (!atomsToRefresh.includes(atomToRefresh)) {
|
|
525
|
-
// atomsToRefresh.push(atomToRefresh)
|
|
526
|
-
// }
|
|
527
|
-
// } else {
|
|
528
|
-
// throw e
|
|
529
|
-
// }
|
|
530
|
-
// }
|
|
531
|
-
|
|
532
|
-
// debugInfoForRefreshedAtoms.push(debugInfoForAtom)
|
|
533
|
-
|
|
534
|
-
// if (!resultChanged) {
|
|
535
|
-
// continue
|
|
536
|
-
// }
|
|
537
|
-
|
|
538
|
-
// // Schedule supercomputations
|
|
539
|
-
// for (const superComp of atomToRefresh.super) {
|
|
540
|
-
// switch (superComp._tag) {
|
|
541
|
-
// case 'ref':
|
|
542
|
-
// case 'thunk': {
|
|
543
|
-
// if (!atomsToRefresh.includes(superComp)) {
|
|
544
|
-
// atomsToRefresh.push(superComp)
|
|
545
|
-
// }
|
|
546
|
-
// break
|
|
547
|
-
// }
|
|
548
|
-
// case 'effect': {
|
|
549
|
-
// effectsToRun.add(superComp)
|
|
550
|
-
// break
|
|
551
|
-
// }
|
|
552
|
-
// }
|
|
553
|
-
// }
|
|
554
|
-
// }
|
|
555
|
-
|
|
556
|
-
// this.effectsWrapper(() => {
|
|
557
|
-
// for (const effect of effectsToRun) {
|
|
558
|
-
// effect.doEffect((atom: Atom<any, TContext>) => this.get(atom, effect))
|
|
559
|
-
// this.dirtyNodes.delete(effect)
|
|
560
|
-
// }
|
|
561
|
-
// })
|
|
562
|
-
|
|
563
|
-
// span.end()
|
|
564
|
-
|
|
565
|
-
// const spanDurationMs = getDurationMsFromSpan(span)
|
|
566
|
-
|
|
567
|
-
// const refreshDebugInfo: RefreshDebugInfo<
|
|
568
|
-
// RefreshReasonWithGenericReasons<TDebugRefreshReason>,
|
|
569
|
-
// TDebugThunkInfo
|
|
570
|
-
// > = {
|
|
571
|
-
// id: uniqueRefreshInfoId(),
|
|
572
|
-
// reason: debugRefreshReason ?? unknownRefreshReason(),
|
|
573
|
-
// refreshedAtoms: debugInfoForRefreshedAtoms,
|
|
574
|
-
// skippedRefresh: false,
|
|
575
|
-
// durationMs: spanDurationMs,
|
|
576
|
-
// completedTimestamp: Date.now(),
|
|
577
|
-
// graphSnapshot: this.getSnapshot(),
|
|
578
|
-
// }
|
|
579
|
-
|
|
580
|
-
// this.debugRefreshInfos.push(refreshDebugInfo)
|
|
581
|
-
// })
|
|
582
|
-
// }
|
|
583
|
-
|
|
584
|
-
label(atom: Atom<any, TContext> | Effect) {
|
|
585
|
-
if (atom._tag === 'effect') {
|
|
586
|
-
return `unknown effect`
|
|
587
|
-
} else {
|
|
588
|
-
return atom.label ?? `unknown ${atom._tag}`
|
|
589
|
-
}
|
|
396
|
+
const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
|
|
397
|
+
const durationMs = performance.now() - this.currentDebugRefresh.startMs
|
|
398
|
+
this.currentDebugRefresh = undefined
|
|
399
|
+
|
|
400
|
+
const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
|
|
401
|
+
id: uniqueRefreshInfoId(),
|
|
402
|
+
reason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
403
|
+
skippedRefresh: false,
|
|
404
|
+
refreshedAtoms,
|
|
405
|
+
durationMs,
|
|
406
|
+
completedTimestamp: Date.now(),
|
|
407
|
+
graphSnapshot: this.getSnapshot(),
|
|
408
|
+
}
|
|
409
|
+
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
410
|
+
})
|
|
590
411
|
}
|
|
591
412
|
|
|
592
|
-
addEdge(
|
|
413
|
+
addEdge(
|
|
414
|
+
superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
|
|
415
|
+
subComp: Atom<any, TContext, TDebugRefreshReason>,
|
|
416
|
+
) {
|
|
593
417
|
superComp.sub.add(subComp)
|
|
594
418
|
subComp.super.add(superComp)
|
|
595
|
-
this.updateAtomHeight(superComp)
|
|
596
419
|
}
|
|
597
420
|
|
|
598
|
-
removeEdge(
|
|
421
|
+
removeEdge(
|
|
422
|
+
superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
|
|
423
|
+
subComp: Atom<any, TContext, TDebugRefreshReason>,
|
|
424
|
+
) {
|
|
599
425
|
superComp.sub.delete(subComp)
|
|
600
426
|
subComp.super.delete(superComp)
|
|
601
|
-
this.updateAtomHeight(superComp)
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
updateAtomHeight(atom: Atom<any, TContext> | Effect) {
|
|
605
|
-
switch (atom._tag) {
|
|
606
|
-
case 'ref': {
|
|
607
|
-
atom.height = 0
|
|
608
|
-
break
|
|
609
|
-
}
|
|
610
|
-
case 'thunk': {
|
|
611
|
-
atom.height = (max([...atom.sub].map((atom) => atom.height)) || 0) + 1
|
|
612
|
-
break
|
|
613
|
-
}
|
|
614
|
-
case 'effect': {
|
|
615
|
-
break
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
427
|
}
|
|
619
428
|
|
|
620
429
|
private getSnapshot = (): ReactiveGraphSnapshot => ({
|
|
@@ -622,17 +431,13 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
622
431
|
// effects: Array.from(this.effects).map(serializeEffect),
|
|
623
432
|
// dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
|
|
624
433
|
})
|
|
625
|
-
|
|
626
|
-
get atomsCount() {
|
|
627
|
-
return this.atoms.size
|
|
628
|
-
}
|
|
629
434
|
}
|
|
630
435
|
|
|
631
436
|
// const isAtom = <T, TContext>(a: Atom<T, TContext> | Effect): a is Atom<T, TContext> =>
|
|
632
437
|
// a._tag === 'ref' || a._tag === 'thunk'
|
|
633
438
|
// const isEffect = <T, TContext>(a: Atom<T, TContext> | Effect): a is Effect => a._tag === 'effect'
|
|
634
439
|
|
|
635
|
-
const compute = <T>(atom: Atom<T, any>, otelContext: otel.Context): T => {
|
|
440
|
+
const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
|
|
636
441
|
// const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
|
|
637
442
|
if (atom.isDirty) {
|
|
638
443
|
// console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
|
|
@@ -646,7 +451,7 @@ const compute = <T>(atom: Atom<T, any>, otelContext: otel.Context): T => {
|
|
|
646
451
|
}
|
|
647
452
|
}
|
|
648
453
|
|
|
649
|
-
const markSuperCompDirtyRec = <T>(atom: Atom<T, any>, effectsToRefresh: Set<Effect>) => {
|
|
454
|
+
const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect>) => {
|
|
650
455
|
for (const superComp of atom.super) {
|
|
651
456
|
if (superComp._tag === 'thunk' || superComp._tag === 'ref') {
|
|
652
457
|
superComp.isDirty = true
|
|
@@ -656,3 +461,7 @@ const markSuperCompDirtyRec = <T>(atom: Atom<T, any>, effectsToRefresh: Set<Effe
|
|
|
656
461
|
}
|
|
657
462
|
}
|
|
658
463
|
}
|
|
464
|
+
|
|
465
|
+
const throwContextNotSetError = (): never => {
|
|
466
|
+
throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph`)
|
|
467
|
+
}
|