@livestore/livestore 0.0.12 → 0.0.14

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 (226) hide show
  1. package/README.md +25 -28
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/QueryCache.d.ts +20 -0
  4. package/dist/QueryCache.d.ts.map +1 -0
  5. package/dist/QueryCache.js +71 -0
  6. package/dist/QueryCache.js.map +1 -0
  7. package/dist/__tests__/react/fixture.d.ts +26 -0
  8. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  9. package/dist/__tests__/react/fixture.js +60 -0
  10. package/dist/__tests__/react/fixture.js.map +1 -0
  11. package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
  12. package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
  13. package/dist/__tests__/react/useComponentState.test.js +68 -0
  14. package/dist/__tests__/react/useComponentState.test.js.map +1 -0
  15. package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
  16. package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
  17. package/dist/__tests__/react/useLQuery.test.js +38 -0
  18. package/dist/__tests__/react/useLQuery.test.js.map +1 -0
  19. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  20. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  21. package/dist/__tests__/react/useLiveStoreComponent.test.js +73 -0
  22. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  23. package/dist/__tests__/react/useQuery.test.d.ts +2 -0
  24. package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
  25. package/dist/__tests__/react/useQuery.test.js +33 -0
  26. package/dist/__tests__/react/useQuery.test.js.map +1 -0
  27. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
  28. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
  29. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
  30. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
  31. package/dist/__tests__/reactive.test.d.ts +2 -0
  32. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  33. package/dist/__tests__/reactive.test.js +271 -0
  34. package/dist/__tests__/reactive.test.js.map +1 -0
  35. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  36. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  37. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  38. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  39. package/dist/bounded-collections.d.ts +34 -0
  40. package/dist/bounded-collections.d.ts.map +1 -0
  41. package/dist/bounded-collections.js +103 -0
  42. package/dist/bounded-collections.js.map +1 -0
  43. package/dist/componentKey.d.ts +20 -0
  44. package/dist/componentKey.d.ts.map +1 -0
  45. package/dist/componentKey.js +3 -0
  46. package/dist/componentKey.js.map +1 -0
  47. package/dist/effect/LiveStore.d.ts +36 -0
  48. package/dist/effect/LiveStore.d.ts.map +1 -0
  49. package/dist/effect/LiveStore.js +41 -0
  50. package/dist/effect/LiveStore.js.map +1 -0
  51. package/dist/effect/index.d.ts +2 -0
  52. package/dist/effect/index.d.ts.map +1 -0
  53. package/dist/effect/index.js +2 -0
  54. package/dist/effect/index.js.map +1 -0
  55. package/dist/events.d.ts +7 -0
  56. package/dist/events.d.ts.map +1 -0
  57. package/dist/events.js +2 -0
  58. package/dist/events.js.map +1 -0
  59. package/dist/inMemoryDatabase.d.ts +56 -0
  60. package/dist/inMemoryDatabase.d.ts.map +1 -0
  61. package/dist/inMemoryDatabase.js +223 -0
  62. package/dist/inMemoryDatabase.js.map +1 -0
  63. package/dist/index.d.ts +22 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +13 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/migrations.d.ts +16 -0
  68. package/dist/migrations.d.ts.map +1 -0
  69. package/dist/migrations.js +67 -0
  70. package/dist/migrations.js.map +1 -0
  71. package/dist/otel.d.ts +4 -0
  72. package/dist/otel.d.ts.map +1 -0
  73. package/dist/otel.js +6 -0
  74. package/dist/otel.js.map +1 -0
  75. package/dist/react/LiveStoreContext.d.ts +11 -0
  76. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  77. package/dist/react/LiveStoreContext.js +10 -0
  78. package/dist/react/LiveStoreContext.js.map +1 -0
  79. package/dist/react/LiveStoreProvider.d.ts +20 -0
  80. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  81. package/dist/react/LiveStoreProvider.js +52 -0
  82. package/dist/react/LiveStoreProvider.js.map +1 -0
  83. package/dist/react/index.d.ts +8 -0
  84. package/dist/react/index.d.ts.map +1 -0
  85. package/dist/react/index.js +6 -0
  86. package/dist/react/index.js.map +1 -0
  87. package/dist/react/useComponentState.d.ts +50 -0
  88. package/dist/react/useComponentState.d.ts.map +1 -0
  89. package/dist/react/useComponentState.js +248 -0
  90. package/dist/react/useComponentState.js.map +1 -0
  91. package/dist/react/useGlobalQuery.d.ts +3 -0
  92. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  93. package/dist/react/useGlobalQuery.js +26 -0
  94. package/dist/react/useGlobalQuery.js.map +1 -0
  95. package/dist/react/useGraphQL.d.ts +13 -0
  96. package/dist/react/useGraphQL.d.ts.map +1 -0
  97. package/dist/react/useGraphQL.js +87 -0
  98. package/dist/react/useGraphQL.js.map +1 -0
  99. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  100. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  101. package/dist/react/useLiveStoreComponent.js +361 -0
  102. package/dist/react/useLiveStoreComponent.js.map +1 -0
  103. package/dist/react/useQuery.d.ts +3 -0
  104. package/dist/react/useQuery.d.ts.map +1 -0
  105. package/dist/react/useQuery.js +42 -0
  106. package/dist/react/useQuery.js.map +1 -0
  107. package/dist/react/useTemporaryQuery.d.ts +8 -0
  108. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  109. package/dist/react/useTemporaryQuery.js +17 -0
  110. package/dist/react/useTemporaryQuery.js.map +1 -0
  111. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  112. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  113. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  114. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  115. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  116. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  117. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  118. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  119. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  120. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  121. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  122. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  123. package/dist/reactive.d.ts +134 -0
  124. package/dist/reactive.d.ts.map +1 -0
  125. package/dist/reactive.js +409 -0
  126. package/dist/reactive.js.map +1 -0
  127. package/dist/reactiveQueries/base-class.d.ts +32 -0
  128. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  129. package/dist/reactiveQueries/base-class.js +30 -0
  130. package/dist/reactiveQueries/base-class.js.map +1 -0
  131. package/dist/reactiveQueries/graph.d.ts +10 -0
  132. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  133. package/dist/reactiveQueries/graph.js +6 -0
  134. package/dist/reactiveQueries/graph.js.map +1 -0
  135. package/dist/reactiveQueries/graphql.d.ts +42 -0
  136. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  137. package/dist/reactiveQueries/graphql.js +99 -0
  138. package/dist/reactiveQueries/graphql.js.map +1 -0
  139. package/dist/reactiveQueries/js.d.ts +23 -0
  140. package/dist/reactiveQueries/js.d.ts.map +1 -0
  141. package/dist/reactiveQueries/js.js +36 -0
  142. package/dist/reactiveQueries/js.js.map +1 -0
  143. package/dist/reactiveQueries/sql.d.ts +35 -0
  144. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  145. package/dist/reactiveQueries/sql.js +97 -0
  146. package/dist/reactiveQueries/sql.js.map +1 -0
  147. package/dist/schema.d.ts +81 -0
  148. package/dist/schema.d.ts.map +1 -0
  149. package/dist/schema.js +46 -0
  150. package/dist/schema.js.map +1 -0
  151. package/dist/storage/in-memory/index.d.ts +15 -0
  152. package/dist/storage/in-memory/index.d.ts.map +1 -0
  153. package/dist/storage/in-memory/index.js +14 -0
  154. package/dist/storage/in-memory/index.js.map +1 -0
  155. package/dist/storage/index.d.ts +14 -0
  156. package/dist/storage/index.d.ts.map +1 -0
  157. package/dist/storage/index.js +9 -0
  158. package/dist/storage/index.js.map +1 -0
  159. package/dist/storage/tauri/index.d.ts +19 -0
  160. package/dist/storage/tauri/index.d.ts.map +1 -0
  161. package/dist/storage/tauri/index.js +38 -0
  162. package/dist/storage/tauri/index.js.map +1 -0
  163. package/dist/storage/utils/idb.d.ts +10 -0
  164. package/dist/storage/utils/idb.d.ts.map +1 -0
  165. package/dist/storage/utils/idb.js +58 -0
  166. package/dist/storage/utils/idb.js.map +1 -0
  167. package/dist/storage/web-worker/index.d.ts +27 -0
  168. package/dist/storage/web-worker/index.d.ts.map +1 -0
  169. package/dist/storage/web-worker/index.js +74 -0
  170. package/dist/storage/web-worker/index.js.map +1 -0
  171. package/dist/storage/web-worker/worker.d.ts +13 -0
  172. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  173. package/dist/storage/web-worker/worker.js +110 -0
  174. package/dist/storage/web-worker/worker.js.map +1 -0
  175. package/dist/store.d.ts +159 -0
  176. package/dist/store.d.ts.map +1 -0
  177. package/dist/store.js +626 -0
  178. package/dist/store.js.map +1 -0
  179. package/dist/util.d.ts +28 -0
  180. package/dist/util.d.ts.map +1 -0
  181. package/dist/util.js +55 -0
  182. package/dist/util.js.map +1 -0
  183. package/package.json +47 -19
  184. package/src/QueryCache.ts +1 -1
  185. package/src/__tests__/react/fixture.tsx +35 -39
  186. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  187. package/src/__tests__/react/useQuery.test.tsx +48 -0
  188. package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
  189. package/src/__tests__/reactive.test.ts +194 -142
  190. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  191. package/src/effect/LiveStore.ts +22 -31
  192. package/src/events.ts +1 -1
  193. package/src/inMemoryDatabase.ts +117 -142
  194. package/src/index.ts +18 -22
  195. package/src/migrations.ts +119 -0
  196. package/src/otel.ts +0 -11
  197. package/src/react/LiveStoreProvider.tsx +24 -23
  198. package/src/react/index.ts +12 -7
  199. package/src/react/useComponentState.ts +409 -0
  200. package/src/react/useQuery.ts +58 -0
  201. package/src/react/useTemporaryQuery.ts +21 -0
  202. package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
  203. package/src/reactive.ts +386 -267
  204. package/src/reactiveQueries/base-class.ts +61 -39
  205. package/src/reactiveQueries/graph.ts +15 -0
  206. package/src/reactiveQueries/graphql.ts +147 -31
  207. package/src/reactiveQueries/js.ts +54 -21
  208. package/src/reactiveQueries/sql.ts +128 -37
  209. package/src/schema.ts +69 -145
  210. package/src/storage/in-memory/index.ts +21 -0
  211. package/src/storage/index.ts +27 -0
  212. package/src/{backends/tauri.ts → storage/tauri/index.ts} +14 -28
  213. package/src/storage/web-worker/index.ts +116 -0
  214. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  215. package/src/store.ts +466 -457
  216. package/src/util.ts +13 -3
  217. package/tsconfig.json +1 -3
  218. package/src/backends/base.ts +0 -67
  219. package/src/backends/index.ts +0 -98
  220. package/src/backends/noop.ts +0 -32
  221. package/src/backends/web-in-memory.ts +0 -65
  222. package/src/backends/web.ts +0 -97
  223. package/src/react/useGlobalQuery.ts +0 -40
  224. package/src/react/useGraphQL.ts +0 -112
  225. package/src/react/useLiveStoreComponent.ts +0 -483
  226. /package/src/{backends → storage}/utils/idb.ts +0 -0
package/src/store.ts CHANGED
@@ -1,28 +1,27 @@
1
- import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
1
  import { assertNever, makeNoopSpan, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
2
+ import { identity } from '@livestore/utils/effect'
3
3
  import * as otel from '@opentelemetry/api'
4
4
  import type { GraphQLSchema } from 'graphql'
5
- import * as graphql from 'graphql'
6
- import { uniqueId } from 'lodash-es'
7
- import * as ReactDOM from 'react-dom'
5
+ import type * as Sqlite from 'sqlite-esm'
8
6
  import { v4 as uuid } from 'uuid'
9
7
 
10
- import type { Backend, BackendOptions } from './backends/index.js'
11
- import { createBackend } from './backends/index.js'
12
8
  import type { ComponentKey } from './componentKey.js'
13
9
  import { tableNameForComponentKey } from './componentKey.js'
14
10
  import type { LiveStoreEvent } from './events.js'
15
11
  import { InMemoryDatabase } from './inMemoryDatabase.js'
12
+ import { migrateDb } from './migrations.js'
16
13
  import { getDurationMsFromSpan } from './otel.js'
17
- import type { GetAtom, Ref } from './reactive.js'
18
- import { ReactiveGraph } from './reactive.js'
19
- import { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
20
- import { LiveStoreJSQuery } from './reactiveQueries/js.js'
21
- import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
22
- import type { ActionDefinition, GetActionArgs, Schema } from './schema.js'
23
- import { componentStateTables, loadSchema } from './schema.js'
24
- import type { Bindable, ParamsObject } from './util.js'
25
- import { sql } from './util.js'
14
+ import type { ReactiveGraph, Ref } from './reactive.js'
15
+ import type { ILiveStoreQuery } from './reactiveQueries/base-class.js'
16
+ import { type DbContext, dbGraph } from './reactiveQueries/graph.js'
17
+ import type { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
18
+ import type { LiveStoreJSQuery } from './reactiveQueries/js.js'
19
+ import type { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
20
+ import type { ActionDefinition, GetActionArgs, Schema, SQLWriteStatement } from './schema.js'
21
+ import { componentStateTables } from './schema.js'
22
+ import type { Storage, StorageInit } from './storage/index.js'
23
+ import type { ParamsObject } from './util.js'
24
+ import { isPromise, prepareBindValues, sql } from './util.js'
26
25
 
27
26
  export type LiveStoreQuery<TResult extends Record<string, any> = any> =
28
27
  | LiveStoreSQLQuery<TResult>
@@ -35,8 +34,6 @@ export type BaseGraphQLContext = {
35
34
  otelContext?: otel.Context
36
35
  }
37
36
 
38
- export const RESET_DB_LOCAL_STORAGE_KEY = 'livestore-reset'
39
-
40
37
  export type QueryResult<TQuery> = TQuery extends LiveStoreSQLQuery<infer R>
41
38
  ? ReadonlyArray<Readonly<R>>
42
39
  : TQuery extends LiveStoreJSQuery<infer S>
@@ -45,7 +42,7 @@ export type QueryResult<TQuery> = TQuery extends LiveStoreSQLQuery<infer R>
45
42
  ? Readonly<Result>
46
43
  : never
47
44
 
48
- const globalComponentKey: ComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' }
45
+ export const globalComponentKey: ComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' }
49
46
 
50
47
  export type GraphQLOptions<TContext> = {
51
48
  schema: GraphQLSchema
@@ -54,8 +51,10 @@ export type GraphQLOptions<TContext> = {
54
51
 
55
52
  export type StoreOptions<TGraphQLContext extends BaseGraphQLContext> = {
56
53
  db: InMemoryDatabase
54
+ /** A `Proxy`d version of `db` except that it also mirrors `execute` calls to the storage */
55
+ dbProxy: InMemoryDatabase
57
56
  schema: Schema
58
- backend?: Backend
57
+ storage?: Storage
59
58
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
60
59
  otelTracer: otel.Tracer
61
60
  otelRootSpanContext: otel.Context
@@ -98,9 +97,11 @@ export type StoreOtel = {
98
97
  queriesSpanContext: otel.Context
99
98
  }
100
99
 
101
- export class Store<TGraphQLContext extends BaseGraphQLContext> {
102
- graph: ReactiveGraph<RefreshReason, QueryDebugInfo>
100
+ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext> {
101
+ graph: ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
103
102
  inMemoryDB: InMemoryDatabase
103
+ // TODO refactor
104
+ _proxyDb: InMemoryDatabase
104
105
  schema: Schema
105
106
  graphQLSchema?: GraphQLSchema
106
107
  graphQLContext?: TGraphQLContext
@@ -111,29 +112,31 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
111
112
  */
112
113
  tableRefs: { [key: string]: Ref<null> }
113
114
  activeQueries: Set<LiveStoreQuery>
114
- backend?: Backend
115
+ storage?: Storage
115
116
  temporaryQueries: Set<LiveStoreQuery> | undefined
116
117
 
117
118
  private constructor({
118
119
  db,
120
+ dbProxy,
119
121
  schema,
120
- backend,
122
+ storage,
121
123
  graphQLOptions,
122
124
  otelTracer,
123
125
  otelRootSpanContext,
124
126
  }: StoreOptions<TGraphQLContext>) {
125
127
  this.inMemoryDB = db
126
- this.graph = new ReactiveGraph({
127
- // TODO move this into React module
128
- // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
129
- effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
130
- otelTracer,
131
- })
128
+ this._proxyDb = dbProxy
129
+ // this.graph = new ReactiveGraph({
130
+ // // TODO move this into React module
131
+ // // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
132
+ // effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
133
+ // otelTracer,
134
+ // })
132
135
  this.schema = schema
133
136
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
134
137
  this.tableRefs = {}
135
138
  this.activeQueries = new Set()
136
- this.backend = backend
139
+ this.storage = storage
137
140
 
138
141
  const applyEventsSpan = otelTracer.startSpan('LiveStore:applyEvents', {}, otelRootSpanContext)
139
142
  const otelApplyEventsSpanContext = otel.trace.setSpan(otel.context.active(), applyEventsSpan)
@@ -141,6 +144,9 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
141
144
  const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext)
142
145
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
143
146
 
147
+ this.graph = dbGraph
148
+ this.graph.context = { store: this, otelTracer, rootOtelContext: otelQueriesSpanContext }
149
+
144
150
  this.otel = {
145
151
  tracer: otelTracer,
146
152
  applyEventsSpanContext: otelApplyEventsSpanContext,
@@ -185,372 +191,340 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
185
191
  *
186
192
  * NOTE The query is actually running (even if no one has subscribed to it yet) and will be kept up to date.
187
193
  */
188
- querySQL = <TResult>(
189
- genQueryString: (get: GetAtom) => string,
190
- {
191
- queriedTables,
192
- bindValues,
193
- componentKey,
194
- label,
195
- otelContext = otel.context.active(),
196
- }: {
197
- /**
198
- * List of tables that are queried in this query;
199
- * used to determine reactive dependencies.
200
- *
201
- * NOTE In the future we want to auto-generate this via parsing the query
202
- */
203
- queriedTables: string[]
204
- bindValues?: Bindable | undefined
205
- componentKey?: ComponentKey | undefined
206
- label?: string | undefined
207
- otelContext?: otel.Context
208
- },
209
- ): LiveStoreSQLQuery<TResult> =>
210
- this.otel.tracer.startActiveSpan(
211
- 'querySQL', // NOTE span name will be overridden further down
212
- { attributes: { label } },
213
- otelContext,
214
- (span) => {
215
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
216
-
217
- const queryString$ = this.graph.makeThunk(
218
- (get, addDebugInfo) => {
219
- const queryString = genQueryString(get)
220
- addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
221
- return queryString
222
- },
223
- { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
224
- otelContext,
225
- )
226
-
227
- label = label ?? queryString$.result
228
- span.updateName(`querySQL:${label}`)
229
-
230
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
231
-
232
- const results$ = this.graph.makeThunk<TResult[]>(
233
- (get, addDebugInfo) =>
234
- this.otel.tracer.startActiveSpan(
235
- 'sql', // NOTE span name will be overridden further down
236
- {},
237
- otelContext,
238
- (span) => {
239
- try {
240
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
241
-
242
- // Establish a reactive dependency on the tables used in the query
243
- for (const tableName of queriedTables) {
244
- const tableRef =
245
- this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
246
- get(tableRef)
247
- }
248
- const sqlString = get(queryString$)
249
-
250
- span.setAttribute('sql.query', sqlString)
251
- span.updateName(`sql:${sqlString.slice(0, 50)}`)
252
-
253
- const results = this.inMemoryDB.select(sqlString, { queriedTables, bindValues, otelContext })
254
-
255
- span.setAttribute('sql.rowsCount', results.length)
256
- addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
257
-
258
- return results as unknown as TResult[]
259
- } finally {
260
- span.end()
261
- }
262
- },
263
- ),
264
- { label: queryLabel },
265
- otelContext,
266
- )
267
-
268
- const query = new LiveStoreSQLQuery<TResult>({
269
- label,
270
- queryString$,
271
- results$,
272
- componentKey: componentKey ?? globalComponentKey,
273
- store: this,
274
- otelContext,
275
- })
276
-
277
- this.activeQueries.add(query)
278
-
279
- // TODO get rid of temporary query workaround
280
- if (this.temporaryQueries !== undefined) {
281
- this.temporaryQueries.add(query)
282
- }
283
-
284
- // NOTE we are not ending the span here but in the query `destroy` method
285
- return query
286
- },
287
- )
288
-
289
- queryJS = <TResult>(
290
- genResults: (get: GetAtom) => TResult,
291
- {
292
- componentKey = globalComponentKey,
293
- label = `js${uniqueId()}`,
294
- otelContext = otel.context.active(),
295
- }: { componentKey?: ComponentKey; label?: string; otelContext?: otel.Context },
296
- ): LiveStoreJSQuery<TResult> =>
297
- this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
298
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
299
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
300
- const results$ = this.graph.makeThunk(
301
- (get, addDebugInfo) => {
302
- addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
303
- return genResults(get)
304
- },
305
- { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
306
- otelContext,
307
- )
308
-
309
- const query = new LiveStoreJSQuery<TResult>({
310
- label,
311
- results$,
312
- componentKey,
313
- store: this,
314
- otelContext,
315
- })
316
-
317
- this.activeQueries.add(query)
318
-
319
- // TODO get rid of temporary query workaround
320
- if (this.temporaryQueries !== undefined) {
321
- this.temporaryQueries.add(query)
322
- }
323
-
324
- // NOTE we are not ending the span here but in the query `destroy` method
325
- return query
326
- })
327
-
328
- queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
329
- document: DocumentNode<TResult, TVariableValues>,
330
- genVariableValues: (get: GetAtom) => TVariableValues,
331
- {
332
- componentKey,
333
- label,
334
- otelContext = otel.context.active(),
335
- }: {
336
- componentKey: ComponentKey
337
- label?: string
338
- otelContext?: otel.Context
339
- },
340
- ): LiveStoreGraphQLQuery<TResult, TVariableValues, TGraphQLContext> =>
341
- this.otel.tracer.startActiveSpan(
342
- `queryGraphQL:`, // NOTE span name will be overridden further down
343
- {},
344
- otelContext,
345
- (span) => {
346
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
347
-
348
- if (this.graphQLContext === undefined) {
349
- return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
350
- }
351
-
352
- const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
353
-
354
- span.updateName(`queryGraphQL:${labelWithDefault}`)
355
-
356
- const variableValues$ = this.graph.makeThunk(
357
- genVariableValues,
358
- { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
359
- otelContext,
360
- )
361
-
362
- const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
363
- const results$ = this.graph.makeThunk<TResult>(
364
- (get, addDebugInfo) => {
365
- const variableValues = get(variableValues$)
366
- const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext)
367
-
368
- // Add dependencies on any tables that were used
369
- for (const tableName of queriedTables) {
370
- const tableRef = this.tableRefs[tableName]
371
- assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
372
- get(tableRef!)
373
- }
374
-
375
- addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
376
-
377
- return result
378
- },
379
- { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
380
- otelContext,
381
- )
382
-
383
- const query = new LiveStoreGraphQLQuery({
384
- document,
385
- context: this.graphQLContext,
386
- results$,
387
- componentKey,
388
- label: labelWithDefault,
389
- store: this,
390
- otelContext,
391
- })
392
-
393
- this.activeQueries.add(query)
394
-
395
- // TODO get rid of temporary query workaround
396
- if (this.temporaryQueries !== undefined) {
397
- this.temporaryQueries.add(query)
398
- }
399
-
400
- // NOTE we are not ending the span here but in the query `destroy` method
401
- return query
402
- },
403
- )
404
-
405
- queryGraphQLOnce = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
406
- document: DocumentNode<TResult, TVariableValues>,
407
- variableValues: TVariableValues,
408
- otelContext: otel.Context = this.otel.queriesSpanContext,
409
- ): { result: TResult; queriedTables: string[] } => {
410
- const schema =
411
- this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
412
- const context =
413
- this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
414
- const tracer = this.otel.tracer
415
-
416
- const operationName = graphql.getOperationAST(document)?.name?.value
417
-
418
- return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
419
- try {
420
- span.setAttribute('graphql.variables', JSON.stringify(variableValues))
421
- span.setAttribute('graphql.query', graphql.print(document))
422
-
423
- context.queriedTables.clear()
424
-
425
- context.otelContext = otel.trace.setSpan(otel.context.active(), span)
426
-
427
- const res = graphql.executeSync({
428
- document,
429
- contextValue: context,
430
- schema: schema,
431
- variableValues,
432
- })
433
-
434
- // TODO track number of nested SQL queries via Otel + debug info
435
-
436
- if (res.errors) {
437
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
438
- span.setAttribute('graphql.error', res.errors.join('\n'))
439
- span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
440
- console.error(`graphql error (${operationName})`, res.errors)
441
- }
442
-
443
- return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
444
- } finally {
445
- span.end()
446
- }
447
- })
448
- }
194
+ // querySQL = <TResult>(
195
+ // genQueryString: string | ((get: GetAtomResult) => string),
196
+ // {
197
+ // queriedTables,
198
+ // bindValues,
199
+ // componentKey,
200
+ // label,
201
+ // otelContext = otel.context.active(),
202
+ // }: {
203
+ // /**
204
+ // * List of tables that are queried in this query;
205
+ // * used to determine reactive dependencies.
206
+ // *
207
+ // * NOTE In the future we want to auto-generate this via parsing the query
208
+ // */
209
+ // queriedTables: string[]
210
+ // bindValues?: Bindable | undefined
211
+ // componentKey?: ComponentKey | undefined
212
+ // label?: string | undefined
213
+ // otelContext?: otel.Context
214
+ // },
215
+ // ): LiveStoreSQLQuery<TResult> =>
216
+ // this.otel.tracer.startActiveSpan(
217
+ // 'querySQL', // NOTE span name will be overridden further down
218
+ // { attributes: { label } },
219
+ // otelContext,
220
+ // (span) => {
221
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
222
+
223
+ // const queryString$ = this.graph.makeThunk(
224
+ // (get, addDebugInfo) => {
225
+ // if (typeof genQueryString === 'function') {
226
+ // const queryString = genQueryString(makeGetAtomResult(get))
227
+ // addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
228
+ // return queryString
229
+ // } else {
230
+ // return genQueryString
231
+ // }
232
+ // },
233
+ // { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
234
+ // otelContext,
235
+ // )
236
+
237
+ // label = label ?? queryString$.result
238
+ // span.updateName(`querySQL:${label}`)
239
+
240
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
241
+
242
+ // const results$ = this.graph.makeThunk<ReadonlyArray<TResult>>(
243
+ // (get, addDebugInfo) =>
244
+ // this.otel.tracer.startActiveSpan(
245
+ // 'sql:', // NOTE span name will be overridden further down
246
+ // {},
247
+ // otelContext,
248
+ // (span) => {
249
+ // try {
250
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
251
+
252
+ // // Establish a reactive dependency on the tables used in the query
253
+ // for (const tableName of queriedTables) {
254
+ // const tableRef =
255
+ // this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
256
+ // get(tableRef)
257
+ // }
258
+ // const sqlString = get(queryString$)
259
+
260
+ // span.setAttribute('sql.query', sqlString)
261
+ // span.updateName(`sql:${sqlString.slice(0, 50)}`)
262
+
263
+ // const results = this.inMemoryDB.select<TResult>(sqlString, {
264
+ // queriedTables,
265
+ // bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
266
+ // otelContext,
267
+ // })
268
+
269
+ // span.setAttribute('sql.rowsCount', results.length)
270
+ // addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
271
+
272
+ // return results
273
+ // } finally {
274
+ // span.end()
275
+ // }
276
+ // },
277
+ // ),
278
+ // { label: queryLabel },
279
+ // otelContext,
280
+ // )
281
+
282
+ // const query = new LiveStoreSQLQuery<TResult>({
283
+ // label,
284
+ // queryString$,
285
+ // results$,
286
+ // componentKey: componentKey ?? globalComponentKey,
287
+ // store: this,
288
+ // otelContext,
289
+ // })
290
+
291
+ // this.activeQueries.add(query)
292
+
293
+ // // TODO get rid of temporary query workaround
294
+ // if (this.temporaryQueries !== undefined) {
295
+ // this.temporaryQueries.add(query)
296
+ // }
297
+
298
+ // // NOTE we are not ending the span here but in the query `destroy` method
299
+ // return query
300
+ // },
301
+ // )
302
+
303
+ // queryJS = <TResult>(
304
+ // genResults: (get: GetAtomResult) => TResult,
305
+ // {
306
+ // componentKey = globalComponentKey,
307
+ // label = `js${uniqueId()}`,
308
+ // otelContext = otel.context.active(),
309
+ // }: { componentKey?: ComponentKey; label?: string; otelContext?: otel.Context },
310
+ // ): LiveStoreJSQuery<TResult> =>
311
+ // this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
312
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
313
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
314
+ // const results$ = this.graph.makeThunk(
315
+ // (get, addDebugInfo) => {
316
+ // addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
317
+ // return genResults(makeGetAtomResult(get))
318
+ // },
319
+ // { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
320
+ // otelContext,
321
+ // )
322
+
323
+ // // const query = new LiveStoreJSQuery<TResult>({
324
+ // // label,
325
+ // // results$,
326
+ // // componentKey,
327
+ // // store: this,
328
+ // // otelContext,
329
+ // // })
330
+
331
+ // this.activeQueries.add(query)
332
+
333
+ // // TODO get rid of temporary query workaround
334
+ // if (this.temporaryQueries !== undefined) {
335
+ // this.temporaryQueries.add(query)
336
+ // }
337
+
338
+ // // NOTE we are not ending the span here but in the query `destroy` method
339
+ // return query
340
+ // })
341
+
342
+ // queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
343
+ // document: DocumentNode<TResult, TVariableValues>,
344
+ // genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
345
+ // {
346
+ // componentKey,
347
+ // label,
348
+ // otelContext = otel.context.active(),
349
+ // }: {
350
+ // componentKey: ComponentKey
351
+ // label?: string
352
+ // otelContext?: otel.Context
353
+ // },
354
+ // ): LiveStoreGraphQLQuery<TResult, TVariableValues, TGraphQLContext> =>
355
+ // this.otel.tracer.startActiveSpan(
356
+ // `queryGraphQL:`, // NOTE span name will be overridden further down
357
+ // {},
358
+ // otelContext,
359
+ // (span) => {
360
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
361
+
362
+ // if (this.graphQLContext === undefined) {
363
+ // return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
364
+ // }
365
+
366
+ // const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
367
+
368
+ // span.updateName(`queryGraphQL:${labelWithDefault}`)
369
+
370
+ // const variableValues$ = this.graph.makeThunk(
371
+ // (get) => {
372
+ // if (typeof genVariableValues === 'function') {
373
+ // return genVariableValues(makeGetAtomResult(get))
374
+ // } else {
375
+ // return genVariableValues
376
+ // }
377
+ // },
378
+ // { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
379
+ // // otelContext,
380
+ // )
381
+
382
+ // const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
383
+ // const results$ = this.graph.makeThunk<TResult>(
384
+ // (get, addDebugInfo) => {
385
+ // const variableValues = get(variableValues$)
386
+ // const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext)
387
+
388
+ // // Add dependencies on any tables that were used
389
+ // for (const tableName of queriedTables) {
390
+ // const tableRef = this.tableRefs[tableName]
391
+ // assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
392
+ // get(tableRef!)
393
+ // }
394
+
395
+ // addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
396
+
397
+ // return result
398
+ // },
399
+ // { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
400
+ // // otelContext,
401
+ // )
402
+
403
+ // const query = new LiveStoreGraphQLQuery({
404
+ // document,
405
+ // context: this.graphQLContext,
406
+ // results$,
407
+ // componentKey,
408
+ // label: labelWithDefault,
409
+ // store: this,
410
+ // otelContext,
411
+ // })
412
+
413
+ // this.activeQueries.add(query)
414
+
415
+ // // TODO get rid of temporary query workaround
416
+ // if (this.temporaryQueries !== undefined) {
417
+ // this.temporaryQueries.add(query)
418
+ // }
419
+
420
+ // // NOTE we are not ending the span here but in the query `destroy` method
421
+ // return query
422
+ // },
423
+ // )
424
+
425
+ // queryGraphQLOnce = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
426
+ // document: DocumentNode<TResult, TVariableValues>,
427
+ // variableValues: TVariableValues,
428
+ // otelContext: otel.Context = this.otel.queriesSpanContext,
429
+ // ): { result: TResult; queriedTables: string[] } => {
430
+ // const schema =
431
+ // this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
432
+ // const context =
433
+ // this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
434
+ // const tracer = this.otel.tracer
435
+
436
+ // const operationName = graphql.getOperationAST(document)?.name?.value
437
+
438
+ // return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
439
+ // try {
440
+ // span.setAttribute('graphql.variables', JSON.stringify(variableValues))
441
+ // span.setAttribute('graphql.query', graphql.print(document))
442
+
443
+ // context.queriedTables.clear()
444
+
445
+ // context.otelContext = otel.trace.setSpan(otel.context.active(), span)
446
+
447
+ // const res = graphql.executeSync({
448
+ // document,
449
+ // contextValue: context,
450
+ // schema: schema,
451
+ // variableValues,
452
+ // })
453
+
454
+ // // TODO track number of nested SQL queries via Otel + debug info
455
+
456
+ // if (res.errors) {
457
+ // span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
458
+ // span.setAttribute('graphql.error', res.errors.join('\n'))
459
+ // span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
460
+ // console.error(`graphql error (${operationName})`, res.errors)
461
+ // }
462
+
463
+ // return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
464
+ // } finally {
465
+ // span.end()
466
+ // }
467
+ // })
468
+ // }
449
469
 
450
470
  /**
451
471
  * Subscribe to the results of a query
452
472
  * Returns a function to cancel the subscription.
453
473
  */
454
- subscribe = <TQuery extends LiveStoreQuery>(
455
- query: TQuery,
456
- onNewValue: (value: QueryResult<TQuery>) => void,
474
+ subscribe = <TResult>(
475
+ query: ILiveStoreQuery<TResult>,
476
+ onNewValue: (value: TResult) => void,
457
477
  onSubsubscribe?: () => void,
458
- options?: { label?: string } | undefined,
478
+ options?: { label?: string; otelContext?: otel.Context } | undefined,
459
479
  ): (() => void) =>
460
480
  this.otel.tracer.startActiveSpan(
461
481
  `LiveStore.subscribe`,
462
482
  { attributes: { label: options?.label } },
463
- query.otelContext,
483
+ options?.otelContext ?? this.otel.queriesSpanContext,
464
484
  (span) => {
465
485
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
466
486
 
467
- const effect = this.graph.makeEffect(
468
- (get) => {
469
- const result = get(query.results$) as QueryResult<TQuery>
470
- onNewValue(result)
471
- },
472
- { label: `subscribe:${options?.label}` },
473
- otelContext,
474
- )
487
+ const effect = this.graph.makeEffect((get) => onNewValue(get(query.results$)), {
488
+ label: `subscribe:${options?.label}`,
489
+ })
490
+
491
+ effect.doEffect(otelContext)
475
492
 
476
- const subscriptionKey = uuid()
493
+ // const subscriptionKey = uuid()
477
494
 
478
495
  const unsubscribe = () => {
479
496
  try {
480
497
  this.graph.destroy(effect)
481
- query.activeSubscriptions.delete(subscriptionKey)
498
+ this.activeQueries.delete(query as LiveStoreQuery)
499
+ // query.activeSubscriptions.delete(subscriptionKey)
482
500
  onSubsubscribe?.()
483
501
  } finally {
484
502
  span.end()
485
503
  }
486
504
  }
487
505
 
488
- query.activeSubscriptions.set(subscriptionKey, unsubscribe)
506
+ this.activeQueries.add(query as LiveStoreQuery)
507
+
508
+ // query.activeSubscriptions.set(subscriptionKey, unsubscribe)
489
509
 
490
510
  return unsubscribe
491
511
  },
492
512
  )
493
513
 
494
- /**
495
- * Any queries created in the callback will be destroyed when the callback is complete.
496
- * Useful for temporarily creating reactive queries, which is an idempotent operation
497
- * that can be safely called inside a React useMemo hook.
498
- */
499
- inTempQueryContext = <TResult>(callback: () => TResult): TResult => {
500
- this.temporaryQueries = new Set()
501
- // TODO: consider errors / try/finally here?
502
- const result = callback()
503
- for (const query of this.temporaryQueries) {
504
- this.destroyQuery(query)
505
- }
506
- this.temporaryQueries = undefined
507
- return result
508
- }
509
-
510
514
  /**
511
515
  * Destroys the entire store, including all queries and subscriptions.
512
516
  *
513
517
  * Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
514
518
  */
515
519
  destroy = () => {
516
- for (const query of this.activeQueries) {
517
- this.destroyQuery(query)
518
- }
519
-
520
520
  Object.values(this.tableRefs).forEach((tableRef) => this.graph.destroy(tableRef))
521
521
 
522
- const applyEventsSpan = otel.trace.getSpan(this.otel.applyEventsSpanContext)!
523
- applyEventsSpan.end()
524
-
525
- const queriesSpan = otel.trace.getSpan(this.otel.queriesSpanContext)!
526
- queriesSpan.end()
522
+ otel.trace.getSpan(this.otel.applyEventsSpanContext)!.end()
523
+ otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
527
524
 
528
525
  // TODO destroy active subscriptions
529
526
  }
530
527
 
531
- private destroyQuery = (query: LiveStoreQuery) => {
532
- if (query._tag === 'sql') {
533
- // results are downstream of query string, so will automatically be destroyed together
534
- this.graph.destroy(query.queryString$)
535
- } else {
536
- this.graph.destroy(query.results$)
537
- }
538
- this.activeQueries.delete(query)
539
- query.destroy()
540
- }
541
-
542
- /**
543
- * Clean up queries and downstream subscriptions associated with a component.
544
- * This is critical to avoid memory leaks.
545
- */
546
- unmountComponent = (componentKey: ComponentKey) => {
547
- for (const query of this.activeQueries) {
548
- if (query.componentKey === componentKey) {
549
- this.destroyQuery(query)
550
- }
551
- }
552
- }
553
-
554
528
  /* Apply a single write event to the store, and refresh all queries in response */
555
529
  applyEvent = <TEventType extends string & keyof LiveStoreActionDefinitionsTypes>(
556
530
  eventType: TEventType,
@@ -579,20 +553,25 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
579
553
  tablesToUpdate.push([tableRef!, null])
580
554
  }
581
555
 
556
+ const debugRefreshReason = {
557
+ _tag: 'applyEvent' as const,
558
+ event: { type: eventType, args },
559
+ writeTables: [...writeTables],
560
+ }
561
+
582
562
  // Update all table refs together in a batch, to only trigger one reactive update
583
- this.graph.setRefs(
584
- tablesToUpdate,
585
- {
586
- otelHint: 'applyEvents',
587
- skipRefresh,
588
- debugRefreshReason: {
589
- _tag: 'applyEvent',
590
- event: { type: eventType, args },
591
- writeTables: [...writeTables],
592
- },
593
- },
594
- otelContext,
595
- )
563
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext })
564
+
565
+ if (skipRefresh === false) {
566
+ // TODO update the graph
567
+ // this.graph.refresh(
568
+ // {
569
+ // otelHint: 'applyEvents',
570
+ // debugRefreshReason,
571
+ // },
572
+ // otelContext,
573
+ // )
574
+ }
596
575
  } catch (e: any) {
597
576
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
598
577
 
@@ -642,7 +621,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
642
621
  try {
643
622
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
644
623
 
645
- // TODO: what to do about backend transaction here?
624
+ // TODO: what to do about storage transaction here?
646
625
  this.inMemoryDB.txn(() => {
647
626
  for (const event of events) {
648
627
  try {
@@ -675,20 +654,18 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
675
654
  tablesToUpdate.push([tableRef!, null])
676
655
  }
677
656
 
657
+ const debugRefreshReason = {
658
+ _tag: 'applyEvents' as const,
659
+ events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
660
+ writeTables: [...writeTables],
661
+ }
678
662
  // Update all table refs together in a batch, to only trigger one reactive update
679
- this.graph.setRefs(
680
- tablesToUpdate,
681
- {
682
- otelHint: 'applyEvents',
683
- skipRefresh,
684
- debugRefreshReason: {
685
- _tag: 'applyEvents',
686
- events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
687
- writeTables: [...writeTables],
688
- },
689
- },
690
- otelContext,
691
- )
663
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext })
664
+
665
+ if (skipRefresh === false) {
666
+ // TODO update the graph
667
+ // this.graph.refresh({ debugRefreshReason, otelHint: 'applyEvents' }, otelContext)
668
+ }
692
669
  } catch (e: any) {
693
670
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
694
671
  } finally {
@@ -711,8 +688,9 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
711
688
  { attributes: { 'livestore.manualRefreshLabel': label } },
712
689
  this.otel.applyEventsSpanContext,
713
690
  (span) => {
714
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
715
- this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext)
691
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
692
+ // TODO update the graph
693
+ // this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext)
716
694
  span.end()
717
695
  },
718
696
  )
@@ -757,6 +735,15 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
757
735
  }
758
736
  },
759
737
  },
738
+
739
+ RawSql: {
740
+ statement: ({ sql, writeTables }: { sql: string; writeTables: string[] }) => ({
741
+ sql,
742
+ writeTables,
743
+ argsAlreadyBound: false,
744
+ }),
745
+ prepareBindValues: ({ bindValues }) => bindValues,
746
+ },
760
747
  }
761
748
 
762
749
  const actionDefinition = actionDefinitions[eventType] ?? shouldNeverHappen(`Unknown event type: ${eventType}`)
@@ -765,20 +752,30 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
765
752
  const eventWithId: LiveStoreEvent = { id: uuid(), type: eventType, args }
766
753
 
767
754
  // Synchronously apply the event to the in-memory database
768
- const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
755
+ // const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
756
+ const { statement, bindValues } = eventToSql(eventWithId, actionDefinition)
757
+ const { durationMs } = this.inMemoryDB.execute(
758
+ statement.sql,
759
+ prepareBindValues(bindValues, statement.sql),
760
+ statement.writeTables,
761
+ {
762
+ otelContext,
763
+ },
764
+ )
769
765
 
770
- // Asynchronously apply the event to a persistent backend (we're not awaiting this promise here)
771
- if (this.backend !== undefined) {
772
- this.backend.applyEvent(eventWithId, actionDefinition, span)
766
+ // Asynchronously apply the event to a persistent storage (we're not awaiting this promise here)
767
+ if (this.storage !== undefined) {
768
+ // this.storage.applyEvent(eventWithId, actionDefinition, span)
769
+ this.storage.execute(statement.sql, prepareBindValues(bindValues, statement.sql), span)
773
770
  }
774
771
 
775
772
  // Uncomment to print a list of queries currently registered on the store
776
773
  // console.log(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
777
774
 
778
- const statement =
779
- typeof actionDefinition.statement === 'function'
780
- ? actionDefinition.statement(args)
781
- : actionDefinition.statement
775
+ // const statement =
776
+ // typeof actionDefinition.statement === 'function'
777
+ // ? actionDefinition.statement(args)
778
+ // : actionDefinition.statement
782
779
 
783
780
  span.end()
784
781
 
@@ -792,12 +789,12 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
792
789
  * This should only be used for framework-internal purposes;
793
790
  * all app writes should go through applyEvent.
794
791
  */
795
- execute = async (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
796
- this.inMemoryDB.execute(query, params, writeTables)
792
+ execute = (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
793
+ this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables)
797
794
 
798
- if (this.backend !== undefined) {
795
+ if (this.storage !== undefined) {
799
796
  const parentSpan = otel.trace.getSpan(otel.context.active())
800
- this.backend.execute(query, params, parentSpan)
797
+ this.storage.execute(query, prepareBindValues(params, query), parentSpan)
801
798
  }
802
799
  }
803
800
  }
@@ -805,83 +802,95 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
805
802
  /** Create a new LiveStore Store */
806
803
  export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
807
804
  schema,
808
- backendOptions,
805
+ loadStorage,
809
806
  graphQLOptions,
810
807
  otelTracer = makeNoopTracer(),
811
808
  otelRootSpanContext = otel.context.active(),
812
809
  boot,
810
+ sqlite3,
813
811
  }: {
814
812
  schema: Schema
815
- backendOptions: BackendOptions
813
+ loadStorage: () => StorageInit | Promise<StorageInit>
816
814
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
817
815
  otelTracer?: otel.Tracer
818
816
  otelRootSpanContext?: otel.Context
819
- boot?: (backend: Backend, parentSpan: otel.Span) => Promise<void>
817
+ boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
818
+ sqlite3: Sqlite.Sqlite3Static
820
819
  }): Promise<Store<TGraphQLContext>> => {
821
820
  return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
822
821
  try {
823
- let persistedData: Uint8Array | undefined
824
- const backend = await createBackend(backendOptions, {
825
- otelTracer: otelTracer ?? makeNoopTracer(),
826
- parentSpan: otel.trace.getSpan(otelRootSpanContext ?? otel.context.active()) ?? makeNoopSpan(),
827
- })
828
- // if we're resetting the database, run boot here.
822
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
829
823
 
830
- let shouldResetDB = false
831
- // Uncomment this line if you want to reset the database contents.
832
- // let shouldResetDB = true
824
+ const storage = await otelTracer.startActiveSpan('storage:load', {}, otelContext, async (span) => {
825
+ try {
826
+ const init = await loadStorage()
827
+ const parentSpan = otel.trace.getSpan(otel.context.active()) ?? makeNoopSpan()
828
+ return init({ otelTracer, parentSpan })
829
+ } finally {
830
+ span.end()
831
+ }
832
+ })
833
833
 
834
- const existingTablesRaw = await backend.select(
835
- sql`SELECT * FROM sqlite_master WHERE type='table';`,
836
- undefined,
837
- span,
834
+ const persistedData = await otelTracer.startActiveSpan(
835
+ 'storage:getPersistedData',
836
+ {},
837
+ otelContext,
838
+ async (span) => {
839
+ try {
840
+ return await storage.getPersistedData(span)
841
+ } finally {
842
+ span.end()
843
+ }
844
+ },
838
845
  )
839
- const existingTables = existingTablesRaw.results.map((t: { name: string }) => t.name)
840
- const missingTables = Object.keys(schema.tables).filter((tableName) => !existingTables.includes(tableName))
841
- if (existingTables.length === 0) {
842
- console.log('No existing tables found, loading from schema')
843
- shouldResetDB = true
844
- } else if (
845
- missingTables.length > 0 &&
846
- window.confirm(
847
- `Existing DB is missing ${missingTables.length} tables: ${missingTables.join(
848
- ', ',
849
- )}\n\nReset DB? This will reset all of the following tables to empty: ${Object.keys(schema).join(', ')}`,
850
- )
851
- ) {
852
- shouldResetDB = true
853
- }
854
-
855
- if (localStorage.getItem(RESET_DB_LOCAL_STORAGE_KEY) !== null) {
856
- shouldResetDB = true
857
- }
858
846
 
859
- if (shouldResetDB) {
860
- await loadSchema(backend, schema)
861
- localStorage.removeItem(RESET_DB_LOCAL_STORAGE_KEY)
862
- }
847
+ const db = InMemoryDatabase.load({ data: persistedData, otelTracer, otelRootSpanContext, sqlite3 })
863
848
 
864
- if (boot) {
865
- await boot(backend!, span)
866
- }
849
+ // Proxy to `db` that also mirrors `execute` calls to `storage`
850
+ const dbProxy = new Proxy(db, {
851
+ get: (db, prop, receiver) => {
852
+ if (prop === 'execute') {
853
+ const execute: InMemoryDatabase['execute'] = (query, bindValues, writeTables, options) => {
854
+ storage.execute(query, bindValues, span)
855
+ return db.execute(query, bindValues, writeTables, options)
856
+ }
857
+ return execute
858
+ } else if (prop === 'select') {
859
+ // NOTE we're even proxying `select` calls here as some apps (e.g. Overtone) currently rely on this
860
+ // TODO remove this once we've migrated all apps to use `execute` instead of `select`
861
+ const select: InMemoryDatabase['select'] = (query, options = {}) => {
862
+ storage.execute(query, options.bindValues as any)
863
+ return db.select(query, options)
864
+ }
865
+ return select
866
+ } else {
867
+ return Reflect.get(db, prop, receiver)
868
+ }
869
+ },
870
+ })
867
871
 
868
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
869
- await otelTracer.startActiveSpan('backend-getPersistedData', {}, otelContext, async (span) => {
872
+ otelTracer.startActiveSpan('migrateDb', {}, otelContext, async (span) => {
870
873
  try {
871
- persistedData = await backend!.getPersistedData(span)
874
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
875
+ migrateDb({ db: dbProxy, schema, otelContext })
872
876
  } finally {
873
877
  span.end()
874
878
  }
875
879
  })
876
880
 
877
- const db: InMemoryDatabase = await InMemoryDatabase.load(persistedData, otelTracer, otelRootSpanContext)
878
- configureSQLite(db)
881
+ if (boot !== undefined) {
882
+ const booting = boot(dbProxy, span)
883
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
884
+ if (isPromise(booting)) {
885
+ await booting
886
+ }
887
+ }
879
888
 
880
889
  // TODO: we can't apply the schema at this point, we've already loaded persisted data!
881
890
  // Think about what to do about this case.
882
891
  // await applySchema(db, schema)
883
892
  return Store.createStore<TGraphQLContext>(
884
- { db, schema, backend, graphQLOptions, otelTracer, otelRootSpanContext },
893
+ { db, dbProxy, schema, storage, graphQLOptions, otelTracer, otelRootSpanContext },
885
894
  span,
886
895
  )
887
896
  } finally {
@@ -890,17 +899,17 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
890
899
  })
891
900
  }
892
901
 
893
- /** Set up SQLite performance; hasn't been super carefully optimized yet. */
894
- const configureSQLite = (db: InMemoryDatabase) => {
895
- db.execute(
896
- // TODO: revisit these tuning parameters for max performance
897
- sql`
898
- PRAGMA page_size=32768;
899
- PRAGMA cache_size=10000;
900
- PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
901
- PRAGMA synchronous='OFF';
902
- PRAGMA temp_store='MEMORY';
903
- PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
904
- `,
905
- )
902
+ const eventToSql = (
903
+ event: LiveStoreEvent,
904
+ eventDefinition: ActionDefinition,
905
+ ): { statement: SQLWriteStatement; bindValues: ParamsObject } => {
906
+ const statement =
907
+ typeof eventDefinition.statement === 'function' ? eventDefinition.statement(event.args) : eventDefinition.statement
908
+
909
+ const prepareBindValues = eventDefinition.prepareBindValues ?? identity
910
+
911
+ const bindValues =
912
+ typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound ? {} : prepareBindValues(event.args)
913
+
914
+ return { statement, bindValues }
906
915
  }