@livestore/livestore 0.0.25 → 0.0.27

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 (206) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts +1 -1
  3. package/dist/QueryCache.d.ts.map +1 -1
  4. package/dist/QueryCache.js +50 -60
  5. package/dist/QueryCache.js.map +1 -1
  6. package/dist/__tests__/react/fixture.d.ts +21 -6
  7. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  8. package/dist/__tests__/react/fixture.js +13 -14
  9. package/dist/__tests__/react/fixture.js.map +1 -1
  10. package/dist/__tests__/react/useQuery.test.js +5 -5
  11. package/dist/__tests__/react/useQuery.test.js.map +1 -1
  12. package/dist/__tests__/react/useRow.test.d.ts +2 -0
  13. package/dist/__tests__/react/useRow.test.d.ts.map +1 -0
  14. package/dist/__tests__/react/{useComponentState.test.js → useRow.test.js} +21 -26
  15. package/dist/__tests__/react/useRow.test.js.map +1 -0
  16. package/dist/__tests__/react/utils/stack-info.test.js +32 -0
  17. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -1
  18. package/dist/__tests__/reactiveQueries/sql.test.js +4 -4
  19. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
  20. package/dist/effect/LiveStore.d.ts +3 -3
  21. package/dist/effect/LiveStore.d.ts.map +1 -1
  22. package/dist/effect/LiveStore.js +1 -1
  23. package/dist/effect/LiveStore.js.map +1 -1
  24. package/dist/global-state.d.ts +19 -0
  25. package/dist/global-state.d.ts.map +1 -0
  26. package/dist/global-state.js +20 -0
  27. package/dist/global-state.js.map +1 -0
  28. package/dist/inMemoryDatabase.d.ts +3 -3
  29. package/dist/inMemoryDatabase.d.ts.map +1 -1
  30. package/dist/inMemoryDatabase.js +13 -7
  31. package/dist/inMemoryDatabase.js.map +1 -1
  32. package/dist/index.d.ts +5 -9
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -5
  35. package/dist/index.js.map +1 -1
  36. package/dist/migrations.d.ts +4 -4
  37. package/dist/migrations.d.ts.map +1 -1
  38. package/dist/migrations.js +34 -28
  39. package/dist/migrations.js.map +1 -1
  40. package/dist/react/LiveStoreContext.js.map +1 -1
  41. package/dist/react/LiveStoreProvider.d.ts +2 -2
  42. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  43. package/dist/react/LiveStoreProvider.js.map +1 -1
  44. package/dist/react/index.d.ts +1 -2
  45. package/dist/react/index.d.ts.map +1 -1
  46. package/dist/react/index.js +1 -1
  47. package/dist/react/index.js.map +1 -1
  48. package/dist/react/useQuery.d.ts +3 -0
  49. package/dist/react/useQuery.d.ts.map +1 -1
  50. package/dist/react/useQuery.js +5 -4
  51. package/dist/react/useQuery.js.map +1 -1
  52. package/dist/react/useRow.d.ts +33 -0
  53. package/dist/react/useRow.d.ts.map +1 -0
  54. package/dist/react/useRow.js +136 -0
  55. package/dist/react/useRow.js.map +1 -0
  56. package/dist/react/useTemporaryQuery.d.ts +2 -0
  57. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  58. package/dist/react/useTemporaryQuery.js +28 -11
  59. package/dist/react/useTemporaryQuery.js.map +1 -1
  60. package/dist/react/utils/stack-info.d.ts.map +1 -1
  61. package/dist/react/utils/stack-info.js +3 -2
  62. package/dist/react/utils/stack-info.js.map +1 -1
  63. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -1
  64. package/dist/reactive.d.ts +1 -1
  65. package/dist/reactive.d.ts.map +1 -1
  66. package/dist/reactive.js +47 -44
  67. package/dist/reactive.js.map +1 -1
  68. package/dist/reactiveQueries/base-class.d.ts +6 -2
  69. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  70. package/dist/reactiveQueries/base-class.js +10 -12
  71. package/dist/reactiveQueries/base-class.js.map +1 -1
  72. package/dist/reactiveQueries/graphql.d.ts +2 -2
  73. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  74. package/dist/reactiveQueries/graphql.js +56 -50
  75. package/dist/reactiveQueries/graphql.js.map +1 -1
  76. package/dist/reactiveQueries/js.d.ts +1 -2
  77. package/dist/reactiveQueries/js.d.ts.map +1 -1
  78. package/dist/reactiveQueries/js.js +25 -15
  79. package/dist/reactiveQueries/js.js.map +1 -1
  80. package/dist/reactiveQueries/sql.d.ts +3 -3
  81. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  82. package/dist/reactiveQueries/sql.js +39 -34
  83. package/dist/reactiveQueries/sql.js.map +1 -1
  84. package/dist/row-query.d.ts +21 -0
  85. package/dist/row-query.d.ts.map +1 -0
  86. package/dist/row-query.js +77 -0
  87. package/dist/row-query.js.map +1 -0
  88. package/dist/schema/action.d.ts +30 -0
  89. package/dist/schema/action.d.ts.map +1 -0
  90. package/dist/schema/action.js +3 -0
  91. package/dist/schema/action.js.map +1 -0
  92. package/dist/schema/index.d.ts +28 -0
  93. package/dist/schema/index.d.ts.map +1 -0
  94. package/dist/schema/index.js +26 -0
  95. package/dist/schema/index.js.map +1 -0
  96. package/dist/schema/system-tables.d.ts +24 -0
  97. package/dist/schema/system-tables.d.ts.map +1 -0
  98. package/dist/schema/system-tables.js +11 -0
  99. package/dist/schema/system-tables.js.map +1 -0
  100. package/dist/schema/table-def.d.ts +161 -0
  101. package/dist/schema/table-def.d.ts.map +1 -0
  102. package/dist/schema/table-def.js +53 -0
  103. package/dist/schema/table-def.js.map +1 -0
  104. package/dist/storage/in-memory/index.d.ts +1 -1
  105. package/dist/storage/in-memory/index.d.ts.map +1 -1
  106. package/dist/storage/in-memory/index.js +6 -7
  107. package/dist/storage/in-memory/index.js.map +1 -1
  108. package/dist/storage/index.d.ts +1 -1
  109. package/dist/storage/index.d.ts.map +1 -1
  110. package/dist/storage/tauri/index.d.ts +1 -1
  111. package/dist/storage/tauri/index.d.ts.map +1 -1
  112. package/dist/storage/tauri/index.js +25 -23
  113. package/dist/storage/tauri/index.js.map +1 -1
  114. package/dist/storage/utils/idb.js +3 -1
  115. package/dist/storage/utils/idb.js.map +1 -1
  116. package/dist/storage/web-worker/index.d.ts +1 -1
  117. package/dist/storage/web-worker/index.d.ts.map +1 -1
  118. package/dist/storage/web-worker/index.js +38 -34
  119. package/dist/storage/web-worker/index.js.map +1 -1
  120. package/dist/storage/web-worker/worker.d.ts +1 -1
  121. package/dist/storage/web-worker/worker.d.ts.map +1 -1
  122. package/dist/storage/web-worker/worker.js +1 -1
  123. package/dist/storage/web-worker/worker.js.map +1 -1
  124. package/dist/store.d.ts +9 -10
  125. package/dist/store.d.ts.map +1 -1
  126. package/dist/store.js +282 -265
  127. package/dist/store.js.map +1 -1
  128. package/dist/utils/bounded-collections.d.ts.map +1 -0
  129. package/dist/utils/bounded-collections.js +90 -0
  130. package/dist/utils/bounded-collections.js.map +1 -0
  131. package/dist/utils/otel.d.ts.map +1 -0
  132. package/dist/{otel.js → utils/otel.js} +1 -1
  133. package/dist/utils/otel.js.map +1 -0
  134. package/dist/utils/util.d.ts.map +1 -0
  135. package/dist/utils/util.js.map +1 -0
  136. package/package.json +12 -12
  137. package/src/QueryCache.ts +2 -2
  138. package/src/__tests__/react/fixture.tsx +15 -15
  139. package/src/__tests__/react/useQuery.test.tsx +5 -5
  140. package/src/__tests__/react/{useComponentState.test.tsx → useRow.test.tsx} +27 -28
  141. package/src/__tests__/react/utils/stack-info.test.ts +34 -0
  142. package/src/__tests__/reactiveQueries/sql.test.ts +4 -4
  143. package/src/effect/LiveStore.ts +6 -6
  144. package/src/global-state.ts +26 -0
  145. package/src/inMemoryDatabase.ts +6 -4
  146. package/src/index.ts +18 -17
  147. package/src/migrations.ts +41 -35
  148. package/src/react/LiveStoreProvider.tsx +2 -2
  149. package/src/react/index.ts +7 -9
  150. package/src/react/useQuery.ts +10 -4
  151. package/src/react/useRow.ts +221 -0
  152. package/src/react/useTemporaryQuery.ts +43 -11
  153. package/src/react/utils/stack-info.ts +4 -2
  154. package/src/reactive.ts +1 -1
  155. package/src/reactiveQueries/base-class.ts +8 -2
  156. package/src/reactiveQueries/graphql.ts +4 -3
  157. package/src/reactiveQueries/js.ts +3 -4
  158. package/src/reactiveQueries/sql.ts +6 -6
  159. package/src/row-query.ts +142 -0
  160. package/src/schema/action.ts +41 -0
  161. package/src/schema/index.ts +63 -0
  162. package/src/schema/system-tables.ts +21 -0
  163. package/src/schema/table-def.ts +199 -0
  164. package/src/storage/in-memory/index.ts +1 -1
  165. package/src/storage/index.ts +2 -1
  166. package/src/storage/tauri/index.ts +2 -2
  167. package/src/storage/web-worker/index.ts +1 -1
  168. package/src/storage/web-worker/worker.ts +2 -2
  169. package/src/store.ts +39 -27
  170. package/dist/__tests__/react/useComponentState.test.d.ts +0 -2
  171. package/dist/__tests__/react/useComponentState.test.d.ts.map +0 -1
  172. package/dist/__tests__/react/useComponentState.test.js.map +0 -1
  173. package/dist/bounded-collections.d.ts.map +0 -1
  174. package/dist/bounded-collections.js +0 -103
  175. package/dist/bounded-collections.js.map +0 -1
  176. package/dist/componentKey.d.ts +0 -20
  177. package/dist/componentKey.d.ts.map +0 -1
  178. package/dist/componentKey.js +0 -3
  179. package/dist/componentKey.js.map +0 -1
  180. package/dist/otel.d.ts.map +0 -1
  181. package/dist/otel.js.map +0 -1
  182. package/dist/react/useComponentState.d.ts +0 -50
  183. package/dist/react/useComponentState.d.ts.map +0 -1
  184. package/dist/react/useComponentState.js +0 -226
  185. package/dist/react/useComponentState.js.map +0 -1
  186. package/dist/reactiveQueries/graph.d.ts +0 -10
  187. package/dist/reactiveQueries/graph.d.ts.map +0 -1
  188. package/dist/reactiveQueries/graph.js +0 -6
  189. package/dist/reactiveQueries/graph.js.map +0 -1
  190. package/dist/schema.d.ts +0 -81
  191. package/dist/schema.d.ts.map +0 -1
  192. package/dist/schema.js +0 -46
  193. package/dist/schema.js.map +0 -1
  194. package/dist/util.d.ts.map +0 -1
  195. package/dist/util.js.map +0 -1
  196. package/src/componentKey.ts +0 -9
  197. package/src/react/useComponentState.ts +0 -385
  198. package/src/reactiveQueries/graph.ts +0 -15
  199. package/src/schema.ts +0 -143
  200. /package/dist/{bounded-collections.d.ts → utils/bounded-collections.d.ts} +0 -0
  201. /package/dist/{otel.d.ts → utils/otel.d.ts} +0 -0
  202. /package/dist/{util.d.ts → utils/util.d.ts} +0 -0
  203. /package/dist/{util.js → utils/util.js} +0 -0
  204. /package/src/{bounded-collections.ts → utils/bounded-collections.ts} +0 -0
  205. /package/src/{otel.ts → utils/otel.ts} +0 -0
  206. /package/src/{util.ts → utils/util.ts} +0 -0
package/src/index.ts CHANGED
@@ -3,33 +3,34 @@ export type { LiveStoreQuery, BaseGraphQLContext, QueryResult, QueryDebugInfo, R
3
3
 
4
4
  export type { QueryDefinition, LiveStoreCreateStoreOptions, LiveStoreContext } from './effect/LiveStore.js'
5
5
 
6
- export {
7
- defineComponentStateSchema,
8
- defineAction,
9
- defineActions,
10
- defineTables,
11
- defineMaterializedViews,
12
- makeSchema,
13
- } from './schema.js'
14
6
  export { InMemoryDatabase, type DebugInfo, emptyDebugInfo } from './inMemoryDatabase.js'
7
+
15
8
  export type { Storage, StorageType, StorageInit } from './storage/index.js'
9
+
16
10
  export type { GetAtom, AtomDebugInfo, RefreshDebugInfo, SerializedAtom, Atom } from './reactive.js'
17
11
  export { LiveStoreJSQuery, queryJS } from './reactiveQueries/js.js'
18
12
  export { LiveStoreSQLQuery, querySQL } from './reactiveQueries/sql.js'
19
13
  export { LiveStoreGraphQLQuery, queryGraphQL } from './reactiveQueries/graphql.js'
20
14
  export { type GetAtomResult } from './reactiveQueries/base-class.js'
21
- export { dbGraph } from './reactiveQueries/graph.js'
22
15
 
23
- export { labelForKey } from './componentKey.js'
24
- export type { ComponentKey } from './componentKey.js'
25
- export type { Schema, GetActionArgs, GetApplyEventArgs, Index, ActionDefinition, ActionDefinitions } from './schema.js'
16
+ export { dbGraph } from './global-state.js'
26
17
 
27
- export { SqliteAst, SqliteDsl } from 'effect-db-schema'
18
+ export { type RowResult, type RowResultEncoded, type RowQueryArgs, rowQuery } from './row-query.js'
28
19
 
29
- import type { SqliteAst } from 'effect-db-schema'
30
- export type TableDefinition = SqliteAst.Table
20
+ export { defineAction, defineActions, makeSchema, DbSchema } from './schema/index.js'
31
21
 
32
- export { SqliteDsl as DbSchema } from 'effect-db-schema'
22
+ export type {
23
+ LiveStoreSchema,
24
+ InputSchema,
25
+ GetActionArgs,
26
+ GetApplyEventArgs,
27
+ ActionDefinition,
28
+ ActionDefinitions,
29
+ SQLWriteStatement,
30
+ SchemaMetaRow,
31
+ } from './schema/index.js'
32
+
33
+ export { SqliteAst, SqliteDsl } from 'effect-db-schema'
33
34
 
34
- export { prepareBindValues, sql, type Bindable, type PreparedBindValues } from './util.js'
35
+ export { prepareBindValues, sql, type Bindable, type PreparedBindValues } from './utils/util.js'
35
36
  export { isEqual } from 'lodash-es'
package/src/migrations.ts CHANGED
@@ -1,12 +1,14 @@
1
+ import { Schema as EffectSchema } from '@livestore/utils/effect'
1
2
  import type * as otel from '@opentelemetry/api'
2
3
  import { SqliteAst } from 'effect-db-schema'
3
- import { memoize, omit } from 'lodash-es'
4
+ import { memoize } from 'lodash-es'
4
5
 
6
+ import { dynamicallyRegisteredTables } from './global-state.js'
5
7
  import type { InMemoryDatabase } from './index.js'
6
- import type { Schema, SchemaMetaRow } from './schema.js'
7
- import { componentStateTables, SCHEMA_META_TABLE, systemTables } from './schema.js'
8
- import type { PreparedBindValues } from './util.js'
9
- import { sql } from './util.js'
8
+ import type { LiveStoreSchema, SchemaMetaRow } from './schema/index.js'
9
+ import { SCHEMA_META_TABLE, systemTables } from './schema/index.js'
10
+ import type { PreparedBindValues } from './utils/util.js'
11
+ import { sql } from './utils/util.js'
10
12
 
11
13
  const getMemoizedTimestamp = memoize(() => new Date().toISOString())
12
14
 
@@ -18,7 +20,7 @@ export const migrateDb = ({
18
20
  }: {
19
21
  db: InMemoryDatabase
20
22
  otelContext: otel.Context
21
- schema: Schema
23
+ schema: LiveStoreSchema
22
24
  }) => {
23
25
  db.execute(
24
26
  // TODO use schema migration definition from schema.ts instead
@@ -34,16 +36,18 @@ export const migrateDb = ({
34
36
  schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
35
37
  )
36
38
 
37
- const tableDefs = {
39
+ const tableDefs = new Set([
38
40
  // NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
39
- [SCHEMA_META_TABLE]: systemTables[SCHEMA_META_TABLE],
40
- ...omit(schema.tables, [SCHEMA_META_TABLE]),
41
- ...componentStateTables,
42
- }
43
-
44
- for (const [tableName, tableDef] of Object.entries(tableDefs)) {
41
+ ...systemTables,
42
+ ...Array.from(schema.tables.values()).filter((_) => _.schema.name !== SCHEMA_META_TABLE),
43
+ ...dynamicallyRegisteredTables.values(),
44
+ ])
45
+
46
+ for (const tableDef of tableDefs) {
47
+ const tableAst = tableDef.schema.ast
48
+ const tableName = tableAst.name
45
49
  const dbSchemaHash = dbSchemaHashByTable[tableName]
46
- const schemaHash = SqliteAst.hash(tableDef)
50
+ const schemaHash = SqliteAst.hash(tableAst)
47
51
  if (schemaHash !== dbSchemaHash) {
48
52
  if (import.meta.env.VITE_LIVESTORE_SKIP_MIGRATIONS) {
49
53
  console.log(
@@ -54,7 +58,7 @@ export const migrateDb = ({
54
58
  `Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
55
59
  )
56
60
 
57
- migrateTable({ db, tableDef, otelContext, schemaHash })
61
+ migrateTable({ db, tableAst, otelContext, schemaHash })
58
62
  }
59
63
  }
60
64
  }
@@ -62,24 +66,24 @@ export const migrateDb = ({
62
66
 
63
67
  export const migrateTable = ({
64
68
  db,
65
- tableDef,
69
+ tableAst,
66
70
  otelContext,
67
71
  schemaHash,
68
72
  }: {
69
73
  db: InMemoryDatabase
70
- tableDef: SqliteAst.Table
74
+ tableAst: SqliteAst.Table
71
75
  otelContext: otel.Context
72
76
  schemaHash: number
73
77
  }) => {
74
- console.log(`Migrating table '${tableDef.name}'...`)
75
- const tableName = tableDef.name
76
- const columnSpec = makeColumnSpec(tableDef)
78
+ console.log(`Migrating table '${tableAst.name}'...`)
79
+ const tableName = tableAst.name
80
+ const columnSpec = makeColumnSpec(tableAst)
77
81
 
78
82
  // TODO need to possibly handle cascading deletes due to foreign keys
79
83
  db.execute(sql`drop table if exists ${tableName}`, undefined, [], { otelContext })
80
84
  db.execute(sql`create table if not exists ${tableName} (${columnSpec});`, undefined, [], { otelContext })
81
85
 
82
- for (const index of tableDef.indexes) {
86
+ for (const index of tableAst.indexes) {
83
87
  db.execute(createIndexFromDefinition(tableName, index), undefined, [], { otelContext })
84
88
  }
85
89
 
@@ -100,9 +104,9 @@ const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) =>
100
104
  return sql`create ${uniqueStr} index ${index.name} on ${tableName} (${index.columns.join(', ')})`
101
105
  }
102
106
 
103
- const makeColumnSpec = (tableDef: SqliteAst.Table) => {
104
- const primaryKeys = tableDef.columns.filter((_) => _.primaryKey).map((_) => _.name)
105
- const columnDefStrs = tableDef.columns.map(toSqliteColumnSpec)
107
+ const makeColumnSpec = (tableAst: SqliteAst.Table) => {
108
+ const primaryKeys = tableAst.columns.filter((_) => _.primaryKey).map((_) => _.name)
109
+ const columnDefStrs = tableAst.columns.map(toSqliteColumnSpec)
106
110
  if (primaryKeys.length > 0) {
107
111
  columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
108
112
  }
@@ -110,16 +114,18 @@ const makeColumnSpec = (tableDef: SqliteAst.Table) => {
110
114
  return columnDefStrs.join(', ')
111
115
  }
112
116
 
117
+ /** NOTE primary keys are applied on a table level not on a column level to account for multi-column primary keys */
113
118
  const toSqliteColumnSpec = (column: SqliteAst.Column) => {
114
- const columnType = column.type._tag
115
- // const primaryKey = column.primaryKey ? 'primary key' : ''
116
- const nullable = column.nullable === false ? 'not null' : ''
117
- const defaultValue =
118
- column.default === undefined
119
- ? ''
120
- : columnType === 'text'
121
- ? `default '${column.default}'`
122
- : `default ${column.default}`
123
-
124
- return `${column.name} ${columnType} ${nullable} ${defaultValue}`
119
+ const columnTypeStr = column.type._tag
120
+ const nullableStr = column.nullable === false ? 'not null' : ''
121
+ const defaultValueStr = (() => {
122
+ if (column.default === undefined) return ''
123
+
124
+ const encodeValue = EffectSchema.encodeSync(column.codec)
125
+ const encodedDefaultValue = encodeValue(column.default ?? null)
126
+
127
+ return columnTypeStr === 'text' ? `default '${encodedDefaultValue}'` : `default ${encodedDefaultValue}`
128
+ })()
129
+
130
+ return `${column.name} ${columnTypeStr} ${nullableStr} ${defaultValueStr}`
125
131
  }
@@ -6,7 +6,7 @@ import initSqlite3Wasm from 'sqlite-esm'
6
6
  // TODO refactor so the `react` module doesn't depend on `effect` module
7
7
  import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
8
8
  import type { InMemoryDatabase } from '../inMemoryDatabase.js'
9
- import type { Schema } from '../schema.js'
9
+ import type { LiveStoreSchema } from '../schema/index.js'
10
10
  import type { StorageInit } from '../storage/index.js'
11
11
  import type { BaseGraphQLContext, GraphQLOptions } from '../store.js'
12
12
  import { createStore } from '../store.js'
@@ -20,7 +20,7 @@ const sqlite3Promise = initSqlite3Wasm({
20
20
  })
21
21
 
22
22
  interface LiveStoreProviderProps<GraphQLContext> {
23
- schema: Schema
23
+ schema: LiveStoreSchema
24
24
  loadStorage: () => StorageInit | Promise<StorageInit>
25
25
  boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
26
26
  graphQLOptions?: GraphQLOptions<GraphQLContext>
@@ -1,17 +1,15 @@
1
- export type {
2
- Setters,
3
- ComponentKeyConfig,
4
- QueryDefinitions,
5
- ComponentColumns,
6
- GetStateType,
7
- GetStateTypeEncoded,
8
- } from './useComponentState.js'
9
1
  export { LiveStoreContext, useStore } from './LiveStoreContext.js'
10
2
  export { LiveStoreProvider } from './LiveStoreProvider.js'
11
- export { useComponentState } from './useComponentState.js'
12
3
  export { useQuery } from './useQuery.js'
13
4
  export { useTemporaryQuery } from './useTemporaryQuery.js'
14
5
  export { useStackInfo } from './utils/stack-info.js'
6
+ export {
7
+ useRow,
8
+ type StateSetters,
9
+ type SetStateAction,
10
+ type Dispatch,
11
+ type UseRowResult as UseStateResult,
12
+ } from './useRow.js'
15
13
 
16
14
  // Needed to make TS happy
17
15
  export type { TypedDocumentNode } from '@graphql-typed-document-node/core'
@@ -6,6 +6,7 @@ import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
6
6
  import { useStore } from './LiveStoreContext.js'
7
7
  import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
8
8
  import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
9
+
9
10
  /**
10
11
  * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
11
12
  * so we need to "cache" the fact that we've already started a span for this component.
@@ -13,7 +14,12 @@ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInp
13
14
  */
14
15
  const spanAlreadyStartedCache = new Map<ILiveStoreQuery<any>, { span: otel.Span; otelContext: otel.Context }>()
15
16
 
16
- export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
17
+ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => useQueryRef(query).current
18
+
19
+ export const useQueryRef = <TResult>(
20
+ query: ILiveStoreQuery<TResult>,
21
+ parentOtelContext?: otel.Context,
22
+ ): React.MutableRefObject<TResult> => {
17
23
  const { store } = useStore()
18
24
 
19
25
  const stackInfo = React.useMemo(() => {
@@ -32,7 +38,7 @@ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
32
38
  const span = store.otel.tracer.startSpan(
33
39
  `LiveStore:useQuery:${query.label}`,
34
40
  { attributes: { label: query.label, stackInfo: JSON.stringify(stackInfo) } },
35
- store.otel.queriesSpanContext,
41
+ parentOtelContext ?? store.otel.queriesSpanContext,
36
42
  )
37
43
 
38
44
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -40,7 +46,7 @@ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
40
46
  spanAlreadyStartedCache.set(query, { span, otelContext })
41
47
 
42
48
  return { span, otelContext }
43
- }, [query, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
49
+ }, [parentOtelContext, query, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
44
50
 
45
51
  const initialResult = React.useMemo(
46
52
  () =>
@@ -86,5 +92,5 @@ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
86
92
  }
87
93
  }, [stackInfo, query, setValue, store, valueRef, otelContext, span])
88
94
 
89
- return valueRef.current
95
+ return valueRef
90
96
  }
@@ -0,0 +1,221 @@
1
+ import { Schema } from '@livestore/utils/effect'
2
+ import * as otel from '@opentelemetry/api'
3
+ import type { SqliteDsl } from 'effect-db-schema'
4
+ import { mapValues } from 'lodash-es'
5
+ import React from 'react'
6
+
7
+ import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
8
+ import type { RowQueryArgs, RowResult } from '../row-query.js'
9
+ import { rowQuery } from '../row-query.js'
10
+ import type { DefaultSqliteTableDef, TableDef, TableOptions } from '../schema/table-def.js'
11
+ import { useStore } from './LiveStoreContext.js'
12
+ import { useQueryRef } from './useQuery.js'
13
+
14
+ export type UseRowResult<TTableDef extends TableDef> = [
15
+ row: RowResult<TTableDef>,
16
+ setRow: StateSetters<TTableDef>,
17
+ query$: LiveStoreJSQuery<RowResult<TTableDef>>,
18
+ ]
19
+
20
+ /**
21
+ * Similar to `React.useState` but returns a tuple of `[row, setRow, query$]` for a given table where ...
22
+ *
23
+ * - `row` is the current value of the row (fully decoded according to the table schema)
24
+ * - `setRow` is a function that can be used to update the row (values will be encoded according to the table schema)
25
+ * - `query$` is a `LiveStoreJSQuery` that e.g. can be used to subscribe to changes to the row
26
+ *
27
+ * If the table is a singleton table, `useRow` can be called without an `id` argument. Otherwise, the `id` argument is required.
28
+ */
29
+ export const useRow: {
30
+ <TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: true }>>(
31
+ table: TTableDef,
32
+ ): UseRowResult<TTableDef>
33
+ <TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: false }>>(
34
+ table: TTableDef,
35
+ // TODO adjust so it works with arbitrary primary keys or unique constraints
36
+ id: string,
37
+ ): UseRowResult<TTableDef>
38
+ } = <TTableDef extends TableDef>(table: TTableDef, id?: string): UseRowResult<TTableDef> => {
39
+ const sqliteTableDef = table.schema
40
+ type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['schema']['columns']>
41
+
42
+ const { store } = useStore()
43
+
44
+ const reactId = React.useId()
45
+
46
+ const { query$, otelContext } = React.useMemo(() => {
47
+ const cachedItem = rcCache.get(table, id ?? 'singleton')
48
+ if (cachedItem !== undefined) {
49
+ cachedItem.reactIds.add(reactId)
50
+ cachedItem.span.addEvent('new-subscriber', { reactId })
51
+
52
+ return {
53
+ query$: cachedItem.query$ as LiveStoreJSQuery<RowResult<TTableDef>>,
54
+ otelContext: cachedItem.otelContext,
55
+ }
56
+ }
57
+
58
+ const span = store.otel.tracer.startSpan(
59
+ `LiveStore:useState:${table.schema.name}${id === undefined ? '' : `:${id}`}`,
60
+ { attributes: { id } },
61
+ store.otel.queriesSpanContext,
62
+ )
63
+
64
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
65
+
66
+ const query$ = table.options.isSingleton
67
+ ? rowQuery({ table, store, otelContext } as RowQueryArgs<TTableDef>)
68
+ : rowQuery({ table, store, id, otelContext } as RowQueryArgs<TTableDef>)
69
+
70
+ rcCache.set(table, id ?? 'singleton', query$, reactId, otelContext, span)
71
+
72
+ return { query$, otelContext }
73
+ }, [table, id, reactId, store])
74
+
75
+ React.useEffect(
76
+ () => () => {
77
+ const cachedItem = rcCache.get(table, id ?? 'singleton')!
78
+
79
+ cachedItem.reactIds.delete(reactId)
80
+ if (cachedItem.reactIds.size === 0) {
81
+ rcCache.delete(cachedItem.query$)
82
+ cachedItem.query$.destroy()
83
+ cachedItem.span.end()
84
+ }
85
+ },
86
+ [table, id, reactId],
87
+ )
88
+
89
+ const query$Ref = useQueryRef(query$, otelContext)
90
+
91
+ const setState = React.useMemo<StateSetters<TTableDef>>(() => {
92
+ if (table.isSingleColumn) {
93
+ return (newValue: RowResult<TTableDef>) => {
94
+ if (query$Ref.current === newValue) return
95
+
96
+ const encodedValue = Schema.encodeSync(sqliteTableDef.columns['value']!.type.codec)(newValue)
97
+
98
+ store.applyEvent('livestore.UpdateComponentState', {
99
+ tableName: sqliteTableDef.name,
100
+ columnNames: ['value'],
101
+ id,
102
+ bindValues: { ['value']: encodedValue },
103
+ })
104
+ }
105
+ } else {
106
+ const setState = // TODO: do we have a better type for the values that can go in SQLite?
107
+ mapValues(sqliteTableDef.columns, (column, columnName) => (newValue: string | number) => {
108
+ // Don't update the state if it's the same as the value already seen in the component
109
+ // @ts-expect-error TODO fix typing
110
+ if (query$Ref.current[columnName] === newValue) return
111
+
112
+ const encodedValue = Schema.encodeSync(column.type.codec)(newValue)
113
+
114
+ store.applyEvent('livestore.UpdateComponentState', {
115
+ tableName: sqliteTableDef.name,
116
+ columnNames: [columnName],
117
+ id,
118
+ bindValues: { [columnName]: encodedValue },
119
+ })
120
+ })
121
+
122
+ // @ts-expect-error TODO fix typing
123
+ setState.setMany = (columnValues: Partial<TComponentState>) => {
124
+ // TODO use hashing instead
125
+ // Don't update the state if it's the same as the value already seen in the component
126
+ if (
127
+ // @ts-expect-error TODO fix typing
128
+ Object.entries(columnValues).every(([columnName, value]) => query$Ref.current[columnName] === value)
129
+ ) {
130
+ return
131
+ }
132
+
133
+ const columnNames = Object.keys(columnValues)
134
+ const bindValues = mapValues(columnValues, (value, columnName) =>
135
+ Schema.encodeSync(sqliteTableDef.columns[columnName]!.type.codec)(value),
136
+ )
137
+
138
+ store.applyEvent('livestore.UpdateComponentState', {
139
+ tableName: sqliteTableDef.name,
140
+ columnNames,
141
+ id,
142
+ bindValues,
143
+ })
144
+ }
145
+
146
+ return setState as any
147
+ }
148
+ }, [table.isSingleColumn, id, sqliteTableDef.columns, sqliteTableDef.name, store, query$Ref])
149
+
150
+ return [query$Ref.current, setState, query$]
151
+ }
152
+
153
+ export type Dispatch<A> = (action: A) => void
154
+ export type SetStateAction<S> = S | ((previousValue: S) => S)
155
+
156
+ export type StateSetters<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
157
+ ? Dispatch<SetStateAction<RowResult<TTableDef>>>
158
+ : {
159
+ [K in keyof RowResult<TTableDef>]: Dispatch<SetStateAction<RowResult<TTableDef>[K]>>
160
+ } & {
161
+ setMany: Dispatch<SetStateAction<Partial<RowResult<TTableDef>>>>
162
+ }
163
+
164
+ /** Reference counted cache for `query$` and otel context */
165
+ class RCCache {
166
+ private readonly cache = new Map<
167
+ TableDef,
168
+ Map<
169
+ string,
170
+ {
171
+ reactIds: Set<string>
172
+ span: otel.Span
173
+ otelContext: otel.Context
174
+ query$: LiveStoreJSQuery<any>
175
+ }
176
+ >
177
+ >()
178
+ private reverseCache = new Map<LiveStoreJSQuery<any>, [TableDef, string]>()
179
+
180
+ get = (table: TableDef, id: string) => {
181
+ const queries = this.cache.get(table)
182
+ if (queries === undefined) return undefined
183
+ return queries.get(id)
184
+ }
185
+
186
+ set = (
187
+ table: TableDef,
188
+ id: string,
189
+ query$: LiveStoreJSQuery<any>,
190
+ reactId: string,
191
+ otelContext: otel.Context,
192
+ span: otel.Span,
193
+ ) => {
194
+ let queries = this.cache.get(table)
195
+ if (queries === undefined) {
196
+ queries = new Map()
197
+ this.cache.set(table, queries)
198
+ }
199
+ queries.set(id, { query$, otelContext, span, reactIds: new Set([reactId]) })
200
+ this.reverseCache.set(query$, [table, id])
201
+ }
202
+
203
+ delete = (query$: LiveStoreJSQuery<any>) => {
204
+ const item = this.reverseCache.get(query$)
205
+ if (item === undefined) return
206
+
207
+ const [table, id] = item
208
+ const queries = this.cache.get(table)
209
+ if (queries === undefined) return
210
+
211
+ queries.delete(id)
212
+
213
+ if (queries.size === 0) {
214
+ this.cache.delete(table)
215
+ }
216
+
217
+ this.reverseCache.delete(query$)
218
+ }
219
+ }
220
+
221
+ const rcCache = new RCCache()
@@ -1,23 +1,55 @@
1
1
  import React from 'react'
2
2
 
3
3
  import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
4
- import { useQuery } from './useQuery.js'
4
+ import { useQueryRef } from './useQuery.js'
5
+
6
+ /**
7
+ * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷.
8
+ * The map entry is being removed again in the `React.useEffect` call below.
9
+ */
10
+ const queryCache = new Map<() => ILiveStoreQuery<any>, { reactIds: Set<string>; query$: ILiveStoreQuery<any> }>()
5
11
 
6
12
  /**
7
13
  * Creates a query, subscribes and destroys it when the component unmounts.
8
14
  *
9
15
  * Make sure `makeQuery` is a memoized function.
10
16
  */
11
- export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult => {
12
- // TODO cache the query outside of the `useMemo` since `useMemo` might be called multiple times
13
- // also need to update the `useEffect` below https://stackoverflow.com/questions/66446642/react-usememo-memory-clean/77457605#77457605
14
- const query = React.useMemo(() => makeQuery(), [makeQuery])
15
-
16
- React.useEffect(() => {
17
- return () => {
18
- query.destroy()
17
+ export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult =>
18
+ useTemporaryQueryRef(makeQuery).current
19
+
20
+ export const useTemporaryQueryRef = <TResult>(
21
+ makeQuery: () => ILiveStoreQuery<TResult>,
22
+ ): React.MutableRefObject<TResult> => {
23
+ const reactId = React.useId()
24
+
25
+ const query$ = React.useMemo(() => {
26
+ const cachedItem = queryCache.get(makeQuery)
27
+ if (cachedItem !== undefined) {
28
+ cachedItem.reactIds.add(reactId)
29
+
30
+ return cachedItem.query$
19
31
  }
20
- }, [query])
21
32
 
22
- return useQuery(query)
33
+ const query$ = makeQuery()
34
+
35
+ queryCache.set(makeQuery, { reactIds: new Set([reactId]), query$ })
36
+
37
+ return query$
38
+ }, [reactId, makeQuery])
39
+
40
+ React.useEffect(
41
+ () => () => {
42
+ const cachedItem = queryCache.get(makeQuery)!
43
+
44
+ cachedItem.reactIds.delete(reactId)
45
+
46
+ if (cachedItem.reactIds.size === 0) {
47
+ cachedItem.query$.destroy()
48
+ queryCache.delete(makeQuery)
49
+ }
50
+ },
51
+ [makeQuery, reactId],
52
+ )
53
+
54
+ return useQueryRef(query$)
23
55
  }
@@ -38,10 +38,12 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
38
38
 
39
39
  while ((match = namePattern.exec(stackTrace)) !== null) {
40
40
  const [, name, filePath] = match as any as [string, string, string]
41
- if (name.startsWith('use')) {
41
+
42
+ // NOTE No idea where this `Module.` comes from - possibly a Vite thing?
43
+ if ((name.startsWith('use') || name.startsWith('Module.use')) && name.endsWith('QueryRef') === false) {
42
44
  hasReachedStart = true
43
45
 
44
- frames.unshift({ name, filePath })
46
+ frames.unshift({ name: name.replace(/^Module\./, ''), filePath })
45
47
  } else if (hasReachedStart) {
46
48
  // We've reached the end of the `use*` functions, so we're adding the component name and stop
47
49
  frames.unshift({ name, filePath })
package/src/reactive.ts CHANGED
@@ -28,7 +28,7 @@ import { pick } from '@livestore/utils'
28
28
  import type * as otel from '@opentelemetry/api'
29
29
  import { isEqual, uniqueId } from 'lodash-es'
30
30
 
31
- import { BoundArray } from './bounded-collections.js'
31
+ import { BoundArray } from './utils/bounded-collections.js'
32
32
  // import { getDurationMsFromSpan } from './otel.js'
33
33
 
34
34
  export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
@@ -1,11 +1,17 @@
1
1
  import type * as otel from '@opentelemetry/api'
2
2
 
3
+ import { dbGraph } from '../global-state.js'
3
4
  import type { StackInfo } from '../react/utils/stack-info.js'
4
5
  import { type Atom, type GetAtom, throwContextNotSetError, type Thunk } from '../reactive.js'
5
- import type { RefreshReason } from '../store.js'
6
- import { type DbContext, dbGraph } from './graph.js'
6
+ import type { RefreshReason, Store } from '../store.js'
7
7
  import type { LiveStoreJSQuery } from './js.js'
8
8
 
9
+ export type DbContext = {
10
+ store: Store
11
+ otelTracer: otel.Tracer
12
+ rootOtelContext: otel.Context
13
+ }
14
+
9
15
  export type UnsubscribeQuery = () => void
10
16
 
11
17
  let queryIdCounter = 0
@@ -3,11 +3,12 @@ import { assertNever, shouldNeverHappen } from '@livestore/utils'
3
3
  import * as otel from '@opentelemetry/api'
4
4
  import * as graphql from 'graphql'
5
5
 
6
- import { getDurationMsFromSpan } from '../otel.js'
6
+ import { dbGraph } from '../global-state.js'
7
7
  import type { Thunk } from '../reactive.js'
8
8
  import type { BaseGraphQLContext, RefreshReason, Store } from '../store.js'
9
- import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
10
- import { type DbContext, dbGraph } from './graph.js'
9
+ import { getDurationMsFromSpan } from '../utils/otel.js'
10
+ import type { DbContext, GetAtomResult } from './base-class.js'
11
+ import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
11
12
  import { LiveStoreJSQuery } from './js.js'
12
13
 
13
14
  export const queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
@@ -1,11 +1,10 @@
1
1
  import * as otel from '@opentelemetry/api'
2
2
 
3
- import { getDurationMsFromSpan } from '../otel.js'
3
+ import { dbGraph } from '../global-state.js'
4
4
  import type { Thunk } from '../reactive.js'
5
5
  import type { RefreshReason } from '../store.js'
6
- import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
7
- import type { DbContext } from './graph.js'
8
- import { dbGraph } from './graph.js'
6
+ import { getDurationMsFromSpan } from '../utils/otel.js'
7
+ import { type DbContext, type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
9
8
 
10
9
  export const queryJS = <TResult>(fn: (get: GetAtomResult) => TResult, options: { label: string }) =>
11
10
  new LiveStoreJSQuery<TResult>({ fn, label: options.label })