@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
@@ -1,33 +1,17 @@
1
1
  /* eslint-disable prefer-arrow/prefer-arrow-functions */
2
2
 
3
3
  import { shouldNeverHappen } from '@livestore/utils'
4
- import { identity } from '@livestore/utils/effect'
5
4
  import type * as otel from '@opentelemetry/api'
6
- import type * as SqliteWasm from 'sqlite-esm'
7
- import initSqlJs from 'sqlite-esm'
5
+ import type * as Sqlite from 'sqlite-esm'
8
6
 
9
7
  import BoundMap, { BoundArray } from './bounded-collections.js'
10
- import type { LiveStoreEvent } from './events.js'
11
8
  // import { EVENTS_TABLE_NAME } from './events.js'
12
9
  import { sql } from './index.js'
13
10
  import { getDurationMsFromSpan, getStartTimeHighResFromSpan } from './otel.js'
14
11
  import QueryCache from './QueryCache.js'
15
- import type { ActionDefinition } from './schema.js'
16
- import type { Bindable, ParamsObject } from './util.js'
17
- import { prepareBindValues } from './util.js'
12
+ import type { Bindable, PreparedBindValues } from './util.js'
18
13
 
19
- export enum IndexType {
20
- Basic = 'Basic',
21
- FullText = 'FullText',
22
- }
23
-
24
- export interface Index {
25
- indexType: IndexType
26
- name: string
27
- columns: string[]
28
- }
29
-
30
- declare type DatabaseWithCAPI = SqliteWasm.Database & { capi: SqliteWasm.CAPI }
14
+ type DatabaseWithCAPI = Sqlite.Database & { capi: Sqlite.CAPI }
31
15
 
32
16
  export interface DebugInfo {
33
17
  slowQueries: BoundArray<SlowQueryInfo>
@@ -38,10 +22,10 @@ export interface DebugInfo {
38
22
 
39
23
  export type SlowQueryInfo = [
40
24
  queryStr: string,
41
- bindValues: Bindable | undefined,
25
+ bindValues: PreparedBindValues | undefined,
42
26
  durationMs: number,
43
27
  rowsCount: number | undefined,
44
- queriedTables: string[],
28
+ queriedTables: ReadonlyArray<string>,
45
29
  startTimePerfNow: DOMHighResTimeStamp,
46
30
  ]
47
31
 
@@ -54,7 +38,7 @@ export const emptyDebugInfo = (): DebugInfo => ({
54
38
 
55
39
  export class InMemoryDatabase {
56
40
  // TODO: how many unique active statements are expected?
57
- private cachedStmts = new BoundMap<string, SqliteWasm.PreparedStatement>(200)
41
+ private cachedStmts = new BoundMap<string, Sqlite.PreparedStatement>(200)
58
42
  private tablesUsedCache = new BoundMap<string, string[]>(200)
59
43
  private resultCache = new QueryCache()
60
44
  public debugInfo: DebugInfo = emptyDebugInfo()
@@ -63,21 +47,21 @@ export class InMemoryDatabase {
63
47
  private db: DatabaseWithCAPI,
64
48
  private otelTracer: otel.Tracer,
65
49
  private otelRootSpanContext: otel.Context,
66
- public SQL: SqliteWasm.Sqlite3Static,
50
+ public SQL: Sqlite.Sqlite3Static,
67
51
  ) {}
68
52
 
69
- static async load(
70
- data: Uint8Array | undefined,
71
- otelTracer: otel.Tracer,
72
- otelRootSpanContext: otel.Context,
73
- ): Promise<InMemoryDatabase> {
74
- const sqlite3 = await initSqlJs({
75
- // Required to load the wasm binary asynchronously. Of course, you can host it wherever you want
76
- // You can omit locateFile completely when running in node
77
- // locateFile: () => `/sql-wasm.wasm`,
78
- print: (message) => console.log(`[sql-client] ${message}`),
79
- printErr: (message) => console.error(`[sql-client] ${message}`),
80
- })
53
+ static load({
54
+ data,
55
+ otelTracer,
56
+ otelRootSpanContext,
57
+ sqlite3,
58
+ }: {
59
+ data: Uint8Array | undefined
60
+ otelTracer: otel.Tracer
61
+ otelRootSpanContext: otel.Context
62
+ sqlite3: Sqlite.Sqlite3Static
63
+ }): InMemoryDatabase {
64
+ // TODO move WASM init higher up in the init process (to do some other work while it's loading)
81
65
 
82
66
  const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
83
67
  db.capi = sqlite3.capi
@@ -97,7 +81,11 @@ export class InMemoryDatabase {
97
81
  )
98
82
  }
99
83
 
100
- return new InMemoryDatabase(db, otelTracer, otelRootSpanContext, sqlite3)
84
+ const inMemoryDatabase = new InMemoryDatabase(db, otelTracer, otelRootSpanContext, sqlite3)
85
+
86
+ configureSQLite(inMemoryDatabase)
87
+
88
+ return inMemoryDatabase
101
89
  }
102
90
 
103
91
  txn<TRes>(callback: () => TRes): TRes {
@@ -141,56 +129,81 @@ export class InMemoryDatabase {
141
129
  return tablesUsed as string[]
142
130
  }
143
131
 
144
- /**
145
- * NOTE `execute` is untraced since it's usually called from `applyEvent` which is traced
146
- */
147
132
  execute(
148
133
  query: string,
149
- bindValues?: ParamsObject,
134
+ bindValues?: PreparedBindValues,
150
135
  writeTables?: string[],
151
- options?: { hasNoEffects?: boolean },
152
- ): void {
153
- try {
154
- let stmt = this.cachedStmts.get(query)
155
- if (stmt === undefined) {
156
- stmt = this.db.prepare(query)
157
- this.cachedStmts.set(query, stmt)
158
- }
136
+ options?: { hasNoEffects?: boolean; otelContext: otel.Context },
137
+ ): { durationMs: number } {
138
+ return this.otelTracer.startActiveSpan(
139
+ 'livestore.in-memory-db:execute',
140
+ // TODO truncate query string
141
+ { attributes: { 'sql.query': query } },
142
+ options?.otelContext ?? this.otelRootSpanContext,
143
+ (span) => {
144
+ try {
145
+ let stmt = this.cachedStmts.get(query)
146
+ if (stmt === undefined) {
147
+ stmt = this.db.prepare(query)
148
+ this.cachedStmts.set(query, stmt)
149
+ }
159
150
 
160
- if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
161
- stmt.bind(prepareBindValues(bindValues, query))
162
- }
151
+ if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
152
+ stmt.bind(bindValues)
153
+ }
163
154
 
164
- try {
165
- stmt.step()
166
- } finally {
167
- stmt.reset() // Reset is needed for next execution
168
- }
169
- } catch (error) {
170
- shouldNeverHappen(
171
- `Error executing query: ${error} \n ${JSON.stringify({
172
- query,
173
- bindValues,
174
- })}`,
175
- )
176
- }
155
+ if (import.meta.env.DEV) {
156
+ this.debugInfo.events.push([query, bindValues])
157
+ }
177
158
 
178
- if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(query)) {
179
- // TODO use write tables instead
180
- // check what queries actually end up here.
181
- this.resultCache.invalidate(writeTables ?? this.getTablesUsed(query))
182
- }
159
+ try {
160
+ stmt.step()
161
+ } finally {
162
+ stmt.reset() // Reset is needed for next execution
163
+ }
164
+ } catch (error) {
165
+ shouldNeverHappen(
166
+ `Error executing query: ${error} \n ${JSON.stringify({
167
+ query,
168
+ bindValues,
169
+ })}`,
170
+ )
171
+ }
183
172
 
184
- if (options?.hasNoEffects === true) {
185
- return
186
- }
173
+ if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(query)) {
174
+ // TODO use write tables instead
175
+ // check what queries actually end up here.
176
+ this.resultCache.invalidate(writeTables ?? this.getTablesUsed(query))
177
+ }
178
+
179
+ span.end()
180
+
181
+ const durationMs = getDurationMsFromSpan(span)
182
+
183
+ this.debugInfo.queryFrameDuration += durationMs
184
+ this.debugInfo.queryFrameCount++
185
+
186
+ if (durationMs > 5 && import.meta.env.DEV) {
187
+ this.debugInfo.slowQueries.push([
188
+ query,
189
+ bindValues,
190
+ durationMs,
191
+ undefined,
192
+ [],
193
+ getStartTimeHighResFromSpan(span),
194
+ ])
195
+ }
196
+
197
+ return { durationMs }
198
+ },
199
+ )
187
200
  }
188
201
 
189
202
  select<T = any>(
190
203
  query: string,
191
204
  options?: {
192
- queriedTables?: string[]
193
- bindValues?: Bindable
205
+ queriedTables?: ReadonlyArray<string>
206
+ bindValues?: PreparedBindValues
194
207
  skipCache?: boolean
195
208
  otelContext?: otel.Context
196
209
  },
@@ -218,19 +231,27 @@ export class InMemoryDatabase {
218
231
  stmt = this.db.prepare(query)
219
232
  this.cachedStmts.set(query, stmt)
220
233
  }
221
- if (bindValues) {
222
- stmt.bind(bindValues ?? {})
234
+ if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
235
+ stmt.bind(bindValues)
223
236
  }
224
237
 
225
238
  const result: T[] = []
239
+
226
240
  try {
227
- const columns = stmt.getColumnNames()
241
+ // NOTE `getColumnNames` only works for `SELECT` statements, ignoring other statements for now
242
+ let columns = undefined
243
+ try {
244
+ columns = stmt.getColumnNames()
245
+ } catch (_e) {}
246
+
228
247
  while (stmt.step()) {
229
- const obj: { [key: string]: any } = {}
230
- for (const [i, c] of columns.entries()) {
231
- obj[c] = stmt.get(i)
248
+ if (columns !== undefined) {
249
+ const obj: { [key: string]: any } = {}
250
+ for (const [i, c] of columns.entries()) {
251
+ obj[c] = stmt.get(i)
252
+ }
253
+ result.push(obj as unknown as T)
232
254
  }
233
- result.push(obj as unknown as T)
234
255
  }
235
256
  } finally {
236
257
  // we're caching statements in this iteration. do not free.
@@ -275,67 +296,6 @@ export class InMemoryDatabase {
275
296
  )
276
297
  }
277
298
 
278
- // TODO move `applyEvent` logic to Store and only call `execute` here
279
- applyEvent(
280
- event: LiveStoreEvent,
281
- eventDefinition: ActionDefinition,
282
- otelContext: otel.Context,
283
- ): { durationMs: number } {
284
- return this.otelTracer.startActiveSpan('livestore.in-memory-db:applyEvent', {}, otelContext, (span) => {
285
- // TODO: in the future, we'll do more CRDT-style stuff here to decide whether to run effects of the event
286
-
287
- // NOTE: These two updates should happen transactionally;
288
- // we don't create a transaction here because that's handled in the caller.
289
- // The reason for this is that sometimes we want to apply multiple events in a larger transaction.
290
-
291
- // Insert into the events table
292
- // this.execute(sql`insert into ${EVENTS_TABLE_NAME} (id, type, args) values ($id, $type, $args)`, {
293
- // id: event.id,
294
- // type: event.type,
295
- // args: JSON.stringify(event.args ?? {}),
296
- // })
297
-
298
- const statement =
299
- typeof eventDefinition.statement === 'function'
300
- ? eventDefinition.statement(event.args)
301
- : eventDefinition.statement
302
-
303
- const prepareBindValues = eventDefinition.prepareBindValues ?? identity
304
-
305
- const bindValues =
306
- typeof eventDefinition.statement === 'function' && statement.argsAlreadyBound
307
- ? {}
308
- : prepareBindValues(event.args)
309
-
310
- if (import.meta.env.DEV) {
311
- this.debugInfo.events.push([statement.sql, bindValues])
312
- }
313
-
314
- // Run the effects of the event
315
- this.execute(statement.sql, bindValues, statement.writeTables)
316
-
317
- span.end()
318
-
319
- const durationMs = getDurationMsFromSpan(span)
320
-
321
- this.debugInfo.queryFrameDuration += durationMs
322
- this.debugInfo.queryFrameCount++
323
-
324
- if (durationMs > 5 && import.meta.env.DEV) {
325
- this.debugInfo.slowQueries.push([
326
- statement.sql,
327
- bindValues,
328
- durationMs,
329
- undefined,
330
- [],
331
- getStartTimeHighResFromSpan(span),
332
- ])
333
- }
334
-
335
- return { durationMs }
336
- })
337
- }
338
-
339
299
  export() {
340
300
  // Clear statement cache because exporting frees statements
341
301
  for (const key of this.cachedStmts.keys()) {
@@ -345,3 +305,18 @@ export class InMemoryDatabase {
345
305
  return this.db.capi.sqlite3_js_db_export(this.db.pointer)
346
306
  }
347
307
  }
308
+
309
+ /** Set up SQLite performance; hasn't been super carefully optimized yet. */
310
+ const configureSQLite = (db: InMemoryDatabase) => {
311
+ db.execute(
312
+ // TODO: revisit these tuning parameters for max performance
313
+ sql`
314
+ PRAGMA page_size=32768;
315
+ PRAGMA cache_size=10000;
316
+ PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
317
+ PRAGMA synchronous='OFF';
318
+ PRAGMA temp_store='MEMORY';
319
+ PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
320
+ `,
321
+ )
322
+ }
package/src/index.ts CHANGED
@@ -1,23 +1,18 @@
1
- export { Store, createStore, RESET_DB_LOCAL_STORAGE_KEY } from './store.js'
1
+ export { Store, createStore } from './store.js'
2
2
  export type { LiveStoreQuery, BaseGraphQLContext, QueryResult, QueryDebugInfo, RefreshReason } from './store.js'
3
3
 
4
4
  export type { QueryDefinition, LiveStoreCreateStoreOptions, LiveStoreContext } from './effect/LiveStore.js'
5
5
 
6
6
  export {
7
7
  defineComponentStateSchema,
8
- EVENT_CURSOR_TABLE,
9
- defineSchema,
10
8
  defineAction,
11
9
  defineActions,
12
10
  defineTables,
13
11
  defineMaterializedViews,
12
+ makeSchema,
14
13
  } from './schema.js'
15
14
  export { InMemoryDatabase, type DebugInfo, emptyDebugInfo } from './inMemoryDatabase.js'
16
- export { createBackend, IndexType } from './backends/index.js'
17
- export type { BackendOptions, Backend, BackendType } from './backends/index.js'
18
- export { isBackendType } from './backends/index.js'
19
- export type { SelectResponse } from './backends/index.js'
20
- export { WebWorkerBackend } from './backends/web.js'
15
+ export type { Storage, StorageType, StorageInit } from './storage/index.js'
21
16
  export type {
22
17
  GetAtom,
23
18
  AtomDebugInfo,
@@ -25,23 +20,24 @@ export type {
25
20
  RefreshReasonWithGenericReasons,
26
21
  SerializedAtom,
27
22
  SerializedEffect,
23
+ Atom,
28
24
  } from './reactive.js'
29
- export type { LiveStoreJSQuery } from './reactiveQueries/js.js'
30
- export type { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
31
- export type { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
25
+ export { type LiveStoreJSQuery, queryJS } from './reactiveQueries/js.js'
26
+ export { type LiveStoreSQLQuery, querySQL } from './reactiveQueries/sql.js'
27
+ export { type LiveStoreGraphQLQuery, queryGraphQL } from './reactiveQueries/graphql.js'
28
+ export { type GetAtomResult } from './reactiveQueries/base-class.js'
29
+ export { dbGraph } from './reactiveQueries/graph.js'
32
30
 
33
31
  export { labelForKey } from './componentKey.js'
34
32
  export type { ComponentKey } from './componentKey.js'
35
- export type {
36
- Schema,
37
- TableDefinition,
38
- GetActionArgs,
39
- GetApplyEventArgs,
40
- ColumnDefinition,
41
- Index,
42
- ActionDefinition,
43
- ActionDefinitions,
44
- } from './schema.js'
33
+ export type { Schema, GetActionArgs, GetApplyEventArgs, Index, ActionDefinition, ActionDefinitions } from './schema.js'
34
+
35
+ export { SqliteAst, SqliteDsl } from 'effect-db-schema'
36
+
37
+ import type { SqliteAst } from 'effect-db-schema'
38
+ export type TableDefinition = SqliteAst.Table
39
+
40
+ export { SqliteDsl as DbSchema } from 'effect-db-schema'
45
41
 
46
- export { sql, type Bindable } from './util.js'
42
+ export { prepareBindValues, sql, type Bindable, type PreparedBindValues } from './util.js'
47
43
  export { isEqual } from 'lodash-es'
@@ -0,0 +1,119 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+ import { SqliteAst } from 'effect-db-schema'
3
+ import { memoize, omit } from 'lodash-es'
4
+
5
+ import type { InMemoryDatabase } from './index.js'
6
+ import type { Schema, SchemaMetaRow } from './schema.js'
7
+ import { componentStateTables, SCHEMA_META_TABLE, systemTables } from './schema.js'
8
+ import type { PreparedBindValues } from './util.js'
9
+ import { sql } from './util.js'
10
+
11
+ const getMemoizedTimestamp = memoize(() => new Date().toISOString())
12
+
13
+ // TODO more graceful DB migration (e.g. backup DB before destructive migrations)
14
+ export const migrateDb = ({
15
+ db,
16
+ otelContext,
17
+ schema,
18
+ }: {
19
+ db: InMemoryDatabase
20
+ otelContext: otel.Context
21
+ schema: Schema
22
+ }) => {
23
+ db.execute(
24
+ // TODO use schema migration definition from schema.ts instead
25
+ sql`create table if not exists ${SCHEMA_META_TABLE} (tableName text primary key, schemaHash text, updatedAt text);`,
26
+ undefined,
27
+ [],
28
+ { otelContext },
29
+ )
30
+
31
+ const schemaMetaRows = db.select<SchemaMetaRow>(sql`SELECT * FROM ${SCHEMA_META_TABLE}`)
32
+
33
+ const dbSchemaHashByTable = Object.fromEntries(
34
+ schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
35
+ )
36
+
37
+ const tableDefs = {
38
+ // NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
39
+ [SCHEMA_META_TABLE]: systemTables[SCHEMA_META_TABLE],
40
+ ...omit(schema.tables, [SCHEMA_META_TABLE]),
41
+ ...componentStateTables,
42
+ }
43
+
44
+ for (const [tableName, tableDef] of Object.entries(tableDefs)) {
45
+ const dbSchemaHash = dbSchemaHashByTable[tableName]
46
+ const schemaHash = SqliteAst.hash(tableDef)
47
+ if (schemaHash !== dbSchemaHash) {
48
+ console.log(
49
+ `Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
50
+ )
51
+
52
+ migrateTable({ db, tableDef, otelContext, schemaHash })
53
+ }
54
+ }
55
+ }
56
+
57
+ export const migrateTable = ({
58
+ db,
59
+ tableDef,
60
+ otelContext,
61
+ schemaHash,
62
+ }: {
63
+ db: InMemoryDatabase
64
+ tableDef: SqliteAst.Table
65
+ otelContext: otel.Context
66
+ schemaHash: number
67
+ }) => {
68
+ console.log(`Migrating table '${tableDef.name}'...`)
69
+ const tableName = tableDef.name
70
+ const columnSpec = makeColumnSpec(tableDef)
71
+
72
+ // TODO need to possibly handle cascading deletes due to foreign keys
73
+ db.execute(sql`drop table if exists ${tableName}`, undefined, [], { otelContext })
74
+ db.execute(sql`create table if not exists ${tableName} (${columnSpec});`, undefined, [], { otelContext })
75
+
76
+ for (const index of tableDef.indexes) {
77
+ db.execute(createIndexFromDefinition(tableName, index), undefined, [], { otelContext })
78
+ }
79
+
80
+ const updatedAt = getMemoizedTimestamp()
81
+ db.execute(
82
+ sql`
83
+ INSERT INTO ${SCHEMA_META_TABLE} (tableName, schemaHash, updatedAt) VALUES ($tableName, $schemaHash, $updatedAt)
84
+ ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
85
+ `,
86
+ { $tableName: tableName, $schemaHash: schemaHash, $updatedAt: updatedAt } as unknown as PreparedBindValues,
87
+ [],
88
+ { otelContext },
89
+ )
90
+ }
91
+
92
+ const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
93
+ const uniqueStr = index.unique ? 'UNIQUE' : ''
94
+ return sql`create ${uniqueStr} index ${index.name} on ${tableName} (${index.columns.join(', ')})`
95
+ }
96
+
97
+ const makeColumnSpec = (tableDef: SqliteAst.Table) => {
98
+ const primaryKeys = tableDef.columns.filter((_) => _.primaryKey).map((_) => _.name)
99
+ const columnDefStrs = tableDef.columns.map(toSqliteColumnSpec)
100
+ if (primaryKeys.length > 0) {
101
+ columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
102
+ }
103
+
104
+ return columnDefStrs.join(', ')
105
+ }
106
+
107
+ const toSqliteColumnSpec = (column: SqliteAst.Column) => {
108
+ const columnType = column.type._tag
109
+ // const primaryKey = column.primaryKey ? 'primary key' : ''
110
+ const nullable = column.nullable === false ? 'not null' : ''
111
+ const defaultValue =
112
+ column.default === undefined
113
+ ? ''
114
+ : columnType === 'text'
115
+ ? `default '${column.default}'`
116
+ : `default ${column.default}`
117
+
118
+ return `${column.name} ${columnType} ${nullable} ${defaultValue}`
119
+ }
package/src/otel.ts CHANGED
@@ -1,16 +1,5 @@
1
1
  import type * as otel from '@opentelemetry/api'
2
2
 
3
- // TODO improve - see https://www.notion.so/schickling/Better-solution-for-globalThis-inProgressSpans-503cd7a5f4fc4fb8bdec2e60bde1be1f
4
- export const TODO_REMOVE_trackLongRunningSpan = (span: otel.Span): void => {
5
- // @ts-expect-error TODO get rid of this coupling
6
- if (window.inProgressSpans !== undefined && window.inProgressSpans instanceof Set) {
7
- // @ts-expect-error TODO get rid of this coupling
8
- window.inProgressSpans.add(span)
9
- } else {
10
- // debugger
11
- }
12
- }
13
-
14
3
  export const getDurationMsFromSpan = (span: otel.Span): number => {
15
4
  const durationHr: [seconds: number, nanos: number] = (span as any)._duration
16
5
  return durationHr[0] * 1000 + durationHr[1] / 1_000_000
@@ -1,24 +1,28 @@
1
1
  import type * as otel from '@opentelemetry/api'
2
- import { mapValues } from 'lodash-es'
3
2
  import type { ReactElement, ReactNode } from 'react'
4
3
  import React from 'react'
4
+ import initSqlite3Wasm from 'sqlite-esm'
5
5
 
6
- import type { Backend, BackendOptions } from '../backends/index.js'
7
- import type {
8
- GlobalQueryDefs,
9
- LiveStoreContext as StoreContext_,
10
- LiveStoreCreateStoreOptions,
11
- } from '../effect/LiveStore.js'
6
+ // TODO refactor so the `react` module doesn't depend on `effect` module
7
+ import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
8
+ import type { InMemoryDatabase } from '../inMemoryDatabase.js'
12
9
  import type { Schema } from '../schema.js'
10
+ import type { StorageInit } from '../storage/index.js'
13
11
  import type { BaseGraphQLContext, GraphQLOptions } from '../store.js'
14
12
  import { createStore } from '../store.js'
15
13
  import { LiveStoreContext } from './LiveStoreContext.js'
16
14
 
15
+ // NOTE we're starting to initialize the sqlite wasm binary here (already before calling `createStore`),
16
+ // so that it's ready when we need it
17
+ const sqlite3Promise = initSqlite3Wasm({
18
+ print: (message) => console.log(`[livestore sqlite] ${message}`),
19
+ printErr: (message) => console.error(`[livestore sqlite] ${message}`),
20
+ })
21
+
17
22
  interface LiveStoreProviderProps<GraphQLContext> {
18
23
  schema: Schema
19
- backendOptions: BackendOptions
20
- boot?: (backend: Backend, parentSpan: otel.Span) => Promise<void>
21
- globalQueryDefs: GlobalQueryDefs
24
+ loadStorage: () => StorageInit | Promise<StorageInit>
25
+ boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
22
26
  graphQLOptions?: GraphQLOptions<GraphQLContext>
23
27
  otelTracer?: otel.Tracer
24
28
  otelRootSpanContext?: otel.Context
@@ -27,8 +31,7 @@ interface LiveStoreProviderProps<GraphQLContext> {
27
31
 
28
32
  export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
29
33
  fallback,
30
- globalQueryDefs,
31
- backendOptions,
34
+ loadStorage,
32
35
  graphQLOptions,
33
36
  otelTracer,
34
37
  otelRootSpanContext,
@@ -38,8 +41,7 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
38
41
  }: LiveStoreProviderProps<GraphQLContext> & { children?: ReactNode }): JSX.Element => {
39
42
  const store = useCreateStore({
40
43
  schema,
41
- globalQueryDefs,
42
- backendOptions,
44
+ loadStorage,
43
45
  graphQLOptions,
44
46
  otelTracer,
45
47
  otelRootSpanContext,
@@ -50,13 +52,14 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
50
52
  return fallback
51
53
  }
52
54
 
55
+ window.__debugLiveStore = store.store
56
+
53
57
  return <LiveStoreContext.Provider value={store}>{children}</LiveStoreContext.Provider>
54
58
  }
55
59
 
56
60
  const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
57
61
  schema,
58
- globalQueryDefs,
59
- backendOptions,
62
+ loadStorage,
60
63
  graphQLOptions,
61
64
  otelTracer,
62
65
  otelRootSpanContext,
@@ -67,19 +70,17 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
67
70
  React.useEffect(() => {
68
71
  void (async () => {
69
72
  try {
73
+ const sqlite3 = await sqlite3Promise
70
74
  const store = await createStore({
71
75
  schema,
72
- backendOptions,
76
+ loadStorage,
73
77
  graphQLOptions,
74
78
  otelTracer,
75
79
  otelRootSpanContext,
76
80
  boot,
81
+ sqlite3,
77
82
  })
78
- store.otel.tracer.startActiveSpan('LiveStore:makeGlobalQueries', {}, store.otel.queriesSpanContext, (span) => {
79
- const globalQueries = mapValues(globalQueryDefs, (queryDef) => queryDef(store))
80
- setCtxValue({ store, globalQueries })
81
- span.end()
82
- })
83
+ setCtxValue({ store })
83
84
  } catch (e) {
84
85
  console.error(`Error creating LiveStore store:`, e)
85
86
  throw e
@@ -87,7 +88,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
87
88
  })()
88
89
 
89
90
  // TODO: do we need to return any cleanup function here?
90
- }, [schema, backendOptions, globalQueryDefs, graphQLOptions, otelTracer, otelRootSpanContext, boot])
91
+ }, [schema, loadStorage, graphQLOptions, otelTracer, otelRootSpanContext, boot])
91
92
 
92
93
  return ctxValue
93
94
  }
@@ -1,11 +1,16 @@
1
1
  export type {
2
- UseLiveStoreComponentProps as LiveStoreComponentConfig,
3
- ReactiveGraphQL,
4
- ReactiveSQL,
5
2
  Setters,
6
- } from './useLiveStoreComponent.js'
3
+ ComponentKeyConfig,
4
+ QueryDefinitions,
5
+ ComponentColumns,
6
+ GetStateType,
7
+ GetStateTypeEncoded,
8
+ } from './useComponentState.js'
7
9
  export { LiveStoreContext, useStore } from './LiveStoreContext.js'
8
10
  export { LiveStoreProvider } from './LiveStoreProvider.js'
9
- export { useLiveStoreComponent } from './useLiveStoreComponent.js'
10
- export { useGraphQL } from './useGraphQL.js'
11
- export { useGlobalQuery } from './useGlobalQuery.js'
11
+ export { useComponentState } from './useComponentState.js'
12
+ export { useQuery } from './useQuery.js'
13
+ export { useTemporaryQuery } from './useTemporaryQuery.js'
14
+
15
+ // Needed to make TS happy
16
+ export type { TypedDocumentNode } from '@graphql-typed-document-node/core'