@livestore/livestore 0.0.19 → 0.0.21

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.
Files changed (126) hide show
  1. package/README.md +18 -21
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.d.ts +1 -1
  4. package/dist/QueryCache.d.ts.map +1 -1
  5. package/dist/QueryCache.js.map +1 -1
  6. package/dist/__tests__/react/fixture.d.ts +5 -4
  7. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  8. package/dist/__tests__/react/fixture.js +5 -5
  9. package/dist/__tests__/react/fixture.js.map +1 -1
  10. package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
  11. package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
  12. package/dist/__tests__/react/useComponentState.test.js +68 -0
  13. package/dist/__tests__/react/useComponentState.test.js.map +1 -0
  14. package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
  15. package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
  16. package/dist/__tests__/react/useLQuery.test.js +38 -0
  17. package/dist/__tests__/react/useLQuery.test.js.map +1 -0
  18. package/dist/__tests__/react/useLiveStoreComponent.test.js +4 -9
  19. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -1
  20. package/dist/__tests__/react/useQuery.test.d.ts +2 -0
  21. package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
  22. package/dist/__tests__/react/useQuery.test.js +33 -0
  23. package/dist/__tests__/react/useQuery.test.js.map +1 -0
  24. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
  25. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
  26. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
  27. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
  28. package/dist/__tests__/reactive.test.js +167 -93
  29. package/dist/__tests__/reactive.test.js.map +1 -1
  30. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  31. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  32. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  33. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  34. package/dist/inMemoryDatabase.d.ts +2 -2
  35. package/dist/inMemoryDatabase.d.ts.map +1 -1
  36. package/dist/index.d.ts +7 -5
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +3 -3
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +2 -2
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react/useComponentState.d.ts +50 -0
  45. package/dist/react/useComponentState.d.ts.map +1 -0
  46. package/dist/react/useComponentState.js +248 -0
  47. package/dist/react/useComponentState.js.map +1 -0
  48. package/dist/react/useGlobalQuery.d.ts +3 -0
  49. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  50. package/dist/react/useGlobalQuery.js +26 -0
  51. package/dist/react/useGlobalQuery.js.map +1 -0
  52. package/dist/react/useGraphQL.d.ts +3 -3
  53. package/dist/react/useGraphQL.d.ts.map +1 -1
  54. package/dist/react/useGraphQL.js +10 -8
  55. package/dist/react/useGraphQL.js.map +1 -1
  56. package/dist/react/useLiveStoreComponent.d.ts +6 -6
  57. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  58. package/dist/react/useLiveStoreComponent.js +143 -99
  59. package/dist/react/useLiveStoreComponent.js.map +1 -1
  60. package/dist/react/useQuery.d.ts +2 -2
  61. package/dist/react/useQuery.d.ts.map +1 -1
  62. package/dist/react/useQuery.js +26 -22
  63. package/dist/react/useQuery.js.map +1 -1
  64. package/dist/react/useTemporaryQuery.d.ts +8 -0
  65. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  66. package/dist/react/useTemporaryQuery.js +17 -0
  67. package/dist/react/useTemporaryQuery.js.map +1 -0
  68. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  69. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  70. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  71. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  72. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  73. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  74. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  75. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  76. package/dist/reactive.d.ts +42 -48
  77. package/dist/reactive.d.ts.map +1 -1
  78. package/dist/reactive.js +293 -186
  79. package/dist/reactive.js.map +1 -1
  80. package/dist/reactiveQueries/base-class.d.ts +28 -23
  81. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  82. package/dist/reactiveQueries/base-class.js +25 -18
  83. package/dist/reactiveQueries/base-class.js.map +1 -1
  84. package/dist/reactiveQueries/graph.d.ts +10 -0
  85. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  86. package/dist/reactiveQueries/graph.js +6 -0
  87. package/dist/reactiveQueries/graph.js.map +1 -0
  88. package/dist/reactiveQueries/graphql.d.ts +34 -17
  89. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  90. package/dist/reactiveQueries/graphql.js +91 -10
  91. package/dist/reactiveQueries/graphql.js.map +1 -1
  92. package/dist/reactiveQueries/js.d.ts +16 -12
  93. package/dist/reactiveQueries/js.d.ts.map +1 -1
  94. package/dist/reactiveQueries/js.js +31 -8
  95. package/dist/reactiveQueries/js.js.map +1 -1
  96. package/dist/reactiveQueries/sql.d.ts +22 -18
  97. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  98. package/dist/reactiveQueries/sql.js +82 -16
  99. package/dist/reactiveQueries/sql.js.map +1 -1
  100. package/dist/store.d.ts +12 -52
  101. package/dist/store.d.ts.map +1 -1
  102. package/dist/store.js +283 -264
  103. package/dist/store.js.map +1 -1
  104. package/package.json +4 -3
  105. package/src/QueryCache.ts +1 -1
  106. package/src/__tests__/react/fixture.tsx +12 -7
  107. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  108. package/src/__tests__/react/useQuery.test.tsx +48 -0
  109. package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
  110. package/src/__tests__/reactive.test.ts +193 -140
  111. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  112. package/src/inMemoryDatabase.ts +2 -2
  113. package/src/index.ts +7 -11
  114. package/src/react/index.ts +3 -7
  115. package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +89 -247
  116. package/src/react/useQuery.ts +29 -27
  117. package/src/react/useTemporaryQuery.ts +21 -0
  118. package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
  119. package/src/reactive.ts +385 -268
  120. package/src/reactiveQueries/base-class.ts +60 -44
  121. package/src/reactiveQueries/graph.ts +15 -0
  122. package/src/reactiveQueries/graphql.ts +145 -29
  123. package/src/reactiveQueries/js.ts +53 -20
  124. package/src/reactiveQueries/sql.ts +129 -36
  125. package/src/store.ts +338 -408
  126. package/src/react/useGraphQL.ts +0 -138
package/dist/store.js CHANGED
@@ -1,21 +1,15 @@
1
1
  import { assertNever, makeNoopSpan, makeNoopTracer, shouldNeverHappen } from '@livestore/utils';
2
2
  import { identity } from '@livestore/utils/effect';
3
3
  import * as otel from '@opentelemetry/api';
4
- import * as graphql from 'graphql';
5
- import { uniqueId } from 'lodash-es';
6
- import * as ReactDOM from 'react-dom';
7
4
  import { v4 as uuid } from 'uuid';
8
5
  import { tableNameForComponentKey } from './componentKey.js';
9
6
  import { InMemoryDatabase } from './inMemoryDatabase.js';
10
7
  import { migrateDb } from './migrations.js';
11
8
  import { getDurationMsFromSpan } from './otel.js';
12
- import { ReactiveGraph } from './reactive.js';
13
- import { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js';
14
- import { LiveStoreJSQuery } from './reactiveQueries/js.js';
15
- import { LiveStoreSQLQuery } from './reactiveQueries/sql.js';
9
+ import { dbGraph } from './reactiveQueries/graph.js';
16
10
  import { componentStateTables } from './schema.js';
17
11
  import { isPromise, prepareBindValues, sql } from './util.js';
18
- const globalComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' };
12
+ export const globalComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' };
19
13
  export class Store {
20
14
  constructor({ db, dbProxy, schema, storage, graphQLOptions, otelTracer, otelRootSpanContext, }) {
21
15
  /**
@@ -23,254 +17,274 @@ export class Store {
23
17
  *
24
18
  * NOTE The query is actually running (even if no one has subscribed to it yet) and will be kept up to date.
25
19
  */
26
- this.querySQL = (genQueryString, { queriedTables, bindValues, componentKey, label, otelContext = otel.context.active(), }) => this.otel.tracer.startActiveSpan('querySQL', // NOTE span name will be overridden further down
27
- { attributes: { label } }, otelContext, (span) => {
28
- const otelContext = otel.trace.setSpan(otel.context.active(), span);
29
- const queryString$ = this.graph.makeThunk((get, addDebugInfo) => {
30
- if (typeof genQueryString === 'function') {
31
- const getAtom = (atom) => {
32
- if (atom._tag === 'thunk' || atom._tag === 'ref')
33
- return get(atom);
34
- return get(atom.results$);
35
- };
36
- const queryString = genQueryString(getAtom);
37
- addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString });
38
- return queryString;
39
- }
40
- else {
41
- return genQueryString;
42
- }
43
- }, { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } }, otelContext);
44
- label = label ?? queryString$.result;
45
- span.updateName(`querySQL:${label}`);
46
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '');
47
- const results$ = this.graph.makeThunk((get, addDebugInfo) => this.otel.tracer.startActiveSpan('sql:', // NOTE span name will be overridden further down
48
- {}, otelContext, (span) => {
49
- try {
50
- const otelContext = otel.trace.setSpan(otel.context.active(), span);
51
- // Establish a reactive dependency on the tables used in the query
52
- for (const tableName of queriedTables) {
53
- const tableRef = this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`);
54
- get(tableRef);
55
- }
56
- const sqlString = get(queryString$);
57
- span.setAttribute('sql.query', sqlString);
58
- span.updateName(`sql:${sqlString.slice(0, 50)}`);
59
- const results = this.inMemoryDB.select(sqlString, {
60
- queriedTables,
61
- bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
62
- otelContext,
63
- });
64
- span.setAttribute('sql.rowsCount', results.length);
65
- addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString });
66
- return results;
67
- }
68
- finally {
69
- span.end();
70
- }
71
- }), { label: queryLabel }, otelContext);
72
- const query = new LiveStoreSQLQuery({
73
- label,
74
- queryString$,
75
- results$,
76
- componentKey: componentKey ?? globalComponentKey,
77
- store: this,
78
- otelContext,
79
- });
80
- this.activeQueries.add(query);
81
- // TODO get rid of temporary query workaround
82
- if (this.temporaryQueries !== undefined) {
83
- this.temporaryQueries.add(query);
84
- }
85
- // NOTE we are not ending the span here but in the query `destroy` method
86
- return query;
87
- });
88
- this.queryJS = (genResults, { componentKey = globalComponentKey, label = `js${uniqueId()}`, otelContext = otel.context.active(), }) => this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
89
- const otelContext = otel.trace.setSpan(otel.context.active(), span);
90
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '');
91
- const results$ = this.graph.makeThunk((get, addDebugInfo) => {
92
- const getAtom = (atom) => {
93
- if (atom._tag === 'thunk' || atom._tag === 'ref')
94
- return get(atom);
95
- return get(atom.results$);
96
- };
97
- addDebugInfo({ _tag: 'js', label, query: genResults.toString() });
98
- return genResults(getAtom);
99
- }, { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } }, otelContext);
100
- const query = new LiveStoreJSQuery({
101
- label,
102
- results$,
103
- componentKey,
104
- store: this,
105
- otelContext,
106
- });
107
- this.activeQueries.add(query);
108
- // TODO get rid of temporary query workaround
109
- if (this.temporaryQueries !== undefined) {
110
- this.temporaryQueries.add(query);
111
- }
112
- // NOTE we are not ending the span here but in the query `destroy` method
113
- return query;
114
- });
115
- this.queryGraphQL = (document, genVariableValues, { componentKey, label, otelContext = otel.context.active(), }) => this.otel.tracer.startActiveSpan(`queryGraphQL:`, // NOTE span name will be overridden further down
116
- {}, otelContext, (span) => {
117
- const otelContext = otel.trace.setSpan(otel.context.active(), span);
118
- if (this.graphQLContext === undefined) {
119
- return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context");
120
- }
121
- const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql';
122
- span.updateName(`queryGraphQL:${labelWithDefault}`);
123
- const variableValues$ = this.graph.makeThunk((get) => {
124
- if (typeof genVariableValues === 'function') {
125
- const getAtom = (atom) => {
126
- if (atom._tag === 'thunk' || atom._tag === 'ref')
127
- return get(atom);
128
- return get(atom.results$);
129
- };
130
- return genVariableValues(getAtom);
131
- }
132
- else {
133
- return genVariableValues;
134
- }
135
- }, { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } }, otelContext);
136
- const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '');
137
- const results$ = this.graph.makeThunk((get, addDebugInfo) => {
138
- const variableValues = get(variableValues$);
139
- const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext);
140
- // Add dependencies on any tables that were used
141
- for (const tableName of queriedTables) {
142
- const tableRef = this.tableRefs[tableName];
143
- assertNever(tableRef !== undefined, `No table ref found for ${tableName}`);
144
- get(tableRef);
145
- }
146
- addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) });
147
- return result;
148
- }, { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } }, otelContext);
149
- const query = new LiveStoreGraphQLQuery({
150
- document,
151
- context: this.graphQLContext,
152
- results$,
153
- componentKey,
154
- label: labelWithDefault,
155
- store: this,
156
- otelContext,
157
- });
158
- this.activeQueries.add(query);
159
- // TODO get rid of temporary query workaround
160
- if (this.temporaryQueries !== undefined) {
161
- this.temporaryQueries.add(query);
162
- }
163
- // NOTE we are not ending the span here but in the query `destroy` method
164
- return query;
165
- });
166
- this.queryGraphQLOnce = (document, variableValues, otelContext = this.otel.queriesSpanContext) => {
167
- const schema = this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema");
168
- const context = this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context");
169
- const tracer = this.otel.tracer;
170
- const operationName = graphql.getOperationAST(document)?.name?.value;
171
- return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
172
- try {
173
- span.setAttribute('graphql.variables', JSON.stringify(variableValues));
174
- span.setAttribute('graphql.query', graphql.print(document));
175
- context.queriedTables.clear();
176
- context.otelContext = otel.trace.setSpan(otel.context.active(), span);
177
- const res = graphql.executeSync({
178
- document,
179
- contextValue: context,
180
- schema: schema,
181
- variableValues,
182
- });
183
- // TODO track number of nested SQL queries via Otel + debug info
184
- if (res.errors) {
185
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' });
186
- span.setAttribute('graphql.error', res.errors.join('\n'));
187
- span.setAttribute('graphql.error-detail', JSON.stringify(res.errors));
188
- console.error(`graphql error (${operationName})`, res.errors);
189
- }
190
- return { result: res.data, queriedTables: Array.from(context.queriedTables.values()) };
191
- }
192
- finally {
193
- span.end();
194
- }
195
- });
196
- };
20
+ // querySQL = <TResult>(
21
+ // genQueryString: string | ((get: GetAtomResult) => string),
22
+ // {
23
+ // queriedTables,
24
+ // bindValues,
25
+ // componentKey,
26
+ // label,
27
+ // otelContext = otel.context.active(),
28
+ // }: {
29
+ // /**
30
+ // * List of tables that are queried in this query;
31
+ // * used to determine reactive dependencies.
32
+ // *
33
+ // * NOTE In the future we want to auto-generate this via parsing the query
34
+ // */
35
+ // queriedTables: string[]
36
+ // bindValues?: Bindable | undefined
37
+ // componentKey?: ComponentKey | undefined
38
+ // label?: string | undefined
39
+ // otelContext?: otel.Context
40
+ // },
41
+ // ): LiveStoreSQLQuery<TResult> =>
42
+ // this.otel.tracer.startActiveSpan(
43
+ // 'querySQL', // NOTE span name will be overridden further down
44
+ // { attributes: { label } },
45
+ // otelContext,
46
+ // (span) => {
47
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
48
+ // const queryString$ = this.graph.makeThunk(
49
+ // (get, addDebugInfo) => {
50
+ // if (typeof genQueryString === 'function') {
51
+ // const queryString = genQueryString(makeGetAtomResult(get))
52
+ // addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
53
+ // return queryString
54
+ // } else {
55
+ // return genQueryString
56
+ // }
57
+ // },
58
+ // { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
59
+ // otelContext,
60
+ // )
61
+ // label = label ?? queryString$.result
62
+ // span.updateName(`querySQL:${label}`)
63
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
64
+ // const results$ = this.graph.makeThunk<ReadonlyArray<TResult>>(
65
+ // (get, addDebugInfo) =>
66
+ // this.otel.tracer.startActiveSpan(
67
+ // 'sql:', // NOTE span name will be overridden further down
68
+ // {},
69
+ // otelContext,
70
+ // (span) => {
71
+ // try {
72
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
73
+ // // Establish a reactive dependency on the tables used in the query
74
+ // for (const tableName of queriedTables) {
75
+ // const tableRef =
76
+ // this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
77
+ // get(tableRef)
78
+ // }
79
+ // const sqlString = get(queryString$)
80
+ // span.setAttribute('sql.query', sqlString)
81
+ // span.updateName(`sql:${sqlString.slice(0, 50)}`)
82
+ // const results = this.inMemoryDB.select<TResult>(sqlString, {
83
+ // queriedTables,
84
+ // bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
85
+ // otelContext,
86
+ // })
87
+ // span.setAttribute('sql.rowsCount', results.length)
88
+ // addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
89
+ // return results
90
+ // } finally {
91
+ // span.end()
92
+ // }
93
+ // },
94
+ // ),
95
+ // { label: queryLabel },
96
+ // otelContext,
97
+ // )
98
+ // const query = new LiveStoreSQLQuery<TResult>({
99
+ // label,
100
+ // queryString$,
101
+ // results$,
102
+ // componentKey: componentKey ?? globalComponentKey,
103
+ // store: this,
104
+ // otelContext,
105
+ // })
106
+ // this.activeQueries.add(query)
107
+ // // TODO get rid of temporary query workaround
108
+ // if (this.temporaryQueries !== undefined) {
109
+ // this.temporaryQueries.add(query)
110
+ // }
111
+ // // NOTE we are not ending the span here but in the query `destroy` method
112
+ // return query
113
+ // },
114
+ // )
115
+ // queryJS = <TResult>(
116
+ // genResults: (get: GetAtomResult) => TResult,
117
+ // {
118
+ // componentKey = globalComponentKey,
119
+ // label = `js${uniqueId()}`,
120
+ // otelContext = otel.context.active(),
121
+ // }: { componentKey?: ComponentKey; label?: string; otelContext?: otel.Context },
122
+ // ): LiveStoreJSQuery<TResult> =>
123
+ // this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
124
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
125
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
126
+ // const results$ = this.graph.makeThunk(
127
+ // (get, addDebugInfo) => {
128
+ // addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
129
+ // return genResults(makeGetAtomResult(get))
130
+ // },
131
+ // { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
132
+ // otelContext,
133
+ // )
134
+ // // const query = new LiveStoreJSQuery<TResult>({
135
+ // // label,
136
+ // // results$,
137
+ // // componentKey,
138
+ // // store: this,
139
+ // // otelContext,
140
+ // // })
141
+ // this.activeQueries.add(query)
142
+ // // TODO get rid of temporary query workaround
143
+ // if (this.temporaryQueries !== undefined) {
144
+ // this.temporaryQueries.add(query)
145
+ // }
146
+ // // NOTE we are not ending the span here but in the query `destroy` method
147
+ // return query
148
+ // })
149
+ // queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
150
+ // document: DocumentNode<TResult, TVariableValues>,
151
+ // genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
152
+ // {
153
+ // componentKey,
154
+ // label,
155
+ // otelContext = otel.context.active(),
156
+ // }: {
157
+ // componentKey: ComponentKey
158
+ // label?: string
159
+ // otelContext?: otel.Context
160
+ // },
161
+ // ): LiveStoreGraphQLQuery<TResult, TVariableValues, TGraphQLContext> =>
162
+ // this.otel.tracer.startActiveSpan(
163
+ // `queryGraphQL:`, // NOTE span name will be overridden further down
164
+ // {},
165
+ // otelContext,
166
+ // (span) => {
167
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
168
+ // if (this.graphQLContext === undefined) {
169
+ // return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
170
+ // }
171
+ // const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
172
+ // span.updateName(`queryGraphQL:${labelWithDefault}`)
173
+ // const variableValues$ = this.graph.makeThunk(
174
+ // (get) => {
175
+ // if (typeof genVariableValues === 'function') {
176
+ // return genVariableValues(makeGetAtomResult(get))
177
+ // } else {
178
+ // return genVariableValues
179
+ // }
180
+ // },
181
+ // { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
182
+ // // otelContext,
183
+ // )
184
+ // const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
185
+ // const results$ = this.graph.makeThunk<TResult>(
186
+ // (get, addDebugInfo) => {
187
+ // const variableValues = get(variableValues$)
188
+ // const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext)
189
+ // // Add dependencies on any tables that were used
190
+ // for (const tableName of queriedTables) {
191
+ // const tableRef = this.tableRefs[tableName]
192
+ // assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
193
+ // get(tableRef!)
194
+ // }
195
+ // addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
196
+ // return result
197
+ // },
198
+ // { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
199
+ // // otelContext,
200
+ // )
201
+ // const query = new LiveStoreGraphQLQuery({
202
+ // document,
203
+ // context: this.graphQLContext,
204
+ // results$,
205
+ // componentKey,
206
+ // label: labelWithDefault,
207
+ // store: this,
208
+ // otelContext,
209
+ // })
210
+ // this.activeQueries.add(query)
211
+ // // TODO get rid of temporary query workaround
212
+ // if (this.temporaryQueries !== undefined) {
213
+ // this.temporaryQueries.add(query)
214
+ // }
215
+ // // NOTE we are not ending the span here but in the query `destroy` method
216
+ // return query
217
+ // },
218
+ // )
219
+ // queryGraphQLOnce = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
220
+ // document: DocumentNode<TResult, TVariableValues>,
221
+ // variableValues: TVariableValues,
222
+ // otelContext: otel.Context = this.otel.queriesSpanContext,
223
+ // ): { result: TResult; queriedTables: string[] } => {
224
+ // const schema =
225
+ // this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
226
+ // const context =
227
+ // this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
228
+ // const tracer = this.otel.tracer
229
+ // const operationName = graphql.getOperationAST(document)?.name?.value
230
+ // return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
231
+ // try {
232
+ // span.setAttribute('graphql.variables', JSON.stringify(variableValues))
233
+ // span.setAttribute('graphql.query', graphql.print(document))
234
+ // context.queriedTables.clear()
235
+ // context.otelContext = otel.trace.setSpan(otel.context.active(), span)
236
+ // const res = graphql.executeSync({
237
+ // document,
238
+ // contextValue: context,
239
+ // schema: schema,
240
+ // variableValues,
241
+ // })
242
+ // // TODO track number of nested SQL queries via Otel + debug info
243
+ // if (res.errors) {
244
+ // span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
245
+ // span.setAttribute('graphql.error', res.errors.join('\n'))
246
+ // span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
247
+ // console.error(`graphql error (${operationName})`, res.errors)
248
+ // }
249
+ // return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
250
+ // } finally {
251
+ // span.end()
252
+ // }
253
+ // })
254
+ // }
197
255
  /**
198
256
  * Subscribe to the results of a query
199
257
  * Returns a function to cancel the subscription.
200
258
  */
201
- this.subscribe = (query, onNewValue, onSubsubscribe, options) => this.otel.tracer.startActiveSpan(`LiveStore.subscribe`, { attributes: { label: options?.label } }, query.otelContext, (span) => {
259
+ this.subscribe = (query, onNewValue, onSubsubscribe, options) => this.otel.tracer.startActiveSpan(`LiveStore.subscribe`, { attributes: { label: options?.label } }, options?.otelContext ?? this.otel.queriesSpanContext, (span) => {
202
260
  const otelContext = otel.trace.setSpan(otel.context.active(), span);
203
- const effect = this.graph.makeEffect((get) => {
204
- const result = get(query.results$);
205
- onNewValue(result);
206
- }, { label: `subscribe:${options?.label}` }, otelContext);
207
- const subscriptionKey = uuid();
261
+ const label = `subscribe:${options?.label}`;
262
+ const effect = this.graph.makeEffect((get) => onNewValue(get(query.results$)), { label });
263
+ effect.doEffect(otelContext);
208
264
  const unsubscribe = () => {
209
265
  try {
210
266
  this.graph.destroy(effect);
211
- query.activeSubscriptions.delete(subscriptionKey);
267
+ this.activeQueries.delete(query);
212
268
  onSubsubscribe?.();
213
269
  }
214
270
  finally {
215
271
  span.end();
216
272
  }
217
273
  };
218
- query.activeSubscriptions.set(subscriptionKey, unsubscribe);
274
+ this.activeQueries.add(query);
219
275
  return unsubscribe;
220
276
  });
221
- /**
222
- * Any queries created in the callback will be destroyed when the callback is complete.
223
- * Useful for temporarily creating reactive queries, which is an idempotent operation
224
- * that can be safely called inside a React useMemo hook.
225
- */
226
- this.inTempQueryContext = (callback) => {
227
- this.temporaryQueries = new Set();
228
- // TODO: consider errors / try/finally here?
229
- const result = callback();
230
- for (const query of this.temporaryQueries) {
231
- this.destroyQuery(query);
232
- }
233
- this.temporaryQueries = undefined;
234
- return result;
235
- };
236
277
  /**
237
278
  * Destroys the entire store, including all queries and subscriptions.
238
279
  *
239
280
  * Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
240
281
  */
241
282
  this.destroy = () => {
242
- for (const query of this.activeQueries) {
243
- this.destroyQuery(query);
244
- }
245
283
  Object.values(this.tableRefs).forEach((tableRef) => this.graph.destroy(tableRef));
246
- const applyEventsSpan = otel.trace.getSpan(this.otel.applyEventsSpanContext);
247
- applyEventsSpan.end();
248
- const queriesSpan = otel.trace.getSpan(this.otel.queriesSpanContext);
249
- queriesSpan.end();
284
+ otel.trace.getSpan(this.otel.applyEventsSpanContext).end();
285
+ otel.trace.getSpan(this.otel.queriesSpanContext).end();
250
286
  // TODO destroy active subscriptions
251
287
  };
252
- this.destroyQuery = (query) => {
253
- if (query._tag === 'sql') {
254
- // results are downstream of query string, so will automatically be destroyed together
255
- this.graph.destroy(query.queryString$);
256
- }
257
- else {
258
- this.graph.destroy(query.results$);
259
- }
260
- this.activeQueries.delete(query);
261
- query.destroy();
262
- };
263
- /**
264
- * Clean up queries and downstream subscriptions associated with a component.
265
- * This is critical to avoid memory leaks.
266
- */
267
- this.unmountComponent = (componentKey) => {
268
- for (const query of this.activeQueries) {
269
- if (query.componentKey === componentKey) {
270
- this.destroyQuery(query);
271
- }
272
- }
273
- };
274
288
  /* Apply a single write event to the store, and refresh all queries in response */
275
289
  this.applyEvent = (eventType, args = {}, options) => {
276
290
  const skipRefresh = options?.skipRefresh ?? false;
@@ -287,16 +301,23 @@ export class Store {
287
301
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`);
288
302
  tablesToUpdate.push([tableRef, null]);
289
303
  }
304
+ const debugRefreshReason = {
305
+ _tag: 'applyEvent',
306
+ event: { type: eventType, args },
307
+ writeTables: [...writeTables],
308
+ };
290
309
  // Update all table refs together in a batch, to only trigger one reactive update
291
- this.graph.setRefs(tablesToUpdate, {
292
- otelHint: 'applyEvents',
293
- skipRefresh,
294
- debugRefreshReason: {
295
- _tag: 'applyEvent',
296
- event: { type: eventType, args },
297
- writeTables: [...writeTables],
298
- },
299
- }, otelContext);
310
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext });
311
+ if (skipRefresh === false) {
312
+ // TODO update the graph
313
+ // this.graph.refresh(
314
+ // {
315
+ // otelHint: 'applyEvents',
316
+ // debugRefreshReason,
317
+ // },
318
+ // otelContext,
319
+ // )
320
+ }
300
321
  }
301
322
  catch (e) {
302
323
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() });
@@ -358,16 +379,17 @@ export class Store {
358
379
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`);
359
380
  tablesToUpdate.push([tableRef, null]);
360
381
  }
382
+ const debugRefreshReason = {
383
+ _tag: 'applyEvents',
384
+ events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
385
+ writeTables: [...writeTables],
386
+ };
361
387
  // Update all table refs together in a batch, to only trigger one reactive update
362
- this.graph.setRefs(tablesToUpdate, {
363
- otelHint: 'applyEvents',
364
- skipRefresh,
365
- debugRefreshReason: {
366
- _tag: 'applyEvents',
367
- events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
368
- writeTables: [...writeTables],
369
- },
370
- }, otelContext);
388
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext });
389
+ if (skipRefresh === false) {
390
+ // TODO update the graph
391
+ // this.graph.refresh({ debugRefreshReason, otelHint: 'applyEvents' }, otelContext)
392
+ }
371
393
  }
372
394
  catch (e) {
373
395
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() });
@@ -385,17 +407,12 @@ export class Store {
385
407
  this.manualRefresh = (options) => {
386
408
  const { label } = options ?? {};
387
409
  this.otel.tracer.startActiveSpan('LiveStore:manualRefresh', { attributes: { 'livestore.manualRefreshLabel': label } }, this.otel.applyEventsSpanContext, (span) => {
388
- const otelContext = otel.trace.setSpan(otel.context.active(), span);
389
- this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext);
410
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
411
+ // TODO update the graph
412
+ // this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext)
390
413
  span.end();
391
414
  });
392
415
  };
393
- // TODO get rid of this as part of new query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
394
- this.runOnce = (queryDef) => {
395
- return this.inTempQueryContext(() => {
396
- return queryDef(this).results$.result;
397
- });
398
- };
399
416
  /**
400
417
  * Apply an event to the store.
401
418
  * Returns the tables that were affected by the event.
@@ -462,7 +479,7 @@ export class Store {
462
479
  * This should only be used for framework-internal purposes;
463
480
  * all app writes should go through applyEvent.
464
481
  */
465
- this.execute = async (query, params = {}, writeTables) => {
482
+ this.execute = (query, params = {}, writeTables) => {
466
483
  this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables);
467
484
  if (this.storage !== undefined) {
468
485
  const parentSpan = otel.trace.getSpan(otel.context.active());
@@ -471,12 +488,12 @@ export class Store {
471
488
  };
472
489
  this.inMemoryDB = db;
473
490
  this._proxyDb = dbProxy;
474
- this.graph = new ReactiveGraph({
475
- // TODO move this into React module
476
- // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
477
- effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
478
- otelTracer,
479
- });
491
+ // this.graph = new ReactiveGraph({
492
+ // // TODO move this into React module
493
+ // // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
494
+ // effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
495
+ // otelTracer,
496
+ // })
480
497
  this.schema = schema;
481
498
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
482
499
  this.tableRefs = {};
@@ -486,6 +503,8 @@ export class Store {
486
503
  const otelApplyEventsSpanContext = otel.trace.setSpan(otel.context.active(), applyEventsSpan);
487
504
  const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext);
488
505
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan);
506
+ this.graph = dbGraph;
507
+ this.graph.context = { store: this, otelTracer, rootOtelContext: otelQueriesSpanContext };
489
508
  this.otel = {
490
509
  tracer: otelTracer,
491
510
  applyEventsSpanContext: otelApplyEventsSpanContext,