@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.
- package/README.md +7 -7
- package/dist/.tsbuildinfo +1 -0
- package/dist/QueryCache.d.ts +20 -0
- package/dist/QueryCache.d.ts.map +1 -0
- package/dist/QueryCache.js +71 -0
- package/dist/QueryCache.js.map +1 -0
- package/dist/__tests__/react/fixture.d.ts +25 -0
- package/dist/__tests__/react/fixture.d.ts.map +1 -0
- package/dist/__tests__/react/fixture.js +61 -0
- package/dist/__tests__/react/fixture.js.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
- package/dist/__tests__/reactive.test.d.ts +2 -0
- package/dist/__tests__/reactive.test.d.ts.map +1 -0
- package/dist/__tests__/reactive.test.js +198 -0
- package/dist/__tests__/reactive.test.js.map +1 -0
- package/dist/backends/base.d.ts +13 -0
- package/dist/backends/base.d.ts.map +1 -0
- package/dist/backends/base.js +53 -0
- package/dist/backends/base.js.map +1 -0
- package/dist/backends/in-memory/index.d.ts +22 -0
- package/dist/backends/in-memory/index.d.ts.map +1 -0
- package/dist/backends/in-memory/index.js +45 -0
- package/dist/backends/in-memory/index.js.map +1 -0
- package/dist/backends/index.d.ts +41 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +16 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/tauri/index.d.ts +21 -0
- package/dist/backends/tauri/index.d.ts.map +1 -0
- package/dist/backends/tauri/index.js +48 -0
- package/dist/backends/tauri/index.js.map +1 -0
- package/dist/backends/utils/idb.d.ts +10 -0
- package/dist/backends/utils/idb.d.ts.map +1 -0
- package/dist/backends/utils/idb.js +58 -0
- package/dist/backends/utils/idb.js.map +1 -0
- package/dist/backends/web-worker/index.d.ts +26 -0
- package/dist/backends/web-worker/index.d.ts.map +1 -0
- package/dist/backends/web-worker/index.js +63 -0
- package/dist/backends/web-worker/index.js.map +1 -0
- package/dist/backends/web-worker/worker.d.ts +17 -0
- package/dist/backends/web-worker/worker.d.ts.map +1 -0
- package/dist/backends/web-worker/worker.js +139 -0
- package/dist/backends/web-worker/worker.js.map +1 -0
- package/dist/bounded-collections.d.ts +34 -0
- package/dist/bounded-collections.d.ts.map +1 -0
- package/dist/bounded-collections.js +103 -0
- package/dist/bounded-collections.js.map +1 -0
- package/dist/componentKey.d.ts +20 -0
- package/dist/componentKey.d.ts.map +1 -0
- package/dist/componentKey.js +3 -0
- package/dist/componentKey.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +42 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +37 -0
- package/dist/effect/LiveStore.js.map +1 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +2 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/inMemoryDatabase.d.ts +60 -0
- package/dist/inMemoryDatabase.d.ts.map +1 -0
- package/dist/inMemoryDatabase.js +230 -0
- package/dist/inMemoryDatabase.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +62 -0
- package/dist/migrations.js.map +1 -0
- package/dist/otel.d.ts +4 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +6 -0
- package/dist/otel.js.map +1 -0
- package/dist/react/LiveStoreContext.d.ts +11 -0
- package/dist/react/LiveStoreContext.d.ts.map +1 -0
- package/dist/react/LiveStoreContext.js +10 -0
- package/dist/react/LiveStoreContext.js.map +1 -0
- package/dist/react/LiveStoreProvider.d.ts +22 -0
- package/dist/react/LiveStoreProvider.d.ts.map +1 -0
- package/dist/react/LiveStoreProvider.js +49 -0
- package/dist/react/LiveStoreProvider.js.map +1 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/useGlobalQuery.d.ts +3 -0
- package/dist/react/useGlobalQuery.d.ts.map +1 -0
- package/dist/react/useGlobalQuery.js +23 -0
- package/dist/react/useGlobalQuery.js.map +1 -0
- package/dist/react/useGraphQL.d.ts +11 -0
- package/dist/react/useGraphQL.d.ts.map +1 -0
- package/dist/react/useGraphQL.js +67 -0
- package/dist/react/useGraphQL.js.map +1 -0
- package/dist/react/useLiveStoreComponent.d.ts +75 -0
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
- package/dist/react/useLiveStoreComponent.js +301 -0
- package/dist/react/useLiveStoreComponent.js.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
- package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
- package/dist/reactive.d.ts +140 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +302 -0
- package/dist/reactive.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +24 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +22 -0
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +25 -0
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/reactiveQueries/graphql.js +18 -0
- package/dist/reactiveQueries/graphql.js.map +1 -0
- package/dist/reactiveQueries/js.d.ts +19 -0
- package/dist/reactiveQueries/js.d.ts.map +1 -0
- package/dist/reactiveQueries/js.js +13 -0
- package/dist/reactiveQueries/js.js.map +1 -0
- package/dist/reactiveQueries/sql.d.ts +31 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.js +32 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/schema.d.ts +83 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +49 -0
- package/dist/schema.js.map +1 -0
- package/dist/storage/base.d.ts +10 -0
- package/dist/storage/base.d.ts.map +1 -0
- package/dist/storage/base.js +14 -0
- package/dist/storage/base.js.map +1 -0
- package/dist/storage/in-memory/index.d.ts +15 -0
- package/dist/storage/in-memory/index.d.ts.map +1 -0
- package/dist/storage/in-memory/index.js +14 -0
- package/dist/storage/in-memory/index.js.map +1 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/tauri/index.d.ts +19 -0
- package/dist/storage/tauri/index.d.ts.map +1 -0
- package/dist/storage/tauri/index.js +38 -0
- package/dist/storage/tauri/index.js.map +1 -0
- package/dist/storage/utils/idb.d.ts +10 -0
- package/dist/storage/utils/idb.d.ts.map +1 -0
- package/dist/storage/utils/idb.js +58 -0
- package/dist/storage/utils/idb.js.map +1 -0
- package/dist/storage/web-worker/index.d.ts +27 -0
- package/dist/storage/web-worker/index.d.ts.map +1 -0
- package/dist/storage/web-worker/index.js +76 -0
- package/dist/storage/web-worker/index.js.map +1 -0
- package/dist/storage/web-worker/worker.d.ts +13 -0
- package/dist/storage/web-worker/worker.d.ts.map +1 -0
- package/dist/storage/web-worker/worker.js +110 -0
- package/dist/storage/web-worker/worker.js.map +1 -0
- package/dist/store.d.ts +192 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +569 -0
- package/dist/store.js.map +1 -0
- package/dist/util.d.ts +26 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +53 -0
- package/dist/util.js.map +1 -0
- package/package.json +46 -19
- package/src/__tests__/react/fixture.tsx +19 -28
- package/src/effect/LiveStore.ts +8 -13
- package/src/events.ts +1 -1
- package/src/inMemoryDatabase.ts +100 -117
- package/src/index.ts +10 -16
- package/src/migrations.ts +101 -0
- package/src/otel.ts +0 -11
- package/src/react/LiveStoreProvider.tsx +12 -8
- package/src/react/index.ts +9 -0
- package/src/react/useGlobalQuery.ts +0 -3
- package/src/react/useLiveStoreComponent.ts +95 -37
- package/src/schema.ts +72 -145
- package/src/storage/in-memory/index.ts +21 -0
- package/src/storage/index.ts +27 -0
- package/src/{backends/tauri.ts → storage/tauri/index.ts} +13 -27
- package/src/storage/web-worker/index.ts +118 -0
- package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
- package/src/store.ts +112 -79
- package/src/util.ts +5 -1
- package/tsconfig.json +1 -3
- package/src/backends/base.ts +0 -67
- package/src/backends/index.ts +0 -98
- package/src/backends/noop.ts +0 -32
- package/src/backends/web-in-memory.ts +0 -65
- package/src/backends/web.ts +0 -97
- /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
|
-
|
|
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
|
-
|
|
20
|
-
boot?: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
94
|
+
}, [schema, loadStorage, globalQueryDefs, graphQLOptions, otelTracer, otelRootSpanContext, boot])
|
|
91
95
|
|
|
92
96
|
return ctxValue
|
|
93
97
|
}
|
package/src/react/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
/**
|
|
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,
|
|
59
|
-
stateSchema?:
|
|
60
|
-
queries?: GenQueries<TQueries,
|
|
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
|
-
|
|
76
|
-
|
|
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 = <
|
|
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,
|
|
123
|
+
}: UseLiveStoreComponentProps<TQueries, TColumns>): {
|
|
105
124
|
queryResults: QueryResults<TQueries>
|
|
106
|
-
state:
|
|
107
|
-
setState: Setters<
|
|
108
|
-
useLiveStoreJsonState: UseLiveStoreJsonState<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
.querySQL
|
|
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
|
-
|
|
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 =
|
|
256
|
+
const initialComponentState = state$.results$.result
|
|
224
257
|
|
|
225
258
|
const queries = generateQueries({
|
|
226
|
-
state$:
|
|
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(
|
|
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
|
-
}, [
|
|
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, (
|
|
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]:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
)
|