@livestore/livestore 0.0.10 → 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 (151) hide show
  1. package/README.md +7 -7
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/react/fixture.d.ts +4 -120
  4. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  5. package/dist/__tests__/react/fixture.js +19 -26
  6. package/dist/__tests__/react/fixture.js.map +1 -1
  7. package/dist/__tests__/reactive.test.js +31 -0
  8. package/dist/__tests__/reactive.test.js.map +1 -1
  9. package/dist/backends/base.d.ts +4 -4
  10. package/dist/backends/{web-in-memory.d.ts → in-memory/index.d.ts} +6 -6
  11. package/dist/backends/in-memory/index.d.ts.map +1 -0
  12. package/dist/backends/{web-in-memory.js → in-memory/index.js} +7 -7
  13. package/dist/backends/in-memory/index.js.map +1 -0
  14. package/dist/backends/index.d.ts +4 -8
  15. package/dist/backends/index.d.ts.map +1 -1
  16. package/dist/backends/index.js +0 -22
  17. package/dist/backends/index.js.map +1 -1
  18. package/dist/backends/{tauri.d.ts → tauri/index.d.ts} +5 -6
  19. package/dist/backends/tauri/index.d.ts.map +1 -0
  20. package/dist/backends/{tauri.js → tauri/index.js} +4 -4
  21. package/dist/backends/tauri/index.js.map +1 -0
  22. package/dist/backends/{web.d.ts → web-worker/index.d.ts} +6 -7
  23. package/dist/backends/web-worker/index.d.ts.map +1 -0
  24. package/dist/backends/{web.js → web-worker/index.js} +6 -6
  25. package/dist/backends/web-worker/index.js.map +1 -0
  26. package/dist/backends/{web-worker.d.ts → web-worker/worker.d.ts} +3 -3
  27. package/dist/backends/web-worker/worker.d.ts.map +1 -0
  28. package/dist/backends/{web-worker.js → web-worker/worker.js} +3 -3
  29. package/dist/backends/web-worker/worker.js.map +1 -0
  30. package/dist/effect/LiveStore.d.ts +6 -6
  31. package/dist/effect/LiveStore.d.ts.map +1 -1
  32. package/dist/effect/LiveStore.js +2 -5
  33. package/dist/effect/LiveStore.js.map +1 -1
  34. package/dist/events.d.ts +1 -1
  35. package/dist/events.d.ts.map +1 -1
  36. package/dist/events.js +1 -1
  37. package/dist/events.js.map +1 -1
  38. package/dist/inMemoryDatabase.d.ts +5 -10
  39. package/dist/inMemoryDatabase.d.ts.map +1 -1
  40. package/dist/inMemoryDatabase.js +78 -89
  41. package/dist/inMemoryDatabase.js.map +1 -1
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +3 -4
  45. package/dist/index.js.map +1 -1
  46. package/dist/migrations.d.ts +9 -0
  47. package/dist/migrations.d.ts.map +1 -0
  48. package/dist/migrations.js +62 -0
  49. package/dist/migrations.js.map +1 -0
  50. package/dist/otel.d.ts +0 -1
  51. package/dist/otel.d.ts.map +1 -1
  52. package/dist/otel.js +0 -11
  53. package/dist/otel.js.map +1 -1
  54. package/dist/react/LiveStoreProvider.d.ts +5 -4
  55. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  56. package/dist/react/LiveStoreProvider.js +6 -5
  57. package/dist/react/LiveStoreProvider.js.map +1 -1
  58. package/dist/react/index.d.ts +2 -1
  59. package/dist/react/index.d.ts.map +1 -1
  60. package/dist/react/index.js.map +1 -1
  61. package/dist/react/useGlobalQuery.d.ts.map +1 -1
  62. package/dist/react/useGlobalQuery.js +0 -2
  63. package/dist/react/useGlobalQuery.js.map +1 -1
  64. package/dist/react/useLiveStoreComponent.d.ts +22 -17
  65. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  66. package/dist/react/useLiveStoreComponent.js +46 -17
  67. package/dist/react/useLiveStoreComponent.js.map +1 -1
  68. package/dist/reactive.d.ts.map +1 -1
  69. package/dist/reactive.js +1 -0
  70. package/dist/reactive.js.map +1 -1
  71. package/dist/schema.d.ts +32 -112
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +36 -79
  74. package/dist/schema.js.map +1 -1
  75. package/dist/storage/base.d.ts +10 -0
  76. package/dist/storage/base.d.ts.map +1 -0
  77. package/dist/storage/base.js +14 -0
  78. package/dist/storage/base.js.map +1 -0
  79. package/dist/storage/in-memory/index.d.ts +15 -0
  80. package/dist/storage/in-memory/index.d.ts.map +1 -0
  81. package/dist/storage/in-memory/index.js +14 -0
  82. package/dist/storage/in-memory/index.js.map +1 -0
  83. package/dist/storage/index.d.ts +14 -0
  84. package/dist/storage/index.d.ts.map +1 -0
  85. package/dist/storage/index.js +9 -0
  86. package/dist/storage/index.js.map +1 -0
  87. package/dist/storage/tauri/index.d.ts +19 -0
  88. package/dist/storage/tauri/index.d.ts.map +1 -0
  89. package/dist/storage/tauri/index.js +38 -0
  90. package/dist/storage/tauri/index.js.map +1 -0
  91. package/dist/storage/utils/idb.d.ts +10 -0
  92. package/dist/storage/utils/idb.d.ts.map +1 -0
  93. package/dist/storage/utils/idb.js +58 -0
  94. package/dist/storage/utils/idb.js.map +1 -0
  95. package/dist/storage/web-worker/index.d.ts +27 -0
  96. package/dist/storage/web-worker/index.d.ts.map +1 -0
  97. package/dist/storage/web-worker/index.js +76 -0
  98. package/dist/storage/web-worker/index.js.map +1 -0
  99. package/dist/storage/web-worker/worker.d.ts +13 -0
  100. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  101. package/dist/storage/web-worker/worker.js +110 -0
  102. package/dist/storage/web-worker/worker.js.map +1 -0
  103. package/dist/store.d.ts +6 -6
  104. package/dist/store.d.ts.map +1 -1
  105. package/dist/store.js +93 -63
  106. package/dist/store.js.map +1 -1
  107. package/dist/util.d.ts +3 -1
  108. package/dist/util.d.ts.map +1 -1
  109. package/dist/util.js +2 -0
  110. package/dist/util.js.map +1 -1
  111. package/package.json +50 -23
  112. package/src/__tests__/react/fixture.tsx +19 -28
  113. package/src/__tests__/reactive.test.ts +39 -0
  114. package/src/effect/LiveStore.ts +8 -13
  115. package/src/events.ts +1 -1
  116. package/src/inMemoryDatabase.ts +100 -117
  117. package/src/index.ts +10 -16
  118. package/src/migrations.ts +101 -0
  119. package/src/otel.ts +0 -11
  120. package/src/react/LiveStoreProvider.tsx +12 -8
  121. package/src/react/index.ts +9 -0
  122. package/src/react/useGlobalQuery.ts +0 -3
  123. package/src/react/useLiveStoreComponent.ts +98 -38
  124. package/src/reactive.ts +2 -1
  125. package/src/schema.ts +72 -145
  126. package/src/storage/in-memory/index.ts +21 -0
  127. package/src/storage/index.ts +27 -0
  128. package/src/{backends/tauri.ts → storage/tauri/index.ts} +13 -27
  129. package/src/storage/web-worker/index.ts +118 -0
  130. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  131. package/src/store.ts +112 -79
  132. package/src/util.ts +5 -1
  133. package/tsconfig.json +1 -3
  134. package/dist/backends/noop.d.ts +0 -18
  135. package/dist/backends/noop.d.ts.map +0 -1
  136. package/dist/backends/noop.js +0 -21
  137. package/dist/backends/noop.js.map +0 -1
  138. package/dist/backends/tauri.d.ts.map +0 -1
  139. package/dist/backends/tauri.js.map +0 -1
  140. package/dist/backends/web-in-memory.d.ts.map +0 -1
  141. package/dist/backends/web-in-memory.js.map +0 -1
  142. package/dist/backends/web-worker.d.ts.map +0 -1
  143. package/dist/backends/web-worker.js.map +0 -1
  144. package/dist/backends/web.d.ts.map +0 -1
  145. package/dist/backends/web.js.map +0 -1
  146. package/src/backends/base.ts +0 -67
  147. package/src/backends/index.ts +0 -98
  148. package/src/backends/noop.ts +0 -32
  149. package/src/backends/web-in-memory.ts +0 -65
  150. package/src/backends/web.ts +0 -97
  151. /package/src/{backends → storage}/utils/idb.ts +0 -0
@@ -1,18 +1,14 @@
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
5
  import type * as SqliteWasm from 'sqlite-esm'
7
- import initSqlJs 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
12
  import type { Bindable, ParamsObject } from './util.js'
17
13
  import { prepareBindValues } from './util.js'
18
14
 
@@ -66,18 +62,13 @@ export class InMemoryDatabase {
66
62
  public SQL: SqliteWasm.Sqlite3Static,
67
63
  ) {}
68
64
 
69
- static async load(
65
+ static load(
70
66
  data: Uint8Array | undefined,
71
67
  otelTracer: otel.Tracer,
72
68
  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
- })
69
+ sqlite3: SqliteWasm.Sqlite3Static,
70
+ ): InMemoryDatabase {
71
+ // TODO move WASM init higher up in the init process (to do some other work while it's loading)
81
72
 
82
73
  const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
83
74
  db.capi = sqlite3.capi
@@ -97,7 +88,11 @@ export class InMemoryDatabase {
97
88
  )
98
89
  }
99
90
 
100
- return new InMemoryDatabase(db, otelTracer, otelRootSpanContext, sqlite3)
91
+ const inMemoryDatabase = new InMemoryDatabase(db, otelTracer, otelRootSpanContext, sqlite3)
92
+
93
+ configureSQLite(inMemoryDatabase)
94
+
95
+ return inMemoryDatabase
101
96
  }
102
97
 
103
98
  txn<TRes>(callback: () => TRes): TRes {
@@ -141,49 +136,75 @@ export class InMemoryDatabase {
141
136
  return tablesUsed as string[]
142
137
  }
143
138
 
144
- /**
145
- * NOTE `execute` is untraced since it's usually called from `applyEvent` which is traced
146
- */
147
139
  execute(
148
140
  query: string,
149
141
  bindValues?: ParamsObject,
150
142
  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
- }
143
+ options?: { hasNoEffects?: boolean; otelContext: otel.Context },
144
+ ): { durationMs: number } {
145
+ return this.otelTracer.startActiveSpan(
146
+ 'livestore.in-memory-db:execute',
147
+ // TODO truncate query string
148
+ { attributes: { 'sql.query': query } },
149
+ options?.otelContext ?? this.otelRootSpanContext,
150
+ (span) => {
151
+ try {
152
+ let stmt = this.cachedStmts.get(query)
153
+ if (stmt === undefined) {
154
+ stmt = this.db.prepare(query)
155
+ this.cachedStmts.set(query, stmt)
156
+ }
159
157
 
160
- if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
161
- stmt.bind(prepareBindValues(bindValues, query))
162
- }
158
+ // TODO check whether we can remove the extra `prepareBindValues` call here (e.g. enforce proper type in API)
159
+ if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
160
+ stmt.bind(prepareBindValues(bindValues, query))
161
+ }
163
162
 
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
- }
163
+ if (import.meta.env.DEV) {
164
+ this.debugInfo.events.push([query, bindValues])
165
+ }
177
166
 
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
- }
167
+ try {
168
+ stmt.step()
169
+ } finally {
170
+ stmt.reset() // Reset is needed for next execution
171
+ }
172
+ } catch (error) {
173
+ shouldNeverHappen(
174
+ `Error executing query: ${error} \n ${JSON.stringify({
175
+ query,
176
+ bindValues,
177
+ })}`,
178
+ )
179
+ }
183
180
 
184
- if (options?.hasNoEffects === true) {
185
- return
186
- }
181
+ if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(query)) {
182
+ // TODO use write tables instead
183
+ // check what queries actually end up here.
184
+ this.resultCache.invalidate(writeTables ?? this.getTablesUsed(query))
185
+ }
186
+
187
+ span.end()
188
+
189
+ const durationMs = getDurationMsFromSpan(span)
190
+
191
+ this.debugInfo.queryFrameDuration += durationMs
192
+ this.debugInfo.queryFrameCount++
193
+
194
+ if (durationMs > 5 && import.meta.env.DEV) {
195
+ this.debugInfo.slowQueries.push([
196
+ query,
197
+ bindValues,
198
+ durationMs,
199
+ undefined,
200
+ [],
201
+ getStartTimeHighResFromSpan(span),
202
+ ])
203
+ }
204
+
205
+ return { durationMs }
206
+ },
207
+ )
187
208
  }
188
209
 
189
210
  select<T = any>(
@@ -218,19 +239,27 @@ export class InMemoryDatabase {
218
239
  stmt = this.db.prepare(query)
219
240
  this.cachedStmts.set(query, stmt)
220
241
  }
221
- if (bindValues) {
222
- stmt.bind(bindValues ?? {})
242
+ if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
243
+ stmt.bind(bindValues)
223
244
  }
224
245
 
225
246
  const result: T[] = []
247
+
226
248
  try {
227
- const columns = stmt.getColumnNames()
249
+ // NOTE `getColumnNames` only works for `SELECT` statements, ignoring other statements for now
250
+ let columns = undefined
251
+ try {
252
+ columns = stmt.getColumnNames()
253
+ } catch (_e) {}
254
+
228
255
  while (stmt.step()) {
229
- const obj: { [key: string]: any } = {}
230
- for (const [i, c] of columns.entries()) {
231
- obj[c] = stmt.get(i)
256
+ if (columns !== undefined) {
257
+ const obj: { [key: string]: any } = {}
258
+ for (const [i, c] of columns.entries()) {
259
+ obj[c] = stmt.get(i)
260
+ }
261
+ result.push(obj as unknown as T)
232
262
  }
233
- result.push(obj as unknown as T)
234
263
  }
235
264
  } finally {
236
265
  // we're caching statements in this iteration. do not free.
@@ -275,67 +304,6 @@ export class InMemoryDatabase {
275
304
  )
276
305
  }
277
306
 
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
307
  export() {
340
308
  // Clear statement cache because exporting frees statements
341
309
  for (const key of this.cachedStmts.keys()) {
@@ -345,3 +313,18 @@ export class InMemoryDatabase {
345
313
  return this.db.capi.sqlite3_js_db_export(this.db.pointer)
346
314
  }
347
315
  }
316
+
317
+ /** Set up SQLite performance; hasn't been super carefully optimized yet. */
318
+ const configureSQLite = (db: InMemoryDatabase) => {
319
+ db.execute(
320
+ // TODO: revisit these tuning parameters for max performance
321
+ sql`
322
+ PRAGMA page_size=32768;
323
+ PRAGMA cache_size=10000;
324
+ PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
325
+ PRAGMA synchronous='OFF';
326
+ PRAGMA temp_store='MEMORY';
327
+ PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
328
+ `,
329
+ )
330
+ }
package/src/index.ts CHANGED
@@ -6,18 +6,14 @@ export type { QueryDefinition, LiveStoreCreateStoreOptions, LiveStoreContext } f
6
6
  export {
7
7
  defineComponentStateSchema,
8
8
  EVENT_CURSOR_TABLE,
9
- defineSchema,
10
9
  defineAction,
11
10
  defineActions,
12
11
  defineTables,
13
12
  defineMaterializedViews,
13
+ makeSchema,
14
14
  } from './schema.js'
15
15
  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'
16
+ export type { Storage, StorageType, StorageInit } from './storage/index.js'
21
17
  export type {
22
18
  GetAtom,
23
19
  AtomDebugInfo,
@@ -32,16 +28,14 @@ export type { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
32
28
 
33
29
  export { labelForKey } from './componentKey.js'
34
30
  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'
31
+ export type { Schema, GetActionArgs, GetApplyEventArgs, Index, ActionDefinition, ActionDefinitions } from './schema.js'
32
+
33
+ export { SqliteAst, SqliteDsl } from 'effect-db-schema'
34
+
35
+ import type { SqliteAst } from 'effect-db-schema'
36
+ export type TableDefinition = SqliteAst.Table
37
+
38
+ export { SqliteDsl as DbSchema } from 'effect-db-schema'
45
39
 
46
40
  export { sql, type Bindable } from './util.js'
47
41
  export { isEqual } from 'lodash-es'
@@ -0,0 +1,101 @@
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 { sql } from './util.js'
9
+
10
+ // TODO more graceful DB migration (e.g. backup DB before destructive migrations)
11
+ export const migrateDb = ({
12
+ db,
13
+ otelContext,
14
+ schema,
15
+ }: {
16
+ db: InMemoryDatabase
17
+ otelContext: otel.Context
18
+ schema: Schema
19
+ }) => {
20
+ db.execute(
21
+ // TODO use schema migration definition from schema.ts instead
22
+ sql`create table if not exists ${SCHEMA_META_TABLE} (tableName text primary key, schemaHash text, updatedAt text);`,
23
+ undefined,
24
+ [],
25
+ { otelContext },
26
+ )
27
+
28
+ const schemaMetaRows = db.select<SchemaMetaRow>(sql`SELECT * FROM ${SCHEMA_META_TABLE}`)
29
+
30
+ const dbSchemaHashByTable = Object.fromEntries(
31
+ schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
32
+ )
33
+
34
+ const getMemoizedTimestamp = memoize(() => new Date().toISOString())
35
+ const tableDefs = {
36
+ // NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
37
+ [SCHEMA_META_TABLE]: systemTables[SCHEMA_META_TABLE],
38
+ ...omit(schema.tables, [SCHEMA_META_TABLE]),
39
+ ...componentStateTables,
40
+ }
41
+
42
+ for (const [tableName, tableDef] of Object.entries(tableDefs)) {
43
+ const dbSchemaHash = dbSchemaHashByTable[tableName]
44
+ const schemaHash = SqliteAst.hash(tableDef)
45
+ if (schemaHash !== dbSchemaHash) {
46
+ console.log(
47
+ `Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
48
+ )
49
+
50
+ const columnSpec = makeColumnSpec(tableDef)
51
+
52
+ // TODO need to possibly handle cascading deletes due to foreign keys
53
+ db.execute(sql`drop table if exists ${tableName}`, undefined, [], { otelContext })
54
+ db.execute(sql`create table if not exists ${tableName} (${columnSpec});`, undefined, [], { otelContext })
55
+
56
+ for (const index of tableDef.indexes) {
57
+ db.execute(createIndexFromDefinition(tableName, index), undefined, [], { otelContext })
58
+ }
59
+
60
+ const updatedAt = getMemoizedTimestamp()
61
+ db.execute(
62
+ sql`
63
+ INSERT INTO ${SCHEMA_META_TABLE} (tableName, schemaHash, updatedAt) VALUES ($tableName, $schemaHash, $updatedAt)
64
+ ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
65
+ `,
66
+ { tableName, schemaHash, updatedAt },
67
+ [],
68
+ { otelContext },
69
+ )
70
+ }
71
+ }
72
+ }
73
+
74
+ const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
75
+ const uniqueStr = index.unique ? 'UNIQUE' : ''
76
+ return sql`create ${uniqueStr} index ${index.name} on ${tableName} (${index.columns.join(', ')})`
77
+ }
78
+
79
+ const makeColumnSpec = (tableDef: SqliteAst.Table) => {
80
+ const primaryKeys = tableDef.columns.filter((_) => _.primaryKey).map((_) => _.name)
81
+ const columnDefStrs = tableDef.columns.map(toSqliteColumnSpec)
82
+ if (primaryKeys.length > 0) {
83
+ columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
84
+ }
85
+
86
+ return columnDefStrs.join(', ')
87
+ }
88
+
89
+ const toSqliteColumnSpec = (column: SqliteAst.Column) => {
90
+ const columnType = column.type._tag
91
+ // const primaryKey = column.primaryKey ? 'primary key' : ''
92
+ const nullable = column.nullable === false ? 'not null' : ''
93
+ const defaultValue =
94
+ column.default === undefined
95
+ ? ''
96
+ : columnType === 'text'
97
+ ? `default '${column.default}'`
98
+ : `default ${column.default}`
99
+
100
+ return `${column.name} ${columnType} ${nullable} ${defaultValue}`
101
+ }
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
@@ -3,21 +3,23 @@ import { mapValues } from 'lodash-es'
3
3
  import type { ReactElement, ReactNode } from 'react'
4
4
  import React from 'react'
5
5
 
6
- import type { Backend, BackendOptions } from '../backends/index.js'
6
+ // TODO refactor so the `react` module doesn't depend on `effect` module
7
7
  import type {
8
8
  GlobalQueryDefs,
9
9
  LiveStoreContext as StoreContext_,
10
10
  LiveStoreCreateStoreOptions,
11
11
  } from '../effect/LiveStore.js'
12
+ import type { InMemoryDatabase } from '../inMemoryDatabase.js'
12
13
  import type { Schema } from '../schema.js'
14
+ import type { StorageInit } from '../storage/index.js'
13
15
  import type { BaseGraphQLContext, GraphQLOptions } from '../store.js'
14
16
  import { createStore } from '../store.js'
15
17
  import { LiveStoreContext } from './LiveStoreContext.js'
16
18
 
17
19
  interface LiveStoreProviderProps<GraphQLContext> {
18
20
  schema: Schema
19
- backendOptions: BackendOptions
20
- boot?: (backend: Backend, parentSpan: otel.Span) => Promise<void>
21
+ loadStorage: () => StorageInit | Promise<StorageInit>
22
+ boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
21
23
  globalQueryDefs: GlobalQueryDefs
22
24
  graphQLOptions?: GraphQLOptions<GraphQLContext>
23
25
  otelTracer?: otel.Tracer
@@ -28,7 +30,7 @@ interface LiveStoreProviderProps<GraphQLContext> {
28
30
  export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
29
31
  fallback,
30
32
  globalQueryDefs,
31
- backendOptions,
33
+ loadStorage,
32
34
  graphQLOptions,
33
35
  otelTracer,
34
36
  otelRootSpanContext,
@@ -39,7 +41,7 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
39
41
  const store = useCreateStore({
40
42
  schema,
41
43
  globalQueryDefs,
42
- backendOptions,
44
+ loadStorage,
43
45
  graphQLOptions,
44
46
  otelTracer,
45
47
  otelRootSpanContext,
@@ -50,13 +52,15 @@ 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
62
  globalQueryDefs,
59
- backendOptions,
63
+ loadStorage,
60
64
  graphQLOptions,
61
65
  otelTracer,
62
66
  otelRootSpanContext,
@@ -69,7 +73,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
69
73
  try {
70
74
  const store = await createStore({
71
75
  schema,
72
- backendOptions,
76
+ loadStorage,
73
77
  graphQLOptions,
74
78
  otelTracer,
75
79
  otelRootSpanContext,
@@ -87,7 +91,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
87
91
  })()
88
92
 
89
93
  // TODO: do we need to return any cleanup function here?
90
- }, [schema, backendOptions, globalQueryDefs, graphQLOptions, otelTracer, otelRootSpanContext, boot])
94
+ }, [schema, loadStorage, globalQueryDefs, graphQLOptions, otelTracer, otelRootSpanContext, boot])
91
95
 
92
96
  return ctxValue
93
97
  }
@@ -3,9 +3,18 @@ export type {
3
3
  ReactiveGraphQL,
4
4
  ReactiveSQL,
5
5
  Setters,
6
+ ComponentKeyConfig,
7
+ QueryResults,
8
+ QueryDefinitions,
9
+ ComponentColumns,
10
+ GetStateType,
11
+ GetStateTypeEncoded,
6
12
  } from './useLiveStoreComponent.js'
7
13
  export { LiveStoreContext, useStore } from './LiveStoreContext.js'
8
14
  export { LiveStoreProvider } from './LiveStoreProvider.js'
9
15
  export { useLiveStoreComponent } from './useLiveStoreComponent.js'
10
16
  export { useGraphQL } from './useGraphQL.js'
11
17
  export { useGlobalQuery } from './useGlobalQuery.js'
18
+
19
+ // Needed to make TS happy
20
+ export type { TypedDocumentNode } from '@graphql-typed-document-node/core'
@@ -1,7 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
 
3
3
  import { labelForKey } from '../componentKey.js'
4
- import { TODO_REMOVE_trackLongRunningSpan } from '../otel.js'
5
4
  import type { LiveStoreQuery, QueryResult } from '../store.js'
6
5
 
7
6
  export const useGlobalQuery = <Q extends LiveStoreQuery>(query: Q): QueryResult<Q> => {
@@ -15,8 +14,6 @@ export const useGlobalQuery = <Q extends LiveStoreQuery>(query: Q): QueryResult<
15
14
  {},
16
15
  query.store.otel.queriesSpanContext,
17
16
  (span) => {
18
- TODO_REMOVE_trackLongRunningSpan(span)
19
-
20
17
  const cancel = query.store.subscribe(
21
18
  query,
22
19
  (v) => {