@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,6 +1,9 @@
1
1
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
- import { type LiteralUnion, omit, shouldNeverHappen } from '@livestore/utils'
2
+ import type { LiteralUnion, PrettifyFlat } from '@livestore/utils'
3
+ import { omit, shouldNeverHappen } from '@livestore/utils'
4
+ import { Schema } from '@livestore/utils/effect'
3
5
  import * as otel from '@opentelemetry/api'
6
+ import { SqliteDsl } from 'effect-db-schema'
4
7
  import { isEqual, mapValues } from 'lodash-es'
5
8
  import type { DependencyList } from 'react'
6
9
  import React from 'react'
@@ -12,8 +15,8 @@ import type { GetAtom } from '../reactive.js'
12
15
  import type { LiveStoreGraphQLQuery } from '../reactiveQueries/graphql.js'
13
16
  import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
14
17
  import type { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
15
- import type { ComponentStateSchema } from '../schema.js'
16
18
  import type { BaseGraphQLContext, LiveStoreQuery, QueryResult, Store } from '../store.js'
19
+ import type { Bindable } from '../util.js'
17
20
  import { sql } from '../util.js'
18
21
  import { useStore } from './LiveStoreContext.js'
19
22
  import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
@@ -21,11 +24,13 @@ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInp
21
24
  export interface QueryDefinitions {
22
25
  [queryName: string]: LiveStoreQuery
23
26
  }
24
- export type QueryResults<TQuery> = { [queryName in keyof TQuery]: QueryResult<TQuery[queryName]> }
27
+
28
+ export type QueryResults<TQuery> = { [queryName in keyof TQuery]: PrettifyFlat<QueryResult<TQuery[queryName]>> }
25
29
 
26
30
  export type ReactiveSQL = <TResult>(
27
31
  genQuery: (get: GetAtom) => string,
28
32
  queriedTables: string[],
33
+ bindValues?: Bindable | undefined,
29
34
  ) => LiveStoreSQLQuery<TResult>
30
35
  export type ReactiveGraphQL = <
31
36
  TResult extends Record<string, any>,
@@ -48,14 +53,18 @@ type GenQueries<TQueries, TStateResult> = (args: {
48
53
  rxGraphQL: ReactiveGraphQL
49
54
  globalQueries: QueryDefinitions
50
55
  state$: LiveStoreJSQuery<TStateResult>
51
- /** Registers a subscription */
56
+ /**
57
+ * Registers a subscription.
58
+ *
59
+ * Passed down for some manual subscribing. Use carefully.
60
+ */
52
61
  subscribe: RegisterSubscription
53
62
  isTemporaryQuery: boolean
54
63
  }) => TQueries
55
64
 
56
- export type UseLiveStoreComponentProps<TQueries, TComponentState> = {
57
- stateSchema?: ComponentStateSchema<TComponentState>
58
- queries?: GenQueries<TQueries, TComponentState>
65
+ export type UseLiveStoreComponentProps<TQueries, TColumns extends ComponentColumns> = {
66
+ stateSchema?: SqliteDsl.TableDefinition<string, TColumns>
67
+ queries?: GenQueries<TQueries, SqliteDsl.FromColumns.RowDecoded<TColumns>>
59
68
  reactDeps?: React.DependencyList
60
69
  componentKey: ComponentKeyConfig
61
70
  }
@@ -70,12 +79,17 @@ export type ComponentKeyConfig = {
70
79
  id: LiteralUnion<'singleton' | '__ephemeral__', string>
71
80
  }
72
81
 
73
- type ComponentState = {
74
- /** Equivalent to `componentKey.key` */
75
- id: string
76
- [key: string]: string | number | boolean | null
82
+ // TODO enforce columns are non-nullable or have a default
83
+ export interface ComponentColumns extends SqliteDsl.Columns {
84
+ id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false>
77
85
  }
78
86
 
87
+ // type ComponentState = {
88
+ // /** Equivalent to `componentKey.key` */
89
+ // id: string
90
+ // [key: string]: string | number | boolean | null
91
+ // }
92
+
79
93
  /**
80
94
  * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
81
95
  * so we need to "cache" the fact that we've already started a span for this component.
@@ -88,23 +102,33 @@ type UseLiveStoreJsonState<TState> = <TResult>(
88
102
  parse?: (_: unknown) => TResult,
89
103
  ) => [value: TResult, setValue: (newVal: TResult | ((prevVal: TResult) => TResult)) => void]
90
104
 
105
+ export type GetStateType<TTableDef extends SqliteDsl.TableDefinition<any, any>> = SqliteDsl.FromColumns.RowDecoded<
106
+ TTableDef['columns']
107
+ >
108
+
109
+ export type GetStateTypeEncoded<TTableDef extends SqliteDsl.TableDefinition<any, any>> =
110
+ SqliteDsl.FromColumns.RowEncoded<TTableDef['columns']>
111
+
91
112
  /**
92
113
  * Create reactive queries within a component.
93
114
  * @param config.queries A function that returns a map of named reactive queries.
94
115
  * @param config.componentKey A function that returns a unique key for this component.
95
116
  * @param config.reactDeps A list of React-level dependencies that will refresh the queries.
96
117
  */
97
- export const useLiveStoreComponent = <TComponentState extends ComponentState, TQueries extends QueryDefinitions>({
118
+ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQueries extends QueryDefinitions>({
98
119
  stateSchema: stateSchema_,
99
120
  queries = () => ({}) as TQueries,
100
121
  componentKey: componentKeyConfig,
101
122
  reactDeps = [],
102
- }: UseLiveStoreComponentProps<TQueries, TComponentState>): {
123
+ }: UseLiveStoreComponentProps<TQueries, TColumns>): {
103
124
  queryResults: QueryResults<TQueries>
104
- state: TComponentState
105
- setState: Setters<TComponentState>
106
- useLiveStoreJsonState: UseLiveStoreJsonState<TComponentState>
125
+ state: SqliteDsl.FromColumns.RowDecoded<TColumns>
126
+ setState: Setters<SqliteDsl.FromColumns.RowDecoded<TColumns>>
127
+ useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TColumns>>
107
128
  } => {
129
+ type TComponentState = SqliteDsl.FromColumns.RowDecoded<TColumns>
130
+
131
+ // TODO validate schema to make sure each column has a default value
108
132
  // TODO we should clean up the state schema handling to remove this special handling for the `id` column
109
133
  const stateSchema = React.useMemo(
110
134
  () => (stateSchema_ ? { ...stateSchema_, columns: omit(stateSchema_.columns, 'id' as any) } : undefined),
@@ -156,8 +180,8 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
156
180
  isTemporaryQuery: boolean
157
181
  }) =>
158
182
  queries({
159
- rxSQL: <T>(genQuery: (get: GetAtom) => string, queriedTables: string[]) =>
160
- store.querySQL<T>(genQuery, { queriedTables, otelContext }),
183
+ rxSQL: <T>(genQuery: (get: GetAtom) => string, queriedTables: string[], bindValues?: Bindable) =>
184
+ store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext, componentKey }),
161
185
  rxGraphQL: <Result extends Record<string, any>, Variables extends Record<string, any>>(
162
186
  query: DocumentNode<Result, Variables>,
163
187
  genVariableValues: (get: GetAtom) => Variables,
@@ -183,11 +207,17 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
183
207
  stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
184
208
  ) as TComponentState
185
209
 
210
+ // @ts-expect-error TODO fix typing
186
211
  defaultState.id = componentKeyConfig.id
187
212
 
188
213
  return defaultState
189
214
  }, [componentKeyConfig.id, stateSchema])
190
215
 
216
+ const componentStateEffectSchema = React.useMemo(
217
+ () => (stateSchema ? SqliteDsl.structSchemaForTable(stateSchema) : Schema.any),
218
+ [stateSchema],
219
+ )
220
+
191
221
  // Step 1:
192
222
  // Synchronously create state and queries for initial render pass.
193
223
  // We do this in a temporary query context which cleans up after itself, making it idempotent
@@ -199,29 +229,34 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
199
229
  return store.inTempQueryContext(() => {
200
230
  try {
201
231
  // create state query
202
- let stateQuery: LiveStoreJSQuery<TComponentState>
232
+ let state$: LiveStoreJSQuery<TComponentState>
203
233
  if (stateSchema === undefined) {
204
234
  // TODO don't set up a query if there's no state schema (keeps the graph more clean)
205
- stateQuery = store.queryJS(() => ({}), {
235
+ state$ = store.queryJS(() => ({}), {
206
236
  componentKey,
207
237
  otelContext,
208
238
  }) as unknown as LiveStoreJSQuery<TComponentState>
209
239
  } else {
210
240
  const componentTableName = tableNameForComponentKey(componentKey)
211
241
  const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
212
- stateQuery = store
213
- .querySQL<TComponentState>(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
242
+ state$ = store
243
+ .querySQL(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
214
244
  queriedTables: [componentTableName],
215
245
  componentKey,
216
246
  label: `localState:query:${componentKeyLabel}`,
217
247
  otelContext,
218
248
  })
219
- .getFirstRow({ defaultValue: defaultComponentState })
249
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
250
+ .pipe<TComponentState>((results) =>
251
+ results.length === 1
252
+ ? Schema.parseSync(componentStateEffectSchema)(results[0]!)
253
+ : defaultComponentState,
254
+ )
220
255
  }
221
- const initialComponentState = stateQuery.results$.result
256
+ const initialComponentState = state$.results$.result
222
257
 
223
258
  const queries = generateQueries({
224
- state$: stateQuery,
259
+ state$: state$,
225
260
  otelContext,
226
261
  registerSubscription: () => {},
227
262
  isTemporaryQuery: true,
@@ -229,7 +264,11 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
229
264
  for (const [name, query] of Object.entries(queries)) {
230
265
  query.label = name
231
266
  }
232
- const initialQueryResults = mapValues(queries, (query) => query.results$.result) as QueryResults<TQueries>
267
+ const initialQueryResults = mapValues(
268
+ queries,
269
+ (query) => query.results$.result,
270
+ // TODO improve typing
271
+ ) as unknown as QueryResults<TQueries>
233
272
 
234
273
  return { initialComponentState, initialQueryResults }
235
274
  } finally {
@@ -237,7 +276,16 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
237
276
  }
238
277
  })
239
278
  })
240
- }, [store, otelContext, stateSchema, generateQueries, componentKey, componentKeyLabel, defaultComponentState])
279
+ }, [
280
+ store,
281
+ otelContext,
282
+ stateSchema,
283
+ generateQueries,
284
+ componentKey,
285
+ componentKeyLabel,
286
+ componentStateEffectSchema,
287
+ defaultComponentState,
288
+ ])
241
289
 
242
290
  // Now that we've computed the initial state synchronously,
243
291
  // we can set up our useState calls w/ a default value populated...
@@ -249,10 +297,13 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
249
297
  stateSchema === undefined
250
298
  ? {}
251
299
  : // TODO: do we have a better type for the values that can go in SQLite?
252
- mapValues(stateSchema.columns, (_, columnName) => (value: string | number) => {
300
+ mapValues(stateSchema.columns, (column, columnName) => (value: string | number) => {
253
301
  // Don't update the state if it's the same as the value already seen in the component
302
+ // @ts-expect-error TODO fix typing
254
303
  if (componentStateRef.current[columnName] === value) return
255
304
 
305
+ const encodedValue = Schema.encodeSync(column.type.codec)(value)
306
+
256
307
  if (['componentKey', 'columnNames'].includes(columnName)) {
257
308
  shouldNeverHappen(`Can't use reserved column name ${columnName}`)
258
309
  }
@@ -260,7 +311,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
260
311
  return store.applyEvent('updateComponentState', {
261
312
  componentKey,
262
313
  columnNames: [columnName],
263
- [columnName]: value,
314
+ [columnName]: encodedValue,
264
315
  })
265
316
  })
266
317
  ) as Setters<TComponentState>
@@ -268,6 +319,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
268
319
  setState.setMany = (columnValues: Partial<TComponentState>) => {
269
320
  // TODO use hashing instead
270
321
  // Don't update the state if it's the same as the value already seen in the component
322
+ // @ts-expect-error TODO fix typing
271
323
  if (Object.entries(columnValues).every(([columnName, value]) => componentStateRef.current[columnName] === value)) {
272
324
  return
273
325
  }
@@ -291,7 +343,12 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
291
343
  // create state query
292
344
  let state$: LiveStoreJSQuery<TComponentState>
293
345
  if (stateSchema === undefined) {
294
- state$ = store.queryJS(() => ({}) as TComponentState, { componentKey, otelContext })
346
+ // TODO remove this query
347
+ state$ = store.queryJS(() => ({}) as TComponentState, {
348
+ componentKey,
349
+ otelContext,
350
+ label: 'empty-component-state',
351
+ })
295
352
  } else {
296
353
  const componentTableName = tableNameForComponentKey(componentKey)
297
354
  insertRowForComponentInstance({ store, componentKey, stateSchema })
@@ -304,7 +361,10 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
304
361
  label: `localState:query:${componentKeyLabel}`,
305
362
  otelContext,
306
363
  })
307
- .getFirstRow({ defaultValue: defaultComponentState })
364
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
365
+ .pipe<TComponentState>((results) =>
366
+ results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
367
+ )
308
368
  }
309
369
 
310
370
  unsubs.push(
@@ -334,11 +394,11 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
334
394
  }
335
395
 
336
396
  const queries = generateQueries({ state$, otelContext, registerSubscription, isTemporaryQuery: false })
337
- // Use the name given to this query in the useQueries hook as its label
338
- for (const [name, query] of Object.entries(queries)) {
339
- query.label = name
340
- }
397
+
341
398
  for (const [key, query] of Object.entries(queries)) {
399
+ // Use the field name given to this query in the useQueries hook as its label
400
+ query.label = key
401
+
342
402
  unsubs.push(
343
403
  store.subscribe(
344
404
  query,
@@ -445,14 +505,14 @@ export const useComponentKey = ({ name, id }: ComponentKeyConfig, deps: Dependen
445
505
  * Create a row storing the state for a component instance, if none exists yet.
446
506
  * Initialized with default values, and keyed on the component key.
447
507
  */
448
- const insertRowForComponentInstance = <T>({
508
+ const insertRowForComponentInstance = ({
449
509
  store,
450
510
  componentKey,
451
511
  stateSchema,
452
512
  }: {
453
513
  store: Store<BaseGraphQLContext>
454
514
  componentKey: ComponentKey
455
- stateSchema: ComponentStateSchema<T>
515
+ stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
456
516
  }) => {
457
517
  const columnNames = ['id', ...Object.keys(stateSchema.columns)]
458
518
  const columnValues = columnNames.map((name) => `$${name}`).join(', ')
@@ -465,8 +525,8 @@ const insertRowForComponentInstance = <T>({
465
525
  void store.execute(
466
526
  insertQuery,
467
527
  {
468
- id: componentKey.id,
469
528
  ...mapValues(stateSchema.columns, (column) => prepareValueForSql(column.default ?? null)),
529
+ id: componentKey.id,
470
530
  },
471
531
  [tableName],
472
532
  )
package/src/reactive.ts CHANGED
@@ -350,7 +350,8 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
350
350
  )
351
351
  }
352
352
 
353
- return atom.result!
353
+ // TODO handle case when `atom.result` is undefined
354
+ return atom.result
354
355
  }
355
356
 
356
357
  /**
package/src/schema.ts CHANGED
@@ -1,45 +1,10 @@
1
- import type { Backend } from './backends/index.js'
2
- import { EVENTS_TABLE_NAME } from './events.js'
3
- import type { InMemoryDatabase } from './inMemoryDatabase.js'
4
- import { sql } from './util.js'
5
-
6
- export type ColumnDefinition = {
7
- nullable?: boolean
8
- primaryKey?: boolean
9
- } & (
10
- | { type: 'text'; default?: string }
11
- | { type: 'json'; default?: string }
12
- | { type: 'integer'; default?: number }
13
- | { type: 'boolean'; default?: boolean }
14
- | { type: 'real'; default?: number }
15
- | { type: 'blob'; default?: any }
16
- ) // sqlite uses numbers for booleans but we fake it
17
-
18
- // TODO: defaults should be nullable for nullable columns
19
- type ColumnDefinitionWithDefault = {
20
- primaryKey?: boolean
21
- } & (
22
- | { type: 'text'; nullable?: true; default: string }
23
- | { type: 'json'; nullable?: true; default: string }
24
- | { type: 'integer'; nullable?: true; default: number }
25
- | { type: 'boolean'; nullable?: true; default: boolean }
26
- | { type: 'real'; nullable: true; default: number | null }
27
- | { type: 'blob'; nullable: true; default: any | null }
28
- )
29
-
30
- export type TableDefinition = {
31
- columns: {
32
- [key: string]: ColumnDefinition
33
- }
34
- /**
35
- * Can be used for various purposes e.g. to provide a foreign key constraint like below:
36
- * ```ts
37
- * columnsRaw: (columnsStr) => `${columnsStr}, foreign key (userId) references users(id)`
38
- * ```
39
- */
40
- columnsRaw?: (columnsStr: string) => string
41
- indexes?: Index[]
42
- }
1
+ import type { PrettifyFlat } from '@livestore/utils'
2
+ import { mapObjectValues } from '@livestore/utils'
3
+ import type { Schema } from '@livestore/utils/effect'
4
+ import type { SqliteAst } from 'effect-db-schema'
5
+ import { SqliteDsl } from 'effect-db-schema'
6
+
7
+ import { DbSchema } from './index.js'
43
8
 
44
9
  export type Index = {
45
10
  name: string
@@ -48,35 +13,60 @@ export type Index = {
48
13
  isUnique?: boolean
49
14
  }
50
15
 
51
- export type ComponentStateSchema<T> = {
52
- componentType: string
53
- columns: {
54
- [k in keyof T]: ColumnDefinitionWithDefault
16
+ // A global variable representing component state tables we should create in the database
17
+ export const componentStateTables: { [key: string]: SqliteAst.Table } = {}
18
+
19
+ export type InputSchema = {
20
+ tables: {
21
+ [tableName: string]: SqliteDsl.TableDefinition<any, any>
55
22
  }
23
+ materializedViews?: MaterializedViewDefinitions
24
+ actions: ActionDefinitions<any>
56
25
  }
57
26
 
58
- // A global variable representing component state tables we should create in the database
59
- export const componentStateTables: { [key: string]: TableDefinition } = {}
27
+ export const makeSchema = <TSchema extends InputSchema>(schema: TSchema): Schema =>
28
+ ({
29
+ tables: { ...mapObjectValues(schema.tables, (_tableName, table) => table.ast), ...systemTables },
30
+ materializedViews: schema.materializedViews ?? {},
31
+ actions: schema.actions,
32
+ }) satisfies Schema
60
33
 
61
- export const defineComponentStateSchema = <T>(
62
- schema: ComponentStateSchema<T>,
63
- ): ComponentStateSchema<T & { id: string }> => {
64
- const tablePath = `components__${schema.componentType}`
34
+ export type ComponentStateSchema = SqliteDsl.TableDefinition<any, any> & {
35
+ // TODO
36
+ register: () => void
37
+ }
38
+
39
+ // TODO get rid of "side effect" in this function (via explicit register fn)
40
+ export const defineComponentStateSchema = <TName extends string, TColumns extends SqliteDsl.Columns>(
41
+ name: TName,
42
+ columns: TColumns,
43
+ ): SqliteDsl.TableDefinition<
44
+ `components__${TName}`,
45
+ PrettifyFlat<TColumns & { id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false> }>
46
+ > => {
47
+ const tablePath = `components__${name}` as const
65
48
  if (Object.keys(componentStateTables).includes(tablePath)) {
66
49
  // throw new Error(`Can't register duplicate component: ${name}`)
67
50
  console.error(`Can't register duplicate component: ${tablePath}`)
68
51
  }
69
52
 
70
- const schemaWithId = schema as ComponentStateSchema<T & { id: string }>
53
+ const schemaWithId = columns as unknown as PrettifyFlat<
54
+ TColumns & {
55
+ id: SqliteDsl.ColumnDefinition<SqliteDsl.FieldType.FieldTypeText<string, string>, false>
56
+ }
57
+ >
58
+
59
+ schemaWithId.id = DbSchema.text({ primaryKey: true })
71
60
 
72
- schemaWithId.columns.id = { type: 'text', primaryKey: true } as any
61
+ const tableDef = SqliteDsl.table(tablePath, schemaWithId, [])
73
62
 
74
- componentStateTables[tablePath] = schemaWithId as any
63
+ // TODO move into register fn
64
+ componentStateTables[tablePath] = tableDef.ast
75
65
 
76
- return schemaWithId
66
+ return tableDef
77
67
  }
78
68
 
79
- type SQLWriteStatement = {
69
+ export type SQLWriteStatement = {
80
70
  sql: string
81
71
 
82
72
  /** Tables written by the statement */
@@ -96,31 +86,36 @@ export type Schema = {
96
86
  actions: ActionDefinitions<any>
97
87
  }
98
88
 
99
- export type TableDefinitions = { [key: string]: TableDefinition }
89
+ export type TableDefinitions = { [key: string]: SqliteAst.Table }
100
90
  export type MaterializedViewDefinitions = { [key: string]: {} }
101
91
  export type ActionDefinitions<TArgsMap extends Record<string, any>> = {
102
92
  [key in keyof TArgsMap]: ActionDefinition<TArgsMap[key]>
103
93
  }
104
94
 
105
- export const EVENT_CURSOR_TABLE = 'livestore__event_cursor'
106
-
107
- const systemTables = {
108
- [EVENTS_TABLE_NAME]: {
109
- columns: {
110
- id: { type: 'text', primaryKey: true },
111
- type: { type: 'text', nullable: false },
112
- args: { type: 'text', nullable: false },
113
- },
114
- },
115
- [EVENT_CURSOR_TABLE]: {
116
- columns: {
117
- id: { type: 'text', primaryKey: true },
118
- cursor: { type: 'text', nullable: false },
119
- },
120
- },
121
- } as const
122
-
123
- export const defineSchema = <S extends Schema>(schema: S) => mergeSystemSchema(schema)
95
+ export const EVENT_CURSOR_TABLE = '__livestore_event_cursor'
96
+ export const SCHEMA_META_TABLE = '__livestore_schema'
97
+
98
+ const schemaMetaTable = SqliteDsl.table(SCHEMA_META_TABLE, {
99
+ tableName: SqliteDsl.text({ primaryKey: true }),
100
+ schemaHash: SqliteDsl.integer({ nullable: false }),
101
+ /** ISO date format */
102
+ updatedAt: SqliteDsl.text({ nullable: false }),
103
+ })
104
+
105
+ export type SchemaMetaRow = SqliteDsl.FromTable.RowDecoded<typeof schemaMetaTable>
106
+
107
+ export const systemTables = {
108
+ // [EVENTS_TABLE_NAME]: SqliteDsl.table(EVENTS_TABLE_NAME, {
109
+ // id: SqliteDsl.text({ primaryKey: true }),
110
+ // type: SqliteDsl.text({ nullable: false }),
111
+ // args: SqliteDsl.text({ nullable: false }),
112
+ // }).ast,
113
+ [EVENT_CURSOR_TABLE]: SqliteDsl.table(EVENT_CURSOR_TABLE, {
114
+ id: SqliteDsl.text({ primaryKey: true }),
115
+ cursor: SqliteDsl.text({ nullable: false }),
116
+ }).ast,
117
+ [SCHEMA_META_TABLE]: schemaMetaTable.ast,
118
+ } satisfies TableDefinitions
124
119
 
125
120
  export const defineTables = <T extends TableDefinitions>(tables: T) => tables
126
121
 
@@ -149,71 +144,3 @@ declare global {
149
144
  [key: string]: ActionDefinition
150
145
  }
151
146
  }
152
-
153
- const mergeSystemSchema = <S extends Schema>(schema: S) => {
154
- return {
155
- ...schema,
156
- tables: {
157
- ...schema.tables,
158
- ...systemTables,
159
- },
160
- }
161
- }
162
-
163
- /**
164
- * Destructively load a schema into a database,
165
- * dropping any existing tables and creating new ones.
166
- */
167
- export const loadSchema = async (backend: InMemoryDatabase | Backend, schema: Schema) => {
168
- const fullSchemaWithComponents = { ...schema, tables: { ...schema.tables, ...componentStateTables } }
169
-
170
- // Loop through all the tables and create them in the SQLite database
171
- for (const [tableName, tableDefinition] of Object.entries(fullSchemaWithComponents.tables)) {
172
- const primaryKeys = Object.entries(tableDefinition.columns)
173
- .filter(([_, columnDef]) => columnDef.primaryKey)
174
- .map(([columnName, _]) => columnName)
175
- const columnDefStrs = Object.entries(tableDefinition.columns).map(([columnName, column]) =>
176
- toSqliteColumnSpec(columnName, column),
177
- )
178
- if (primaryKeys.length > 0) {
179
- columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
180
- }
181
- const mapColumns = tableDefinition.columnsRaw ?? ((_) => _)
182
- const columnSpec = mapColumns(columnDefStrs.join(', '))
183
-
184
- backend.execute(sql`drop table if exists ${tableName}`)
185
-
186
- backend.execute(sql`create table if not exists ${tableName} (${columnSpec});`)
187
- }
188
-
189
- await createIndexes(backend, schema)
190
- }
191
-
192
- const toSqliteColumnSpec = (columnName: string, column: ColumnDefinition) => {
193
- const columnType = column.type === 'boolean' ? 'integer' : column.type
194
- // const primaryKey = column.primaryKey ? 'primary key' : ''
195
- const nullable = column.nullable === false ? 'not null' : ''
196
- const defaultValue =
197
- column.default === undefined
198
- ? ''
199
- : column.type === 'text'
200
- ? `default '${column.default}'`
201
- : `default ${column.default}`
202
-
203
- return `${columnName} ${columnType} ${nullable} ${defaultValue}`
204
- }
205
-
206
- const createIndexFromDefinition = (tableName: string, index: Index) => {
207
- const uniqueStr = index.isUnique ? 'UNIQUE' : ''
208
- return sql`create ${uniqueStr} index ${index.name} on ${tableName} (${index.columns.join(', ')})`
209
- }
210
-
211
- const createIndexes = async (db: Backend | InMemoryDatabase, schema: Schema) => {
212
- for (const [tableName, tableDefinition] of Object.entries(schema.tables)) {
213
- if (tableDefinition.indexes !== undefined) {
214
- for (const index of tableDefinition.indexes) {
215
- db.execute(createIndexFromDefinition(tableName, index))
216
- }
217
- }
218
- }
219
- }
@@ -0,0 +1,21 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+
3
+ import type { ParamsObject } from '../../util.js'
4
+ import type { Storage, StorageOtelProps } from '../index.js'
5
+
6
+ export type StorageOptionsWebInMemory = {
7
+ type: 'web-in-memory'
8
+ }
9
+
10
+ /** NOTE: This storage is currently only used for testing */
11
+ export class InMemoryStorage implements Storage {
12
+ constructor(readonly otelTracer: otel.Tracer) {}
13
+
14
+ static load = async (_options?: StorageOptionsWebInMemory) => {
15
+ return ({ otelTracer }: StorageOtelProps) => new InMemoryStorage(otelTracer)
16
+ }
17
+
18
+ execute = (_query: string, _bindValues?: ParamsObject): void => {}
19
+
20
+ getPersistedData = async (): Promise<Uint8Array> => new Uint8Array()
21
+ }
@@ -0,0 +1,27 @@
1
+ // A storage 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 { ParamsObject } from '../util.js'
12
+
13
+ export type StorageInit = (otelProps: StorageOtelProps) => Promise<Storage> | Storage
14
+
15
+ export interface Storage {
16
+ execute(query: string, bindValues?: ParamsObject, parentSpan?: otel.Span): void
17
+
18
+ /** Return a snapshot of persisted data from the storage */
19
+ getPersistedData(parentSpan?: otel.Span): Promise<Uint8Array>
20
+ }
21
+
22
+ export type StorageType = 'tauri' | 'web' | 'web-in-memory'
23
+
24
+ export type StorageOtelProps = {
25
+ otelTracer: otel.Tracer
26
+ parentSpan: otel.Span
27
+ }