@livestore/livestore 0.0.34 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +3 -1
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +6 -3
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/useRow.test.js +10 -10
- package/dist/__tests__/react/useRow.test.js.map +1 -1
- package/dist/__tests__/reactive.test.js +13 -2
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/global-state.d.ts +1 -4
- package/dist/global-state.d.ts.map +1 -1
- package/dist/global-state.js +2 -6
- package/dist/global-state.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +9 -3
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +1 -0
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useRow.d.ts +7 -3
- package/dist/react/useRow.d.ts.map +1 -1
- package/dist/react/useRow.js +7 -5
- package/dist/react/useRow.js.map +1 -1
- package/dist/reactive.d.ts +21 -6
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +60 -12
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +5 -2
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +8 -3
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +6 -3
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +10 -7
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +6 -2
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +8 -5
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +5 -2
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +11 -6
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/row-query.d.ts +3 -0
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +11 -6
- package/dist/row-query.js.map +1 -1
- package/dist/store.d.ts +6 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +29 -15
- package/dist/store.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/react/fixture.tsx +8 -2
- package/src/__tests__/react/useRow.test.tsx +10 -10
- package/src/__tests__/reactive.test.ts +20 -2
- package/src/global-state.ts +2 -9
- package/src/index.ts +3 -3
- package/src/react/LiveStoreProvider.tsx +13 -4
- package/src/react/useQuery.ts +2 -0
- package/src/react/useRow.ts +17 -8
- package/src/reactive.ts +96 -16
- package/src/reactiveQueries/base-class.ts +15 -4
- package/src/reactiveQueries/graphql.ts +15 -8
- package/src/reactiveQueries/js.ts +14 -6
- package/src/reactiveQueries/sql.ts +15 -6
- package/src/row-query.ts +20 -7
- package/src/store.ts +38 -17
|
@@ -11,13 +11,13 @@ describe('useRow', () => {
|
|
|
11
11
|
it('should update the data based on component key', async () => {
|
|
12
12
|
let renderCount = 0
|
|
13
13
|
|
|
14
|
-
const { wrapper, AppComponentSchema, store } = await makeTodoMvc()
|
|
14
|
+
const { wrapper, AppComponentSchema, store, dbGraph } = await makeTodoMvc({ useGlobalDbGraph: false })
|
|
15
15
|
|
|
16
16
|
const { result, rerender } = renderHook(
|
|
17
17
|
(userId: string) => {
|
|
18
18
|
renderCount++
|
|
19
19
|
|
|
20
|
-
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId)
|
|
20
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
|
|
21
21
|
return { state, setState }
|
|
22
22
|
},
|
|
23
23
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -41,13 +41,13 @@ describe('useRow', () => {
|
|
|
41
41
|
it('should update the data reactively - via setState', async () => {
|
|
42
42
|
let renderCount = 0
|
|
43
43
|
|
|
44
|
-
const { wrapper, AppComponentSchema } = await makeTodoMvc()
|
|
44
|
+
const { wrapper, AppComponentSchema, dbGraph } = await makeTodoMvc({ useGlobalDbGraph: false })
|
|
45
45
|
|
|
46
46
|
const { result } = renderHook(
|
|
47
47
|
(userId: string) => {
|
|
48
48
|
renderCount++
|
|
49
49
|
|
|
50
|
-
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId)
|
|
50
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
|
|
51
51
|
return { state, setState }
|
|
52
52
|
},
|
|
53
53
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -67,13 +67,13 @@ describe('useRow', () => {
|
|
|
67
67
|
it('should update the data reactively - via raw store update', async () => {
|
|
68
68
|
let renderCount = 0
|
|
69
69
|
|
|
70
|
-
const { wrapper, AppComponentSchema, store } = await makeTodoMvc()
|
|
70
|
+
const { wrapper, AppComponentSchema, store, dbGraph } = await makeTodoMvc({ useGlobalDbGraph: false })
|
|
71
71
|
|
|
72
72
|
const { result } = renderHook(
|
|
73
73
|
(userId: string) => {
|
|
74
74
|
renderCount++
|
|
75
75
|
|
|
76
|
-
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId)
|
|
76
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { dbGraph })
|
|
77
77
|
return { state, setState }
|
|
78
78
|
},
|
|
79
79
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -95,9 +95,9 @@ describe('useRow', () => {
|
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
it('should work for a larger app', async () => {
|
|
98
|
-
const
|
|
98
|
+
const { wrapper, store, dbGraph } = await makeTodoMvc({ useGlobalDbGraph: false })
|
|
99
99
|
|
|
100
|
-
const {
|
|
100
|
+
const allTodos$ = LiveStore.querySQL<Todo>(`select * from todos`, { label: 'allTodos', dbGraph })
|
|
101
101
|
|
|
102
102
|
const AppRouterSchema = LiveStore.DbSchema.table(
|
|
103
103
|
'AppRouter',
|
|
@@ -112,7 +112,7 @@ describe('useRow', () => {
|
|
|
112
112
|
const AppRouter: React.FC = () => {
|
|
113
113
|
appRouterRenderCount++
|
|
114
114
|
|
|
115
|
-
const [state, setState] = LiveStoreReact.useRow(AppRouterSchema)
|
|
115
|
+
const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { dbGraph })
|
|
116
116
|
|
|
117
117
|
globalSetState = setState
|
|
118
118
|
|
|
@@ -141,7 +141,7 @@ describe('useRow', () => {
|
|
|
141
141
|
|
|
142
142
|
const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
|
|
143
143
|
const todo = LiveStoreReact.useTemporaryQuery(() =>
|
|
144
|
-
LiveStore.querySQL<Todo>(`select * from todos where id = '${id}' limit 1
|
|
144
|
+
LiveStore.querySQL<Todo>(`select * from todos where id = '${id}' limit 1`, { dbGraph }).getFirstRow(),
|
|
145
145
|
)
|
|
146
146
|
return <div role="content">{JSON.stringify(todo)}</div>
|
|
147
147
|
}
|
|
@@ -221,7 +221,7 @@ describe('a trivial graph', () => {
|
|
|
221
221
|
expect(numberOfEffect2Runs).toBe(1)
|
|
222
222
|
expect(numberOfRunsForC.runs).toBe(1)
|
|
223
223
|
|
|
224
|
-
graph.
|
|
224
|
+
graph.destroyNode(effect1)
|
|
225
225
|
|
|
226
226
|
graph.runDeferredEffects()
|
|
227
227
|
|
|
@@ -231,6 +231,24 @@ describe('a trivial graph', () => {
|
|
|
231
231
|
})
|
|
232
232
|
})
|
|
233
233
|
})
|
|
234
|
+
|
|
235
|
+
describe('destroying nodes', () => {
|
|
236
|
+
it('marks super node as dirty when a sub node is destroyed', () => {
|
|
237
|
+
const { graph, b, c, d, e } = makeGraph()
|
|
238
|
+
|
|
239
|
+
e.computeResult()
|
|
240
|
+
|
|
241
|
+
graph.destroyNode(b)
|
|
242
|
+
|
|
243
|
+
expect(c.isDirty).toBe(true)
|
|
244
|
+
expect(d.isDirty).toBe(false)
|
|
245
|
+
expect(e.isDirty).toBe(true)
|
|
246
|
+
|
|
247
|
+
expect(() => c.computeResult()).toThrowErrorMatchingInlineSnapshot(
|
|
248
|
+
`[Error: This should never happen LiveStore Error: Attempted to compute destroyed atom]`,
|
|
249
|
+
)
|
|
250
|
+
})
|
|
251
|
+
})
|
|
234
252
|
})
|
|
235
253
|
|
|
236
254
|
describe('a dynamic graph', () => {
|
|
@@ -402,7 +420,7 @@ describe('error handling', () => {
|
|
|
402
420
|
const a = graph.makeRef(1)
|
|
403
421
|
const b = graph.makeThunk((get) => get(a) + 1)
|
|
404
422
|
expect(() => b.computeResult()).toThrowErrorMatchingInlineSnapshot(
|
|
405
|
-
`[Error: LiveStore Error: \`context\` not set on ReactiveGraph]`,
|
|
423
|
+
`[Error: LiveStore Error: \`context\` not set on ReactiveGraph (graph-19)]`,
|
|
406
424
|
)
|
|
407
425
|
})
|
|
408
426
|
})
|
package/src/global-state.ts
CHANGED
|
@@ -11,16 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import { ReactiveGraph } from './reactive.js'
|
|
17
|
-
import type { DbContext } from './reactiveQueries/base-class.js'
|
|
14
|
+
import { makeDbGraph } from './reactiveQueries/base-class.js'
|
|
18
15
|
import type { TableDef } from './schema/table-def.js'
|
|
19
|
-
import type { QueryDebugInfo, RefreshReason } from './store.js'
|
|
20
16
|
|
|
21
|
-
export const
|
|
22
|
-
// TODO also find a better way to only use this effects wrapper when used in a React app
|
|
23
|
-
effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
|
|
24
|
-
})
|
|
17
|
+
export const globalDbGraph = makeDbGraph()
|
|
25
18
|
|
|
26
19
|
export const dynamicallyRegisteredTables: Map<string, TableDef> = new Map()
|
package/src/index.ts
CHANGED
|
@@ -7,13 +7,13 @@ export { InMemoryDatabase, type DebugInfo, emptyDebugInfo } from './inMemoryData
|
|
|
7
7
|
|
|
8
8
|
export type { Storage, StorageType, StorageInit } from './storage/index.js'
|
|
9
9
|
|
|
10
|
-
export type { GetAtom, AtomDebugInfo, RefreshDebugInfo, SerializedAtom, Atom } from './reactive.js'
|
|
10
|
+
export type { GetAtom, AtomDebugInfo, RefreshDebugInfo, SerializedAtom, Atom, Node, Ref, Effect } from './reactive.js'
|
|
11
11
|
export { LiveStoreJSQuery, queryJS } from './reactiveQueries/js.js'
|
|
12
12
|
export { LiveStoreSQLQuery, querySQL } from './reactiveQueries/sql.js'
|
|
13
13
|
export { LiveStoreGraphQLQuery, queryGraphQL } from './reactiveQueries/graphql.js'
|
|
14
|
-
export { type GetAtomResult } from './reactiveQueries/base-class.js'
|
|
14
|
+
export { type GetAtomResult, type DbGraph, makeDbGraph } from './reactiveQueries/base-class.js'
|
|
15
15
|
|
|
16
|
-
export {
|
|
16
|
+
export { globalDbGraph } from './global-state.js'
|
|
17
17
|
|
|
18
18
|
export { type RowResult, type RowResultEncoded, type RowQueryArgs, rowQuery } from './row-query.js'
|
|
19
19
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
1
2
|
import type * as otel from '@opentelemetry/api'
|
|
2
3
|
import type { ReactElement, ReactNode } from 'react'
|
|
3
4
|
import React from 'react'
|
|
@@ -8,7 +9,7 @@ import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } f
|
|
|
8
9
|
import type { InMemoryDatabase } from '../inMemoryDatabase.js'
|
|
9
10
|
import type { LiveStoreSchema } from '../schema/index.js'
|
|
10
11
|
import type { StorageInit } from '../storage/index.js'
|
|
11
|
-
import type { BaseGraphQLContext, GraphQLOptions } from '../store.js'
|
|
12
|
+
import type { BaseGraphQLContext, GraphQLOptions, Store } from '../store.js'
|
|
12
13
|
import { createStore } from '../store.js'
|
|
13
14
|
import { LiveStoreContext } from './LiveStoreContext.js'
|
|
14
15
|
|
|
@@ -68,10 +69,15 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
68
69
|
const [ctxValue, setCtxValue] = React.useState<StoreContext_ | undefined>()
|
|
69
70
|
|
|
70
71
|
React.useEffect(() => {
|
|
72
|
+
let store: Store | undefined
|
|
73
|
+
|
|
74
|
+
// resetting the store context while we're creating a new store
|
|
75
|
+
setCtxValue(undefined)
|
|
76
|
+
|
|
71
77
|
void (async () => {
|
|
72
78
|
try {
|
|
73
79
|
const sqlite3 = await sqlite3Promise
|
|
74
|
-
|
|
80
|
+
store = await createStore({
|
|
75
81
|
schema,
|
|
76
82
|
loadStorage,
|
|
77
83
|
graphQLOptions,
|
|
@@ -82,11 +88,14 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
82
88
|
})
|
|
83
89
|
setCtxValue({ store })
|
|
84
90
|
} catch (e) {
|
|
85
|
-
|
|
86
|
-
throw e
|
|
91
|
+
shouldNeverHappen(`Error creating LiveStore store: ${e}`)
|
|
87
92
|
}
|
|
88
93
|
})()
|
|
89
94
|
|
|
95
|
+
return () => {
|
|
96
|
+
store?.destroy()
|
|
97
|
+
}
|
|
98
|
+
|
|
90
99
|
// TODO: do we need to return any cleanup function here?
|
|
91
100
|
}, [schema, loadStorage, graphQLOptions, otelTracer, otelRootSpanContext, boot])
|
|
92
101
|
|
package/src/react/useQuery.ts
CHANGED
|
@@ -22,6 +22,8 @@ export const useQueryRef = <TResult>(
|
|
|
22
22
|
): React.MutableRefObject<TResult> => {
|
|
23
23
|
const { store } = useStore()
|
|
24
24
|
|
|
25
|
+
React.useDebugValue(`LiveStore:useQuery:${query.id}:${query.label}`)
|
|
26
|
+
|
|
25
27
|
const stackInfo = React.useMemo(() => {
|
|
26
28
|
Error.stackTraceLimit = 10
|
|
27
29
|
// eslint-disable-next-line unicorn/error-message
|
package/src/react/useRow.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { SqliteDsl } from 'effect-db-schema'
|
|
|
4
4
|
import { mapValues } from 'lodash-es'
|
|
5
5
|
import React from 'react'
|
|
6
6
|
|
|
7
|
+
import type { DbGraph } from '../index.js'
|
|
7
8
|
import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
|
|
8
9
|
import type { RowQueryArgs, RowResult } from '../row-query.js'
|
|
9
10
|
import { rowQuery } from '../row-query.js'
|
|
@@ -17,10 +18,14 @@ export type UseRowResult<TTableDef extends TableDef> = [
|
|
|
17
18
|
query$: LiveStoreJSQuery<RowResult<TTableDef>>,
|
|
18
19
|
]
|
|
19
20
|
|
|
20
|
-
export type
|
|
21
|
+
export type UseRowOptionsDefaulValues<TTableDef extends TableDef> = {
|
|
21
22
|
defaultValues?: Partial<RowResult<TTableDef>>
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export type UseRowOptionsBase = {
|
|
26
|
+
dbGraph?: DbGraph
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
/**
|
|
25
30
|
* Similar to `React.useState` but returns a tuple of `[row, setRow, query$]` for a given table where ...
|
|
26
31
|
*
|
|
@@ -33,20 +38,24 @@ export type UseRowOptions<TTableDef extends TableDef> = {
|
|
|
33
38
|
export const useRow: {
|
|
34
39
|
<TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: true }>>(
|
|
35
40
|
table: TTableDef,
|
|
41
|
+
options?: UseRowOptionsBase,
|
|
36
42
|
): UseRowResult<TTableDef>
|
|
37
43
|
<TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: false }>>(
|
|
38
44
|
table: TTableDef,
|
|
39
45
|
// TODO adjust so it works with arbitrary primary keys or unique constraints
|
|
40
46
|
id: string,
|
|
41
|
-
options?:
|
|
47
|
+
options?: UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>,
|
|
42
48
|
): UseRowResult<TTableDef>
|
|
43
49
|
} = <TTableDef extends TableDef>(
|
|
44
50
|
table: TTableDef,
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
idOrOptions?: string | UseRowOptionsBase,
|
|
52
|
+
options_?: UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>,
|
|
47
53
|
): UseRowResult<TTableDef> => {
|
|
48
54
|
const sqliteTableDef = table.schema
|
|
49
|
-
const
|
|
55
|
+
const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
|
|
56
|
+
const options: (UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>) | undefined =
|
|
57
|
+
typeof idOrOptions === 'string' ? options_ : idOrOptions
|
|
58
|
+
const { defaultValues, dbGraph } = options ?? {}
|
|
50
59
|
type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['schema']['columns']>
|
|
51
60
|
|
|
52
61
|
const { store } = useStore()
|
|
@@ -74,13 +83,13 @@ export const useRow: {
|
|
|
74
83
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
75
84
|
|
|
76
85
|
const query$ = table.options.isSingleton
|
|
77
|
-
? rowQuery({ table, store, otelContext, defaultValues } as RowQueryArgs<TTableDef>)
|
|
78
|
-
: rowQuery({ table, store, id, otelContext, defaultValues } as RowQueryArgs<TTableDef>)
|
|
86
|
+
? rowQuery({ table, store, otelContext, defaultValues, dbGraph } as RowQueryArgs<TTableDef>)
|
|
87
|
+
: rowQuery({ table, store, id, otelContext, defaultValues, dbGraph } as RowQueryArgs<TTableDef>)
|
|
79
88
|
|
|
80
89
|
rcCache.set(table, id ?? 'singleton', query$, reactId, otelContext, span)
|
|
81
90
|
|
|
82
91
|
return { query$, otelContext }
|
|
83
|
-
}, [table, id, reactId, store, defaultValues])
|
|
92
|
+
}, [table, id, reactId, store, defaultValues, dbGraph])
|
|
84
93
|
|
|
85
94
|
React.useEffect(
|
|
86
95
|
() => () => {
|
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 } from '@livestore/utils'
|
|
27
|
+
import { pick, shouldNeverHappen } from '@livestore/utils'
|
|
28
28
|
import type * as otel from '@opentelemetry/api'
|
|
29
|
-
import { isEqual
|
|
29
|
+
import { isEqual } from 'lodash-es'
|
|
30
30
|
|
|
31
31
|
import { BoundArray } from './utils/bounded-collections.js'
|
|
32
32
|
// import { getDurationMsFromSpan } from './otel.js'
|
|
@@ -40,24 +40,27 @@ export type Ref<T, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
|
|
|
40
40
|
_tag: 'ref'
|
|
41
41
|
id: string
|
|
42
42
|
isDirty: false
|
|
43
|
+
isDestroyed: boolean
|
|
43
44
|
previousResult: T
|
|
44
45
|
computeResult: () => T
|
|
45
46
|
sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
|
|
46
|
-
super: Set<
|
|
47
|
+
super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect>
|
|
47
48
|
label?: string
|
|
48
49
|
/** Container for meta information (e.g. the LiveStore Store) */
|
|
49
50
|
meta?: any
|
|
50
51
|
equal: (a: T, b: T) => boolean
|
|
52
|
+
refreshes: number
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
export type Thunk<TResult, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
|
|
54
56
|
_tag: 'thunk'
|
|
55
57
|
id: string
|
|
56
58
|
isDirty: boolean
|
|
59
|
+
isDestroyed: boolean
|
|
57
60
|
computeResult: (otelContext?: otel.Context, debugRefreshReason?: TDebugRefreshReason) => TResult
|
|
58
61
|
previousResult: TResult | NOT_REFRESHED_YET
|
|
59
62
|
sub: Set<Atom<any, TContext, TDebugRefreshReason>>
|
|
60
|
-
super: Set<
|
|
63
|
+
super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect>
|
|
61
64
|
label?: string
|
|
62
65
|
/** Container for meta information (e.g. the LiveStore Store) */
|
|
63
66
|
meta?: any
|
|
@@ -74,11 +77,17 @@ export type Atom<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
|
|
|
74
77
|
export type Effect = {
|
|
75
78
|
_tag: 'effect'
|
|
76
79
|
id: string
|
|
80
|
+
isDestroyed: boolean
|
|
77
81
|
doEffect: (otelContext?: otel.Context) => void
|
|
78
82
|
sub: Set<Atom<any, TODO, TODO>>
|
|
79
83
|
label?: string
|
|
84
|
+
invocations: number
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
export type Node<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
|
|
88
|
+
| Atom<T, TContext, TDebugRefreshReason>
|
|
89
|
+
| Effect
|
|
90
|
+
|
|
82
91
|
export type DebugThunkInfo<T extends string = string> = {
|
|
83
92
|
_tag: T
|
|
84
93
|
durationMs: number
|
|
@@ -133,14 +142,25 @@ export type SerializedAtom = Readonly<
|
|
|
133
142
|
>
|
|
134
143
|
>
|
|
135
144
|
|
|
145
|
+
export type SerializedEffect = Readonly<
|
|
146
|
+
PrettifyFlat<
|
|
147
|
+
Pick<Effect, '_tag' | 'id' | 'label'> & {
|
|
148
|
+
sub: ReadonlyArray<string>
|
|
149
|
+
}
|
|
150
|
+
>
|
|
151
|
+
>
|
|
152
|
+
|
|
136
153
|
type ReactiveGraphSnapshot = {
|
|
137
154
|
readonly atoms: ReadonlyArray<SerializedAtom>
|
|
155
|
+
readonly effects: ReadonlyArray<SerializedEffect>
|
|
138
156
|
/** IDs of deferred effects */
|
|
139
157
|
readonly deferredEffects: ReadonlyArray<string>
|
|
140
158
|
}
|
|
141
159
|
|
|
142
|
-
|
|
143
|
-
const
|
|
160
|
+
let nodeIdCounter = 0
|
|
161
|
+
const uniqueNodeId = () => `node-${++nodeIdCounter}`
|
|
162
|
+
let refreshInfoIdCounter = 0
|
|
163
|
+
const uniqueRefreshInfoId = () => `refresh-info-${++refreshInfoIdCounter}`
|
|
144
164
|
|
|
145
165
|
const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
|
|
146
166
|
...pick(atom, ['_tag', 'id', 'label', 'meta', 'isDirty']),
|
|
@@ -148,12 +168,23 @@ const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
|
|
|
148
168
|
super: Array.from(atom.super).map((a) => a.id),
|
|
149
169
|
})
|
|
150
170
|
|
|
171
|
+
const serializeEffect = (effect: Effect): SerializedEffect => ({
|
|
172
|
+
...pick(effect, ['_tag', 'id', 'label']),
|
|
173
|
+
sub: Array.from(effect.sub).map((a) => a.id),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
let globalGraphIdCounter = 0
|
|
177
|
+
const uniqueGraphId = () => `graph-${++globalGraphIdCounter}`
|
|
178
|
+
|
|
151
179
|
export class ReactiveGraph<
|
|
152
180
|
TDebugRefreshReason extends DebugRefreshReason,
|
|
153
181
|
TDebugThunkInfo extends DebugThunkInfo,
|
|
154
182
|
TContext = {},
|
|
155
183
|
> {
|
|
184
|
+
id = uniqueGraphId()
|
|
185
|
+
|
|
156
186
|
readonly atoms: Set<Atom<any, TContext, TDebugRefreshReason>> = new Set()
|
|
187
|
+
readonly effects: Set<Effect> = new Set()
|
|
157
188
|
effectsWrapper: (runEffects: () => void) => void
|
|
158
189
|
|
|
159
190
|
context: TContext | undefined
|
|
@@ -166,6 +197,8 @@ export class ReactiveGraph<
|
|
|
166
197
|
|
|
167
198
|
private deferredEffects: Map<Effect, Set<TDebugRefreshReason>> = new Map()
|
|
168
199
|
|
|
200
|
+
private refreshCallbacks: Set<() => void> = new Set()
|
|
201
|
+
|
|
169
202
|
constructor(options: ReactiveGraphOptions) {
|
|
170
203
|
this.effectsWrapper = options?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
|
|
171
204
|
}
|
|
@@ -178,6 +211,7 @@ export class ReactiveGraph<
|
|
|
178
211
|
_tag: 'ref',
|
|
179
212
|
id: uniqueNodeId(),
|
|
180
213
|
isDirty: false,
|
|
214
|
+
isDestroyed: false,
|
|
181
215
|
previousResult: val,
|
|
182
216
|
computeResult: () => ref.previousResult,
|
|
183
217
|
sub: new Set(),
|
|
@@ -185,6 +219,7 @@ export class ReactiveGraph<
|
|
|
185
219
|
label: options?.label,
|
|
186
220
|
meta: options?.meta,
|
|
187
221
|
equal: options?.equal ?? isEqual,
|
|
222
|
+
refreshes: 0,
|
|
188
223
|
}
|
|
189
224
|
|
|
190
225
|
this.atoms.add(ref)
|
|
@@ -212,6 +247,7 @@ export class ReactiveGraph<
|
|
|
212
247
|
id: uniqueNodeId(),
|
|
213
248
|
previousResult: NOT_REFRESHED_YET,
|
|
214
249
|
isDirty: true,
|
|
250
|
+
isDestroyed: false,
|
|
215
251
|
computeResult: (otelContext, debugRefreshReason) => {
|
|
216
252
|
if (thunk.isDirty) {
|
|
217
253
|
const neededCurrentRefresh = this.currentDebugRefresh === undefined
|
|
@@ -235,7 +271,7 @@ export class ReactiveGraph<
|
|
|
235
271
|
const result = getResult(
|
|
236
272
|
getAtom as GetAtom,
|
|
237
273
|
setDebugInfo,
|
|
238
|
-
this.context ?? throwContextNotSetError(),
|
|
274
|
+
this.context ?? throwContextNotSetError(this),
|
|
239
275
|
otelContext,
|
|
240
276
|
)
|
|
241
277
|
|
|
@@ -288,24 +324,38 @@ export class ReactiveGraph<
|
|
|
288
324
|
return thunk
|
|
289
325
|
}
|
|
290
326
|
|
|
291
|
-
|
|
327
|
+
destroyNode(node: Node<any, TContext, TDebugRefreshReason>) {
|
|
328
|
+
// console.debug(`destroying node (${node._tag})`, node.id, node.label)
|
|
329
|
+
|
|
292
330
|
// Recursively destroy any supercomputations
|
|
293
331
|
if (node._tag === 'ref' || node._tag === 'thunk') {
|
|
294
332
|
for (const superComp of node.super) {
|
|
295
|
-
this.
|
|
333
|
+
this.destroyNode(superComp)
|
|
296
334
|
}
|
|
297
335
|
}
|
|
298
336
|
|
|
299
337
|
// Destroy this node
|
|
300
|
-
|
|
301
|
-
|
|
338
|
+
if (node._tag !== 'ref') {
|
|
339
|
+
for (const subComp of node.sub) {
|
|
340
|
+
this.removeEdge(node, subComp)
|
|
341
|
+
}
|
|
302
342
|
}
|
|
303
343
|
|
|
304
344
|
if (node._tag === 'effect') {
|
|
305
345
|
this.deferredEffects.delete(node)
|
|
346
|
+
this.effects.delete(node)
|
|
306
347
|
} else {
|
|
307
348
|
this.atoms.delete(node)
|
|
308
349
|
}
|
|
350
|
+
|
|
351
|
+
node.isDestroyed = true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
destroy() {
|
|
355
|
+
// NOTE we don't need to sort the atoms first, as `destroyNode` will recursively destroy all supercomputations
|
|
356
|
+
for (const node of this.atoms) {
|
|
357
|
+
this.destroyNode(node)
|
|
358
|
+
}
|
|
309
359
|
}
|
|
310
360
|
|
|
311
361
|
makeEffect(
|
|
@@ -315,7 +365,10 @@ export class ReactiveGraph<
|
|
|
315
365
|
const effect: Effect = {
|
|
316
366
|
_tag: 'effect',
|
|
317
367
|
id: uniqueNodeId(),
|
|
368
|
+
isDestroyed: false,
|
|
318
369
|
doEffect: (otelContext) => {
|
|
370
|
+
effect.invocations++
|
|
371
|
+
|
|
319
372
|
// NOTE we're not tracking any debug refresh info for effects as they're tracked by the thunks they depend on
|
|
320
373
|
|
|
321
374
|
// Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
|
|
@@ -330,8 +383,11 @@ export class ReactiveGraph<
|
|
|
330
383
|
},
|
|
331
384
|
sub: new Set(),
|
|
332
385
|
label: options?.label,
|
|
386
|
+
invocations: 0,
|
|
333
387
|
}
|
|
334
388
|
|
|
389
|
+
this.effects.add(effect)
|
|
390
|
+
|
|
335
391
|
return effect
|
|
336
392
|
}
|
|
337
393
|
|
|
@@ -362,6 +418,7 @@ export class ReactiveGraph<
|
|
|
362
418
|
const effectsToRefresh = new Set<Effect>()
|
|
363
419
|
for (const [ref, val] of refs) {
|
|
364
420
|
ref.previousResult = val
|
|
421
|
+
ref.refreshes++
|
|
365
422
|
|
|
366
423
|
markSuperCompDirtyRec(ref, effectsToRefresh)
|
|
367
424
|
}
|
|
@@ -412,6 +469,10 @@ export class ReactiveGraph<
|
|
|
412
469
|
graphSnapshot: this.getSnapshot(),
|
|
413
470
|
}
|
|
414
471
|
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
472
|
+
|
|
473
|
+
for (const cb of this.refreshCallbacks) {
|
|
474
|
+
cb()
|
|
475
|
+
}
|
|
415
476
|
})
|
|
416
477
|
}
|
|
417
478
|
|
|
@@ -432,7 +493,7 @@ export class ReactiveGraph<
|
|
|
432
493
|
}
|
|
433
494
|
|
|
434
495
|
addEdge(
|
|
435
|
-
superComp:
|
|
496
|
+
superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect,
|
|
436
497
|
subComp: Atom<any, TContext, TDebugRefreshReason>,
|
|
437
498
|
) {
|
|
438
499
|
superComp.sub.add(subComp)
|
|
@@ -440,21 +501,40 @@ export class ReactiveGraph<
|
|
|
440
501
|
}
|
|
441
502
|
|
|
442
503
|
removeEdge(
|
|
443
|
-
superComp:
|
|
504
|
+
superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect,
|
|
444
505
|
subComp: Atom<any, TContext, TDebugRefreshReason>,
|
|
445
506
|
) {
|
|
446
507
|
superComp.sub.delete(subComp)
|
|
508
|
+
const effectsToRefresh = new Set<Effect>()
|
|
509
|
+
markSuperCompDirtyRec(subComp, effectsToRefresh)
|
|
510
|
+
|
|
511
|
+
for (const effect of effectsToRefresh) {
|
|
512
|
+
this.deferredEffects.set(effect, new Set())
|
|
513
|
+
}
|
|
514
|
+
|
|
447
515
|
subComp.super.delete(superComp)
|
|
448
516
|
}
|
|
449
517
|
|
|
450
518
|
getSnapshot = (): ReactiveGraphSnapshot => ({
|
|
451
519
|
atoms: Array.from(this.atoms).map(serializeAtom),
|
|
520
|
+
effects: Array.from(this.effects).map(serializeEffect),
|
|
452
521
|
deferredEffects: Array.from(this.deferredEffects.keys()).map((_) => _.id),
|
|
453
522
|
})
|
|
523
|
+
|
|
524
|
+
subscribeToRefresh = (cb: () => void) => {
|
|
525
|
+
this.refreshCallbacks.add(cb)
|
|
526
|
+
return () => {
|
|
527
|
+
this.refreshCallbacks.delete(cb)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
454
530
|
}
|
|
455
531
|
|
|
456
532
|
const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
|
|
457
533
|
// const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
|
|
534
|
+
if (atom.isDestroyed) {
|
|
535
|
+
shouldNeverHappen(`LiveStore Error: Attempted to compute destroyed atom`)
|
|
536
|
+
}
|
|
537
|
+
|
|
458
538
|
if (atom.isDirty) {
|
|
459
539
|
// console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
|
|
460
540
|
const result = atom.computeResult(otelContext)
|
|
@@ -469,7 +549,7 @@ const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T =
|
|
|
469
549
|
|
|
470
550
|
const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect>) => {
|
|
471
551
|
for (const superComp of atom.super) {
|
|
472
|
-
if (superComp._tag === 'thunk'
|
|
552
|
+
if (superComp._tag === 'thunk') {
|
|
473
553
|
superComp.isDirty = true
|
|
474
554
|
markSuperCompDirtyRec(superComp, effectsToRefresh)
|
|
475
555
|
} else {
|
|
@@ -478,6 +558,6 @@ const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh:
|
|
|
478
558
|
}
|
|
479
559
|
}
|
|
480
560
|
|
|
481
|
-
export const throwContextNotSetError = (): never => {
|
|
482
|
-
throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph`)
|
|
561
|
+
export const throwContextNotSetError = (graph: ReactiveGraph<any, any, any>): never => {
|
|
562
|
+
throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph (${graph.id})`)
|
|
483
563
|
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
import ReactDOM from 'react-dom'
|
|
2
3
|
|
|
3
|
-
import { dbGraph } from '../global-state.js'
|
|
4
4
|
import type { StackInfo } from '../react/utils/stack-info.js'
|
|
5
|
-
import { type Atom, type GetAtom, throwContextNotSetError, type Thunk } from '../reactive.js'
|
|
6
|
-
import type { RefreshReason, Store } from '../store.js'
|
|
5
|
+
import { type Atom, type GetAtom, ReactiveGraph, throwContextNotSetError, type Thunk } from '../reactive.js'
|
|
6
|
+
import type { QueryDebugInfo, RefreshReason, Store } from '../store.js'
|
|
7
7
|
import type { LiveStoreJSQuery } from './js.js'
|
|
8
8
|
|
|
9
|
+
export type DbGraph = ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
|
|
10
|
+
|
|
11
|
+
export const makeDbGraph = (): DbGraph =>
|
|
12
|
+
new ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>({
|
|
13
|
+
// TODO also find a better way to only use this effects wrapper when used in a React app
|
|
14
|
+
effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
|
|
15
|
+
})
|
|
16
|
+
|
|
9
17
|
export type DbContext = {
|
|
10
18
|
store: Store
|
|
11
19
|
otelTracer: otel.Tracer
|
|
@@ -41,6 +49,8 @@ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TRe
|
|
|
41
49
|
|
|
42
50
|
activeSubscriptions: Set<StackInfo> = new Set()
|
|
43
51
|
|
|
52
|
+
protected abstract dbGraph: DbGraph
|
|
53
|
+
|
|
44
54
|
get runs() {
|
|
45
55
|
return this.results$.recomputations
|
|
46
56
|
}
|
|
@@ -61,7 +71,8 @@ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TRe
|
|
|
61
71
|
onUnsubsubscribe?: () => void,
|
|
62
72
|
options?: { label?: string; otelContext?: otel.Context } | undefined,
|
|
63
73
|
): (() => void) =>
|
|
64
|
-
dbGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ??
|
|
74
|
+
this.dbGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ??
|
|
75
|
+
throwContextNotSetError(this.dbGraph)
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
export type GetAtomResult = <T>(atom: Atom<T, any, RefreshReason> | LiveStoreJSQuery<T>) => T
|