@livestore/livestore 0.0.0
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 +108 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/LiveRiffleStore.d.ts +42 -0
- package/dist/LiveRiffleStore.d.ts.map +1 -0
- package/dist/LiveRiffleStore.js +36 -0
- package/dist/LiveRiffleStore.js.map +1 -0
- package/dist/QueryCache.d.ts +20 -0
- package/dist/QueryCache.d.ts.map +1 -0
- package/dist/QueryCache.js +71 -0
- package/dist/QueryCache.js.map +1 -0
- package/dist/__tests__/react/fixture.d.ts +141 -0
- package/dist/__tests__/react/fixture.d.ts.map +1 -0
- package/dist/__tests__/react/fixture.js +72 -0
- package/dist/__tests__/react/fixture.js.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
- package/dist/__tests__/react/useRiffleComponent.test.d.ts +2 -0
- package/dist/__tests__/react/useRiffleComponent.test.d.ts.map +1 -0
- package/dist/__tests__/react/useRiffleComponent.test.js +78 -0
- package/dist/__tests__/react/useRiffleComponent.test.js.map +1 -0
- package/dist/__tests__/reactive.test.d.ts +2 -0
- package/dist/__tests__/reactive.test.d.ts.map +1 -0
- package/dist/__tests__/reactive.test.js +167 -0
- package/dist/__tests__/reactive.test.js.map +1 -0
- package/dist/backends/base.d.ts +13 -0
- package/dist/backends/base.d.ts.map +1 -0
- package/dist/backends/base.js +53 -0
- package/dist/backends/base.js.map +1 -0
- package/dist/backends/index.d.ts +41 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +38 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/noop.d.ts +18 -0
- package/dist/backends/noop.d.ts.map +1 -0
- package/dist/backends/noop.js +21 -0
- package/dist/backends/noop.js.map +1 -0
- package/dist/backends/tauri.d.ts +24 -0
- package/dist/backends/tauri.d.ts.map +1 -0
- package/dist/backends/tauri.js +48 -0
- package/dist/backends/tauri.js.map +1 -0
- package/dist/backends/utils/idb.d.ts +10 -0
- package/dist/backends/utils/idb.d.ts.map +1 -0
- package/dist/backends/utils/idb.js +58 -0
- package/dist/backends/utils/idb.js.map +1 -0
- package/dist/backends/web-in-memory.d.ts +24 -0
- package/dist/backends/web-in-memory.d.ts.map +1 -0
- package/dist/backends/web-in-memory.js +46 -0
- package/dist/backends/web-in-memory.js.map +1 -0
- package/dist/backends/web-worker.d.ts +17 -0
- package/dist/backends/web-worker.d.ts.map +1 -0
- package/dist/backends/web-worker.js +139 -0
- package/dist/backends/web-worker.js.map +1 -0
- package/dist/backends/web.d.ts +28 -0
- package/dist/backends/web.d.ts.map +1 -0
- package/dist/backends/web.js +64 -0
- package/dist/backends/web.js.map +1 -0
- package/dist/bounded-collections.d.ts +34 -0
- package/dist/bounded-collections.d.ts.map +1 -0
- package/dist/bounded-collections.js +103 -0
- package/dist/bounded-collections.js.map +1 -0
- package/dist/componentKey.d.ts +20 -0
- package/dist/componentKey.d.ts.map +1 -0
- package/dist/componentKey.js +3 -0
- package/dist/componentKey.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +42 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +36 -0
- package/dist/effect/LiveStore.js.map +1 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +2 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/inMemoryDatabase.d.ts +65 -0
- package/dist/inMemoryDatabase.d.ts.map +1 -0
- package/dist/inMemoryDatabase.js +241 -0
- package/dist/inMemoryDatabase.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/otel.d.ts +5 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +17 -0
- package/dist/otel.js.map +1 -0
- package/dist/react/LiveStoreContext.d.ts +11 -0
- package/dist/react/LiveStoreContext.d.ts.map +1 -0
- package/dist/react/LiveStoreContext.js +10 -0
- package/dist/react/LiveStoreContext.js.map +1 -0
- package/dist/react/LiveStoreProvider.d.ts +21 -0
- package/dist/react/LiveStoreProvider.d.ts.map +1 -0
- package/dist/react/LiveStoreProvider.js +48 -0
- package/dist/react/LiveStoreProvider.js.map +1 -0
- package/dist/react/RiffleProvider.d.ts +21 -0
- package/dist/react/RiffleProvider.d.ts.map +1 -0
- package/dist/react/RiffleProvider.js +48 -0
- package/dist/react/RiffleProvider.js.map +1 -0
- package/dist/react/StoreContext.d.ts +11 -0
- package/dist/react/StoreContext.d.ts.map +1 -0
- package/dist/react/StoreContext.js +10 -0
- package/dist/react/StoreContext.js.map +1 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.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 +25 -0
- package/dist/react/useGlobalQuery.js.map +1 -0
- package/dist/react/useGraphQL.d.ts +11 -0
- package/dist/react/useGraphQL.d.ts.map +1 -0
- package/dist/react/useGraphQL.js +68 -0
- package/dist/react/useGraphQL.js.map +1 -0
- package/dist/react/useLiveStoreComponent.d.ts +70 -0
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
- package/dist/react/useLiveStoreComponent.js +261 -0
- package/dist/react/useLiveStoreComponent.js.map +1 -0
- package/dist/react/useRiffleComponent.d.ts +70 -0
- package/dist/react/useRiffleComponent.d.ts.map +1 -0
- package/dist/react/useRiffleComponent.js +261 -0
- package/dist/react/useRiffleComponent.js.map +1 -0
- package/dist/react/useRiffleJsonHook.d.ts +4 -0
- package/dist/react/useRiffleJsonHook.d.ts.map +1 -0
- package/dist/react/useRiffleJsonHook.js +21 -0
- package/dist/react/useRiffleJsonHook.js.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
- package/dist/reactive.d.ts +140 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +301 -0
- package/dist/reactive.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +24 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +22 -0
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +25 -0
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/reactiveQueries/graphql.js +14 -0
- package/dist/reactiveQueries/graphql.js.map +1 -0
- package/dist/reactiveQueries/js.d.ts +19 -0
- package/dist/reactiveQueries/js.d.ts.map +1 -0
- package/dist/reactiveQueries/js.js +13 -0
- package/dist/reactiveQueries/js.js.map +1 -0
- package/dist/reactiveQueries/sql.d.ts +31 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.js +28 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/schema.d.ts +163 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +92 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +175 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +546 -0
- package/dist/store.js.map +1 -0
- package/dist/util.d.ts +24 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +51 -0
- package/dist/util.js.map +1 -0
- package/package.json +52 -0
- package/src/QueryCache.ts +81 -0
- package/src/__tests__/react/fixture.tsx +106 -0
- package/src/__tests__/react/useLiveStoreComponent.test.tsx +111 -0
- package/src/__tests__/reactive.test.ts +227 -0
- package/src/ambient.d.ts +7 -0
- package/src/backends/base.ts +67 -0
- package/src/backends/index.ts +94 -0
- package/src/backends/noop.ts +32 -0
- package/src/backends/tauri.ts +74 -0
- package/src/backends/utils/idb.ts +71 -0
- package/src/backends/web-in-memory.ts +65 -0
- package/src/backends/web-worker.ts +176 -0
- package/src/backends/web.ts +96 -0
- package/src/bounded-collections.ts +112 -0
- package/src/componentKey.ts +9 -0
- package/src/effect/LiveStore.ts +123 -0
- package/src/effect/index.ts +7 -0
- package/src/events.ts +8 -0
- package/src/inMemoryDatabase.ts +347 -0
- package/src/index.ts +47 -0
- package/src/otel.ts +20 -0
- package/src/react/LiveStoreContext.ts +23 -0
- package/src/react/LiveStoreProvider.tsx +93 -0
- package/src/react/index.ts +11 -0
- package/src/react/useGlobalQuery.ts +40 -0
- package/src/react/useGraphQL.ts +113 -0
- package/src/react/useLiveStoreComponent.ts +493 -0
- package/src/react/utils/useStateRefWithReactiveInput.ts +51 -0
- package/src/reactive.ts +538 -0
- package/src/reactiveQueries/base-class.ts +49 -0
- package/src/reactiveQueries/graphql.ts +52 -0
- package/src/reactiveQueries/js.ts +38 -0
- package/src/reactiveQueries/sql.ts +65 -0
- package/src/schema.ts +219 -0
- package/src/store.ts +889 -0
- package/src/util.ts +59 -0
- package/tsconfig.json +15 -0
- package/vitest.config.js +13 -0
package/src/reactive.ts
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
// This is a simple implementation of a reactive dependency graph.
|
|
2
|
+
|
|
3
|
+
// Key Terminology:
|
|
4
|
+
// Ref: a mutable cell where values can be set
|
|
5
|
+
// Thunk: a pure computation that depends on other values
|
|
6
|
+
// Effect: a side effect that runs when a value changes; return value is ignored
|
|
7
|
+
// Atom: a node returning a value that can be depended on: Ref | Thunk
|
|
8
|
+
|
|
9
|
+
// Super computation: Nodes that depend on a given node
|
|
10
|
+
// Sub computation: Nodes that a given node depends on
|
|
11
|
+
|
|
12
|
+
// This vocabulary comes from the MiniAdapton paper linked below, although
|
|
13
|
+
// we don't actually implement the MiniAdapton algorithm because we don't need lazy recomputation.
|
|
14
|
+
// https://arxiv.org/abs/1609.05337
|
|
15
|
+
|
|
16
|
+
// Features:
|
|
17
|
+
// - Dependencies are tracked automatically in thunk computations by using a getter function
|
|
18
|
+
// to reference other atoms.
|
|
19
|
+
// - Whenever a ref is updated, the graph is eagerly refreshed to be consistent with the new values.
|
|
20
|
+
// - We minimize recomputation by refreshing the graph in topological sort order. (The topological height
|
|
21
|
+
// is maintained eagerly as edges are added and removed.)
|
|
22
|
+
// - At every thunk we check value equality with the previous value and cutoff propagation if possible.
|
|
23
|
+
|
|
24
|
+
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
25
|
+
|
|
26
|
+
import type { PrettifyFlat } from '@livestore/utils'
|
|
27
|
+
import { pick } from '@livestore/utils'
|
|
28
|
+
import type * as otel from '@opentelemetry/api'
|
|
29
|
+
import { isEqual, max, uniqueId } from 'lodash-es'
|
|
30
|
+
|
|
31
|
+
import { BoundArray } from './bounded-collections.js'
|
|
32
|
+
|
|
33
|
+
export type GetAtom = <T>(atom: Atom<T>) => T
|
|
34
|
+
|
|
35
|
+
export type Ref<T> = {
|
|
36
|
+
_tag: 'ref'
|
|
37
|
+
id: string
|
|
38
|
+
result: T
|
|
39
|
+
height: 0
|
|
40
|
+
getResult: () => T
|
|
41
|
+
sub: Set<Atom<any>> // always empty
|
|
42
|
+
super: Set<Atom<any> | Effect>
|
|
43
|
+
label?: string
|
|
44
|
+
/** Container for meta information (e.g. the LiveStore Store) */
|
|
45
|
+
meta?: any
|
|
46
|
+
equal: (a: T, b: T) => boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type BaseThunk<T> = {
|
|
50
|
+
_tag: 'thunk'
|
|
51
|
+
id: string
|
|
52
|
+
height: number
|
|
53
|
+
getResult: (get: GetAtom, addDebugInfo: (debugInfo: any) => void) => T
|
|
54
|
+
sub: Set<Atom<any>>
|
|
55
|
+
super: Set<Atom<any> | Effect>
|
|
56
|
+
label?: string
|
|
57
|
+
/** Container for meta information (e.g. the LiveStore Store) */
|
|
58
|
+
meta?: any
|
|
59
|
+
equal: (a: T, b: T) => boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type UnevaluatedThunk<T> = BaseThunk<T> & { result: undefined }
|
|
63
|
+
export type Thunk<T> = BaseThunk<T> & { result: T }
|
|
64
|
+
|
|
65
|
+
export type Atom<T> = Ref<T> | Thunk<T>
|
|
66
|
+
|
|
67
|
+
export type Effect = {
|
|
68
|
+
_tag: 'effect'
|
|
69
|
+
id: string
|
|
70
|
+
doEffect: (get: GetAtom) => void
|
|
71
|
+
sub: Set<Atom<any>>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class DependencyNotReadyError extends Error {
|
|
75
|
+
constructor(message: string) {
|
|
76
|
+
super(message)
|
|
77
|
+
this.name = 'DependencyNotReadyError'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type Taggable<T extends string = string> = { _tag: T }
|
|
82
|
+
|
|
83
|
+
export type ReactiveGraphOptions = {
|
|
84
|
+
effectsWrapper?: (runEffects: () => void) => void
|
|
85
|
+
otelTracer: otel.Tracer
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type AtomDebugInfo<TDebugThunkInfo extends Taggable> = {
|
|
89
|
+
atom: SerializedAtom
|
|
90
|
+
resultChanged: boolean
|
|
91
|
+
durationMs: number
|
|
92
|
+
debugInfo: TDebugThunkInfo
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends Taggable> = {
|
|
96
|
+
/** Currently only used for easier handling in React (e.g. as key) */
|
|
97
|
+
id: string
|
|
98
|
+
reason: TDebugRefreshReason
|
|
99
|
+
refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]
|
|
100
|
+
skippedRefresh: boolean
|
|
101
|
+
durationMs: number
|
|
102
|
+
/** Note we're using a regular `Date.now()` timestamp here as it's faster to produce and we don't need the fine accuracy */
|
|
103
|
+
completedTimestamp: number
|
|
104
|
+
graphSnapshot: ReactiveGraphSnapshot
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type RefreshReasonWithGenericReasons<T extends Taggable> =
|
|
108
|
+
| T
|
|
109
|
+
| {
|
|
110
|
+
_tag: 'makeThunk'
|
|
111
|
+
label?: string
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
_tag: 'makeEffect'
|
|
115
|
+
label?: string
|
|
116
|
+
}
|
|
117
|
+
| { _tag: 'unknown' }
|
|
118
|
+
|
|
119
|
+
export const unknownRefreshReason = () => {
|
|
120
|
+
debugger
|
|
121
|
+
return { _tag: 'unknown' as const }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type SerializedAtom = Readonly<
|
|
125
|
+
PrettifyFlat<
|
|
126
|
+
Pick<Atom<unknown>, '_tag' | 'height' | 'id' | 'label' | 'meta' | 'result'> & {
|
|
127
|
+
sub: string[]
|
|
128
|
+
super: string[]
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
>
|
|
132
|
+
|
|
133
|
+
export type SerializedEffect = Readonly<PrettifyFlat<Pick<Effect, '_tag' | 'id'>>>
|
|
134
|
+
|
|
135
|
+
type ReactiveGraphSnapshot = {
|
|
136
|
+
readonly atoms: SerializedAtom[]
|
|
137
|
+
readonly effects: SerializedEffect[]
|
|
138
|
+
/** IDs of atoms and effects that are dirty */
|
|
139
|
+
readonly dirtyNodes: string[]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const uniqueNodeId = () => uniqueId('node-')
|
|
143
|
+
const uniqueRefreshInfoId = () => uniqueId('refresh-info-')
|
|
144
|
+
|
|
145
|
+
const serializeAtom = (atom: Atom<any>): SerializedAtom => ({
|
|
146
|
+
...pick(atom, ['_tag', 'height', 'id', 'label', 'meta', 'result']),
|
|
147
|
+
sub: Array.from(atom.sub).map((a) => a.id),
|
|
148
|
+
super: Array.from(atom.super).map((a) => a.id),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
|
|
152
|
+
|
|
153
|
+
export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends Taggable> {
|
|
154
|
+
private atoms: Set<Atom<any>> = new Set()
|
|
155
|
+
private effects: Set<Effect> = new Set()
|
|
156
|
+
private otelTracer: otel.Tracer
|
|
157
|
+
readonly dirtyNodes: Set<Atom<any> | Effect> = new Set()
|
|
158
|
+
effectsWrapper: (runEffects: () => void) => void
|
|
159
|
+
|
|
160
|
+
debugRefreshInfos: BoundArray<
|
|
161
|
+
RefreshDebugInfo<RefreshReasonWithGenericReasons<TDebugRefreshReason>, TDebugThunkInfo>
|
|
162
|
+
> = new BoundArray(5000)
|
|
163
|
+
|
|
164
|
+
constructor(options: ReactiveGraphOptions) {
|
|
165
|
+
this.effectsWrapper = options?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
|
|
166
|
+
this.otelTracer = options.otelTracer
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
makeRef<T>(val: T, options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean }): Ref<T> {
|
|
170
|
+
const ref: Ref<T> = {
|
|
171
|
+
_tag: 'ref',
|
|
172
|
+
id: uniqueNodeId(),
|
|
173
|
+
result: val,
|
|
174
|
+
height: 0,
|
|
175
|
+
getResult: () => ref.result,
|
|
176
|
+
sub: new Set(),
|
|
177
|
+
super: new Set(),
|
|
178
|
+
label: options?.label,
|
|
179
|
+
meta: options?.meta,
|
|
180
|
+
equal: options?.equal ?? isEqual,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.atoms.add(ref)
|
|
184
|
+
|
|
185
|
+
return ref
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
makeThunk<T>(
|
|
189
|
+
getResult: (get: GetAtom, addDebugInfo: (debugInfo: TDebugThunkInfo) => void) => T,
|
|
190
|
+
options:
|
|
191
|
+
| {
|
|
192
|
+
label?: string
|
|
193
|
+
meta?: any
|
|
194
|
+
equal?: (a: T, b: T) => boolean
|
|
195
|
+
/** Debug info for initializing the thunk (i.e. running it the first time) */
|
|
196
|
+
debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
197
|
+
}
|
|
198
|
+
| undefined,
|
|
199
|
+
parentSpanContext: otel.Context,
|
|
200
|
+
): Thunk<T> {
|
|
201
|
+
const thunk: UnevaluatedThunk<T> = {
|
|
202
|
+
_tag: 'thunk',
|
|
203
|
+
id: uniqueNodeId(),
|
|
204
|
+
result: undefined,
|
|
205
|
+
height: 0,
|
|
206
|
+
getResult,
|
|
207
|
+
sub: new Set(),
|
|
208
|
+
super: new Set(),
|
|
209
|
+
label: options?.label,
|
|
210
|
+
meta: options?.meta,
|
|
211
|
+
equal: options?.equal ?? isEqual,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.atoms.add(thunk)
|
|
215
|
+
this.dirtyNodes.add(thunk)
|
|
216
|
+
this.refresh(
|
|
217
|
+
{
|
|
218
|
+
otelHint: options?.label ?? 'makeThunk',
|
|
219
|
+
debugRefreshReason: options?.debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label },
|
|
220
|
+
},
|
|
221
|
+
parentSpanContext,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// Manually tell the typesystem this thunk is guaranteed to have a result at this point
|
|
225
|
+
return thunk as unknown as Thunk<T>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
destroy(node: Atom<any> | Effect) {
|
|
229
|
+
// Recursively destroy any supercomputations
|
|
230
|
+
if (node._tag === 'ref' || node._tag === 'thunk') {
|
|
231
|
+
for (const superComp of node.super) {
|
|
232
|
+
this.destroy(superComp)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Destroy this node
|
|
237
|
+
for (const subComp of node.sub) {
|
|
238
|
+
this.removeEdge(node, subComp)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (node._tag === 'effect') {
|
|
242
|
+
this.effects.delete(node)
|
|
243
|
+
} else {
|
|
244
|
+
this.atoms.delete(node)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
makeEffect(
|
|
249
|
+
doEffect: (get: GetAtom) => void,
|
|
250
|
+
options: { label?: string } | undefined,
|
|
251
|
+
parentSpanContext: otel.Context,
|
|
252
|
+
): Effect {
|
|
253
|
+
const effect: Effect = {
|
|
254
|
+
_tag: 'effect',
|
|
255
|
+
id: uniqueNodeId(),
|
|
256
|
+
doEffect,
|
|
257
|
+
sub: new Set(),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.effects.add(effect)
|
|
261
|
+
this.dirtyNodes.add(effect)
|
|
262
|
+
this.refresh(
|
|
263
|
+
{ otelHint: 'makeEffect', debugRefreshReason: { _tag: 'makeEffect', label: options?.label } },
|
|
264
|
+
parentSpanContext,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return effect
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
setRef<T>(
|
|
271
|
+
ref: Ref<T>,
|
|
272
|
+
val: T,
|
|
273
|
+
options:
|
|
274
|
+
| {
|
|
275
|
+
otelHint?: string
|
|
276
|
+
skipRefresh?: boolean
|
|
277
|
+
debugRefreshReason?: TDebugRefreshReason
|
|
278
|
+
}
|
|
279
|
+
| undefined,
|
|
280
|
+
parentSpanContext: otel.Context,
|
|
281
|
+
) {
|
|
282
|
+
const { otelHint, skipRefresh, debugRefreshReason } = options ?? {}
|
|
283
|
+
ref.result = val
|
|
284
|
+
this.dirtyNodes.add(ref)
|
|
285
|
+
|
|
286
|
+
if (skipRefresh) {
|
|
287
|
+
const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
|
|
288
|
+
id: uniqueRefreshInfoId(),
|
|
289
|
+
reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
290
|
+
skippedRefresh: true,
|
|
291
|
+
refreshedAtoms: [],
|
|
292
|
+
durationMs: 0,
|
|
293
|
+
completedTimestamp: Date.now(),
|
|
294
|
+
graphSnapshot: this.getSnapshot(),
|
|
295
|
+
}
|
|
296
|
+
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.refresh({ otelHint, debugRefreshReason }, parentSpanContext)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
setRefs<T>(
|
|
304
|
+
refs: [Ref<T>, T][],
|
|
305
|
+
options:
|
|
306
|
+
| {
|
|
307
|
+
otelHint?: string
|
|
308
|
+
skipRefresh?: boolean
|
|
309
|
+
debugRefreshReason?: TDebugRefreshReason
|
|
310
|
+
}
|
|
311
|
+
| undefined,
|
|
312
|
+
parentSpanContext: otel.Context,
|
|
313
|
+
) {
|
|
314
|
+
const otelHint = options?.otelHint ?? ''
|
|
315
|
+
const skipRefresh = options?.skipRefresh ?? false
|
|
316
|
+
const debugRefreshReason = options?.debugRefreshReason
|
|
317
|
+
for (const [ref, val] of refs) {
|
|
318
|
+
ref.result = val
|
|
319
|
+
this.dirtyNodes.add(ref)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (skipRefresh) {
|
|
323
|
+
const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
|
|
324
|
+
id: uniqueRefreshInfoId(),
|
|
325
|
+
reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
|
|
326
|
+
skippedRefresh: true,
|
|
327
|
+
refreshedAtoms: [],
|
|
328
|
+
durationMs: 0,
|
|
329
|
+
completedTimestamp: Date.now(),
|
|
330
|
+
graphSnapshot: this.getSnapshot(),
|
|
331
|
+
}
|
|
332
|
+
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.refresh({ otelHint, debugRefreshReason }, parentSpanContext)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get<T>(atom: Atom<T>, context: Atom<any> | Effect): T {
|
|
340
|
+
// Autotracking: if we're getting the value of an atom,
|
|
341
|
+
// that means it's a subcomputation for the currently refreshing atom.
|
|
342
|
+
this.addEdge(context, atom)
|
|
343
|
+
|
|
344
|
+
const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
|
|
345
|
+
const dependencyNotRefreshedYet = atom.result === undefined
|
|
346
|
+
|
|
347
|
+
if (dependencyMightBeStale || dependencyNotRefreshedYet) {
|
|
348
|
+
throw new DependencyNotReadyError(
|
|
349
|
+
`${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return atom.result!
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Update the graph to be consistent with the current values of the root atoms.
|
|
358
|
+
* Generally we run this after a ref is updated.
|
|
359
|
+
* At the end of the refresh, we run any effects that were scheduled.
|
|
360
|
+
*
|
|
361
|
+
* @param roots Root atoms to start the refresh from
|
|
362
|
+
*/
|
|
363
|
+
refresh(
|
|
364
|
+
options:
|
|
365
|
+
| {
|
|
366
|
+
otelHint?: string
|
|
367
|
+
debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
|
|
368
|
+
}
|
|
369
|
+
| undefined,
|
|
370
|
+
parentSpanContext: otel.Context,
|
|
371
|
+
): void {
|
|
372
|
+
const otelHint = options?.otelHint ?? ''
|
|
373
|
+
const debugRefreshReason = options?.debugRefreshReason
|
|
374
|
+
|
|
375
|
+
const roots = [...this.dirtyNodes]
|
|
376
|
+
|
|
377
|
+
const debugInfoForRefreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[] = []
|
|
378
|
+
|
|
379
|
+
// if (otelHint.includes('tableName')) {
|
|
380
|
+
// console.log('refresh', otelHint, { shouldTrace })
|
|
381
|
+
// }
|
|
382
|
+
|
|
383
|
+
this.otelTracer.startActiveSpan(`LiveStore.refresh:${otelHint}`, {}, parentSpanContext, (span) => {
|
|
384
|
+
const atomsToRefresh = roots.filter(isAtom)
|
|
385
|
+
const effectsToRun = new Set(roots.filter(isEffect))
|
|
386
|
+
|
|
387
|
+
span.setAttribute('livestore.hint', otelHint)
|
|
388
|
+
span.setAttribute('livestore.rootsCount', roots.length)
|
|
389
|
+
// span.setAttribute('sstack', new Error().stack!)
|
|
390
|
+
|
|
391
|
+
// Sort in topological order, starting with minimum height
|
|
392
|
+
while (atomsToRefresh.length > 0) {
|
|
393
|
+
atomsToRefresh.sort((a, b) => a.height - b.height)
|
|
394
|
+
const atomToRefresh = atomsToRefresh.shift()!
|
|
395
|
+
|
|
396
|
+
// Recompute the value
|
|
397
|
+
let resultChanged = false
|
|
398
|
+
const debugInfoForAtom = {
|
|
399
|
+
atom: serializeAtom(atomToRefresh),
|
|
400
|
+
resultChanged,
|
|
401
|
+
// debugInfo: unknownRefreshReason() as TDebugThunkInfo,
|
|
402
|
+
debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
|
|
403
|
+
durationMs: 0,
|
|
404
|
+
} satisfies AtomDebugInfo<TDebugThunkInfo>
|
|
405
|
+
try {
|
|
406
|
+
atomToRefresh.sub = new Set()
|
|
407
|
+
const beforeTimestamp = performance.now()
|
|
408
|
+
const newResult = atomToRefresh.getResult(
|
|
409
|
+
(atom) => this.get(atom, atomToRefresh),
|
|
410
|
+
(debugInfo) => {
|
|
411
|
+
debugInfoForAtom.debugInfo = debugInfo
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
const afterTimestamp = performance.now()
|
|
415
|
+
debugInfoForAtom.durationMs = afterTimestamp - beforeTimestamp
|
|
416
|
+
|
|
417
|
+
// Determine if the result changed to do early cutoff and avoid further unnecessary updates.
|
|
418
|
+
// Refs never depend on anything, so if a ref is being refreshed it definitely changed.
|
|
419
|
+
// For thunks, we use a deep equality check.
|
|
420
|
+
resultChanged =
|
|
421
|
+
atomToRefresh._tag === 'ref' ||
|
|
422
|
+
(atomToRefresh._tag === 'thunk' && !atomToRefresh.equal(atomToRefresh.result, newResult))
|
|
423
|
+
|
|
424
|
+
if (resultChanged) {
|
|
425
|
+
atomToRefresh.result = newResult
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.dirtyNodes.delete(atomToRefresh)
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (e instanceof DependencyNotReadyError) {
|
|
431
|
+
// If we hit a dependency that wasn't ready yet,
|
|
432
|
+
// abort this recomputation and try again later.
|
|
433
|
+
if (!atomsToRefresh.includes(atomToRefresh)) {
|
|
434
|
+
atomsToRefresh.push(atomToRefresh)
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
throw e
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
debugInfoForRefreshedAtoms.push(debugInfoForAtom)
|
|
442
|
+
|
|
443
|
+
if (!resultChanged) {
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Schedule supercomputations
|
|
448
|
+
for (const superComp of atomToRefresh.super) {
|
|
449
|
+
switch (superComp._tag) {
|
|
450
|
+
case 'ref':
|
|
451
|
+
case 'thunk': {
|
|
452
|
+
if (!atomsToRefresh.includes(superComp)) {
|
|
453
|
+
atomsToRefresh.push(superComp)
|
|
454
|
+
}
|
|
455
|
+
break
|
|
456
|
+
}
|
|
457
|
+
case 'effect': {
|
|
458
|
+
effectsToRun.add(superComp)
|
|
459
|
+
break
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this.effectsWrapper(() => {
|
|
466
|
+
for (const effect of effectsToRun) {
|
|
467
|
+
effect.doEffect((atom: Atom<any>) => this.get(atom, effect))
|
|
468
|
+
this.dirtyNodes.delete(effect)
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
span.end()
|
|
473
|
+
|
|
474
|
+
const spanDurationHr = (span as any)._duration
|
|
475
|
+
const spanDurationMs = spanDurationHr[0] * 1000 + spanDurationHr[1] / 1_000_000
|
|
476
|
+
|
|
477
|
+
const refreshDebugInfo: RefreshDebugInfo<
|
|
478
|
+
RefreshReasonWithGenericReasons<TDebugRefreshReason>,
|
|
479
|
+
TDebugThunkInfo
|
|
480
|
+
> = {
|
|
481
|
+
id: uniqueRefreshInfoId(),
|
|
482
|
+
reason: debugRefreshReason ?? unknownRefreshReason(),
|
|
483
|
+
refreshedAtoms: debugInfoForRefreshedAtoms,
|
|
484
|
+
skippedRefresh: false,
|
|
485
|
+
durationMs: spanDurationMs,
|
|
486
|
+
completedTimestamp: Date.now(),
|
|
487
|
+
graphSnapshot: this.getSnapshot(),
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
label(atom: Atom<any> | Effect) {
|
|
495
|
+
if (atom._tag === 'effect') {
|
|
496
|
+
return `unknown effect`
|
|
497
|
+
} else {
|
|
498
|
+
return atom.label ?? `unknown ${atom._tag}`
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
addEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
|
|
503
|
+
superComp.sub.add(subComp)
|
|
504
|
+
subComp.super.add(superComp)
|
|
505
|
+
this.updateAtomHeight(superComp)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
removeEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
|
|
509
|
+
superComp.sub.delete(subComp)
|
|
510
|
+
subComp.super.delete(superComp)
|
|
511
|
+
this.updateAtomHeight(superComp)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
updateAtomHeight(atom: Atom<any> | Effect) {
|
|
515
|
+
switch (atom._tag) {
|
|
516
|
+
case 'ref': {
|
|
517
|
+
atom.height = 0
|
|
518
|
+
break
|
|
519
|
+
}
|
|
520
|
+
case 'thunk': {
|
|
521
|
+
atom.height = (max([...atom.sub].map((atom) => atom.height)) || 0) + 1
|
|
522
|
+
break
|
|
523
|
+
}
|
|
524
|
+
case 'effect': {
|
|
525
|
+
break
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private getSnapshot = (): ReactiveGraphSnapshot => ({
|
|
531
|
+
atoms: Array.from(this.atoms).map(serializeAtom),
|
|
532
|
+
effects: Array.from(this.effects).map(serializeEffect),
|
|
533
|
+
dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const isAtom = <T>(a: Atom<T> | Effect): a is Atom<T> => a._tag === 'ref' || a._tag === 'thunk'
|
|
538
|
+
const isEffect = <T>(a: Atom<T> | Effect): a is Effect => a._tag === 'effect'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
3
|
+
import type { ComponentKey } from '../componentKey.js'
|
|
4
|
+
import type { Store } from '../store.js'
|
|
5
|
+
|
|
6
|
+
export type UnsubscribeQuery = () => void
|
|
7
|
+
|
|
8
|
+
export abstract class LiveStoreQueryBase {
|
|
9
|
+
/** The key for the associated component */
|
|
10
|
+
componentKey: ComponentKey
|
|
11
|
+
/** Human-readable label for the query for debugging */
|
|
12
|
+
label: string
|
|
13
|
+
/** A pointer back to the store containing this query */
|
|
14
|
+
store: Store<any>
|
|
15
|
+
/** Otel Span is started in LiveStore store but ended in this query */
|
|
16
|
+
otelContext: otel.Context
|
|
17
|
+
|
|
18
|
+
/** The string key is used to identify a subscription from "outside" */
|
|
19
|
+
activeSubscriptions: Map<string, UnsubscribeQuery> = new Map()
|
|
20
|
+
|
|
21
|
+
constructor({
|
|
22
|
+
componentKey,
|
|
23
|
+
label,
|
|
24
|
+
store,
|
|
25
|
+
otelContext,
|
|
26
|
+
}: {
|
|
27
|
+
componentKey: ComponentKey
|
|
28
|
+
label: string
|
|
29
|
+
store: Store<any>
|
|
30
|
+
otelContext: otel.Context
|
|
31
|
+
}) {
|
|
32
|
+
this.componentKey = componentKey
|
|
33
|
+
this.label = label
|
|
34
|
+
this.store = store
|
|
35
|
+
this.otelContext = otelContext
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
destroy = () => {
|
|
39
|
+
const span = otel.trace.getSpan(this.otelContext)!
|
|
40
|
+
span.end()
|
|
41
|
+
|
|
42
|
+
// NOTE usually the `unsubscribe` function is called by `useLiveStoreComponent` but this code path
|
|
43
|
+
// is used for manual store destruction, so we need to manually unsubscribe here
|
|
44
|
+
for (const [_key, unsubscribe] of this.activeSubscriptions) {
|
|
45
|
+
// unsubscribe from the query
|
|
46
|
+
unsubscribe()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
|
|
2
|
+
import type * as otel from '@opentelemetry/api'
|
|
3
|
+
|
|
4
|
+
import type { ComponentKey } from '../componentKey.js'
|
|
5
|
+
import type { GetAtom, Thunk } from '../reactive.js'
|
|
6
|
+
import type { BaseGraphQLContext, Store } from '../store.js'
|
|
7
|
+
import { LiveStoreQueryBase } from './base-class.js'
|
|
8
|
+
import type { LiveStoreJSQuery } from './js.js'
|
|
9
|
+
|
|
10
|
+
export class LiveStoreGraphQLQuery<
|
|
11
|
+
TResult extends Record<string, any>,
|
|
12
|
+
VariableValues extends Record<string, any>,
|
|
13
|
+
TContext extends BaseGraphQLContext,
|
|
14
|
+
> extends LiveStoreQueryBase {
|
|
15
|
+
_tag: 'graphql' = 'graphql'
|
|
16
|
+
|
|
17
|
+
/** The abstract GraphQL query */
|
|
18
|
+
document: DocumentNode<TResult, VariableValues>
|
|
19
|
+
|
|
20
|
+
/** A reactive thunk representing the query results */
|
|
21
|
+
results$: Thunk<TResult>
|
|
22
|
+
|
|
23
|
+
constructor({
|
|
24
|
+
document,
|
|
25
|
+
results$,
|
|
26
|
+
...baseProps
|
|
27
|
+
}: {
|
|
28
|
+
document: DocumentNode<TResult, VariableValues>
|
|
29
|
+
context: TContext
|
|
30
|
+
results$: Thunk<TResult>
|
|
31
|
+
componentKey: ComponentKey
|
|
32
|
+
label: string
|
|
33
|
+
store: Store<TContext>
|
|
34
|
+
otelContext: otel.Context
|
|
35
|
+
}) {
|
|
36
|
+
super(baseProps)
|
|
37
|
+
|
|
38
|
+
this.document = document
|
|
39
|
+
this.results$ = results$
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pipe = <U>(f: (x: TResult, get: GetAtom) => U): LiveStoreJSQuery<U> =>
|
|
43
|
+
this.store.queryJS(
|
|
44
|
+
(get) => {
|
|
45
|
+
const results = get(this.results$)
|
|
46
|
+
return f(results, get)
|
|
47
|
+
},
|
|
48
|
+
this.componentKey,
|
|
49
|
+
`${this.label}:js`,
|
|
50
|
+
this.otelContext,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
3
|
+
import type { ComponentKey } from '../componentKey.js'
|
|
4
|
+
import type { GetAtom, Thunk } from '../reactive.js'
|
|
5
|
+
import type { Store } from '../store.js'
|
|
6
|
+
import { LiveStoreQueryBase } from './base-class.js'
|
|
7
|
+
|
|
8
|
+
export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase {
|
|
9
|
+
_tag: 'js' = 'js'
|
|
10
|
+
/** A reactive thunk representing the query results */
|
|
11
|
+
results$: Thunk<TResult>
|
|
12
|
+
|
|
13
|
+
constructor({
|
|
14
|
+
results$,
|
|
15
|
+
...baseProps
|
|
16
|
+
}: {
|
|
17
|
+
results$: Thunk<TResult>
|
|
18
|
+
componentKey: ComponentKey
|
|
19
|
+
label: string
|
|
20
|
+
store: Store<any>
|
|
21
|
+
otelContext: otel.Context
|
|
22
|
+
}) {
|
|
23
|
+
super(baseProps)
|
|
24
|
+
|
|
25
|
+
this.results$ = results$
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pipe = <U>(f: (x: TResult, get: GetAtom) => U): LiveStoreJSQuery<U> =>
|
|
29
|
+
this.store.queryJS(
|
|
30
|
+
(get) => {
|
|
31
|
+
const results = get(this.results$)
|
|
32
|
+
return f(results, get)
|
|
33
|
+
},
|
|
34
|
+
this.componentKey,
|
|
35
|
+
`${this.label}:js`,
|
|
36
|
+
this.otelContext,
|
|
37
|
+
)
|
|
38
|
+
}
|