@livestore/livestore 0.0.12 → 0.0.15

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 (173) hide show
  1. package/README.md +7 -7
  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 +25 -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/useLiveStoreComponent.test.d.ts +2 -0
  12. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  13. package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
  14. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  15. package/dist/__tests__/reactive.test.d.ts +2 -0
  16. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  17. package/dist/__tests__/reactive.test.js +197 -0
  18. package/dist/__tests__/reactive.test.js.map +1 -0
  19. package/dist/bounded-collections.d.ts +34 -0
  20. package/dist/bounded-collections.d.ts.map +1 -0
  21. package/dist/bounded-collections.js +103 -0
  22. package/dist/bounded-collections.js.map +1 -0
  23. package/dist/componentKey.d.ts +20 -0
  24. package/dist/componentKey.d.ts.map +1 -0
  25. package/dist/componentKey.js +3 -0
  26. package/dist/componentKey.js.map +1 -0
  27. package/dist/effect/LiveStore.d.ts +36 -0
  28. package/dist/effect/LiveStore.d.ts.map +1 -0
  29. package/dist/effect/LiveStore.js +41 -0
  30. package/dist/effect/LiveStore.js.map +1 -0
  31. package/dist/effect/index.d.ts +2 -0
  32. package/dist/effect/index.d.ts.map +1 -0
  33. package/dist/effect/index.js +2 -0
  34. package/dist/effect/index.js.map +1 -0
  35. package/dist/events.d.ts +7 -0
  36. package/dist/events.d.ts.map +1 -0
  37. package/dist/events.js +2 -0
  38. package/dist/events.js.map +1 -0
  39. package/dist/inMemoryDatabase.d.ts +56 -0
  40. package/dist/inMemoryDatabase.d.ts.map +1 -0
  41. package/dist/inMemoryDatabase.js +223 -0
  42. package/dist/inMemoryDatabase.js.map +1 -0
  43. package/dist/index.d.ts +20 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +9 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/migrations.d.ts +16 -0
  48. package/dist/migrations.d.ts.map +1 -0
  49. package/dist/migrations.js +67 -0
  50. package/dist/migrations.js.map +1 -0
  51. package/dist/otel.d.ts +4 -0
  52. package/dist/otel.d.ts.map +1 -0
  53. package/dist/otel.js +6 -0
  54. package/dist/otel.js.map +1 -0
  55. package/dist/react/LiveStoreContext.d.ts +11 -0
  56. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  57. package/dist/react/LiveStoreContext.js +10 -0
  58. package/dist/react/LiveStoreContext.js.map +1 -0
  59. package/dist/react/LiveStoreProvider.d.ts +20 -0
  60. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  61. package/dist/react/LiveStoreProvider.js +52 -0
  62. package/dist/react/LiveStoreProvider.js.map +1 -0
  63. package/dist/react/index.d.ts +8 -0
  64. package/dist/react/index.d.ts.map +1 -0
  65. package/dist/react/index.js +6 -0
  66. package/dist/react/index.js.map +1 -0
  67. package/dist/react/useGraphQL.d.ts +13 -0
  68. package/dist/react/useGraphQL.d.ts.map +1 -0
  69. package/dist/react/useGraphQL.js +85 -0
  70. package/dist/react/useGraphQL.js.map +1 -0
  71. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  72. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  73. package/dist/react/useLiveStoreComponent.js +317 -0
  74. package/dist/react/useLiveStoreComponent.js.map +1 -0
  75. package/dist/react/useQuery.d.ts +3 -0
  76. package/dist/react/useQuery.d.ts.map +1 -0
  77. package/dist/react/useQuery.js +38 -0
  78. package/dist/react/useQuery.js.map +1 -0
  79. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  80. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  81. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  82. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  83. package/dist/reactive.d.ts +140 -0
  84. package/dist/reactive.d.ts.map +1 -0
  85. package/dist/reactive.js +302 -0
  86. package/dist/reactive.js.map +1 -0
  87. package/dist/reactiveQueries/base-class.d.ts +27 -0
  88. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  89. package/dist/reactiveQueries/base-class.js +23 -0
  90. package/dist/reactiveQueries/base-class.js.map +1 -0
  91. package/dist/reactiveQueries/graphql.d.ts +25 -0
  92. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  93. package/dist/reactiveQueries/graphql.js +18 -0
  94. package/dist/reactiveQueries/graphql.js.map +1 -0
  95. package/dist/reactiveQueries/js.d.ts +19 -0
  96. package/dist/reactiveQueries/js.d.ts.map +1 -0
  97. package/dist/reactiveQueries/js.js +13 -0
  98. package/dist/reactiveQueries/js.js.map +1 -0
  99. package/dist/reactiveQueries/sql.d.ts +31 -0
  100. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  101. package/dist/reactiveQueries/sql.js +32 -0
  102. package/dist/reactiveQueries/sql.js.map +1 -0
  103. package/dist/schema.d.ts +81 -0
  104. package/dist/schema.d.ts.map +1 -0
  105. package/dist/schema.js +46 -0
  106. package/dist/schema.js.map +1 -0
  107. package/dist/storage/in-memory/index.d.ts +15 -0
  108. package/dist/storage/in-memory/index.d.ts.map +1 -0
  109. package/dist/storage/in-memory/index.js +14 -0
  110. package/dist/storage/in-memory/index.js.map +1 -0
  111. package/dist/storage/index.d.ts +14 -0
  112. package/dist/storage/index.d.ts.map +1 -0
  113. package/dist/storage/index.js +9 -0
  114. package/dist/storage/index.js.map +1 -0
  115. package/dist/storage/tauri/index.d.ts +19 -0
  116. package/dist/storage/tauri/index.d.ts.map +1 -0
  117. package/dist/storage/tauri/index.js +38 -0
  118. package/dist/storage/tauri/index.js.map +1 -0
  119. package/dist/storage/utils/idb.d.ts +10 -0
  120. package/dist/storage/utils/idb.d.ts.map +1 -0
  121. package/dist/storage/utils/idb.js +58 -0
  122. package/dist/storage/utils/idb.js.map +1 -0
  123. package/dist/storage/web-worker/index.d.ts +27 -0
  124. package/dist/storage/web-worker/index.d.ts.map +1 -0
  125. package/dist/storage/web-worker/index.js +74 -0
  126. package/dist/storage/web-worker/index.js.map +1 -0
  127. package/dist/storage/web-worker/worker.d.ts +13 -0
  128. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  129. package/dist/storage/web-worker/worker.js +110 -0
  130. package/dist/storage/web-worker/worker.js.map +1 -0
  131. package/dist/store.d.ts +199 -0
  132. package/dist/store.d.ts.map +1 -0
  133. package/dist/store.js +603 -0
  134. package/dist/store.js.map +1 -0
  135. package/dist/util.d.ts +28 -0
  136. package/dist/util.d.ts.map +1 -0
  137. package/dist/util.js +55 -0
  138. package/dist/util.js.map +1 -0
  139. package/package.json +46 -19
  140. package/src/__tests__/react/fixture.tsx +23 -32
  141. package/src/__tests__/reactive.test.ts +3 -4
  142. package/src/effect/LiveStore.ts +22 -31
  143. package/src/events.ts +1 -1
  144. package/src/inMemoryDatabase.ts +115 -140
  145. package/src/index.ts +20 -20
  146. package/src/migrations.ts +119 -0
  147. package/src/otel.ts +0 -11
  148. package/src/react/LiveStoreProvider.tsx +24 -23
  149. package/src/react/index.ts +10 -1
  150. package/src/react/useGraphQL.ts +28 -2
  151. package/src/react/useLiveStoreComponent.ts +134 -50
  152. package/src/react/useQuery.ts +56 -0
  153. package/src/reactive.ts +6 -4
  154. package/src/reactiveQueries/base-class.ts +9 -3
  155. package/src/reactiveQueries/graphql.ts +4 -4
  156. package/src/reactiveQueries/js.ts +2 -2
  157. package/src/reactiveQueries/sql.ts +6 -6
  158. package/src/schema.ts +69 -145
  159. package/src/storage/in-memory/index.ts +21 -0
  160. package/src/storage/index.ts +27 -0
  161. package/src/{backends/tauri.ts → storage/tauri/index.ts} +14 -28
  162. package/src/storage/web-worker/index.ts +116 -0
  163. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  164. package/src/store.ts +171 -98
  165. package/src/util.ts +13 -3
  166. package/tsconfig.json +1 -3
  167. package/src/backends/base.ts +0 -67
  168. package/src/backends/index.ts +0 -98
  169. package/src/backends/noop.ts +0 -32
  170. package/src/backends/web-in-memory.ts +0 -65
  171. package/src/backends/web.ts +0 -97
  172. package/src/react/useGlobalQuery.ts +0 -40
  173. /package/src/{backends → storage}/utils/idb.ts +0 -0
package/src/store.ts CHANGED
@@ -1,28 +1,33 @@
1
1
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
2
  import { assertNever, makeNoopSpan, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
3
+ import { identity } from '@livestore/utils/effect'
3
4
  import * as otel from '@opentelemetry/api'
4
5
  import type { GraphQLSchema } from 'graphql'
5
6
  import * as graphql from 'graphql'
6
7
  import { uniqueId } from 'lodash-es'
7
8
  import * as ReactDOM from 'react-dom'
9
+ import type * as Sqlite from 'sqlite-esm'
8
10
  import { v4 as uuid } from 'uuid'
9
11
 
10
- import type { Backend, BackendOptions } from './backends/index.js'
11
- import { createBackend } from './backends/index.js'
12
12
  import type { ComponentKey } from './componentKey.js'
13
13
  import { tableNameForComponentKey } from './componentKey.js'
14
+ import type { QueryDefinition } from './effect/LiveStore.js'
14
15
  import type { LiveStoreEvent } from './events.js'
15
16
  import { InMemoryDatabase } from './inMemoryDatabase.js'
17
+ import { migrateDb } from './migrations.js'
16
18
  import { getDurationMsFromSpan } from './otel.js'
17
- import type { GetAtom, Ref } from './reactive.js'
19
+ import type { Atom, Ref } from './reactive.js'
18
20
  import { ReactiveGraph } from './reactive.js'
19
21
  import { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
20
22
  import { LiveStoreJSQuery } from './reactiveQueries/js.js'
21
23
  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 { ActionDefinition, GetActionArgs, Schema, SQLWriteStatement } from './schema.js'
25
+ import { componentStateTables } from './schema.js'
26
+ import type { Storage, StorageInit } from './storage/index.js'
24
27
  import type { Bindable, ParamsObject } from './util.js'
25
- import { sql } from './util.js'
28
+ import { isPromise, prepareBindValues, sql } from './util.js'
29
+
30
+ export type GetAtomResult = <T>(atom: Atom<T> | LiveStoreJSQuery<T>) => T
26
31
 
27
32
  export type LiveStoreQuery<TResult extends Record<string, any> = any> =
28
33
  | LiveStoreSQLQuery<TResult>
@@ -35,8 +40,6 @@ export type BaseGraphQLContext = {
35
40
  otelContext?: otel.Context
36
41
  }
37
42
 
38
- export const RESET_DB_LOCAL_STORAGE_KEY = 'livestore-reset'
39
-
40
43
  export type QueryResult<TQuery> = TQuery extends LiveStoreSQLQuery<infer R>
41
44
  ? ReadonlyArray<Readonly<R>>
42
45
  : TQuery extends LiveStoreJSQuery<infer S>
@@ -54,8 +57,10 @@ export type GraphQLOptions<TContext> = {
54
57
 
55
58
  export type StoreOptions<TGraphQLContext extends BaseGraphQLContext> = {
56
59
  db: InMemoryDatabase
60
+ /** A `Proxy`d version of `db` except that it also mirrors `execute` calls to the storage */
61
+ dbProxy: InMemoryDatabase
57
62
  schema: Schema
58
- backend?: Backend
63
+ storage?: Storage
59
64
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
60
65
  otelTracer: otel.Tracer
61
66
  otelRootSpanContext: otel.Context
@@ -98,9 +103,11 @@ export type StoreOtel = {
98
103
  queriesSpanContext: otel.Context
99
104
  }
100
105
 
101
- export class Store<TGraphQLContext extends BaseGraphQLContext> {
106
+ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext> {
102
107
  graph: ReactiveGraph<RefreshReason, QueryDebugInfo>
103
108
  inMemoryDB: InMemoryDatabase
109
+ // TODO refactor
110
+ _proxyDb: InMemoryDatabase
104
111
  schema: Schema
105
112
  graphQLSchema?: GraphQLSchema
106
113
  graphQLContext?: TGraphQLContext
@@ -111,18 +118,20 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
111
118
  */
112
119
  tableRefs: { [key: string]: Ref<null> }
113
120
  activeQueries: Set<LiveStoreQuery>
114
- backend?: Backend
121
+ storage?: Storage
115
122
  temporaryQueries: Set<LiveStoreQuery> | undefined
116
123
 
117
124
  private constructor({
118
125
  db,
126
+ dbProxy,
119
127
  schema,
120
- backend,
128
+ storage,
121
129
  graphQLOptions,
122
130
  otelTracer,
123
131
  otelRootSpanContext,
124
132
  }: StoreOptions<TGraphQLContext>) {
125
133
  this.inMemoryDB = db
134
+ this._proxyDb = dbProxy
126
135
  this.graph = new ReactiveGraph({
127
136
  // TODO move this into React module
128
137
  // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
@@ -133,7 +142,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
133
142
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
134
143
  this.tableRefs = {}
135
144
  this.activeQueries = new Set()
136
- this.backend = backend
145
+ this.storage = storage
137
146
 
138
147
  const applyEventsSpan = otelTracer.startSpan('LiveStore:applyEvents', {}, otelRootSpanContext)
139
148
  const otelApplyEventsSpanContext = otel.trace.setSpan(otel.context.active(), applyEventsSpan)
@@ -186,7 +195,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
186
195
  * NOTE The query is actually running (even if no one has subscribed to it yet) and will be kept up to date.
187
196
  */
188
197
  querySQL = <TResult>(
189
- genQueryString: (get: GetAtom) => string,
198
+ genQueryString: string | ((get: GetAtomResult) => string),
190
199
  {
191
200
  queriedTables,
192
201
  bindValues,
@@ -216,9 +225,17 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
216
225
 
217
226
  const queryString$ = this.graph.makeThunk(
218
227
  (get, addDebugInfo) => {
219
- const queryString = genQueryString(get)
220
- addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
221
- return queryString
228
+ if (typeof genQueryString === 'function') {
229
+ const getAtom: GetAtomResult = (atom) => {
230
+ if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
231
+ return get(atom.results$)
232
+ }
233
+ const queryString = genQueryString(getAtom)
234
+ addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
235
+ return queryString
236
+ } else {
237
+ return genQueryString
238
+ }
222
239
  },
223
240
  { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
224
241
  otelContext,
@@ -229,10 +246,10 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
229
246
 
230
247
  const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
231
248
 
232
- const results$ = this.graph.makeThunk<TResult[]>(
249
+ const results$ = this.graph.makeThunk<ReadonlyArray<TResult>>(
233
250
  (get, addDebugInfo) =>
234
251
  this.otel.tracer.startActiveSpan(
235
- 'sql', // NOTE span name will be overridden further down
252
+ 'sql:', // NOTE span name will be overridden further down
236
253
  {},
237
254
  otelContext,
238
255
  (span) => {
@@ -250,12 +267,16 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
250
267
  span.setAttribute('sql.query', sqlString)
251
268
  span.updateName(`sql:${sqlString.slice(0, 50)}`)
252
269
 
253
- const results = this.inMemoryDB.select(sqlString, { queriedTables, bindValues, otelContext })
270
+ const results = this.inMemoryDB.select<TResult>(sqlString, {
271
+ queriedTables,
272
+ bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
273
+ otelContext,
274
+ })
254
275
 
255
276
  span.setAttribute('sql.rowsCount', results.length)
256
277
  addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
257
278
 
258
- return results as unknown as TResult[]
279
+ return results
259
280
  } finally {
260
281
  span.end()
261
282
  }
@@ -287,7 +308,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
287
308
  )
288
309
 
289
310
  queryJS = <TResult>(
290
- genResults: (get: GetAtom) => TResult,
311
+ genResults: (get: GetAtomResult) => TResult,
291
312
  {
292
313
  componentKey = globalComponentKey,
293
314
  label = `js${uniqueId()}`,
@@ -299,8 +320,12 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
299
320
  const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
300
321
  const results$ = this.graph.makeThunk(
301
322
  (get, addDebugInfo) => {
323
+ const getAtom: GetAtomResult = (atom) => {
324
+ if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
325
+ return get(atom.results$)
326
+ }
302
327
  addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
303
- return genResults(get)
328
+ return genResults(getAtom)
304
329
  },
305
330
  { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
306
331
  otelContext,
@@ -327,7 +352,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
327
352
 
328
353
  queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
329
354
  document: DocumentNode<TResult, TVariableValues>,
330
- genVariableValues: (get: GetAtom) => TVariableValues,
355
+ genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
331
356
  {
332
357
  componentKey,
333
358
  label,
@@ -354,7 +379,17 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
354
379
  span.updateName(`queryGraphQL:${labelWithDefault}`)
355
380
 
356
381
  const variableValues$ = this.graph.makeThunk(
357
- genVariableValues,
382
+ (get) => {
383
+ if (typeof genVariableValues === 'function') {
384
+ const getAtom: GetAtomResult = (atom) => {
385
+ if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
386
+ return get(atom.results$)
387
+ }
388
+ return genVariableValues(getAtom)
389
+ } else {
390
+ return genVariableValues
391
+ }
392
+ },
358
393
  { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
359
394
  otelContext,
360
395
  )
@@ -642,7 +677,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
642
677
  try {
643
678
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
644
679
 
645
- // TODO: what to do about backend transaction here?
680
+ // TODO: what to do about storage transaction here?
646
681
  this.inMemoryDB.txn(() => {
647
682
  for (const event of events) {
648
683
  try {
@@ -718,6 +753,13 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
718
753
  )
719
754
  }
720
755
 
756
+ // TODO get rid of this as part of new query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
757
+ runOnce = <TQueryDef extends QueryDefinition>(queryDef: TQueryDef): QueryResult<ReturnType<TQueryDef>> => {
758
+ return this.inTempQueryContext(() => {
759
+ return queryDef(this).results$.result
760
+ })
761
+ }
762
+
721
763
  /**
722
764
  * Apply an event to the store.
723
765
  * Returns the tables that were affected by the event.
@@ -757,6 +799,15 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
757
799
  }
758
800
  },
759
801
  },
802
+
803
+ RawSql: {
804
+ statement: ({ sql, writeTables }: { sql: string; writeTables: string[] }) => ({
805
+ sql,
806
+ writeTables,
807
+ argsAlreadyBound: false,
808
+ }),
809
+ prepareBindValues: ({ bindValues }) => bindValues,
810
+ },
760
811
  }
761
812
 
762
813
  const actionDefinition = actionDefinitions[eventType] ?? shouldNeverHappen(`Unknown event type: ${eventType}`)
@@ -765,20 +816,30 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
765
816
  const eventWithId: LiveStoreEvent = { id: uuid(), type: eventType, args }
766
817
 
767
818
  // Synchronously apply the event to the in-memory database
768
- const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
819
+ // const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
820
+ const { statement, bindValues } = eventToSql(eventWithId, actionDefinition)
821
+ const { durationMs } = this.inMemoryDB.execute(
822
+ statement.sql,
823
+ prepareBindValues(bindValues, statement.sql),
824
+ statement.writeTables,
825
+ {
826
+ otelContext,
827
+ },
828
+ )
769
829
 
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)
830
+ // Asynchronously apply the event to a persistent storage (we're not awaiting this promise here)
831
+ if (this.storage !== undefined) {
832
+ // this.storage.applyEvent(eventWithId, actionDefinition, span)
833
+ this.storage.execute(statement.sql, prepareBindValues(bindValues, statement.sql), span)
773
834
  }
774
835
 
775
836
  // Uncomment to print a list of queries currently registered on the store
776
837
  // console.log(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
777
838
 
778
- const statement =
779
- typeof actionDefinition.statement === 'function'
780
- ? actionDefinition.statement(args)
781
- : actionDefinition.statement
839
+ // const statement =
840
+ // typeof actionDefinition.statement === 'function'
841
+ // ? actionDefinition.statement(args)
842
+ // : actionDefinition.statement
782
843
 
783
844
  span.end()
784
845
 
@@ -793,11 +854,11 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
793
854
  * all app writes should go through applyEvent.
794
855
  */
795
856
  execute = async (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
796
- this.inMemoryDB.execute(query, params, writeTables)
857
+ this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables)
797
858
 
798
- if (this.backend !== undefined) {
859
+ if (this.storage !== undefined) {
799
860
  const parentSpan = otel.trace.getSpan(otel.context.active())
800
- this.backend.execute(query, params, parentSpan)
861
+ this.storage.execute(query, prepareBindValues(params, query), parentSpan)
801
862
  }
802
863
  }
803
864
  }
@@ -805,83 +866,95 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
805
866
  /** Create a new LiveStore Store */
806
867
  export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
807
868
  schema,
808
- backendOptions,
869
+ loadStorage,
809
870
  graphQLOptions,
810
871
  otelTracer = makeNoopTracer(),
811
872
  otelRootSpanContext = otel.context.active(),
812
873
  boot,
874
+ sqlite3,
813
875
  }: {
814
876
  schema: Schema
815
- backendOptions: BackendOptions
877
+ loadStorage: () => StorageInit | Promise<StorageInit>
816
878
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
817
879
  otelTracer?: otel.Tracer
818
880
  otelRootSpanContext?: otel.Context
819
- boot?: (backend: Backend, parentSpan: otel.Span) => Promise<void>
881
+ boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
882
+ sqlite3: Sqlite.Sqlite3Static
820
883
  }): Promise<Store<TGraphQLContext>> => {
821
884
  return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
822
885
  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.
886
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
829
887
 
830
- let shouldResetDB = false
831
- // Uncomment this line if you want to reset the database contents.
832
- // let shouldResetDB = true
888
+ const storage = await otelTracer.startActiveSpan('storage:load', {}, otelContext, async (span) => {
889
+ try {
890
+ const init = await loadStorage()
891
+ const parentSpan = otel.trace.getSpan(otel.context.active()) ?? makeNoopSpan()
892
+ return init({ otelTracer, parentSpan })
893
+ } finally {
894
+ span.end()
895
+ }
896
+ })
833
897
 
834
- const existingTablesRaw = await backend.select(
835
- sql`SELECT * FROM sqlite_master WHERE type='table';`,
836
- undefined,
837
- span,
898
+ const persistedData = await otelTracer.startActiveSpan(
899
+ 'storage:getPersistedData',
900
+ {},
901
+ otelContext,
902
+ async (span) => {
903
+ try {
904
+ return await storage.getPersistedData(span)
905
+ } finally {
906
+ span.end()
907
+ }
908
+ },
838
909
  )
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
910
 
859
- if (shouldResetDB) {
860
- await loadSchema(backend, schema)
861
- localStorage.removeItem(RESET_DB_LOCAL_STORAGE_KEY)
862
- }
911
+ const db = InMemoryDatabase.load({ data: persistedData, otelTracer, otelRootSpanContext, sqlite3 })
863
912
 
864
- if (boot) {
865
- await boot(backend!, span)
866
- }
913
+ // Proxy to `db` that also mirrors `execute` calls to `storage`
914
+ const dbProxy = new Proxy(db, {
915
+ get: (db, prop, receiver) => {
916
+ if (prop === 'execute') {
917
+ const execute: InMemoryDatabase['execute'] = (query, bindValues, writeTables, options) => {
918
+ storage.execute(query, bindValues, span)
919
+ return db.execute(query, bindValues, writeTables, options)
920
+ }
921
+ return execute
922
+ } else if (prop === 'select') {
923
+ // NOTE we're even proxying `select` calls here as some apps (e.g. Overtone) currently rely on this
924
+ // TODO remove this once we've migrated all apps to use `execute` instead of `select`
925
+ const select: InMemoryDatabase['select'] = (query, options = {}) => {
926
+ storage.execute(query, options.bindValues as any)
927
+ return db.select(query, options)
928
+ }
929
+ return select
930
+ } else {
931
+ return Reflect.get(db, prop, receiver)
932
+ }
933
+ },
934
+ })
867
935
 
868
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
869
- await otelTracer.startActiveSpan('backend-getPersistedData', {}, otelContext, async (span) => {
936
+ otelTracer.startActiveSpan('migrateDb', {}, otelContext, async (span) => {
870
937
  try {
871
- persistedData = await backend!.getPersistedData(span)
938
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
939
+ migrateDb({ db: dbProxy, schema, otelContext })
872
940
  } finally {
873
941
  span.end()
874
942
  }
875
943
  })
876
944
 
877
- const db: InMemoryDatabase = await InMemoryDatabase.load(persistedData, otelTracer, otelRootSpanContext)
878
- configureSQLite(db)
945
+ if (boot !== undefined) {
946
+ const booting = boot(dbProxy, span)
947
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
948
+ if (isPromise(booting)) {
949
+ await booting
950
+ }
951
+ }
879
952
 
880
953
  // TODO: we can't apply the schema at this point, we've already loaded persisted data!
881
954
  // Think about what to do about this case.
882
955
  // await applySchema(db, schema)
883
956
  return Store.createStore<TGraphQLContext>(
884
- { db, schema, backend, graphQLOptions, otelTracer, otelRootSpanContext },
957
+ { db, dbProxy, schema, storage, graphQLOptions, otelTracer, otelRootSpanContext },
885
958
  span,
886
959
  )
887
960
  } finally {
@@ -890,17 +963,17 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
890
963
  })
891
964
  }
892
965
 
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
- )
966
+ const eventToSql = (
967
+ event: LiveStoreEvent,
968
+ eventDefinition: ActionDefinition,
969
+ ): { statement: SQLWriteStatement; bindValues: ParamsObject } => {
970
+ const statement =
971
+ typeof eventDefinition.statement === 'function' ? eventDefinition.statement(event.args) : eventDefinition.statement
972
+
973
+ const prepareBindValues = eventDefinition.prepareBindValues ?? identity
974
+
975
+ const bindValues =
976
+ typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound ? {} : prepareBindValues(event.args)
977
+
978
+ return { statement, bindValues }
906
979
  }
package/src/util.ts CHANGED
@@ -1,8 +1,14 @@
1
+ /// <reference lib="es2022" />
2
+
3
+ import type { Brand } from '@livestore/utils/effect'
4
+
1
5
  export type ParamsObject = Record<string, SqlValue>
2
- export type SqlValue = string | number | Uint8Array
6
+ export type SqlValue = string | number | Uint8Array | null
3
7
 
4
8
  export type Bindable = SqlValue[] | ParamsObject
5
9
 
10
+ export type PreparedBindValues = Brand.Branded<Bindable, 'PreparedBindValues'>
11
+
6
12
  /**
7
13
  * This is a tag function for tagged literals.
8
14
  * it lets us get syntax highlighting on SQL queries in VSCode, but
@@ -23,7 +29,9 @@ export const sql = (template: TemplateStringsArray, ...args: unknown[]): string
23
29
  /* because rusqlite doesn't allow unused named params
24
30
  /* TODO: Search for unused params via proper parsing, not string search
25
31
  **/
26
- export const prepareBindValues = (values: ParamsObject, statement: string): ParamsObject => {
32
+ export const prepareBindValues = (values: Bindable, statement: string): PreparedBindValues => {
33
+ if (Array.isArray(values)) return values as PreparedBindValues
34
+
27
35
  const result: ParamsObject = {}
28
36
  for (const [key, value] of Object.entries(values)) {
29
37
  if (statement.includes(key)) {
@@ -31,7 +39,7 @@ export const prepareBindValues = (values: ParamsObject, statement: string): Para
31
39
  }
32
40
  }
33
41
 
34
- return result
42
+ return result as PreparedBindValues
35
43
  }
36
44
 
37
45
  /**
@@ -57,3 +65,5 @@ export const objectToString = (error: any): string => {
57
65
  return 'Error while printing error: ' + e
58
66
  }
59
67
  }
68
+
69
+ export const isPromise = (value: any): value is Promise<unknown> => typeof value?.then === 'function'
package/tsconfig.json CHANGED
@@ -9,7 +9,5 @@
9
9
  "tsBuildInfoFile": "./dist/.tsbuildinfo"
10
10
  },
11
11
  "include": ["./src"],
12
- "references": [
13
- { "path": "../utils" },
14
- ]
12
+ "references": [{ "path": "../../effect-db-schema" }, { "path": "../utils" }]
15
13
  }
@@ -1,67 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- import { errorToString } from '@livestore/utils'
3
- import { identity } from '@livestore/utils/effect'
4
- import * as otel from '@opentelemetry/api'
5
-
6
- import type { LiveStoreEvent } from '../events.js'
7
- // import { EVENTS_TABLE_NAME } from '../events.js'
8
- import type { ActionDefinition } from '../schema.js'
9
- import type { ParamsObject } from '../util.js'
10
- import type { Backend, SelectResponse } from './index.js'
11
-
12
- export abstract class BaseBackend implements Backend {
13
- abstract otelTracer: otel.Tracer
14
-
15
- select = async <T = any>(query: string, bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
16
- throw new Error('Method not implemented.')
17
- }
18
-
19
- execute = (query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void => {
20
- throw new Error('Method not implemented.')
21
- }
22
-
23
- getPersistedData = async (parentSpan?: otel.Span): Promise<Uint8Array> => {
24
- throw new Error('Method not implemented.')
25
- }
26
-
27
- // TODO move `applyEvent` logic to Store and only call `execute` here
28
- applyEvent = (event: LiveStoreEvent, eventDefinition: ActionDefinition, parentSpan?: otel.Span): void => {
29
- const ctx = parentSpan ? otel.trace.setSpan(otel.context.active(), parentSpan) : otel.context.active()
30
- this.otelTracer.startActiveSpan('LiveStore:backend:applyEvent', {}, ctx, (span) => {
31
- try {
32
- // Careful: this SQL statement is duplicated in the backend.
33
- // Remember to update it in src-tauri/src/store.rs:apply_event as well.
34
- // await this.execute(sql`insert into ${EVENTS_TABLE_NAME} (id, type, args) values ($id, $type, $args)`, {
35
- // id: event.id,
36
- // type: event.type,
37
- // args: JSON.stringify(event.args ?? {}),
38
- // })
39
-
40
- const statement =
41
- typeof eventDefinition.statement === 'function'
42
- ? eventDefinition.statement(event.args)
43
- : eventDefinition.statement
44
-
45
- const prepareBindValues = eventDefinition.prepareBindValues ?? identity
46
-
47
- const bindValues =
48
- typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound
49
- ? {}
50
- : prepareBindValues(event.args)
51
-
52
- span.setAttributes({
53
- 'livestore.statement.sql': statement.sql,
54
- 'livestore.statement.writeTables': statement.writeTables,
55
- 'livestore.statement.bindVales': JSON.stringify(bindValues),
56
- })
57
-
58
- this.execute(statement.sql, bindValues, span)
59
- } catch (e: any) {
60
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: errorToString(e) })
61
- throw e
62
- } finally {
63
- span.end()
64
- }
65
- })
66
- }
67
- }