@livestore/livestore 0.0.19 → 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 +29 -22
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.d.ts +1 -1
- package/dist/QueryCache.d.ts.map +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/__tests__/react/fixture.d.ts +5 -4
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +3 -5
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
- package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
- package/dist/__tests__/react/useComponentState.test.js +68 -0
- package/dist/__tests__/react/useComponentState.test.js.map +1 -0
- package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
- package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
- package/dist/__tests__/react/useLQuery.test.js +38 -0
- package/dist/__tests__/react/useLQuery.test.js.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js +4 -9
- package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -1
- package/dist/__tests__/react/useQuery.test.d.ts +2 -0
- package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
- package/dist/__tests__/react/useQuery.test.js +33 -0
- package/dist/__tests__/react/useQuery.test.js.map +1 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
- 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 +179 -93
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
- package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
- package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
- package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
- package/dist/inMemoryDatabase.d.ts +4 -3
- package/dist/inMemoryDatabase.d.ts.map +1 -1
- package/dist/inMemoryDatabase.js +3 -2
- package/dist/inMemoryDatabase.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +4 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/useComponentState.d.ts +50 -0
- package/dist/react/useComponentState.d.ts.map +1 -0
- package/dist/react/useComponentState.js +240 -0
- package/dist/react/useComponentState.js.map +1 -0
- package/dist/react/useGlobalQuery.d.ts +3 -0
- package/dist/react/useGlobalQuery.d.ts.map +1 -0
- package/dist/react/useGlobalQuery.js +26 -0
- package/dist/react/useGlobalQuery.js.map +1 -0
- package/dist/react/useGraphQL.d.ts +3 -3
- package/dist/react/useGraphQL.d.ts.map +1 -1
- package/dist/react/useGraphQL.js +10 -8
- package/dist/react/useGraphQL.js.map +1 -1
- package/dist/react/useLiveStoreComponent.d.ts +6 -6
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
- package/dist/react/useLiveStoreComponent.js +143 -99
- package/dist/react/useLiveStoreComponent.js.map +1 -1
- package/dist/react/useQuery.d.ts +2 -2
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +54 -30
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts +8 -0
- package/dist/react/useTemporaryQuery.d.ts.map +1 -0
- package/dist/react/useTemporaryQuery.js +19 -0
- package/dist/react/useTemporaryQuery.js.map +1 -0
- package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
- package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
- package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
- package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
- 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 +51 -67
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +138 -220
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +28 -21
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +22 -18
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graph.d.ts +10 -0
- package/dist/reactiveQueries/graph.d.ts.map +1 -0
- package/dist/reactiveQueries/graph.js +6 -0
- package/dist/reactiveQueries/graph.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +35 -17
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +86 -10
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +17 -12
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +30 -8
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +28 -18
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +79 -16
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/store.d.ts +35 -61
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +77 -272
- package/dist/store.js.map +1 -1
- package/package.json +4 -3
- package/src/QueryCache.ts +1 -1
- package/src/__tests__/react/fixture.tsx +10 -8
- package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
- package/src/__tests__/react/useQuery.test.tsx +48 -0
- package/src/__tests__/react/utils/stack-info.test.ts +45 -0
- package/src/__tests__/reactive.test.ts +212 -140
- package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
- package/src/inMemoryDatabase.ts +11 -8
- package/src/index.ts +7 -11
- package/src/react/index.ts +4 -7
- package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +90 -253
- package/src/react/useQuery.ts +74 -40
- package/src/react/useTemporaryQuery.ts +23 -0
- package/src/react/utils/stack-info.ts +63 -0
- package/src/reactive.ts +234 -308
- package/src/reactiveQueries/base-class.ts +59 -42
- package/src/reactiveQueries/graph.ts +15 -0
- package/src/reactiveQueries/graphql.ts +143 -29
- package/src/reactiveQueries/js.ts +57 -20
- package/src/reactiveQueries/sql.ts +136 -36
- package/src/store.ts +121 -426
- package/src/react/useGraphQL.ts +0 -138
package/src/reactive.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// Effect: a side effect that runs when a value changes; return value is ignored
|
|
7
7
|
// Atom: a node returning a value that can be depended on: Ref | Thunk
|
|
8
8
|
|
|
9
|
-
// Super computation: Nodes that depend on a given node
|
|
10
|
-
// Sub computation: Nodes that a given node depends on
|
|
9
|
+
// Super computation: Nodes that depend on a given node ("downstream")
|
|
10
|
+
// Sub computation: Nodes that a given node depends on ("upstream")
|
|
11
11
|
|
|
12
12
|
// This vocabulary comes from the MiniAdapton paper linked below, although
|
|
13
13
|
// we don't actually implement the MiniAdapton algorithm because we don't need lazy recomputation.
|
|
@@ -26,76 +26,81 @@
|
|
|
26
26
|
import type { PrettifyFlat } from '@livestore/utils'
|
|
27
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
|
+
// import { getDurationMsFromSpan } from './otel.js'
|
|
32
33
|
|
|
33
|
-
const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
|
|
34
|
-
type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
|
|
34
|
+
export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
|
|
35
|
+
export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
|
|
35
36
|
|
|
36
|
-
export type GetAtom = <T>(atom: Atom<T
|
|
37
|
+
export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context) => T
|
|
37
38
|
|
|
38
|
-
export type Ref<T> = {
|
|
39
|
+
export type Ref<T, TContext, TDebugRefreshReason extends Taggable> = {
|
|
39
40
|
_tag: 'ref'
|
|
40
41
|
id: string
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
sub: Set<Atom<any>> // always empty
|
|
45
|
-
super: Set<Atom<any> | Effect>
|
|
42
|
+
isDirty: false
|
|
43
|
+
previousResult: T
|
|
44
|
+
computeResult: () => T
|
|
45
|
+
sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
|
|
46
|
+
super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
|
|
46
47
|
label?: string
|
|
47
48
|
/** Container for meta information (e.g. the LiveStore Store) */
|
|
48
49
|
meta?: any
|
|
49
50
|
equal: (a: T, b: T) => boolean
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
type
|
|
53
|
+
export type Thunk<TResult, TContext, TDebugRefreshReason extends Taggable> = {
|
|
53
54
|
_tag: 'thunk'
|
|
54
55
|
id: string
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
isDirty: boolean
|
|
57
|
+
computeResult: (
|
|
58
|
+
otelContext?: otel.Context,
|
|
59
|
+
debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>,
|
|
60
|
+
) => TResult
|
|
61
|
+
previousResult: TResult | NOT_REFRESHED_YET
|
|
62
|
+
sub: Set<Atom<any, TContext, TDebugRefreshReason>>
|
|
63
|
+
super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
|
|
59
64
|
label?: string
|
|
60
65
|
/** Container for meta information (e.g. the LiveStore Store) */
|
|
61
66
|
meta?: any
|
|
62
|
-
equal: (a:
|
|
63
|
-
|
|
67
|
+
equal: (a: TResult, b: TResult) => boolean
|
|
68
|
+
recomputations: number
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
__getResult: any
|
|
71
|
+
}
|
|
67
72
|
|
|
68
|
-
export type Atom<T
|
|
73
|
+
export type Atom<T, TContext, TDebugRefreshReason extends Taggable> =
|
|
74
|
+
| Ref<T, TContext, TDebugRefreshReason>
|
|
75
|
+
| Thunk<T, TContext, TDebugRefreshReason>
|
|
69
76
|
|
|
70
77
|
export type Effect = {
|
|
71
78
|
_tag: 'effect'
|
|
72
79
|
id: string
|
|
73
|
-
doEffect: (
|
|
74
|
-
sub: Set<Atom<any>>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class DependencyNotReadyError extends Error {
|
|
78
|
-
constructor(message: string) {
|
|
79
|
-
super(message)
|
|
80
|
-
this.name = 'DependencyNotReadyError'
|
|
81
|
-
}
|
|
80
|
+
doEffect: (otelContext?: otel.Context) => void
|
|
81
|
+
sub: Set<Atom<any, TODO, TODO>>
|
|
82
|
+
label?: string
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export type Taggable<T extends string = string> = { _tag: T }
|
|
85
86
|
|
|
87
|
+
export type DebugThunkInfo<T extends string = string> = {
|
|
88
|
+
_tag: T
|
|
89
|
+
durationMs: number
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
export type ReactiveGraphOptions = {
|
|
87
93
|
effectsWrapper?: (runEffects: () => void) => void
|
|
88
|
-
otelTracer: otel.Tracer
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
export type AtomDebugInfo<TDebugThunkInfo extends
|
|
96
|
+
export type AtomDebugInfo<TDebugThunkInfo extends DebugThunkInfo> = {
|
|
92
97
|
atom: SerializedAtom
|
|
93
98
|
resultChanged: boolean
|
|
94
|
-
durationMs: number
|
|
95
99
|
debugInfo: TDebugThunkInfo
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
// TODO possibly find a better name for "refresh"
|
|
103
|
+
export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends DebugThunkInfo> = {
|
|
99
104
|
/** Currently only used for easier handling in React (e.g. as key) */
|
|
100
105
|
id: string
|
|
101
106
|
reason: TDebugRefreshReason
|
|
@@ -113,20 +118,16 @@ export type RefreshReasonWithGenericReasons<T extends Taggable> =
|
|
|
113
118
|
_tag: 'makeThunk'
|
|
114
119
|
label?: string
|
|
115
120
|
}
|
|
116
|
-
| {
|
|
117
|
-
_tag: 'makeEffect'
|
|
118
|
-
label?: string
|
|
119
|
-
}
|
|
120
121
|
| { _tag: 'unknown' }
|
|
121
122
|
|
|
122
123
|
export const unknownRefreshReason = () => {
|
|
123
|
-
debugger
|
|
124
|
+
// debugger
|
|
124
125
|
return { _tag: 'unknown' as const }
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
export type SerializedAtom = Readonly<
|
|
128
129
|
PrettifyFlat<
|
|
129
|
-
Pick<Atom<unknown>, '_tag' | '
|
|
130
|
+
Pick<Atom<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta'> & {
|
|
130
131
|
sub: string[]
|
|
131
132
|
super: string[]
|
|
132
133
|
}
|
|
@@ -137,45 +138,52 @@ export type SerializedEffect = Readonly<PrettifyFlat<Pick<Effect, '_tag' | 'id'>
|
|
|
137
138
|
|
|
138
139
|
type ReactiveGraphSnapshot = {
|
|
139
140
|
readonly atoms: SerializedAtom[]
|
|
140
|
-
readonly effects: SerializedEffect[]
|
|
141
|
+
// readonly effects: SerializedEffect[]
|
|
141
142
|
/** IDs of atoms and effects that are dirty */
|
|
142
|
-
readonly dirtyNodes: string[]
|
|
143
|
+
// readonly dirtyNodes: string[]
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
const uniqueNodeId = () => uniqueId('node-')
|
|
146
147
|
const uniqueRefreshInfoId = () => uniqueId('refresh-info-')
|
|
147
148
|
|
|
148
|
-
const serializeAtom = (atom: Atom<any>): SerializedAtom => ({
|
|
149
|
-
...pick(atom, ['_tag', '
|
|
149
|
+
const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
|
|
150
|
+
...pick(atom, ['_tag', 'id', 'label', 'meta']),
|
|
150
151
|
sub: Array.from(atom.sub).map((a) => a.id),
|
|
151
152
|
super: Array.from(atom.super).map((a) => a.id),
|
|
152
153
|
})
|
|
153
154
|
|
|
154
|
-
const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
|
|
155
|
+
// const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
|
|
155
156
|
|
|
156
|
-
export class ReactiveGraph<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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()
|
|
161
163
|
effectsWrapper: (runEffects: () => void) => void
|
|
162
164
|
|
|
165
|
+
context: TContext | undefined
|
|
166
|
+
|
|
163
167
|
debugRefreshInfos: BoundArray<
|
|
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
|
-
this.otelTracer = options.otelTracer
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
makeRef<T>(
|
|
173
|
-
|
|
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> = {
|
|
174
182
|
_tag: 'ref',
|
|
175
183
|
id: uniqueNodeId(),
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
isDirty: false,
|
|
185
|
+
previousResult: val,
|
|
186
|
+
computeResult: () => ref.previousResult,
|
|
179
187
|
sub: new Set(),
|
|
180
188
|
super: new Set(),
|
|
181
189
|
label: options?.label,
|
|
@@ -189,46 +197,105 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
makeThunk<T>(
|
|
192
|
-
getResult: (
|
|
193
|
-
|
|
200
|
+
getResult: (
|
|
201
|
+
get: GetAtom,
|
|
202
|
+
setDebugInfo: (debugInfo: TDebugThunkInfo) => void,
|
|
203
|
+
ctx: TContext,
|
|
204
|
+
otelContext: otel.Context | undefined,
|
|
205
|
+
) => T,
|
|
206
|
+
options?:
|
|
194
207
|
| {
|
|
195
208
|
label?: string
|
|
196
209
|
meta?: any
|
|
197
210
|
equal?: (a: T, b: T) => boolean
|
|
198
211
|
/** Debug info for initializing the thunk (i.e. running it the first time) */
|
|
199
|
-
debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
212
|
+
// debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
200
213
|
}
|
|
201
214
|
| undefined,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const thunk: UnevaluatedThunk<T> = {
|
|
215
|
+
): Thunk<T, TContext, TDebugRefreshReason> {
|
|
216
|
+
const thunk: Thunk<T, TContext, TDebugRefreshReason> = {
|
|
205
217
|
_tag: 'thunk',
|
|
206
218
|
id: uniqueNodeId(),
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
previousResult: NOT_REFRESHED_YET,
|
|
220
|
+
isDirty: true,
|
|
221
|
+
computeResult: (otelContext, debugRefreshReason) => {
|
|
222
|
+
if (thunk.isDirty) {
|
|
223
|
+
const neededCurrentRefresh = this.currentDebugRefresh === undefined
|
|
224
|
+
if (neededCurrentRefresh) {
|
|
225
|
+
this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
|
|
229
|
+
thunk.sub = new Set()
|
|
230
|
+
|
|
231
|
+
const getAtom = (atom: Atom<T, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
|
|
232
|
+
this.addEdge(thunk, atom)
|
|
233
|
+
return compute(atom, otelContext)
|
|
234
|
+
}
|
|
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(),
|
|
245
|
+
otelContext,
|
|
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
|
+
|
|
258
|
+
thunk.isDirty = false
|
|
259
|
+
thunk.previousResult = result
|
|
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
|
+
|
|
279
|
+
return result
|
|
280
|
+
} else {
|
|
281
|
+
return thunk.previousResult as T
|
|
282
|
+
}
|
|
283
|
+
},
|
|
210
284
|
sub: new Set(),
|
|
211
285
|
super: new Set(),
|
|
286
|
+
recomputations: 0,
|
|
212
287
|
label: options?.label,
|
|
213
288
|
meta: options?.meta,
|
|
214
289
|
equal: options?.equal ?? isEqual,
|
|
290
|
+
__getResult: getResult,
|
|
215
291
|
}
|
|
216
292
|
|
|
217
293
|
this.atoms.add(thunk)
|
|
218
|
-
this.dirtyNodes.add(thunk)
|
|
219
|
-
this.refresh(
|
|
220
|
-
{
|
|
221
|
-
otelHint: options?.label ?? 'makeThunk',
|
|
222
|
-
debugRefreshReason: options?.debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label },
|
|
223
|
-
},
|
|
224
|
-
otelContext,
|
|
225
|
-
)
|
|
226
294
|
|
|
227
|
-
|
|
228
|
-
return thunk as unknown as Thunk<T>
|
|
295
|
+
return thunk
|
|
229
296
|
}
|
|
230
297
|
|
|
231
|
-
destroy(node: Atom<any> | Effect) {
|
|
298
|
+
destroy(node: Atom<any, TContext, TDebugRefreshReason> | Effect) {
|
|
232
299
|
// Recursively destroy any supercomputations
|
|
233
300
|
if (node._tag === 'ref' || node._tag === 'thunk') {
|
|
234
301
|
for (const superComp of node.super) {
|
|
@@ -241,301 +308,160 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
|
|
|
241
308
|
this.removeEdge(node, subComp)
|
|
242
309
|
}
|
|
243
310
|
|
|
244
|
-
if (node._tag
|
|
245
|
-
this.effects.delete(node)
|
|
246
|
-
} else {
|
|
311
|
+
if (node._tag !== 'effect') {
|
|
247
312
|
this.atoms.delete(node)
|
|
248
313
|
}
|
|
249
314
|
}
|
|
250
315
|
|
|
251
316
|
makeEffect(
|
|
252
|
-
doEffect: (get: GetAtom) => void,
|
|
253
|
-
options
|
|
254
|
-
otelContext: otel.Context,
|
|
317
|
+
doEffect: (get: GetAtom, otelContext?: otel.Context) => void,
|
|
318
|
+
options?: { label?: string } | undefined,
|
|
255
319
|
): Effect {
|
|
256
320
|
const effect: Effect = {
|
|
257
321
|
_tag: 'effect',
|
|
258
322
|
id: uniqueNodeId(),
|
|
259
|
-
doEffect
|
|
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
|
+
|
|
326
|
+
// Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
|
|
327
|
+
effect.sub = new Set()
|
|
328
|
+
|
|
329
|
+
const getAtom = (atom: Atom<any, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
|
|
330
|
+
this.addEdge(effect, atom)
|
|
331
|
+
return compute(atom, otelContext)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
doEffect(getAtom as GetAtom, otelContext)
|
|
335
|
+
},
|
|
260
336
|
sub: new Set(),
|
|
337
|
+
label: options?.label,
|
|
261
338
|
}
|
|
262
339
|
|
|
263
|
-
this.effects.add(effect)
|
|
264
|
-
this.dirtyNodes.add(effect)
|
|
265
|
-
this.refresh(
|
|
266
|
-
{ otelHint: 'makeEffect', debugRefreshReason: { _tag: 'makeEffect', label: options?.label } },
|
|
267
|
-
otelContext,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
340
|
return effect
|
|
271
341
|
}
|
|
272
342
|
|
|
273
343
|
setRef<T>(
|
|
274
|
-
ref: Ref<T>,
|
|
344
|
+
ref: Ref<T, TContext, TDebugRefreshReason>,
|
|
275
345
|
val: T,
|
|
276
|
-
options
|
|
346
|
+
options?:
|
|
277
347
|
| {
|
|
278
|
-
otelHint?: string
|
|
279
|
-
skipRefresh?: boolean
|
|
280
348
|
debugRefreshReason?: TDebugRefreshReason
|
|
349
|
+
otelContext?: otel.Context
|
|
281
350
|
}
|
|
282
351
|
| undefined,
|
|
283
|
-
otelContext: otel.Context,
|
|
284
352
|
) {
|
|
285
|
-
|
|
286
|
-
ref.result = val
|
|
287
|
-
this.dirtyNodes.add(ref)
|
|
353
|
+
ref.previousResult = val
|
|
288
354
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
id: uniqueRefreshInfoId(),
|
|
292
|
-
reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
293
|
-
skippedRefresh: true,
|
|
294
|
-
refreshedAtoms: [],
|
|
295
|
-
durationMs: 0,
|
|
296
|
-
completedTimestamp: Date.now(),
|
|
297
|
-
graphSnapshot: this.getSnapshot(),
|
|
298
|
-
}
|
|
299
|
-
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
300
|
-
return
|
|
301
|
-
}
|
|
355
|
+
const effectsToRefresh = new Set<Effect>()
|
|
356
|
+
markSuperCompDirtyRec(ref, effectsToRefresh)
|
|
302
357
|
|
|
303
|
-
this.
|
|
358
|
+
this.runEffects(effectsToRefresh, options)
|
|
304
359
|
}
|
|
305
360
|
|
|
306
361
|
setRefs<T>(
|
|
307
|
-
refs: [Ref<T>, T][],
|
|
308
|
-
options
|
|
362
|
+
refs: [Ref<T, TContext, TDebugRefreshReason>, T][],
|
|
363
|
+
options?:
|
|
309
364
|
| {
|
|
310
|
-
otelHint?: string
|
|
311
|
-
skipRefresh?: boolean
|
|
312
365
|
debugRefreshReason?: TDebugRefreshReason
|
|
366
|
+
otelContext?: otel.Context
|
|
313
367
|
}
|
|
314
368
|
| undefined,
|
|
315
|
-
otelContext: otel.Context,
|
|
316
369
|
) {
|
|
317
|
-
const
|
|
318
|
-
const skipRefresh = options?.skipRefresh ?? false
|
|
319
|
-
const debugRefreshReason = options?.debugRefreshReason
|
|
370
|
+
const effectsToRefresh = new Set<Effect>()
|
|
320
371
|
for (const [ref, val] of refs) {
|
|
321
|
-
ref.
|
|
322
|
-
this.dirtyNodes.add(ref)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (skipRefresh) {
|
|
326
|
-
const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
|
|
327
|
-
id: uniqueRefreshInfoId(),
|
|
328
|
-
reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
329
|
-
skippedRefresh: true,
|
|
330
|
-
refreshedAtoms: [],
|
|
331
|
-
durationMs: 0,
|
|
332
|
-
completedTimestamp: Date.now(),
|
|
333
|
-
graphSnapshot: this.getSnapshot(),
|
|
334
|
-
}
|
|
335
|
-
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
336
|
-
return
|
|
337
|
-
}
|
|
372
|
+
ref.previousResult = val
|
|
338
373
|
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
get<T>(atom: Atom<T>, context: Atom<any> | Effect): T {
|
|
343
|
-
// Autotracking: if we're getting the value of an atom,
|
|
344
|
-
// that means it's a subcomputation for the currently refreshing atom.
|
|
345
|
-
this.addEdge(context, atom)
|
|
346
|
-
|
|
347
|
-
const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
|
|
348
|
-
const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
|
|
349
|
-
|
|
350
|
-
if (dependencyMightBeStale || dependencyNotRefreshedYet) {
|
|
351
|
-
throw new DependencyNotReadyError(
|
|
352
|
-
`${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
|
|
353
|
-
)
|
|
374
|
+
markSuperCompDirtyRec(ref, effectsToRefresh)
|
|
354
375
|
}
|
|
355
376
|
|
|
356
|
-
|
|
377
|
+
this.runEffects(effectsToRefresh, options)
|
|
357
378
|
}
|
|
358
379
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
* At the end of the refresh, we run any effects that were scheduled.
|
|
363
|
-
*
|
|
364
|
-
* @param roots Root atoms to start the refresh from
|
|
365
|
-
*/
|
|
366
|
-
refresh(
|
|
367
|
-
options:
|
|
380
|
+
private runEffects = (
|
|
381
|
+
effectsToRefresh: Set<Effect>,
|
|
382
|
+
options?:
|
|
368
383
|
| {
|
|
369
|
-
|
|
370
|
-
|
|
384
|
+
debugRefreshReason?: TDebugRefreshReason
|
|
385
|
+
otelContext?: otel.Context
|
|
371
386
|
}
|
|
372
387
|
| undefined,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const debugRefreshReason = options?.debugRefreshReason
|
|
377
|
-
|
|
378
|
-
const roots = [...this.dirtyNodes]
|
|
379
|
-
|
|
380
|
-
const debugInfoForRefreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[] = []
|
|
381
|
-
|
|
382
|
-
// if (otelHint.includes('tableName')) {
|
|
383
|
-
// console.log('refresh', otelHint, { shouldTrace })
|
|
384
|
-
// }
|
|
385
|
-
|
|
386
|
-
this.otelTracer.startActiveSpan(`LiveStore.refresh:${otelHint}`, {}, otelContext, (span) => {
|
|
387
|
-
const atomsToRefresh = roots.filter(isAtom)
|
|
388
|
-
const effectsToRun = new Set(roots.filter(isEffect))
|
|
389
|
-
|
|
390
|
-
span.setAttribute('livestore.hint', otelHint)
|
|
391
|
-
span.setAttribute('livestore.rootsCount', roots.length)
|
|
392
|
-
// span.setAttribute('sstack', new Error().stack!)
|
|
393
|
-
|
|
394
|
-
// Sort in topological order, starting with minimum height
|
|
395
|
-
while (atomsToRefresh.length > 0) {
|
|
396
|
-
atomsToRefresh.sort((a, b) => a.height - b.height)
|
|
397
|
-
const atomToRefresh = atomsToRefresh.shift()!
|
|
398
|
-
|
|
399
|
-
// Recompute the value
|
|
400
|
-
let resultChanged = false
|
|
401
|
-
const debugInfoForAtom = {
|
|
402
|
-
atom: serializeAtom(atomToRefresh),
|
|
403
|
-
resultChanged,
|
|
404
|
-
// debugInfo: unknownRefreshReason() as TDebugThunkInfo,
|
|
405
|
-
debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
|
|
406
|
-
durationMs: 0,
|
|
407
|
-
} satisfies AtomDebugInfo<TDebugThunkInfo>
|
|
408
|
-
try {
|
|
409
|
-
atomToRefresh.sub = new Set()
|
|
410
|
-
const beforeTimestamp = performance.now()
|
|
411
|
-
const newResult = atomToRefresh.getResult(
|
|
412
|
-
(atom) => this.get(atom, atomToRefresh),
|
|
413
|
-
(debugInfo) => {
|
|
414
|
-
debugInfoForAtom.debugInfo = debugInfo
|
|
415
|
-
},
|
|
416
|
-
)
|
|
417
|
-
const afterTimestamp = performance.now()
|
|
418
|
-
debugInfoForAtom.durationMs = afterTimestamp - beforeTimestamp
|
|
419
|
-
|
|
420
|
-
// Determine if the result changed to do early cutoff and avoid further unnecessary updates.
|
|
421
|
-
// Refs never depend on anything, so if a ref is being refreshed it definitely changed.
|
|
422
|
-
// For thunks, we use a deep equality check.
|
|
423
|
-
resultChanged =
|
|
424
|
-
atomToRefresh._tag === 'ref' ||
|
|
425
|
-
(atomToRefresh._tag === 'thunk' && !atomToRefresh.equal(atomToRefresh.result, newResult))
|
|
426
|
-
|
|
427
|
-
if (resultChanged) {
|
|
428
|
-
atomToRefresh.result = newResult
|
|
429
|
-
}
|
|
388
|
+
) => {
|
|
389
|
+
this.effectsWrapper(() => {
|
|
390
|
+
this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
|
|
430
391
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (e instanceof DependencyNotReadyError) {
|
|
434
|
-
// If we hit a dependency that wasn't ready yet,
|
|
435
|
-
// abort this recomputation and try again later.
|
|
436
|
-
if (!atomsToRefresh.includes(atomToRefresh)) {
|
|
437
|
-
atomsToRefresh.push(atomToRefresh)
|
|
438
|
-
}
|
|
439
|
-
} else {
|
|
440
|
-
throw e
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
debugInfoForRefreshedAtoms.push(debugInfoForAtom)
|
|
445
|
-
|
|
446
|
-
if (!resultChanged) {
|
|
447
|
-
continue
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Schedule supercomputations
|
|
451
|
-
for (const superComp of atomToRefresh.super) {
|
|
452
|
-
switch (superComp._tag) {
|
|
453
|
-
case 'ref':
|
|
454
|
-
case 'thunk': {
|
|
455
|
-
if (!atomsToRefresh.includes(superComp)) {
|
|
456
|
-
atomsToRefresh.push(superComp)
|
|
457
|
-
}
|
|
458
|
-
break
|
|
459
|
-
}
|
|
460
|
-
case 'effect': {
|
|
461
|
-
effectsToRun.add(superComp)
|
|
462
|
-
break
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
392
|
+
for (const effect of effectsToRefresh) {
|
|
393
|
+
effect.doEffect(options?.otelContext)
|
|
466
394
|
}
|
|
467
395
|
|
|
468
|
-
this.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
this.dirtyNodes.delete(effect)
|
|
472
|
-
}
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
span.end()
|
|
476
|
-
|
|
477
|
-
const spanDurationHr = (span as any)._duration
|
|
478
|
-
const spanDurationMs = spanDurationHr[0] * 1000 + spanDurationHr[1] / 1_000_000
|
|
396
|
+
const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
|
|
397
|
+
const durationMs = performance.now() - this.currentDebugRefresh.startMs
|
|
398
|
+
this.currentDebugRefresh = undefined
|
|
479
399
|
|
|
480
|
-
const refreshDebugInfo: RefreshDebugInfo<
|
|
481
|
-
RefreshReasonWithGenericReasons<TDebugRefreshReason>,
|
|
482
|
-
TDebugThunkInfo
|
|
483
|
-
> = {
|
|
400
|
+
const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
|
|
484
401
|
id: uniqueRefreshInfoId(),
|
|
485
|
-
reason: debugRefreshReason ?? unknownRefreshReason(),
|
|
486
|
-
refreshedAtoms: debugInfoForRefreshedAtoms,
|
|
402
|
+
reason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
487
403
|
skippedRefresh: false,
|
|
488
|
-
|
|
404
|
+
refreshedAtoms,
|
|
405
|
+
durationMs,
|
|
489
406
|
completedTimestamp: Date.now(),
|
|
490
407
|
graphSnapshot: this.getSnapshot(),
|
|
491
408
|
}
|
|
492
|
-
|
|
493
409
|
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
494
410
|
})
|
|
495
411
|
}
|
|
496
412
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return atom.label ?? `unknown ${atom._tag}`
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
addEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
|
|
413
|
+
addEdge(
|
|
414
|
+
superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
|
|
415
|
+
subComp: Atom<any, TContext, TDebugRefreshReason>,
|
|
416
|
+
) {
|
|
506
417
|
superComp.sub.add(subComp)
|
|
507
418
|
subComp.super.add(superComp)
|
|
508
|
-
this.updateAtomHeight(superComp)
|
|
509
419
|
}
|
|
510
420
|
|
|
511
|
-
removeEdge(
|
|
421
|
+
removeEdge(
|
|
422
|
+
superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
|
|
423
|
+
subComp: Atom<any, TContext, TDebugRefreshReason>,
|
|
424
|
+
) {
|
|
512
425
|
superComp.sub.delete(subComp)
|
|
513
426
|
subComp.super.delete(superComp)
|
|
514
|
-
this.updateAtomHeight(superComp)
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
updateAtomHeight(atom: Atom<any> | Effect) {
|
|
518
|
-
switch (atom._tag) {
|
|
519
|
-
case 'ref': {
|
|
520
|
-
atom.height = 0
|
|
521
|
-
break
|
|
522
|
-
}
|
|
523
|
-
case 'thunk': {
|
|
524
|
-
atom.height = (max([...atom.sub].map((atom) => atom.height)) || 0) + 1
|
|
525
|
-
break
|
|
526
|
-
}
|
|
527
|
-
case 'effect': {
|
|
528
|
-
break
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
427
|
}
|
|
532
428
|
|
|
533
429
|
private getSnapshot = (): ReactiveGraphSnapshot => ({
|
|
534
430
|
atoms: Array.from(this.atoms).map(serializeAtom),
|
|
535
|
-
effects: Array.from(this.effects).map(serializeEffect),
|
|
536
|
-
dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
|
|
431
|
+
// effects: Array.from(this.effects).map(serializeEffect),
|
|
432
|
+
// dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
|
|
537
433
|
})
|
|
538
434
|
}
|
|
539
435
|
|
|
540
|
-
const isAtom = <T>(a: Atom<T> | Effect): a is Atom<T> =>
|
|
541
|
-
|
|
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
|
+
const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
|
|
441
|
+
// const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
|
|
442
|
+
if (atom.isDirty) {
|
|
443
|
+
// console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
|
|
444
|
+
const result = atom.computeResult(otelContext)
|
|
445
|
+
atom.isDirty = false
|
|
446
|
+
atom.previousResult = result
|
|
447
|
+
return result
|
|
448
|
+
} else {
|
|
449
|
+
// console.log('atom is clean', atom.id, atom.label ?? '', atom._tag, __getResult)
|
|
450
|
+
return atom.previousResult as T
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect>) => {
|
|
455
|
+
for (const superComp of atom.super) {
|
|
456
|
+
if (superComp._tag === 'thunk' || superComp._tag === 'ref') {
|
|
457
|
+
superComp.isDirty = true
|
|
458
|
+
markSuperCompDirtyRec(superComp, effectsToRefresh)
|
|
459
|
+
} else {
|
|
460
|
+
effectsToRefresh.add(superComp)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const throwContextNotSetError = (): never => {
|
|
466
|
+
throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph`)
|
|
467
|
+
}
|