@livestore/livestore 0.0.12 → 0.0.13

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 (197) 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 +61 -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 +198 -0
  18. package/dist/__tests__/reactive.test.js.map +1 -0
  19. package/dist/backends/base.d.ts +13 -0
  20. package/dist/backends/base.d.ts.map +1 -0
  21. package/dist/backends/base.js +53 -0
  22. package/dist/backends/base.js.map +1 -0
  23. package/dist/backends/in-memory/index.d.ts +22 -0
  24. package/dist/backends/in-memory/index.d.ts.map +1 -0
  25. package/dist/backends/in-memory/index.js +45 -0
  26. package/dist/backends/in-memory/index.js.map +1 -0
  27. package/dist/backends/index.d.ts +41 -0
  28. package/dist/backends/index.d.ts.map +1 -0
  29. package/dist/backends/index.js +16 -0
  30. package/dist/backends/index.js.map +1 -0
  31. package/dist/backends/tauri/index.d.ts +21 -0
  32. package/dist/backends/tauri/index.d.ts.map +1 -0
  33. package/dist/backends/tauri/index.js +48 -0
  34. package/dist/backends/tauri/index.js.map +1 -0
  35. package/dist/backends/utils/idb.d.ts +10 -0
  36. package/dist/backends/utils/idb.d.ts.map +1 -0
  37. package/dist/backends/utils/idb.js +58 -0
  38. package/dist/backends/utils/idb.js.map +1 -0
  39. package/dist/backends/web-worker/index.d.ts +26 -0
  40. package/dist/backends/web-worker/index.d.ts.map +1 -0
  41. package/dist/backends/web-worker/index.js +63 -0
  42. package/dist/backends/web-worker/index.js.map +1 -0
  43. package/dist/backends/web-worker/worker.d.ts +17 -0
  44. package/dist/backends/web-worker/worker.d.ts.map +1 -0
  45. package/dist/backends/web-worker/worker.js +139 -0
  46. package/dist/backends/web-worker/worker.js.map +1 -0
  47. package/dist/bounded-collections.d.ts +34 -0
  48. package/dist/bounded-collections.d.ts.map +1 -0
  49. package/dist/bounded-collections.js +103 -0
  50. package/dist/bounded-collections.js.map +1 -0
  51. package/dist/componentKey.d.ts +20 -0
  52. package/dist/componentKey.d.ts.map +1 -0
  53. package/dist/componentKey.js +3 -0
  54. package/dist/componentKey.js.map +1 -0
  55. package/dist/effect/LiveStore.d.ts +42 -0
  56. package/dist/effect/LiveStore.d.ts.map +1 -0
  57. package/dist/effect/LiveStore.js +37 -0
  58. package/dist/effect/LiveStore.js.map +1 -0
  59. package/dist/effect/index.d.ts +2 -0
  60. package/dist/effect/index.d.ts.map +1 -0
  61. package/dist/effect/index.js +2 -0
  62. package/dist/effect/index.js.map +1 -0
  63. package/dist/events.d.ts +7 -0
  64. package/dist/events.d.ts.map +1 -0
  65. package/dist/events.js +2 -0
  66. package/dist/events.js.map +1 -0
  67. package/dist/inMemoryDatabase.d.ts +60 -0
  68. package/dist/inMemoryDatabase.d.ts.map +1 -0
  69. package/dist/inMemoryDatabase.js +230 -0
  70. package/dist/inMemoryDatabase.js.map +1 -0
  71. package/dist/index.d.ts +20 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +9 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/migrations.d.ts +9 -0
  76. package/dist/migrations.d.ts.map +1 -0
  77. package/dist/migrations.js +62 -0
  78. package/dist/migrations.js.map +1 -0
  79. package/dist/otel.d.ts +4 -0
  80. package/dist/otel.d.ts.map +1 -0
  81. package/dist/otel.js +6 -0
  82. package/dist/otel.js.map +1 -0
  83. package/dist/react/LiveStoreContext.d.ts +11 -0
  84. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  85. package/dist/react/LiveStoreContext.js +10 -0
  86. package/dist/react/LiveStoreContext.js.map +1 -0
  87. package/dist/react/LiveStoreProvider.d.ts +22 -0
  88. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  89. package/dist/react/LiveStoreProvider.js +49 -0
  90. package/dist/react/LiveStoreProvider.js.map +1 -0
  91. package/dist/react/index.d.ts +8 -0
  92. package/dist/react/index.d.ts.map +1 -0
  93. package/dist/react/index.js +6 -0
  94. package/dist/react/index.js.map +1 -0
  95. package/dist/react/useGlobalQuery.d.ts +3 -0
  96. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  97. package/dist/react/useGlobalQuery.js +23 -0
  98. package/dist/react/useGlobalQuery.js.map +1 -0
  99. package/dist/react/useGraphQL.d.ts +11 -0
  100. package/dist/react/useGraphQL.d.ts.map +1 -0
  101. package/dist/react/useGraphQL.js +67 -0
  102. package/dist/react/useGraphQL.js.map +1 -0
  103. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  104. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  105. package/dist/react/useLiveStoreComponent.js +301 -0
  106. package/dist/react/useLiveStoreComponent.js.map +1 -0
  107. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  108. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  109. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  110. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  111. package/dist/reactive.d.ts +140 -0
  112. package/dist/reactive.d.ts.map +1 -0
  113. package/dist/reactive.js +302 -0
  114. package/dist/reactive.js.map +1 -0
  115. package/dist/reactiveQueries/base-class.d.ts +24 -0
  116. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  117. package/dist/reactiveQueries/base-class.js +22 -0
  118. package/dist/reactiveQueries/base-class.js.map +1 -0
  119. package/dist/reactiveQueries/graphql.d.ts +25 -0
  120. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  121. package/dist/reactiveQueries/graphql.js +18 -0
  122. package/dist/reactiveQueries/graphql.js.map +1 -0
  123. package/dist/reactiveQueries/js.d.ts +19 -0
  124. package/dist/reactiveQueries/js.d.ts.map +1 -0
  125. package/dist/reactiveQueries/js.js +13 -0
  126. package/dist/reactiveQueries/js.js.map +1 -0
  127. package/dist/reactiveQueries/sql.d.ts +31 -0
  128. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  129. package/dist/reactiveQueries/sql.js +32 -0
  130. package/dist/reactiveQueries/sql.js.map +1 -0
  131. package/dist/schema.d.ts +83 -0
  132. package/dist/schema.d.ts.map +1 -0
  133. package/dist/schema.js +49 -0
  134. package/dist/schema.js.map +1 -0
  135. package/dist/storage/base.d.ts +10 -0
  136. package/dist/storage/base.d.ts.map +1 -0
  137. package/dist/storage/base.js +14 -0
  138. package/dist/storage/base.js.map +1 -0
  139. package/dist/storage/in-memory/index.d.ts +15 -0
  140. package/dist/storage/in-memory/index.d.ts.map +1 -0
  141. package/dist/storage/in-memory/index.js +14 -0
  142. package/dist/storage/in-memory/index.js.map +1 -0
  143. package/dist/storage/index.d.ts +14 -0
  144. package/dist/storage/index.d.ts.map +1 -0
  145. package/dist/storage/index.js +9 -0
  146. package/dist/storage/index.js.map +1 -0
  147. package/dist/storage/tauri/index.d.ts +19 -0
  148. package/dist/storage/tauri/index.d.ts.map +1 -0
  149. package/dist/storage/tauri/index.js +38 -0
  150. package/dist/storage/tauri/index.js.map +1 -0
  151. package/dist/storage/utils/idb.d.ts +10 -0
  152. package/dist/storage/utils/idb.d.ts.map +1 -0
  153. package/dist/storage/utils/idb.js +58 -0
  154. package/dist/storage/utils/idb.js.map +1 -0
  155. package/dist/storage/web-worker/index.d.ts +27 -0
  156. package/dist/storage/web-worker/index.d.ts.map +1 -0
  157. package/dist/storage/web-worker/index.js +76 -0
  158. package/dist/storage/web-worker/index.js.map +1 -0
  159. package/dist/storage/web-worker/worker.d.ts +13 -0
  160. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  161. package/dist/storage/web-worker/worker.js +110 -0
  162. package/dist/storage/web-worker/worker.js.map +1 -0
  163. package/dist/store.d.ts +192 -0
  164. package/dist/store.d.ts.map +1 -0
  165. package/dist/store.js +569 -0
  166. package/dist/store.js.map +1 -0
  167. package/dist/util.d.ts +26 -0
  168. package/dist/util.d.ts.map +1 -0
  169. package/dist/util.js +53 -0
  170. package/dist/util.js.map +1 -0
  171. package/package.json +46 -19
  172. package/src/__tests__/react/fixture.tsx +19 -28
  173. package/src/effect/LiveStore.ts +8 -13
  174. package/src/events.ts +1 -1
  175. package/src/inMemoryDatabase.ts +100 -117
  176. package/src/index.ts +10 -16
  177. package/src/migrations.ts +101 -0
  178. package/src/otel.ts +0 -11
  179. package/src/react/LiveStoreProvider.tsx +12 -8
  180. package/src/react/index.ts +9 -0
  181. package/src/react/useGlobalQuery.ts +0 -3
  182. package/src/react/useLiveStoreComponent.ts +95 -37
  183. package/src/schema.ts +72 -145
  184. package/src/storage/in-memory/index.ts +21 -0
  185. package/src/storage/index.ts +27 -0
  186. package/src/{backends/tauri.ts → storage/tauri/index.ts} +13 -27
  187. package/src/storage/web-worker/index.ts +118 -0
  188. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  189. package/src/store.ts +112 -79
  190. package/src/util.ts +5 -1
  191. package/tsconfig.json +1 -3
  192. package/src/backends/base.ts +0 -67
  193. package/src/backends/index.ts +0 -98
  194. package/src/backends/noop.ts +0 -32
  195. package/src/backends/web-in-memory.ts +0 -65
  196. package/src/backends/web.ts +0 -97
  197. /package/src/{backends → storage}/utils/idb.ts +0 -0
package/src/store.ts CHANGED
@@ -1,28 +1,30 @@
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 initSqlite3Wasm 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
14
  import type { LiveStoreEvent } from './events.js'
15
15
  import { InMemoryDatabase } from './inMemoryDatabase.js'
16
+ import { migrateDb } from './migrations.js'
16
17
  import { getDurationMsFromSpan } from './otel.js'
17
18
  import type { GetAtom, Ref } from './reactive.js'
18
19
  import { ReactiveGraph } from './reactive.js'
19
20
  import { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
20
21
  import { LiveStoreJSQuery } from './reactiveQueries/js.js'
21
22
  import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
22
- import type { ActionDefinition, GetActionArgs, Schema } from './schema.js'
23
- import { componentStateTables, loadSchema } from './schema.js'
23
+ import type { ActionDefinition, GetActionArgs, Schema, SQLWriteStatement } from './schema.js'
24
+ import { componentStateTables } from './schema.js'
25
+ import type { Storage, StorageInit } from './storage/index.js'
24
26
  import type { Bindable, ParamsObject } from './util.js'
25
- import { sql } from './util.js'
27
+ import { isPromise, sql } from './util.js'
26
28
 
27
29
  export type LiveStoreQuery<TResult extends Record<string, any> = any> =
28
30
  | LiveStoreSQLQuery<TResult>
@@ -55,7 +57,7 @@ export type GraphQLOptions<TContext> = {
55
57
  export type StoreOptions<TGraphQLContext extends BaseGraphQLContext> = {
56
58
  db: InMemoryDatabase
57
59
  schema: Schema
58
- backend?: Backend
60
+ storage?: Storage
59
61
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
60
62
  otelTracer: otel.Tracer
61
63
  otelRootSpanContext: otel.Context
@@ -111,13 +113,13 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
111
113
  */
112
114
  tableRefs: { [key: string]: Ref<null> }
113
115
  activeQueries: Set<LiveStoreQuery>
114
- backend?: Backend
116
+ storage?: Storage
115
117
  temporaryQueries: Set<LiveStoreQuery> | undefined
116
118
 
117
119
  private constructor({
118
120
  db,
119
121
  schema,
120
- backend,
122
+ storage,
121
123
  graphQLOptions,
122
124
  otelTracer,
123
125
  otelRootSpanContext,
@@ -133,7 +135,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
133
135
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
134
136
  this.tableRefs = {}
135
137
  this.activeQueries = new Set()
136
- this.backend = backend
138
+ this.storage = storage
137
139
 
138
140
  const applyEventsSpan = otelTracer.startSpan('LiveStore:applyEvents', {}, otelRootSpanContext)
139
141
  const otelApplyEventsSpanContext = otel.trace.setSpan(otel.context.active(), applyEventsSpan)
@@ -642,7 +644,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
642
644
  try {
643
645
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
644
646
 
645
- // TODO: what to do about backend transaction here?
647
+ // TODO: what to do about storage transaction here?
646
648
  this.inMemoryDB.txn(() => {
647
649
  for (const event of events) {
648
650
  try {
@@ -757,6 +759,15 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
757
759
  }
758
760
  },
759
761
  },
762
+
763
+ RawSql: {
764
+ statement: ({ sql, writeTables }: { sql: string; writeTables: string[] }) => ({
765
+ sql,
766
+ writeTables,
767
+ argsAlreadyBound: false,
768
+ }),
769
+ prepareBindValues: ({ bindValues }) => bindValues,
770
+ },
760
771
  }
761
772
 
762
773
  const actionDefinition = actionDefinitions[eventType] ?? shouldNeverHappen(`Unknown event type: ${eventType}`)
@@ -765,20 +776,25 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
765
776
  const eventWithId: LiveStoreEvent = { id: uuid(), type: eventType, args }
766
777
 
767
778
  // Synchronously apply the event to the in-memory database
768
- const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
779
+ // const { durationMs } = this.inMemoryDB.applyEvent(eventWithId, actionDefinition, otelContext)
780
+ const { statement, bindValues } = eventToSql(eventWithId, actionDefinition)
781
+ const { durationMs } = this.inMemoryDB.execute(statement.sql, bindValues, statement.writeTables, {
782
+ otelContext,
783
+ })
769
784
 
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)
785
+ // Asynchronously apply the event to a persistent storage (we're not awaiting this promise here)
786
+ if (this.storage !== undefined) {
787
+ // this.storage.applyEvent(eventWithId, actionDefinition, span)
788
+ this.storage.execute(statement.sql, bindValues, span)
773
789
  }
774
790
 
775
791
  // Uncomment to print a list of queries currently registered on the store
776
792
  // console.log(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
777
793
 
778
- const statement =
779
- typeof actionDefinition.statement === 'function'
780
- ? actionDefinition.statement(args)
781
- : actionDefinition.statement
794
+ // const statement =
795
+ // typeof actionDefinition.statement === 'function'
796
+ // ? actionDefinition.statement(args)
797
+ // : actionDefinition.statement
782
798
 
783
799
  span.end()
784
800
 
@@ -795,9 +811,9 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
795
811
  execute = async (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
796
812
  this.inMemoryDB.execute(query, params, writeTables)
797
813
 
798
- if (this.backend !== undefined) {
814
+ if (this.storage !== undefined) {
799
815
  const parentSpan = otel.trace.getSpan(otel.context.active())
800
- this.backend.execute(query, params, parentSpan)
816
+ this.storage.execute(query, params, parentSpan)
801
817
  }
802
818
  }
803
819
  }
@@ -805,83 +821,100 @@ export class Store<TGraphQLContext extends BaseGraphQLContext> {
805
821
  /** Create a new LiveStore Store */
806
822
  export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
807
823
  schema,
808
- backendOptions,
824
+ loadStorage,
809
825
  graphQLOptions,
810
826
  otelTracer = makeNoopTracer(),
811
827
  otelRootSpanContext = otel.context.active(),
812
828
  boot,
813
829
  }: {
814
830
  schema: Schema
815
- backendOptions: BackendOptions
831
+ loadStorage: () => StorageInit | Promise<StorageInit>
816
832
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
817
833
  otelTracer?: otel.Tracer
818
834
  otelRootSpanContext?: otel.Context
819
- boot?: (backend: Backend, parentSpan: otel.Span) => Promise<void>
835
+ boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
820
836
  }): Promise<Store<TGraphQLContext>> => {
821
837
  return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
822
838
  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.
839
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
829
840
 
830
- let shouldResetDB = false
831
- // Uncomment this line if you want to reset the database contents.
832
- // let shouldResetDB = true
841
+ const loadStorageAndPersistedData = async () => {
842
+ const storage = await otelTracer.startActiveSpan('storage:load', {}, otelContext, async (span) => {
843
+ try {
844
+ const init = await loadStorage()
845
+ const parentSpan = otel.trace.getSpan(otel.context.active()) ?? makeNoopSpan()
846
+ return init({ otelTracer, parentSpan })
847
+ } finally {
848
+ span.end()
849
+ }
850
+ })
833
851
 
834
- const existingTablesRaw = await backend.select(
835
- sql`SELECT * FROM sqlite_master WHERE type='table';`,
836
- undefined,
837
- span,
838
- )
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(', ')}`,
852
+ const persistedData = await otelTracer.startActiveSpan(
853
+ 'storage:getPersistedData',
854
+ {},
855
+ otelContext,
856
+ async (span) => {
857
+ try {
858
+ return await storage.getPersistedData(span)
859
+ } finally {
860
+ span.end()
861
+ }
862
+ },
850
863
  )
851
- ) {
852
- shouldResetDB = true
853
- }
854
864
 
855
- if (localStorage.getItem(RESET_DB_LOCAL_STORAGE_KEY) !== null) {
856
- shouldResetDB = true
865
+ return { storage, persistedData }
857
866
  }
858
867
 
859
- if (shouldResetDB) {
860
- await loadSchema(backend, schema)
861
- localStorage.removeItem(RESET_DB_LOCAL_STORAGE_KEY)
862
- }
868
+ const loadSqlite3 = () =>
869
+ initSqlite3Wasm({
870
+ // Required to load the wasm binary asynchronously. Of course, you can host it wherever you want
871
+ // You can omit locateFile completely when running in node
872
+ // locateFile: () => `/sql-wasm.wasm`,
873
+ print: (message) => console.log(`[livestore sqlite] ${message}`),
874
+ printErr: (message) => console.error(`[livestore sqlite] ${message}`),
875
+ })
863
876
 
864
- if (boot) {
865
- await boot(backend!, span)
866
- }
877
+ const [{ storage, persistedData }, sqlite3] = await Promise.all([loadStorageAndPersistedData(), loadSqlite3()])
867
878
 
868
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
869
- await otelTracer.startActiveSpan('backend-getPersistedData', {}, otelContext, async (span) => {
879
+ const db = InMemoryDatabase.load(persistedData, otelTracer, otelRootSpanContext, sqlite3)
880
+
881
+ // Proxy to `db` that also mirrors `execute` calls to `storage`
882
+ const dbProxy = new Proxy(db, {
883
+ get: (db, prop, receiver) => {
884
+ if (prop === 'execute') {
885
+ const execute: InMemoryDatabase['execute'] = (query, bindValues, writeTables, options) => {
886
+ storage.execute(query, bindValues, span)
887
+ return db.execute(query, bindValues, writeTables, options)
888
+ }
889
+ return execute
890
+ } else {
891
+ return Reflect.get(db, prop, receiver)
892
+ }
893
+ },
894
+ })
895
+
896
+ otelTracer.startActiveSpan('migrateDb', {}, otelContext, async (span) => {
870
897
  try {
871
- persistedData = await backend!.getPersistedData(span)
898
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
899
+ migrateDb({ db: dbProxy, schema, otelContext })
872
900
  } finally {
873
901
  span.end()
874
902
  }
875
903
  })
876
904
 
877
- const db: InMemoryDatabase = await InMemoryDatabase.load(persistedData, otelTracer, otelRootSpanContext)
878
- configureSQLite(db)
905
+ if (boot !== undefined) {
906
+ const booting = boot(dbProxy, span)
907
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
908
+ if (isPromise(booting)) {
909
+ await booting
910
+ }
911
+ }
879
912
 
880
913
  // TODO: we can't apply the schema at this point, we've already loaded persisted data!
881
914
  // Think about what to do about this case.
882
915
  // await applySchema(db, schema)
883
916
  return Store.createStore<TGraphQLContext>(
884
- { db, schema, backend, graphQLOptions, otelTracer, otelRootSpanContext },
917
+ { db, schema, storage, graphQLOptions, otelTracer, otelRootSpanContext },
885
918
  span,
886
919
  )
887
920
  } finally {
@@ -890,17 +923,17 @@ export const createStore = async <TGraphQLContext extends BaseGraphQLContext>({
890
923
  })
891
924
  }
892
925
 
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
- )
926
+ const eventToSql = (
927
+ event: LiveStoreEvent,
928
+ eventDefinition: ActionDefinition,
929
+ ): { statement: SQLWriteStatement; bindValues: ParamsObject } => {
930
+ const statement =
931
+ typeof eventDefinition.statement === 'function' ? eventDefinition.statement(event.args) : eventDefinition.statement
932
+
933
+ const prepareBindValues = eventDefinition.prepareBindValues ?? identity
934
+
935
+ const bindValues =
936
+ typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound ? {} : prepareBindValues(event.args)
937
+
938
+ return { statement, bindValues }
906
939
  }
package/src/util.ts CHANGED
@@ -1,5 +1,7 @@
1
+ /// <reference lib="es2022" />
2
+
1
3
  export type ParamsObject = Record<string, SqlValue>
2
- export type SqlValue = string | number | Uint8Array
4
+ export type SqlValue = string | number | Uint8Array | null
3
5
 
4
6
  export type Bindable = SqlValue[] | ParamsObject
5
7
 
@@ -57,3 +59,5 @@ export const objectToString = (error: any): string => {
57
59
  return 'Error while printing error: ' + e
58
60
  }
59
61
  }
62
+
63
+ 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
- }
@@ -1,98 +0,0 @@
1
- // A backend represents a raw SQLite database.
2
- // Examples include:
3
- // - A native SQLite process running in a Tauri Rust process
4
- // - A SQL.js WASM version of SQLite running in a web worker
5
- //
6
- // We can send commands to execute various kinds of queries,
7
- // and respond to various events from the database.
8
-
9
- import type * as otel from '@opentelemetry/api'
10
-
11
- import type { LiveStoreEvent } from '../events.js'
12
- import type { ActionDefinition } from '../schema.js'
13
- import type { ParamsObject } from '../util.js'
14
- import { casesHandled } from '../util.js'
15
- import type { BackendOptionsTauri } from './tauri.js'
16
- import type { BackendOptionsWeb } from './web.js'
17
- import { WebWorkerBackend } from './web.js'
18
- import type { BackendOptionsWebInMemory } from './web-in-memory.js'
19
- import { WebInMemoryBackend } from './web-in-memory.js'
20
-
21
- /* A location of a persistent writable SQLite file */
22
- export type WritableDatabaseLocation =
23
- | {
24
- type: 'opfs'
25
- virtualFilename: string
26
- }
27
- | {
28
- type: 'indexeddb'
29
- virtualFilename: string
30
- }
31
- | {
32
- type: 'filesystem'
33
- directory: string
34
- filename: string
35
- }
36
- | {
37
- type: 'volatile-in-memory'
38
- }
39
-
40
- export interface Backend {
41
- // Select some data from the DB.
42
- // This should only do reads, not writes, but we don't strongly enforce that.
43
- select<T = any>(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): Promise<SelectResponse<T>>
44
-
45
- // Execute a query where you don't care about the result.
46
- // Used for writes and configuration changes.
47
- execute(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void
48
-
49
- /** Apply an event to the backend */
50
- applyEvent(event: LiveStoreEvent, eventDefiniton: ActionDefinition, parentSpan?: otel.Span): void
51
-
52
- /** Return a snapshot of persisted data from the backend */
53
- getPersistedData(parentSpan?: otel.Span): Promise<Uint8Array>
54
- }
55
-
56
- export type BackendType = 'tauri' | 'web' | 'web-in-memory'
57
-
58
- export const isBackendType = (type: string): type is BackendType => {
59
- return type === 'tauri' || type === 'web' || type === 'web-in-memory'
60
- }
61
-
62
- export type SelectResponse<T = any> = {
63
- results: T[]
64
-
65
- // other perf stats metadata about how long the query took
66
- [key: string]: any
67
- }
68
-
69
- export enum IndexType {
70
- Basic = 'Basic',
71
- FullText = 'FullText',
72
- }
73
-
74
- export type BackendOptions = BackendOptionsTauri | BackendOptionsWeb | BackendOptionsWebInMemory
75
- export type BackendOtelProps = {
76
- otelTracer: otel.Tracer
77
- parentSpan: otel.Span
78
- }
79
-
80
- export const createBackend = async (options: BackendOptions, otelProps: BackendOtelProps): Promise<Backend> => {
81
- switch (options.type) {
82
- case 'tauri': {
83
- // NOTE Dynamic import is needed to avoid Tauri is a dependency of LiveStore (e.g. when used in the web)
84
- const { TauriBackend } = await import('./tauri.js')
85
- return await TauriBackend.load(options, otelProps)
86
- }
87
- case 'web': {
88
- return WebWorkerBackend.load(options, otelProps)
89
- }
90
- // NOTE currently only needed for testing
91
- case 'web-in-memory': {
92
- return WebInMemoryBackend.load(options, otelProps)
93
- }
94
- default: {
95
- casesHandled(options)
96
- }
97
- }
98
- }
@@ -1,32 +0,0 @@
1
- import { makeNoopTracer } from '@livestore/utils'
2
- import type * as otel from '@opentelemetry/api'
3
-
4
- import type { ParamsObject } from '../util.js'
5
- import { BaseBackend } from './base.js'
6
- import type { SelectResponse } from './index.js'
7
-
8
- export type BackendOptionsNoop = {
9
- type: 'noop'
10
- /** Specifies where to persist data for this backend */
11
- otelTracer?: otel.Tracer
12
- }
13
-
14
- export class NoopBackend extends BaseBackend {
15
- constructor(readonly otelTracer: otel.Tracer) {
16
- super()
17
- }
18
-
19
- static load = async (options: BackendOptionsNoop): Promise<NoopBackend> => {
20
- return new NoopBackend(options.otelTracer ?? makeNoopTracer())
21
- }
22
-
23
- execute = (_query: string, _bindValues?: ParamsObject): void => {}
24
-
25
- select = async <T>(_query: string, _bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
26
- return { results: [] }
27
- }
28
-
29
- getPersistedData = async (): Promise<Uint8Array> => {
30
- return new Uint8Array()
31
- }
32
- }
@@ -1,65 +0,0 @@
1
- import type * as otel from '@opentelemetry/api'
2
- import type * as SqliteWasm from 'sqlite-esm'
3
- import sqlite3InitModule from 'sqlite-esm'
4
-
5
- import type { ParamsObject } from '../util.js'
6
- import { prepareBindValues } from '../util.js'
7
- import { BaseBackend } from './base.js'
8
- import type { BackendOtelProps, SelectResponse } from './index.js'
9
-
10
- export type BackendOptionsWebInMemory = {
11
- type: 'web-in-memory'
12
- }
13
-
14
- declare type DatabaseWithCAPI = SqliteWasm.Database & { capi: SqliteWasm.CAPI }
15
-
16
- // NOTE: This backend is currently only used for testing
17
- export class WebInMemoryBackend extends BaseBackend {
18
- constructor(
19
- readonly otelTracer: otel.Tracer,
20
- readonly db: DatabaseWithCAPI,
21
- ) {
22
- super()
23
- }
24
-
25
- static load = async (
26
- _options: BackendOptionsWebInMemory,
27
- { otelTracer }: BackendOtelProps,
28
- ): Promise<WebInMemoryBackend> => {
29
- const sqlite3 = await sqlite3InitModule({
30
- print: (message) => console.log(`[sql-client] ${message}`),
31
- printErr: (message) => console.error(`[sql-client] ${message}`),
32
- })
33
- const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
34
- db.capi = sqlite3.capi
35
-
36
- return new WebInMemoryBackend(otelTracer, db)
37
- }
38
-
39
- execute = (query: string, bindValues?: ParamsObject): void => {
40
- this.db.exec({
41
- sql: query,
42
- bind: prepareBindValues(bindValues ?? {}, query) as TODO,
43
- returnValue: 'resultRows',
44
- rowMode: 'object',
45
- })
46
- }
47
-
48
- select = async <T>(query: string, bindValues?: ParamsObject): Promise<SelectResponse<T>> => {
49
- const resultRows: T[] = []
50
-
51
- this.db.exec({
52
- sql: query,
53
- bind: prepareBindValues(bindValues ?? {}, query) as TODO,
54
- rowMode: 'object',
55
- resultRows,
56
- // callback: (row: any) => console.log('select result', db.filename, query, row),
57
- } as TODO)
58
-
59
- return { results: resultRows }
60
- }
61
-
62
- getPersistedData = async (): Promise<Uint8Array> => {
63
- return this.db.capi.sqlite3_js_db_export(this.db.pointer)
64
- }
65
- }