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