@livestore/livestore 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +7 -7
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/QueryCache.d.ts +20 -0
  4. package/dist/QueryCache.d.ts.map +1 -0
  5. package/dist/QueryCache.js +71 -0
  6. package/dist/QueryCache.js.map +1 -0
  7. package/dist/__tests__/react/fixture.d.ts +25 -0
  8. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  9. package/dist/__tests__/react/fixture.js +61 -0
  10. package/dist/__tests__/react/fixture.js.map +1 -0
  11. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  12. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  13. package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
  14. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  15. package/dist/__tests__/reactive.test.d.ts +2 -0
  16. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  17. package/dist/__tests__/reactive.test.js +198 -0
  18. package/dist/__tests__/reactive.test.js.map +1 -0
  19. package/dist/backends/base.d.ts +13 -0
  20. package/dist/backends/base.d.ts.map +1 -0
  21. package/dist/backends/base.js +53 -0
  22. package/dist/backends/base.js.map +1 -0
  23. package/dist/backends/in-memory/index.d.ts +22 -0
  24. package/dist/backends/in-memory/index.d.ts.map +1 -0
  25. package/dist/backends/in-memory/index.js +45 -0
  26. package/dist/backends/in-memory/index.js.map +1 -0
  27. package/dist/backends/index.d.ts +41 -0
  28. package/dist/backends/index.d.ts.map +1 -0
  29. package/dist/backends/index.js +16 -0
  30. package/dist/backends/index.js.map +1 -0
  31. package/dist/backends/tauri/index.d.ts +21 -0
  32. package/dist/backends/tauri/index.d.ts.map +1 -0
  33. package/dist/backends/tauri/index.js +48 -0
  34. package/dist/backends/tauri/index.js.map +1 -0
  35. package/dist/backends/utils/idb.d.ts +10 -0
  36. package/dist/backends/utils/idb.d.ts.map +1 -0
  37. package/dist/backends/utils/idb.js +58 -0
  38. package/dist/backends/utils/idb.js.map +1 -0
  39. package/dist/backends/web-worker/index.d.ts +26 -0
  40. package/dist/backends/web-worker/index.d.ts.map +1 -0
  41. package/dist/backends/web-worker/index.js +63 -0
  42. package/dist/backends/web-worker/index.js.map +1 -0
  43. package/dist/backends/web-worker/worker.d.ts +17 -0
  44. package/dist/backends/web-worker/worker.d.ts.map +1 -0
  45. package/dist/backends/web-worker/worker.js +139 -0
  46. package/dist/backends/web-worker/worker.js.map +1 -0
  47. package/dist/bounded-collections.d.ts +34 -0
  48. package/dist/bounded-collections.d.ts.map +1 -0
  49. package/dist/bounded-collections.js +103 -0
  50. package/dist/bounded-collections.js.map +1 -0
  51. package/dist/componentKey.d.ts +20 -0
  52. package/dist/componentKey.d.ts.map +1 -0
  53. package/dist/componentKey.js +3 -0
  54. package/dist/componentKey.js.map +1 -0
  55. package/dist/effect/LiveStore.d.ts +42 -0
  56. package/dist/effect/LiveStore.d.ts.map +1 -0
  57. package/dist/effect/LiveStore.js +37 -0
  58. package/dist/effect/LiveStore.js.map +1 -0
  59. package/dist/effect/index.d.ts +2 -0
  60. package/dist/effect/index.d.ts.map +1 -0
  61. package/dist/effect/index.js +2 -0
  62. package/dist/effect/index.js.map +1 -0
  63. package/dist/events.d.ts +7 -0
  64. package/dist/events.d.ts.map +1 -0
  65. package/dist/events.js +2 -0
  66. package/dist/events.js.map +1 -0
  67. package/dist/inMemoryDatabase.d.ts +60 -0
  68. package/dist/inMemoryDatabase.d.ts.map +1 -0
  69. package/dist/inMemoryDatabase.js +230 -0
  70. package/dist/inMemoryDatabase.js.map +1 -0
  71. package/dist/index.d.ts +20 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +9 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/migrations.d.ts +9 -0
  76. package/dist/migrations.d.ts.map +1 -0
  77. package/dist/migrations.js +62 -0
  78. package/dist/migrations.js.map +1 -0
  79. package/dist/otel.d.ts +4 -0
  80. package/dist/otel.d.ts.map +1 -0
  81. package/dist/otel.js +6 -0
  82. package/dist/otel.js.map +1 -0
  83. package/dist/react/LiveStoreContext.d.ts +11 -0
  84. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  85. package/dist/react/LiveStoreContext.js +10 -0
  86. package/dist/react/LiveStoreContext.js.map +1 -0
  87. package/dist/react/LiveStoreProvider.d.ts +22 -0
  88. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  89. package/dist/react/LiveStoreProvider.js +49 -0
  90. package/dist/react/LiveStoreProvider.js.map +1 -0
  91. package/dist/react/index.d.ts +8 -0
  92. package/dist/react/index.d.ts.map +1 -0
  93. package/dist/react/index.js +6 -0
  94. package/dist/react/index.js.map +1 -0
  95. package/dist/react/useGlobalQuery.d.ts +3 -0
  96. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  97. package/dist/react/useGlobalQuery.js +23 -0
  98. package/dist/react/useGlobalQuery.js.map +1 -0
  99. package/dist/react/useGraphQL.d.ts +11 -0
  100. package/dist/react/useGraphQL.d.ts.map +1 -0
  101. package/dist/react/useGraphQL.js +67 -0
  102. package/dist/react/useGraphQL.js.map +1 -0
  103. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  104. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  105. package/dist/react/useLiveStoreComponent.js +301 -0
  106. package/dist/react/useLiveStoreComponent.js.map +1 -0
  107. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  108. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  109. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  110. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  111. package/dist/reactive.d.ts +140 -0
  112. package/dist/reactive.d.ts.map +1 -0
  113. package/dist/reactive.js +302 -0
  114. package/dist/reactive.js.map +1 -0
  115. package/dist/reactiveQueries/base-class.d.ts +24 -0
  116. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  117. package/dist/reactiveQueries/base-class.js +22 -0
  118. package/dist/reactiveQueries/base-class.js.map +1 -0
  119. package/dist/reactiveQueries/graphql.d.ts +25 -0
  120. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  121. package/dist/reactiveQueries/graphql.js +18 -0
  122. package/dist/reactiveQueries/graphql.js.map +1 -0
  123. package/dist/reactiveQueries/js.d.ts +19 -0
  124. package/dist/reactiveQueries/js.d.ts.map +1 -0
  125. package/dist/reactiveQueries/js.js +13 -0
  126. package/dist/reactiveQueries/js.js.map +1 -0
  127. package/dist/reactiveQueries/sql.d.ts +31 -0
  128. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  129. package/dist/reactiveQueries/sql.js +32 -0
  130. package/dist/reactiveQueries/sql.js.map +1 -0
  131. package/dist/schema.d.ts +83 -0
  132. package/dist/schema.d.ts.map +1 -0
  133. package/dist/schema.js +49 -0
  134. package/dist/schema.js.map +1 -0
  135. package/dist/storage/base.d.ts +10 -0
  136. package/dist/storage/base.d.ts.map +1 -0
  137. package/dist/storage/base.js +14 -0
  138. package/dist/storage/base.js.map +1 -0
  139. package/dist/storage/in-memory/index.d.ts +15 -0
  140. package/dist/storage/in-memory/index.d.ts.map +1 -0
  141. package/dist/storage/in-memory/index.js +14 -0
  142. package/dist/storage/in-memory/index.js.map +1 -0
  143. package/dist/storage/index.d.ts +14 -0
  144. package/dist/storage/index.d.ts.map +1 -0
  145. package/dist/storage/index.js +9 -0
  146. package/dist/storage/index.js.map +1 -0
  147. package/dist/storage/tauri/index.d.ts +19 -0
  148. package/dist/storage/tauri/index.d.ts.map +1 -0
  149. package/dist/storage/tauri/index.js +38 -0
  150. package/dist/storage/tauri/index.js.map +1 -0
  151. package/dist/storage/utils/idb.d.ts +10 -0
  152. package/dist/storage/utils/idb.d.ts.map +1 -0
  153. package/dist/storage/utils/idb.js +58 -0
  154. package/dist/storage/utils/idb.js.map +1 -0
  155. package/dist/storage/web-worker/index.d.ts +27 -0
  156. package/dist/storage/web-worker/index.d.ts.map +1 -0
  157. package/dist/storage/web-worker/index.js +76 -0
  158. package/dist/storage/web-worker/index.js.map +1 -0
  159. package/dist/storage/web-worker/worker.d.ts +13 -0
  160. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  161. package/dist/storage/web-worker/worker.js +110 -0
  162. package/dist/storage/web-worker/worker.js.map +1 -0
  163. package/dist/store.d.ts +192 -0
  164. package/dist/store.d.ts.map +1 -0
  165. package/dist/store.js +569 -0
  166. package/dist/store.js.map +1 -0
  167. package/dist/util.d.ts +26 -0
  168. package/dist/util.d.ts.map +1 -0
  169. package/dist/util.js +53 -0
  170. package/dist/util.js.map +1 -0
  171. package/package.json +46 -19
  172. package/src/__tests__/react/fixture.tsx +19 -28
  173. package/src/effect/LiveStore.ts +8 -13
  174. package/src/events.ts +1 -1
  175. package/src/inMemoryDatabase.ts +100 -117
  176. package/src/index.ts +10 -16
  177. package/src/migrations.ts +101 -0
  178. package/src/otel.ts +0 -11
  179. package/src/react/LiveStoreProvider.tsx +12 -8
  180. package/src/react/index.ts +9 -0
  181. package/src/react/useGlobalQuery.ts +0 -3
  182. package/src/react/useLiveStoreComponent.ts +95 -37
  183. package/src/schema.ts +72 -145
  184. package/src/storage/in-memory/index.ts +21 -0
  185. package/src/storage/index.ts +27 -0
  186. package/src/{backends/tauri.ts → storage/tauri/index.ts} +13 -27
  187. package/src/storage/web-worker/index.ts +118 -0
  188. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  189. package/src/store.ts +112 -79
  190. package/src/util.ts +5 -1
  191. package/tsconfig.json +1 -3
  192. package/src/backends/base.ts +0 -67
  193. package/src/backends/index.ts +0 -98
  194. package/src/backends/noop.ts +0 -32
  195. package/src/backends/web-in-memory.ts +0 -65
  196. package/src/backends/web.ts +0 -97
  197. /package/src/{backends → storage}/utils/idb.ts +0 -0
@@ -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) => {
@@ -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,7 +15,6 @@ 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'
17
19
  import type { Bindable } from '../util.js'
18
20
  import { sql } from '../util.js'
@@ -22,7 +24,8 @@ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInp
22
24
  export interface QueryDefinitions {
23
25
  [queryName: string]: LiveStoreQuery
24
26
  }
25
- 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]>> }
26
29
 
27
30
  export type ReactiveSQL = <TResult>(
28
31
  genQuery: (get: GetAtom) => string,
@@ -50,14 +53,18 @@ type GenQueries<TQueries, TStateResult> = (args: {
50
53
  rxGraphQL: ReactiveGraphQL
51
54
  globalQueries: QueryDefinitions
52
55
  state$: LiveStoreJSQuery<TStateResult>
53
- /** Registers a subscription */
56
+ /**
57
+ * Registers a subscription.
58
+ *
59
+ * Passed down for some manual subscribing. Use carefully.
60
+ */
54
61
  subscribe: RegisterSubscription
55
62
  isTemporaryQuery: boolean
56
63
  }) => TQueries
57
64
 
58
- export type UseLiveStoreComponentProps<TQueries, TComponentState> = {
59
- stateSchema?: ComponentStateSchema<TComponentState>
60
- 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>>
61
68
  reactDeps?: React.DependencyList
62
69
  componentKey: ComponentKeyConfig
63
70
  }
@@ -72,12 +79,17 @@ export type ComponentKeyConfig = {
72
79
  id: LiteralUnion<'singleton' | '__ephemeral__', string>
73
80
  }
74
81
 
75
- type ComponentState = {
76
- /** Equivalent to `componentKey.key` */
77
- id: string
78
- [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>
79
85
  }
80
86
 
87
+ // type ComponentState = {
88
+ // /** Equivalent to `componentKey.key` */
89
+ // id: string
90
+ // [key: string]: string | number | boolean | null
91
+ // }
92
+
81
93
  /**
82
94
  * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
83
95
  * so we need to "cache" the fact that we've already started a span for this component.
@@ -90,23 +102,33 @@ type UseLiveStoreJsonState<TState> = <TResult>(
90
102
  parse?: (_: unknown) => TResult,
91
103
  ) => [value: TResult, setValue: (newVal: TResult | ((prevVal: TResult) => TResult)) => void]
92
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
+
93
112
  /**
94
113
  * Create reactive queries within a component.
95
114
  * @param config.queries A function that returns a map of named reactive queries.
96
115
  * @param config.componentKey A function that returns a unique key for this component.
97
116
  * @param config.reactDeps A list of React-level dependencies that will refresh the queries.
98
117
  */
99
- export const useLiveStoreComponent = <TComponentState extends ComponentState, TQueries extends QueryDefinitions>({
118
+ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQueries extends QueryDefinitions>({
100
119
  stateSchema: stateSchema_,
101
120
  queries = () => ({}) as TQueries,
102
121
  componentKey: componentKeyConfig,
103
122
  reactDeps = [],
104
- }: UseLiveStoreComponentProps<TQueries, TComponentState>): {
123
+ }: UseLiveStoreComponentProps<TQueries, TColumns>): {
105
124
  queryResults: QueryResults<TQueries>
106
- state: TComponentState
107
- setState: Setters<TComponentState>
108
- useLiveStoreJsonState: UseLiveStoreJsonState<TComponentState>
125
+ state: SqliteDsl.FromColumns.RowDecoded<TColumns>
126
+ setState: Setters<SqliteDsl.FromColumns.RowDecoded<TColumns>>
127
+ useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TColumns>>
109
128
  } => {
129
+ type TComponentState = SqliteDsl.FromColumns.RowDecoded<TColumns>
130
+
131
+ // TODO validate schema to make sure each column has a default value
110
132
  // TODO we should clean up the state schema handling to remove this special handling for the `id` column
111
133
  const stateSchema = React.useMemo(
112
134
  () => (stateSchema_ ? { ...stateSchema_, columns: omit(stateSchema_.columns, 'id' as any) } : undefined),
@@ -159,7 +181,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
159
181
  }) =>
160
182
  queries({
161
183
  rxSQL: <T>(genQuery: (get: GetAtom) => string, queriedTables: string[], bindValues?: Bindable) =>
162
- store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext }),
184
+ store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext, componentKey }),
163
185
  rxGraphQL: <Result extends Record<string, any>, Variables extends Record<string, any>>(
164
186
  query: DocumentNode<Result, Variables>,
165
187
  genVariableValues: (get: GetAtom) => Variables,
@@ -185,11 +207,17 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
185
207
  stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
186
208
  ) as TComponentState
187
209
 
210
+ // @ts-expect-error TODO fix typing
188
211
  defaultState.id = componentKeyConfig.id
189
212
 
190
213
  return defaultState
191
214
  }, [componentKeyConfig.id, stateSchema])
192
215
 
216
+ const componentStateEffectSchema = React.useMemo(
217
+ () => (stateSchema ? SqliteDsl.structSchemaForTable(stateSchema) : Schema.any),
218
+ [stateSchema],
219
+ )
220
+
193
221
  // Step 1:
194
222
  // Synchronously create state and queries for initial render pass.
195
223
  // We do this in a temporary query context which cleans up after itself, making it idempotent
@@ -201,29 +229,34 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
201
229
  return store.inTempQueryContext(() => {
202
230
  try {
203
231
  // create state query
204
- let stateQuery: LiveStoreJSQuery<TComponentState>
232
+ let state$: LiveStoreJSQuery<TComponentState>
205
233
  if (stateSchema === undefined) {
206
234
  // TODO don't set up a query if there's no state schema (keeps the graph more clean)
207
- stateQuery = store.queryJS(() => ({}), {
235
+ state$ = store.queryJS(() => ({}), {
208
236
  componentKey,
209
237
  otelContext,
210
238
  }) as unknown as LiveStoreJSQuery<TComponentState>
211
239
  } else {
212
240
  const componentTableName = tableNameForComponentKey(componentKey)
213
241
  const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
214
- stateQuery = store
215
- .querySQL<TComponentState>(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
242
+ state$ = store
243
+ .querySQL(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
216
244
  queriedTables: [componentTableName],
217
245
  componentKey,
218
246
  label: `localState:query:${componentKeyLabel}`,
219
247
  otelContext,
220
248
  })
221
- .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
+ )
222
255
  }
223
- const initialComponentState = stateQuery.results$.result
256
+ const initialComponentState = state$.results$.result
224
257
 
225
258
  const queries = generateQueries({
226
- state$: stateQuery,
259
+ state$: state$,
227
260
  otelContext,
228
261
  registerSubscription: () => {},
229
262
  isTemporaryQuery: true,
@@ -231,7 +264,11 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
231
264
  for (const [name, query] of Object.entries(queries)) {
232
265
  query.label = name
233
266
  }
234
- 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>
235
272
 
236
273
  return { initialComponentState, initialQueryResults }
237
274
  } finally {
@@ -239,7 +276,16 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
239
276
  }
240
277
  })
241
278
  })
242
- }, [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
+ ])
243
289
 
244
290
  // Now that we've computed the initial state synchronously,
245
291
  // we can set up our useState calls w/ a default value populated...
@@ -251,10 +297,13 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
251
297
  stateSchema === undefined
252
298
  ? {}
253
299
  : // TODO: do we have a better type for the values that can go in SQLite?
254
- mapValues(stateSchema.columns, (_, columnName) => (value: string | number) => {
300
+ mapValues(stateSchema.columns, (column, columnName) => (value: string | number) => {
255
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
256
303
  if (componentStateRef.current[columnName] === value) return
257
304
 
305
+ const encodedValue = Schema.encodeSync(column.type.codec)(value)
306
+
258
307
  if (['componentKey', 'columnNames'].includes(columnName)) {
259
308
  shouldNeverHappen(`Can't use reserved column name ${columnName}`)
260
309
  }
@@ -262,7 +311,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
262
311
  return store.applyEvent('updateComponentState', {
263
312
  componentKey,
264
313
  columnNames: [columnName],
265
- [columnName]: value,
314
+ [columnName]: encodedValue,
266
315
  })
267
316
  })
268
317
  ) as Setters<TComponentState>
@@ -270,6 +319,7 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
270
319
  setState.setMany = (columnValues: Partial<TComponentState>) => {
271
320
  // TODO use hashing instead
272
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
273
323
  if (Object.entries(columnValues).every(([columnName, value]) => componentStateRef.current[columnName] === value)) {
274
324
  return
275
325
  }
@@ -293,7 +343,12 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
293
343
  // create state query
294
344
  let state$: LiveStoreJSQuery<TComponentState>
295
345
  if (stateSchema === undefined) {
296
- 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
+ })
297
352
  } else {
298
353
  const componentTableName = tableNameForComponentKey(componentKey)
299
354
  insertRowForComponentInstance({ store, componentKey, stateSchema })
@@ -306,7 +361,10 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
306
361
  label: `localState:query:${componentKeyLabel}`,
307
362
  otelContext,
308
363
  })
309
- .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
+ )
310
368
  }
311
369
 
312
370
  unsubs.push(
@@ -336,11 +394,11 @@ export const useLiveStoreComponent = <TComponentState extends ComponentState, TQ
336
394
  }
337
395
 
338
396
  const queries = generateQueries({ state$, otelContext, registerSubscription, isTemporaryQuery: false })
339
- // Use the name given to this query in the useQueries hook as its label
340
- for (const [name, query] of Object.entries(queries)) {
341
- query.label = name
342
- }
397
+
343
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
+
344
402
  unsubs.push(
345
403
  store.subscribe(
346
404
  query,
@@ -447,14 +505,14 @@ export const useComponentKey = ({ name, id }: ComponentKeyConfig, deps: Dependen
447
505
  * Create a row storing the state for a component instance, if none exists yet.
448
506
  * Initialized with default values, and keyed on the component key.
449
507
  */
450
- const insertRowForComponentInstance = <T>({
508
+ const insertRowForComponentInstance = ({
451
509
  store,
452
510
  componentKey,
453
511
  stateSchema,
454
512
  }: {
455
513
  store: Store<BaseGraphQLContext>
456
514
  componentKey: ComponentKey
457
- stateSchema: ComponentStateSchema<T>
515
+ stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
458
516
  }) => {
459
517
  const columnNames = ['id', ...Object.keys(stateSchema.columns)]
460
518
  const columnValues = columnNames.map((name) => `$${name}`).join(', ')
@@ -467,8 +525,8 @@ const insertRowForComponentInstance = <T>({
467
525
  void store.execute(
468
526
  insertQuery,
469
527
  {
470
- id: componentKey.id,
471
528
  ...mapValues(stateSchema.columns, (column) => prepareValueForSql(column.default ?? null)),
529
+ id: componentKey.id,
472
530
  },
473
531
  [tableName],
474
532
  )