@livestore/livestore 0.0.13 → 0.0.15

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 (147) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  3. package/dist/__tests__/react/fixture.js +8 -9
  4. package/dist/__tests__/react/fixture.js.map +1 -1
  5. package/dist/__tests__/reactive.test.js +3 -4
  6. package/dist/__tests__/reactive.test.js.map +1 -1
  7. package/dist/effect/LiveStore.d.ts +3 -9
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +11 -7
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/inMemoryDatabase.d.ts +15 -19
  12. package/dist/inMemoryDatabase.d.ts.map +1 -1
  13. package/dist/inMemoryDatabase.js +2 -9
  14. package/dist/inMemoryDatabase.js.map +1 -1
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/migrations.d.ts +7 -0
  20. package/dist/migrations.d.ts.map +1 -1
  21. package/dist/migrations.js +18 -13
  22. package/dist/migrations.js.map +1 -1
  23. package/dist/react/LiveStoreProvider.d.ts +1 -3
  24. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  25. package/dist/react/LiveStoreProvider.js +13 -10
  26. package/dist/react/LiveStoreProvider.js.map +1 -1
  27. package/dist/react/index.d.ts +1 -1
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/react/index.js +1 -1
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/react/useGraphQL.d.ts +3 -1
  32. package/dist/react/useGraphQL.d.ts.map +1 -1
  33. package/dist/react/useGraphQL.js +19 -1
  34. package/dist/react/useGraphQL.js.map +1 -1
  35. package/dist/react/useLiveStoreComponent.d.ts +12 -12
  36. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  37. package/dist/react/useLiveStoreComponent.js +23 -7
  38. package/dist/react/useLiveStoreComponent.js.map +1 -1
  39. package/dist/react/useQuery.d.ts +3 -0
  40. package/dist/react/useQuery.d.ts.map +1 -0
  41. package/dist/react/useQuery.js +38 -0
  42. package/dist/react/useQuery.js.map +1 -0
  43. package/dist/reactive.d.ts.map +1 -1
  44. package/dist/reactive.js +3 -3
  45. package/dist/reactive.js.map +1 -1
  46. package/dist/reactiveQueries/base-class.d.ts +6 -3
  47. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  48. package/dist/reactiveQueries/base-class.js +1 -0
  49. package/dist/reactiveQueries/base-class.js.map +1 -1
  50. package/dist/reactiveQueries/graphql.d.ts +4 -4
  51. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  52. package/dist/reactiveQueries/graphql.js.map +1 -1
  53. package/dist/reactiveQueries/js.d.ts +2 -2
  54. package/dist/reactiveQueries/js.d.ts.map +1 -1
  55. package/dist/reactiveQueries/js.js.map +1 -1
  56. package/dist/reactiveQueries/sql.d.ts +5 -5
  57. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  58. package/dist/reactiveQueries/sql.js +2 -2
  59. package/dist/reactiveQueries/sql.js.map +1 -1
  60. package/dist/schema.d.ts +0 -2
  61. package/dist/schema.d.ts.map +1 -1
  62. package/dist/schema.js +3 -6
  63. package/dist/schema.js.map +1 -1
  64. package/dist/storage/in-memory/index.d.ts +2 -2
  65. package/dist/storage/in-memory/index.d.ts.map +1 -1
  66. package/dist/storage/in-memory/index.js.map +1 -1
  67. package/dist/storage/index.d.ts +2 -2
  68. package/dist/storage/index.d.ts.map +1 -1
  69. package/dist/storage/tauri/index.d.ts +2 -2
  70. package/dist/storage/tauri/index.d.ts.map +1 -1
  71. package/dist/storage/tauri/index.js.map +1 -1
  72. package/dist/storage/web-worker/index.d.ts +4 -4
  73. package/dist/storage/web-worker/index.d.ts.map +1 -1
  74. package/dist/storage/web-worker/index.js +3 -5
  75. package/dist/storage/web-worker/index.js.map +1 -1
  76. package/dist/storage/web-worker/worker.js +2 -2
  77. package/dist/storage/web-worker/worker.js.map +1 -1
  78. package/dist/store.d.ts +14 -7
  79. package/dist/store.d.ts.map +1 -1
  80. package/dist/store.js +80 -46
  81. package/dist/store.js.map +1 -1
  82. package/dist/util.d.ts +3 -1
  83. package/dist/util.d.ts.map +1 -1
  84. package/dist/util.js +2 -0
  85. package/dist/util.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/__tests__/react/fixture.tsx +9 -9
  88. package/src/__tests__/reactive.test.ts +3 -4
  89. package/src/effect/LiveStore.ts +14 -18
  90. package/src/inMemoryDatabase.ts +20 -28
  91. package/src/index.ts +10 -4
  92. package/src/migrations.ts +39 -21
  93. package/src/react/LiveStoreProvider.tsx +13 -16
  94. package/src/react/index.ts +1 -1
  95. package/src/react/useGraphQL.ts +28 -2
  96. package/src/react/useLiveStoreComponent.ts +50 -24
  97. package/src/react/useQuery.ts +56 -0
  98. package/src/reactive.ts +6 -4
  99. package/src/reactiveQueries/base-class.ts +9 -3
  100. package/src/reactiveQueries/graphql.ts +4 -4
  101. package/src/reactiveQueries/js.ts +2 -2
  102. package/src/reactiveQueries/sql.ts +6 -6
  103. package/src/schema.ts +2 -5
  104. package/src/storage/in-memory/index.ts +2 -2
  105. package/src/storage/index.ts +2 -2
  106. package/src/storage/tauri/index.ts +2 -2
  107. package/src/storage/web-worker/index.ts +6 -8
  108. package/src/storage/web-worker/worker.ts +2 -2
  109. package/src/store.ts +99 -59
  110. package/src/util.ts +8 -2
  111. package/dist/backends/base.d.ts +0 -13
  112. package/dist/backends/base.d.ts.map +0 -1
  113. package/dist/backends/base.js +0 -53
  114. package/dist/backends/base.js.map +0 -1
  115. package/dist/backends/in-memory/index.d.ts +0 -22
  116. package/dist/backends/in-memory/index.d.ts.map +0 -1
  117. package/dist/backends/in-memory/index.js +0 -45
  118. package/dist/backends/in-memory/index.js.map +0 -1
  119. package/dist/backends/index.d.ts +0 -41
  120. package/dist/backends/index.d.ts.map +0 -1
  121. package/dist/backends/index.js +0 -16
  122. package/dist/backends/index.js.map +0 -1
  123. package/dist/backends/tauri/index.d.ts +0 -21
  124. package/dist/backends/tauri/index.d.ts.map +0 -1
  125. package/dist/backends/tauri/index.js +0 -48
  126. package/dist/backends/tauri/index.js.map +0 -1
  127. package/dist/backends/utils/idb.d.ts +0 -10
  128. package/dist/backends/utils/idb.d.ts.map +0 -1
  129. package/dist/backends/utils/idb.js +0 -58
  130. package/dist/backends/utils/idb.js.map +0 -1
  131. package/dist/backends/web-worker/index.d.ts +0 -26
  132. package/dist/backends/web-worker/index.d.ts.map +0 -1
  133. package/dist/backends/web-worker/index.js +0 -63
  134. package/dist/backends/web-worker/index.js.map +0 -1
  135. package/dist/backends/web-worker/worker.d.ts +0 -17
  136. package/dist/backends/web-worker/worker.d.ts.map +0 -1
  137. package/dist/backends/web-worker/worker.js +0 -139
  138. package/dist/backends/web-worker/worker.js.map +0 -1
  139. package/dist/react/useGlobalQuery.d.ts +0 -3
  140. package/dist/react/useGlobalQuery.d.ts.map +0 -1
  141. package/dist/react/useGlobalQuery.js +0 -23
  142. package/dist/react/useGlobalQuery.js.map +0 -1
  143. package/dist/storage/base.d.ts +0 -10
  144. package/dist/storage/base.d.ts.map +0 -1
  145. package/dist/storage/base.js +0 -14
  146. package/dist/storage/base.js.map +0 -1
  147. package/src/react/useGlobalQuery.ts +0 -37
@@ -2,28 +2,16 @@
2
2
 
3
3
  import { shouldNeverHappen } from '@livestore/utils'
4
4
  import type * as otel from '@opentelemetry/api'
5
- import type * as SqliteWasm from 'sqlite-esm'
5
+ import type * as Sqlite from 'sqlite-esm'
6
6
 
7
7
  import BoundMap, { BoundArray } from './bounded-collections.js'
8
8
  // import { EVENTS_TABLE_NAME } from './events.js'
9
9
  import { sql } from './index.js'
10
10
  import { getDurationMsFromSpan, getStartTimeHighResFromSpan } from './otel.js'
11
11
  import QueryCache from './QueryCache.js'
12
- import type { Bindable, ParamsObject } from './util.js'
13
- import { prepareBindValues } from './util.js'
12
+ import type { Bindable, PreparedBindValues } from './util.js'
14
13
 
15
- export enum IndexType {
16
- Basic = 'Basic',
17
- FullText = 'FullText',
18
- }
19
-
20
- export interface Index {
21
- indexType: IndexType
22
- name: string
23
- columns: string[]
24
- }
25
-
26
- declare type DatabaseWithCAPI = SqliteWasm.Database & { capi: SqliteWasm.CAPI }
14
+ type DatabaseWithCAPI = Sqlite.Database & { capi: Sqlite.CAPI }
27
15
 
28
16
  export interface DebugInfo {
29
17
  slowQueries: BoundArray<SlowQueryInfo>
@@ -34,7 +22,7 @@ export interface DebugInfo {
34
22
 
35
23
  export type SlowQueryInfo = [
36
24
  queryStr: string,
37
- bindValues: Bindable | undefined,
25
+ bindValues: PreparedBindValues | undefined,
38
26
  durationMs: number,
39
27
  rowsCount: number | undefined,
40
28
  queriedTables: string[],
@@ -50,7 +38,7 @@ export const emptyDebugInfo = (): DebugInfo => ({
50
38
 
51
39
  export class InMemoryDatabase {
52
40
  // TODO: how many unique active statements are expected?
53
- private cachedStmts = new BoundMap<string, SqliteWasm.PreparedStatement>(200)
41
+ private cachedStmts = new BoundMap<string, Sqlite.PreparedStatement>(200)
54
42
  private tablesUsedCache = new BoundMap<string, string[]>(200)
55
43
  private resultCache = new QueryCache()
56
44
  public debugInfo: DebugInfo = emptyDebugInfo()
@@ -59,15 +47,20 @@ export class InMemoryDatabase {
59
47
  private db: DatabaseWithCAPI,
60
48
  private otelTracer: otel.Tracer,
61
49
  private otelRootSpanContext: otel.Context,
62
- public SQL: SqliteWasm.Sqlite3Static,
50
+ public SQL: Sqlite.Sqlite3Static,
63
51
  ) {}
64
52
 
65
- static load(
66
- data: Uint8Array | undefined,
67
- otelTracer: otel.Tracer,
68
- otelRootSpanContext: otel.Context,
69
- sqlite3: SqliteWasm.Sqlite3Static,
70
- ): InMemoryDatabase {
53
+ static load({
54
+ data,
55
+ otelTracer,
56
+ otelRootSpanContext,
57
+ sqlite3,
58
+ }: {
59
+ data: Uint8Array | undefined
60
+ otelTracer: otel.Tracer
61
+ otelRootSpanContext: otel.Context
62
+ sqlite3: Sqlite.Sqlite3Static
63
+ }): InMemoryDatabase {
71
64
  // TODO move WASM init higher up in the init process (to do some other work while it's loading)
72
65
 
73
66
  const db = new sqlite3.oo1.DB({ filename: ':memory:', flags: 'c' }) as DatabaseWithCAPI
@@ -138,7 +131,7 @@ export class InMemoryDatabase {
138
131
 
139
132
  execute(
140
133
  query: string,
141
- bindValues?: ParamsObject,
134
+ bindValues?: PreparedBindValues,
142
135
  writeTables?: string[],
143
136
  options?: { hasNoEffects?: boolean; otelContext: otel.Context },
144
137
  ): { durationMs: number } {
@@ -155,9 +148,8 @@ export class InMemoryDatabase {
155
148
  this.cachedStmts.set(query, stmt)
156
149
  }
157
150
 
158
- // TODO check whether we can remove the extra `prepareBindValues` call here (e.g. enforce proper type in API)
159
151
  if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
160
- stmt.bind(prepareBindValues(bindValues, query))
152
+ stmt.bind(bindValues)
161
153
  }
162
154
 
163
155
  if (import.meta.env.DEV) {
@@ -211,7 +203,7 @@ export class InMemoryDatabase {
211
203
  query: string,
212
204
  options?: {
213
205
  queriedTables?: string[]
214
- bindValues?: Bindable
206
+ bindValues?: PreparedBindValues
215
207
  skipCache?: boolean
216
208
  otelContext?: otel.Context
217
209
  },
package/src/index.ts CHANGED
@@ -1,11 +1,17 @@
1
- export { Store, createStore, RESET_DB_LOCAL_STORAGE_KEY } from './store.js'
2
- export type { LiveStoreQuery, BaseGraphQLContext, QueryResult, QueryDebugInfo, RefreshReason } from './store.js'
1
+ export { Store, createStore } from './store.js'
2
+ export type {
3
+ LiveStoreQuery,
4
+ GetAtomResult,
5
+ BaseGraphQLContext,
6
+ QueryResult,
7
+ QueryDebugInfo,
8
+ RefreshReason,
9
+ } from './store.js'
3
10
 
4
11
  export type { QueryDefinition, LiveStoreCreateStoreOptions, LiveStoreContext } from './effect/LiveStore.js'
5
12
 
6
13
  export {
7
14
  defineComponentStateSchema,
8
- EVENT_CURSOR_TABLE,
9
15
  defineAction,
10
16
  defineActions,
11
17
  defineTables,
@@ -37,5 +43,5 @@ export type TableDefinition = SqliteAst.Table
37
43
 
38
44
  export { SqliteDsl as DbSchema } from 'effect-db-schema'
39
45
 
40
- export { sql, type Bindable } from './util.js'
46
+ export { prepareBindValues, sql, type Bindable, type PreparedBindValues } from './util.js'
41
47
  export { isEqual } from 'lodash-es'
package/src/migrations.ts CHANGED
@@ -5,8 +5,11 @@ import { memoize, omit } from 'lodash-es'
5
5
  import type { InMemoryDatabase } from './index.js'
6
6
  import type { Schema, SchemaMetaRow } from './schema.js'
7
7
  import { componentStateTables, SCHEMA_META_TABLE, systemTables } from './schema.js'
8
+ import type { PreparedBindValues } from './util.js'
8
9
  import { sql } from './util.js'
9
10
 
11
+ const getMemoizedTimestamp = memoize(() => new Date().toISOString())
12
+
10
13
  // TODO more graceful DB migration (e.g. backup DB before destructive migrations)
11
14
  export const migrateDb = ({
12
15
  db,
@@ -31,7 +34,6 @@ export const migrateDb = ({
31
34
  schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
32
35
  )
33
36
 
34
- const getMemoizedTimestamp = memoize(() => new Date().toISOString())
35
37
  const tableDefs = {
36
38
  // NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
37
39
  [SCHEMA_META_TABLE]: systemTables[SCHEMA_META_TABLE],
@@ -47,30 +49,46 @@ export const migrateDb = ({
47
49
  `Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
48
50
  )
49
51
 
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
- )
52
+ migrateTable({ db, tableDef, otelContext, schemaHash })
70
53
  }
71
54
  }
72
55
  }
73
56
 
57
+ export const migrateTable = ({
58
+ db,
59
+ tableDef,
60
+ otelContext,
61
+ schemaHash,
62
+ }: {
63
+ db: InMemoryDatabase
64
+ tableDef: SqliteAst.Table
65
+ otelContext: otel.Context
66
+ schemaHash: number
67
+ }) => {
68
+ console.log(`Migrating table '${tableDef.name}'...`)
69
+ const tableName = tableDef.name
70
+ const columnSpec = makeColumnSpec(tableDef)
71
+
72
+ // TODO need to possibly handle cascading deletes due to foreign keys
73
+ db.execute(sql`drop table if exists ${tableName}`, undefined, [], { otelContext })
74
+ db.execute(sql`create table if not exists ${tableName} (${columnSpec});`, undefined, [], { otelContext })
75
+
76
+ for (const index of tableDef.indexes) {
77
+ db.execute(createIndexFromDefinition(tableName, index), undefined, [], { otelContext })
78
+ }
79
+
80
+ const updatedAt = getMemoizedTimestamp()
81
+ db.execute(
82
+ sql`
83
+ INSERT INTO ${SCHEMA_META_TABLE} (tableName, schemaHash, updatedAt) VALUES ($tableName, $schemaHash, $updatedAt)
84
+ ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
85
+ `,
86
+ { $tableName: tableName, $schemaHash: schemaHash, $updatedAt: updatedAt } as unknown as PreparedBindValues,
87
+ [],
88
+ { otelContext },
89
+ )
90
+ }
91
+
74
92
  const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
75
93
  const uniqueStr = index.unique ? 'UNIQUE' : ''
76
94
  return sql`create ${uniqueStr} index ${index.name} on ${tableName} (${index.columns.join(', ')})`
@@ -1,14 +1,10 @@
1
1
  import type * as otel from '@opentelemetry/api'
2
- import { mapValues } from 'lodash-es'
3
2
  import type { ReactElement, ReactNode } from 'react'
4
3
  import React from 'react'
4
+ import initSqlite3Wasm from 'sqlite-esm'
5
5
 
6
6
  // TODO refactor so the `react` module doesn't depend on `effect` module
7
- import type {
8
- GlobalQueryDefs,
9
- LiveStoreContext as StoreContext_,
10
- LiveStoreCreateStoreOptions,
11
- } from '../effect/LiveStore.js'
7
+ import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
12
8
  import type { InMemoryDatabase } from '../inMemoryDatabase.js'
13
9
  import type { Schema } from '../schema.js'
14
10
  import type { StorageInit } from '../storage/index.js'
@@ -16,11 +12,17 @@ import type { BaseGraphQLContext, GraphQLOptions } from '../store.js'
16
12
  import { createStore } from '../store.js'
17
13
  import { LiveStoreContext } from './LiveStoreContext.js'
18
14
 
15
+ // NOTE we're starting to initialize the sqlite wasm binary here (already before calling `createStore`),
16
+ // so that it's ready when we need it
17
+ const sqlite3Promise = initSqlite3Wasm({
18
+ print: (message) => console.log(`[livestore sqlite] ${message}`),
19
+ printErr: (message) => console.error(`[livestore sqlite] ${message}`),
20
+ })
21
+
19
22
  interface LiveStoreProviderProps<GraphQLContext> {
20
23
  schema: Schema
21
24
  loadStorage: () => StorageInit | Promise<StorageInit>
22
25
  boot?: (db: InMemoryDatabase, parentSpan: otel.Span) => unknown | Promise<unknown>
23
- globalQueryDefs: GlobalQueryDefs
24
26
  graphQLOptions?: GraphQLOptions<GraphQLContext>
25
27
  otelTracer?: otel.Tracer
26
28
  otelRootSpanContext?: otel.Context
@@ -29,7 +31,6 @@ interface LiveStoreProviderProps<GraphQLContext> {
29
31
 
30
32
  export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
31
33
  fallback,
32
- globalQueryDefs,
33
34
  loadStorage,
34
35
  graphQLOptions,
35
36
  otelTracer,
@@ -40,7 +41,6 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
40
41
  }: LiveStoreProviderProps<GraphQLContext> & { children?: ReactNode }): JSX.Element => {
41
42
  const store = useCreateStore({
42
43
  schema,
43
- globalQueryDefs,
44
44
  loadStorage,
45
45
  graphQLOptions,
46
46
  otelTracer,
@@ -59,7 +59,6 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
59
59
 
60
60
  const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
61
61
  schema,
62
- globalQueryDefs,
63
62
  loadStorage,
64
63
  graphQLOptions,
65
64
  otelTracer,
@@ -71,6 +70,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
71
70
  React.useEffect(() => {
72
71
  void (async () => {
73
72
  try {
73
+ const sqlite3 = await sqlite3Promise
74
74
  const store = await createStore({
75
75
  schema,
76
76
  loadStorage,
@@ -78,12 +78,9 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
78
78
  otelTracer,
79
79
  otelRootSpanContext,
80
80
  boot,
81
+ sqlite3,
81
82
  })
82
- store.otel.tracer.startActiveSpan('LiveStore:makeGlobalQueries', {}, store.otel.queriesSpanContext, (span) => {
83
- const globalQueries = mapValues(globalQueryDefs, (queryDef) => queryDef(store))
84
- setCtxValue({ store, globalQueries })
85
- span.end()
86
- })
83
+ setCtxValue({ store })
87
84
  } catch (e) {
88
85
  console.error(`Error creating LiveStore store:`, e)
89
86
  throw e
@@ -91,7 +88,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
91
88
  })()
92
89
 
93
90
  // TODO: do we need to return any cleanup function here?
94
- }, [schema, loadStorage, globalQueryDefs, graphQLOptions, otelTracer, otelRootSpanContext, boot])
91
+ }, [schema, loadStorage, graphQLOptions, otelTracer, otelRootSpanContext, boot])
95
92
 
96
93
  return ctxValue
97
94
  }
@@ -14,7 +14,7 @@ export { LiveStoreContext, useStore } from './LiveStoreContext.js'
14
14
  export { LiveStoreProvider } from './LiveStoreProvider.js'
15
15
  export { useLiveStoreComponent } from './useLiveStoreComponent.js'
16
16
  export { useGraphQL } from './useGraphQL.js'
17
- export { useGlobalQuery } from './useGlobalQuery.js'
17
+ export { useQuery } from './useQuery.js'
18
18
 
19
19
  // Needed to make TS happy
20
20
  export type { TypedDocumentNode } from '@graphql-typed-document-node/core'
@@ -15,6 +15,12 @@ export type UseLiveStoreComponentProps<TResult extends Record<string, any>, TVar
15
15
  reactDeps?: React.DependencyList
16
16
  }
17
17
 
18
+ type Variables = Record<string, any>
19
+
20
+ // TODO get rid of the query cache in favour of the new side-effect-free query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
21
+ // NOTE we're using a nested map here since we need to resolve 2 levels of object identities (query + variables)
22
+ // const queryCache = new Map<DocumentNode<any, any>, Map<Variables, LiveStoreGraphQLQuery<any, any, any>>>()
23
+
18
24
  /**
19
25
  * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
20
26
  * so we need to "cache" the fact that we've already started a span for this component.
@@ -24,7 +30,7 @@ const spanAlreadyStartedCache = new Map<string, { span: otel.Span; otelContext:
24
30
 
25
31
  // TODO 1) figure out a way to make `variables` optional if the query doesn't have any variables (probably requires positional args)
26
32
  // TODO 2) allow `.pipe` on the resulting query (possibly as a separate optional prop)
27
- export const useGraphQL = <TResult extends Record<string, any>, TVariables extends Record<string, any> = {}>({
33
+ export const useGraphQL = <TResult extends Record<string, any>, TVariables extends Variables = {}>({
28
34
  query,
29
35
  variables,
30
36
  componentKey: componentKeyConfig,
@@ -62,7 +68,27 @@ export const useGraphQL = <TResult extends Record<string, any>, TVariables exten
62
68
  )
63
69
 
64
70
  const makeLiveStoreQuery = React.useCallback(
65
- () => store.queryGraphQL(query, () => variables ?? ({} as TVariables), { componentKey, otelContext }),
71
+ () => {
72
+ return store.queryGraphQL(query, () => variables ?? ({} as TVariables), { componentKey, otelContext })
73
+
74
+ // NOTE I had to disable the caching below as still led to many problems
75
+ // We should just implement the new query definition approach instead
76
+
77
+ // const queryCacheForQuery = queryCache.get(query)
78
+ // if (queryCacheForQuery && queryCacheForQuery.has(variables)) {
79
+ // return queryCacheForQuery.get(variables)!
80
+ // }
81
+
82
+ // const newQuery = store.queryGraphQL(query, () => variables ?? ({} as TVariables), { componentKey, otelContext })
83
+
84
+ // if (queryCacheForQuery) {
85
+ // queryCacheForQuery.set(variables, newQuery)
86
+ // } else {
87
+ // queryCache.set(query, new Map([[variables, newQuery]]))
88
+ // }
89
+
90
+ // return newQuery
91
+ },
66
92
  // NOTE: we don't include the queries function passed in by the user here;
67
93
  // the reason is that we don't want to force them to memoize that function.
68
94
  // Instead, we just assume that the function always has the same contents.
@@ -3,7 +3,7 @@ import type { LiteralUnion, PrettifyFlat } from '@livestore/utils'
3
3
  import { omit, shouldNeverHappen } from '@livestore/utils'
4
4
  import { Schema } from '@livestore/utils/effect'
5
5
  import * as otel from '@opentelemetry/api'
6
- import { SqliteDsl } from 'effect-db-schema'
6
+ import { SqliteAst, SqliteDsl } from 'effect-db-schema'
7
7
  import { isEqual, mapValues } from 'lodash-es'
8
8
  import type { DependencyList } from 'react'
9
9
  import React from 'react'
@@ -11,11 +11,12 @@ import { v4 as uuid } from 'uuid'
11
11
 
12
12
  import type { ComponentKey } from '../componentKey.js'
13
13
  import { labelForKey, tableNameForComponentKey } from '../componentKey.js'
14
- import type { GetAtom } from '../reactive.js'
14
+ import { migrateTable } from '../migrations.js'
15
15
  import type { LiveStoreGraphQLQuery } from '../reactiveQueries/graphql.js'
16
16
  import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
17
17
  import type { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
18
- import type { BaseGraphQLContext, LiveStoreQuery, QueryResult, Store } from '../store.js'
18
+ import { SCHEMA_META_TABLE } from '../schema.js'
19
+ import type { BaseGraphQLContext, GetAtomResult, LiveStoreQuery, QueryResult, Store } from '../store.js'
19
20
  import type { Bindable } from '../util.js'
20
21
  import { sql } from '../util.js'
21
22
  import { useStore } from './LiveStoreContext.js'
@@ -28,17 +29,20 @@ export interface QueryDefinitions {
28
29
  export type QueryResults<TQuery> = { [queryName in keyof TQuery]: PrettifyFlat<QueryResult<TQuery[queryName]>> }
29
30
 
30
31
  export type ReactiveSQL = <TResult>(
31
- genQuery: (get: GetAtom) => string,
32
+ query: string | ((get: GetAtomResult) => string),
32
33
  queriedTables: string[],
33
34
  bindValues?: Bindable | undefined,
34
35
  ) => LiveStoreSQLQuery<TResult>
36
+
37
+ export type ReactiveJS = <TResult>(query: (get: GetAtomResult) => TResult) => LiveStoreJSQuery<TResult>
38
+
35
39
  export type ReactiveGraphQL = <
36
40
  TResult extends Record<string, any>,
37
41
  TVariables extends Record<string, any>,
38
42
  TContext extends BaseGraphQLContext,
39
43
  >(
40
44
  query: DocumentNode<TResult, TVariables>,
41
- genVariableValues: (get: GetAtom) => TVariables,
45
+ variableValues: TVariables | ((get: GetAtomResult) => TVariables),
42
46
  label?: string,
43
47
  ) => LiveStoreGraphQLQuery<TResult, TVariables, TContext>
44
48
 
@@ -51,7 +55,7 @@ type RegisterSubscription = <TQuery extends LiveStoreQuery>(
51
55
  type GenQueries<TQueries, TStateResult> = (args: {
52
56
  rxSQL: ReactiveSQL
53
57
  rxGraphQL: ReactiveGraphQL
54
- globalQueries: QueryDefinitions
58
+ rxJS: ReactiveJS
55
59
  state$: LiveStoreJSQuery<TStateResult>
56
60
  /**
57
61
  * Registers a subscription.
@@ -62,9 +66,9 @@ type GenQueries<TQueries, TStateResult> = (args: {
62
66
  isTemporaryQuery: boolean
63
67
  }) => TQueries
64
68
 
65
- export type UseLiveStoreComponentProps<TQueries, TColumns extends ComponentColumns> = {
66
- stateSchema?: SqliteDsl.TableDefinition<string, TColumns>
67
- queries?: GenQueries<TQueries, SqliteDsl.FromColumns.RowDecoded<TColumns>>
69
+ export type UseLiveStoreComponentProps<TQueries, TStateColumns extends ComponentColumns> = {
70
+ stateSchema?: SqliteDsl.TableDefinition<string, TStateColumns>
71
+ queries?: GenQueries<TQueries, SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
68
72
  reactDeps?: React.DependencyList
69
73
  componentKey: ComponentKeyConfig
70
74
  }
@@ -115,18 +119,18 @@ export type GetStateTypeEncoded<TTableDef extends SqliteDsl.TableDefinition<any,
115
119
  * @param config.componentKey A function that returns a unique key for this component.
116
120
  * @param config.reactDeps A list of React-level dependencies that will refresh the queries.
117
121
  */
118
- export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQueries extends QueryDefinitions>({
122
+ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQueries extends QueryDefinitions>({
119
123
  stateSchema: stateSchema_,
120
124
  queries = () => ({}) as TQueries,
121
125
  componentKey: componentKeyConfig,
122
126
  reactDeps = [],
123
- }: UseLiveStoreComponentProps<TQueries, TColumns>): {
127
+ }: UseLiveStoreComponentProps<TQueries, TStateColumns>): {
124
128
  queryResults: QueryResults<TQueries>
125
- state: SqliteDsl.FromColumns.RowDecoded<TColumns>
126
- setState: Setters<SqliteDsl.FromColumns.RowDecoded<TColumns>>
127
- useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TColumns>>
129
+ state: SqliteDsl.FromColumns.RowDecoded<TStateColumns>
130
+ setState: Setters<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
131
+ useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
128
132
  } => {
129
- type TComponentState = SqliteDsl.FromColumns.RowDecoded<TColumns>
133
+ type TComponentState = SqliteDsl.FromColumns.RowDecoded<TStateColumns>
130
134
 
131
135
  // TODO validate schema to make sure each column has a default value
132
136
  // TODO we should clean up the state schema handling to remove this special handling for the `id` column
@@ -137,7 +141,7 @@ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQuerie
137
141
 
138
142
  // performance.mark('useLiveStoreComponent:start')
139
143
  const componentKey = useComponentKey(componentKeyConfig, reactDeps)
140
- const { store, globalQueries } = useStore()
144
+ const { store } = useStore()
141
145
 
142
146
  const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
143
147
 
@@ -180,14 +184,17 @@ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQuerie
180
184
  isTemporaryQuery: boolean
181
185
  }) =>
182
186
  queries({
183
- rxSQL: <T>(genQuery: (get: GetAtom) => string, queriedTables: string[], bindValues?: Bindable) =>
184
- store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext, componentKey }),
187
+ rxSQL: <T>(
188
+ genQuery: string | ((get: GetAtomResult) => string),
189
+ queriedTables: string[],
190
+ bindValues?: Bindable,
191
+ ) => store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext, componentKey }),
185
192
  rxGraphQL: <Result extends Record<string, any>, Variables extends Record<string, any>>(
186
193
  query: DocumentNode<Result, Variables>,
187
- genVariableValues: (get: GetAtom) => Variables,
194
+ genVariableValues: Variables | ((get: GetAtomResult) => Variables),
188
195
  label?: string,
189
196
  ) => store.queryGraphQL(query, genVariableValues, { componentKey, label, otelContext }),
190
- globalQueries,
197
+ rxJS: <T>(genQuery: (get: GetAtomResult) => T) => store.queryJS(genQuery, { componentKey, otelContext }),
191
198
  state$,
192
199
  subscribe: registerSubscription,
193
200
  isTemporaryQuery,
@@ -199,7 +206,7 @@ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQuerie
199
206
  // This makes sense for LiveStore because the component config should be static.
200
207
  // TODO: document this and consider whether it's the right API surface.
201
208
  // eslint-disable-next-line react-hooks/exhaustive-deps
202
- [store, componentKey, globalQueries],
209
+ [store, componentKey],
203
210
  )
204
211
 
205
212
  const defaultComponentState = React.useMemo(() => {
@@ -226,6 +233,7 @@ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQuerie
226
233
  return store.otel.tracer.startActiveSpan('LiveStore:useLiveStoreComponent:initial', {}, otelContext, (span) => {
227
234
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
228
235
 
236
+ // NOTE `inTempQueryContext` automatically destroys the queries once the callback is done
229
237
  return store.inTempQueryContext(() => {
230
238
  try {
231
239
  // create state query
@@ -239,6 +247,24 @@ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQuerie
239
247
  } else {
240
248
  const componentTableName = tableNameForComponentKey(componentKey)
241
249
  const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
250
+
251
+ // TODO find a better solution for this
252
+ if (store.tableRefs[componentTableName] === undefined) {
253
+ const schemaHash = SqliteAst.hash(stateSchema.ast)
254
+ const res = store.inMemoryDB.select<{ schemaHash: number }>(
255
+ sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
256
+ )
257
+ if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
258
+ migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
259
+ }
260
+
261
+ store.tableRefs[componentTableName] = store.graph.makeRef(null, {
262
+ equal: () => false,
263
+ label: componentTableName,
264
+ meta: { liveStoreRefType: 'table' },
265
+ })
266
+ }
267
+
242
268
  state$ = store
243
269
  .querySQL(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
244
270
  queriedTables: [componentTableName],
@@ -380,15 +406,15 @@ export const useLiveStoreComponent = <TColumns extends ComponentColumns, TQuerie
380
406
  ),
381
407
  )
382
408
 
383
- const registerSubscription: RegisterSubscription = (query, callback, onUnsubscribe) => {
409
+ const registerSubscription: RegisterSubscription = (query$, callback, onUnsubscribe) => {
384
410
  unsubs.push(
385
411
  store.subscribe(
386
- query,
412
+ query$,
387
413
  (results) => {
388
414
  callback(results)
389
415
  },
390
416
  onUnsubscribe,
391
- { label: `useLiveStoreComponent:query:manual-subscribe:${query.label}` },
417
+ { label: `useLiveStoreComponent:query:manual-subscribe:${query$.label}` },
392
418
  ),
393
419
  )
394
420
  }
@@ -0,0 +1,56 @@
1
+ import React from 'react'
2
+
3
+ import { labelForKey } from '../componentKey.js'
4
+ import type { QueryDefinition } from '../effect/LiveStore.js'
5
+ import type { LiveStoreQuery, QueryResult, Store } from '../store.js'
6
+ import { useStore } from './LiveStoreContext.js'
7
+
8
+ // TODO get rid of the query cache in favour of the new side-effect-free query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
9
+ const queryCache = new Map<QueryDefinition, LiveStoreQuery>()
10
+
11
+ export const useQuery = <Q extends LiveStoreQuery>(queryDef: (store: Store) => Q): QueryResult<Q> => {
12
+ const { store } = useStore()
13
+ const query = React.useMemo(() => {
14
+ if (queryCache.has(queryDef)) return queryCache.get(queryDef) as Q
15
+
16
+ const query = queryDef(store)
17
+ queryCache.set(queryDef, query)
18
+ return query
19
+ }, [store, queryDef])
20
+
21
+ // We know the query has a result by the time we use it; so we can synchronously populate a default state
22
+ const [value, setValue] = React.useState<QueryResult<Q>>(query.results$.result)
23
+
24
+ // Subscribe to future updates for this query
25
+ React.useEffect(() => {
26
+ return store.otel.tracer.startActiveSpan(
27
+ `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
28
+ { attributes: { label: query.label } },
29
+ query.otelContext,
30
+ (span) => {
31
+ const cancel = store.subscribe(
32
+ query,
33
+ (v) => {
34
+ // NOTE: we return a reference to the result object within LiveStore;
35
+ // this implies that app code must not mutate the results, or else
36
+ // there may be weird reactivity bugs.
37
+ return setValue(v)
38
+ },
39
+ undefined,
40
+ { label: query.label },
41
+ )
42
+ return () => {
43
+ // // NOTE destroying the whole query will also unsubscribe it
44
+ // query.destroy()
45
+
46
+ // TODO for now we'll still `cancel` manually, but we should remove this once we have some kind of
47
+ // ARC-based system
48
+ cancel()
49
+ span.end()
50
+ }
51
+ },
52
+ )
53
+ }, [query, store])
54
+
55
+ return value
56
+ }
package/src/reactive.ts CHANGED
@@ -30,6 +30,9 @@ import { isEqual, max, uniqueId } from 'lodash-es'
30
30
 
31
31
  import { BoundArray } from './bounded-collections.js'
32
32
 
33
+ const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
34
+ type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
35
+
33
36
  export type GetAtom = <T>(atom: Atom<T>) => T
34
37
 
35
38
  export type Ref<T> = {
@@ -59,7 +62,7 @@ type BaseThunk<T> = {
59
62
  equal: (a: T, b: T) => boolean
60
63
  }
61
64
 
62
- type UnevaluatedThunk<T> = BaseThunk<T> & { result: undefined }
65
+ type UnevaluatedThunk<T> = BaseThunk<T> & { result: NOT_REFRESHED_YET }
63
66
  export type Thunk<T> = BaseThunk<T> & { result: T }
64
67
 
65
68
  export type Atom<T> = Ref<T> | Thunk<T>
@@ -201,7 +204,7 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
201
204
  const thunk: UnevaluatedThunk<T> = {
202
205
  _tag: 'thunk',
203
206
  id: uniqueNodeId(),
204
- result: undefined,
207
+ result: NOT_REFRESHED_YET,
205
208
  height: 0,
206
209
  getResult,
207
210
  sub: new Set(),
@@ -342,7 +345,7 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
342
345
  this.addEdge(context, atom)
343
346
 
344
347
  const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
345
- const dependencyNotRefreshedYet = atom.result === undefined
348
+ const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
346
349
 
347
350
  if (dependencyMightBeStale || dependencyNotRefreshedYet) {
348
351
  throw new DependencyNotReadyError(
@@ -350,7 +353,6 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
350
353
  )
351
354
  }
352
355
 
353
- // TODO handle case when `atom.result` is undefined
354
356
  return atom.result
355
357
  }
356
358