@livestore/livestore 0.0.55-dev.2 → 0.0.55
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/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +2 -5
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +1 -2
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts +11 -1
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +45 -29
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/LiveStoreProvider.test.js +3 -2
- package/dist/react/LiveStoreProvider.test.js.map +1 -1
- package/dist/react/useQuery.test.js +11 -4
- package/dist/react/useQuery.test.js.map +1 -1
- package/dist/react/useRow.test.js +13 -5
- package/dist/react/useRow.test.js.map +1 -1
- package/dist/react/useTemporaryQuery.test.js +5 -2
- package/dist/react/useTemporaryQuery.test.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +12 -12
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +42 -69
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.js +9 -5
- package/dist/reactiveQueries/sql.test.js.map +1 -1
- package/dist/row-query.d.ts +6 -4
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +5 -12
- package/dist/row-query.js.map +1 -1
- package/dist/store-devtools.d.ts +4 -4
- package/dist/store-devtools.d.ts.map +1 -1
- package/dist/store-devtools.js +3 -7
- package/dist/store-devtools.js.map +1 -1
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +24 -46
- package/dist/store.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/react/fixture.tsx +1 -3
- package/src/index.ts +1 -1
- package/src/react/LiveStoreProvider.test.tsx +3 -2
- package/src/react/LiveStoreProvider.tsx +52 -32
- package/src/react/useQuery.test.tsx +11 -4
- package/src/react/useRow.test.tsx +13 -9
- package/src/react/useTemporaryQuery.test.tsx +5 -2
- package/src/reactiveQueries/graphql.ts +5 -1
- package/src/reactiveQueries/sql.test.ts +9 -5
- package/src/reactiveQueries/sql.ts +64 -86
- package/src/row-query.ts +17 -21
- package/src/store-devtools.ts +10 -15
- package/src/store.ts +43 -64
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { type Bindable, prepareBindValues, type QueryInfo, type QueryInfoNone } from '@livestore/common'
|
|
2
2
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
3
|
-
import { Schema, TreeFormatter } from '@livestore/utils/effect'
|
|
3
|
+
import { Schema, SchemaEquivalence, TreeFormatter } from '@livestore/utils/effect'
|
|
4
4
|
import * as otel from '@opentelemetry/api'
|
|
5
5
|
|
|
6
6
|
import { globalReactivityGraph } from '../global-state.js'
|
|
7
7
|
import type { Thunk } from '../reactive.js'
|
|
8
|
+
import { NOT_REFRESHED_YET } from '../reactive.js'
|
|
8
9
|
import type { RefreshReason } from '../store.js'
|
|
9
10
|
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
10
11
|
import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
|
|
11
12
|
import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const querySQL = <TResult, TRaw = any>(
|
|
14
|
+
/**
|
|
15
|
+
* NOTE `querySQL` is only supposed to read data. Don't use it to insert/update/delete data but use mutations instead.
|
|
16
|
+
*/
|
|
17
|
+
export const querySQL = <TResultSchema, TResult = TResultSchema>(
|
|
18
18
|
query: string | ((get: GetAtomResult) => string),
|
|
19
|
-
options
|
|
20
|
-
|
|
19
|
+
options: {
|
|
20
|
+
schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
|
|
21
|
+
map?: (rows: TResultSchema) => TResult
|
|
21
22
|
/**
|
|
22
23
|
* Can be provided explicitly to slightly speed up initial query performance
|
|
23
24
|
*
|
|
@@ -29,21 +30,23 @@ export const querySQL = <TResult, TRaw = any>(
|
|
|
29
30
|
reactivityGraph?: ReactivityGraph
|
|
30
31
|
},
|
|
31
32
|
): LiveQuery<TResult, QueryInfoNone> =>
|
|
32
|
-
new LiveStoreSQLQuery<TResult, QueryInfoNone>({
|
|
33
|
+
new LiveStoreSQLQuery<TResultSchema, TResult, QueryInfoNone>({
|
|
33
34
|
label: options?.label,
|
|
34
35
|
genQueryString: query,
|
|
35
36
|
queriedTables: options?.queriedTables,
|
|
36
37
|
bindValues: options?.bindValues,
|
|
37
38
|
reactivityGraph: options?.reactivityGraph,
|
|
38
39
|
map: options?.map,
|
|
40
|
+
schema: options.schema,
|
|
39
41
|
queryInfo: { _tag: 'None' },
|
|
40
42
|
})
|
|
41
43
|
|
|
42
44
|
/* An object encapsulating a reactive SQL query */
|
|
43
|
-
export class LiveStoreSQLQuery<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
export class LiveStoreSQLQuery<
|
|
46
|
+
TResultSchema,
|
|
47
|
+
TResult = TResultSchema,
|
|
48
|
+
TQueryInfo extends QueryInfo = QueryInfoNone,
|
|
49
|
+
> extends LiveStoreQueryBase<TResult, TQueryInfo> {
|
|
47
50
|
_tag: 'sql' = 'sql'
|
|
48
51
|
|
|
49
52
|
/** A reactive thunk representing the query text */
|
|
@@ -59,7 +62,8 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
|
|
|
59
62
|
/** Currently only used by `rowQuery` for lazy table migrations and eager default row insertion */
|
|
60
63
|
private execBeforeFirstRun
|
|
61
64
|
|
|
62
|
-
private
|
|
65
|
+
private mapResult: (rows: TResultSchema) => TResult
|
|
66
|
+
private schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
|
|
63
67
|
|
|
64
68
|
queryInfo: TQueryInfo
|
|
65
69
|
|
|
@@ -67,8 +71,9 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
|
|
|
67
71
|
genQueryString,
|
|
68
72
|
queriedTables,
|
|
69
73
|
bindValues,
|
|
70
|
-
label
|
|
74
|
+
label = genQueryString.toString(),
|
|
71
75
|
reactivityGraph,
|
|
76
|
+
schema,
|
|
72
77
|
map,
|
|
73
78
|
execBeforeFirstRun,
|
|
74
79
|
queryInfo,
|
|
@@ -78,51 +83,20 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
|
|
|
78
83
|
queriedTables?: Set<string>
|
|
79
84
|
bindValues?: Bindable
|
|
80
85
|
reactivityGraph?: ReactivityGraph
|
|
81
|
-
|
|
86
|
+
schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
|
|
87
|
+
map?: (rows: TResultSchema) => TResult
|
|
82
88
|
execBeforeFirstRun?: (ctx: QueryContext) => void
|
|
83
89
|
queryInfo?: TQueryInfo
|
|
84
90
|
}) {
|
|
85
91
|
super()
|
|
86
92
|
|
|
87
|
-
const label = label_ ?? genQueryString.toString()
|
|
88
93
|
this.label = `sql(${label})`
|
|
89
94
|
this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
|
|
90
95
|
this.execBeforeFirstRun = execBeforeFirstRun
|
|
91
96
|
this.queryInfo = queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
|
|
92
|
-
this.mapRows =
|
|
93
|
-
map === undefined
|
|
94
|
-
? (rows: any) => rows as TResult
|
|
95
|
-
: Schema.isSchema(map)
|
|
96
|
-
? (rows: any, opts: { sqlString: string }) => {
|
|
97
|
-
const parseResult = Schema.decodeEither(map as Schema.Schema<TResult, ReadonlyArray<any>>)(rows)
|
|
98
|
-
if (parseResult._tag === 'Left') {
|
|
99
|
-
const parseErrorStr = TreeFormatter.formatErrorSync(parseResult.left)
|
|
100
|
-
const expectedSchemaStr = String(map.ast)
|
|
101
|
-
const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
|
|
102
|
-
|
|
103
|
-
console.error(
|
|
104
|
-
`\
|
|
105
|
-
Error parsing SQL query result.
|
|
106
|
-
|
|
107
|
-
Query: ${opts.sqlString}\
|
|
108
|
-
${bindValuesStr}
|
|
109
|
-
|
|
110
|
-
Expected schema: ${expectedSchemaStr}
|
|
111
97
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Result:`,
|
|
115
|
-
rows,
|
|
116
|
-
)
|
|
117
|
-
// console.error(`Error parsing SQL query result: ${TreeFormatter.formatErrorSync(parseResult.left)}`)
|
|
118
|
-
return shouldNeverHappen(`Error parsing SQL query result: ${parseResult.left}`)
|
|
119
|
-
} else {
|
|
120
|
-
return parseResult.right as TResult
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
: typeof map === 'function'
|
|
124
|
-
? map
|
|
125
|
-
: shouldNeverHappen(`Invalid map function ${map}`)
|
|
98
|
+
this.schema = schema
|
|
99
|
+
this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
|
|
126
100
|
|
|
127
101
|
let queryString$OrQueryString: string | Thunk<string, QueryContext, RefreshReason>
|
|
128
102
|
if (typeof genQueryString === 'function') {
|
|
@@ -134,7 +108,11 @@ Result:`,
|
|
|
134
108
|
setDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString, durationMs })
|
|
135
109
|
return queryString
|
|
136
110
|
},
|
|
137
|
-
{
|
|
111
|
+
{
|
|
112
|
+
label: `${label}:queryString`,
|
|
113
|
+
meta: { liveStoreThunkType: 'sqlQueryString' },
|
|
114
|
+
equal: (a, b) => a === b,
|
|
115
|
+
},
|
|
138
116
|
)
|
|
139
117
|
|
|
140
118
|
this.queryString$ = queryString$OrQueryString
|
|
@@ -146,6 +124,15 @@ Result:`,
|
|
|
146
124
|
|
|
147
125
|
const queriedTablesRef = { current: queriedTables }
|
|
148
126
|
|
|
127
|
+
const schemaEqual = SchemaEquivalence.make(schema)
|
|
128
|
+
// TODO also support derived equality for `map` (probably will depend on having an easy way to transform a schema without an `encode` step)
|
|
129
|
+
// This would mean dropping the `map` option
|
|
130
|
+
const equal =
|
|
131
|
+
map === undefined
|
|
132
|
+
? (a: TResult, b: TResult) =>
|
|
133
|
+
a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : schemaEqual(a as any, b as any)
|
|
134
|
+
: undefined
|
|
135
|
+
|
|
149
136
|
const results$ = this.reactivityGraph.makeThunk<TResult>(
|
|
150
137
|
(get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
|
|
151
138
|
otelTracer.startActiveSpan(
|
|
@@ -186,7 +173,31 @@ Result:`,
|
|
|
186
173
|
|
|
187
174
|
span.setAttribute('sql.rowsCount', rawResults.length)
|
|
188
175
|
|
|
189
|
-
const
|
|
176
|
+
const parsedResult = Schema.decodeEither(this.schema)(rawResults)
|
|
177
|
+
|
|
178
|
+
if (parsedResult._tag === 'Left') {
|
|
179
|
+
const parseErrorStr = TreeFormatter.formatErrorSync(parsedResult.left)
|
|
180
|
+
const expectedSchemaStr = String(this.schema.ast)
|
|
181
|
+
const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
|
|
182
|
+
|
|
183
|
+
console.error(
|
|
184
|
+
`\
|
|
185
|
+
Error parsing SQL query result.
|
|
186
|
+
|
|
187
|
+
Query: ${sqlString}\
|
|
188
|
+
${bindValuesStr}
|
|
189
|
+
|
|
190
|
+
Expected schema: ${expectedSchemaStr}
|
|
191
|
+
|
|
192
|
+
Error: ${parseErrorStr}
|
|
193
|
+
|
|
194
|
+
Result:`,
|
|
195
|
+
rawResults,
|
|
196
|
+
)
|
|
197
|
+
return shouldNeverHappen(`Error parsing SQL query result: ${parsedResult.left}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result = this.mapResult(parsedResult.right)
|
|
190
201
|
|
|
191
202
|
span.end()
|
|
192
203
|
|
|
@@ -199,45 +210,12 @@ Result:`,
|
|
|
199
210
|
return result
|
|
200
211
|
},
|
|
201
212
|
),
|
|
202
|
-
{ label: queryLabel },
|
|
213
|
+
{ label: queryLabel, equal },
|
|
203
214
|
)
|
|
204
215
|
|
|
205
216
|
this.results$ = results$
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
/**
|
|
209
|
-
* Returns a new reactive query that contains the result of
|
|
210
|
-
* running an arbitrary JS computation on the results of this SQL query.
|
|
211
|
-
*/
|
|
212
|
-
// pipe = <U>(fn: (result: Result, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
|
|
213
|
-
// new LiveStoreJSQuery({
|
|
214
|
-
// fn: (get) => {
|
|
215
|
-
// const results = get(this.results$!)
|
|
216
|
-
// return fn(results, get)
|
|
217
|
-
// },
|
|
218
|
-
// label: `${this.label}:js`,
|
|
219
|
-
// onDestroy: () => this.destroy(),
|
|
220
|
-
// reactivityGraph: this.reactivityGraph,
|
|
221
|
-
// queryInfo: undefined,
|
|
222
|
-
// })
|
|
223
|
-
|
|
224
|
-
/** Returns a reactive query */
|
|
225
|
-
// getFirstRow = (args?: { defaultValue?: Result }) =>
|
|
226
|
-
// new LiveStoreJSQuery({
|
|
227
|
-
// fn: (get) => {
|
|
228
|
-
// const results = get(this.results$!)
|
|
229
|
-
// if (results.length === 0 && args?.defaultValue === undefined) {
|
|
230
|
-
// // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
|
|
231
|
-
// const queryLabel = this.label
|
|
232
|
-
// return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
|
|
233
|
-
// }
|
|
234
|
-
// return results[0] ?? args!.defaultValue!
|
|
235
|
-
// },
|
|
236
|
-
// label: `${this.label}:first`,
|
|
237
|
-
// onDestroy: () => this.destroy(),
|
|
238
|
-
// reactivityGraph: this.reactivityGraph,
|
|
239
|
-
// })
|
|
240
|
-
|
|
241
219
|
destroy = () => {
|
|
242
220
|
if (this.queryString$ !== undefined) {
|
|
243
221
|
this.reactivityGraph.destroyNode(this.queryString$)
|
package/src/row-query.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { sql } from '@livestore/common'
|
|
|
3
3
|
import { DbSchema } from '@livestore/common/schema'
|
|
4
4
|
import type { GetValForKey } from '@livestore/utils'
|
|
5
5
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
6
|
-
import { Schema
|
|
6
|
+
import { Schema } from '@livestore/utils/effect'
|
|
7
7
|
import type * as otel from '@opentelemetry/api'
|
|
8
8
|
import type { SqliteDsl } from 'effect-db-schema'
|
|
9
9
|
|
|
@@ -12,14 +12,16 @@ import { computed } from './reactiveQueries/js.js'
|
|
|
12
12
|
import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
|
|
13
13
|
import type { Store } from './store.js'
|
|
14
14
|
|
|
15
|
-
export type RowQueryOptions = {
|
|
15
|
+
export type RowQueryOptions<TTableDef extends DbSchema.TableDef, TResult = RowResult<TTableDef>> = {
|
|
16
16
|
otelContext?: otel.Context
|
|
17
17
|
skipInsertDefaultRow?: boolean
|
|
18
18
|
reactivityGraph?: ReactivityGraph
|
|
19
|
+
map?: (result: RowResult<TTableDef>) => TResult
|
|
20
|
+
label?: string
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export type RowQueryOptionsDefaulValues<TTableDef extends DbSchema.TableDef> = {
|
|
22
|
-
defaultValues
|
|
24
|
+
defaultValues?: Partial<RowResult<TTableDef>>
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export type MakeRowQuery = {
|
|
@@ -29,9 +31,10 @@ export type MakeRowQuery = {
|
|
|
29
31
|
boolean,
|
|
30
32
|
DbSchema.TableOptions & { isSingleton: true }
|
|
31
33
|
>,
|
|
34
|
+
TResult = RowResult<TTableDef>,
|
|
32
35
|
>(
|
|
33
36
|
table: TTableDef,
|
|
34
|
-
options?: RowQueryOptions,
|
|
37
|
+
options?: RowQueryOptions<TTableDef, TResult>,
|
|
35
38
|
): LiveQuery<RowResult<TTableDef>, QueryInfoRow<TTableDef>>
|
|
36
39
|
<
|
|
37
40
|
TTableDef extends DbSchema.TableDef<
|
|
@@ -39,19 +42,20 @@ export type MakeRowQuery = {
|
|
|
39
42
|
boolean,
|
|
40
43
|
DbSchema.TableOptions & { isSingleton: false }
|
|
41
44
|
>,
|
|
45
|
+
TResult = RowResult<TTableDef>,
|
|
42
46
|
>(
|
|
43
47
|
table: TTableDef,
|
|
44
48
|
// TODO adjust so it works with arbitrary primary keys or unique constraints
|
|
45
49
|
id: string,
|
|
46
|
-
options?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
|
|
47
|
-
): LiveQuery<
|
|
50
|
+
options?: RowQueryOptions<TTableDef, TResult> & RowQueryOptionsDefaulValues<TTableDef>,
|
|
51
|
+
): LiveQuery<TResult, QueryInfoRow<TTableDef>>
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
// TODO also allow other where clauses and multiple rows
|
|
51
55
|
export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
|
|
52
56
|
table: TTableDef,
|
|
53
|
-
idOrOptions?: string | RowQueryOptions,
|
|
54
|
-
options_?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
|
|
57
|
+
idOrOptions?: string | RowQueryOptions<TTableDef, any>,
|
|
58
|
+
options_?: RowQueryOptions<TTableDef, any> & RowQueryOptionsDefaulValues<TTableDef>,
|
|
55
59
|
) => {
|
|
56
60
|
const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
|
|
57
61
|
const options = typeof idOrOptions === 'string' ? options_ : idOrOptions
|
|
@@ -70,8 +74,10 @@ export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
|
|
|
70
74
|
const whereClause = id === undefined ? '' : `where id = '${id}'`
|
|
71
75
|
const queryStr = sql`select * from ${tableName} ${whereClause} limit 1`
|
|
72
76
|
|
|
77
|
+
const rowSchema = table.isSingleColumn === true ? table.schema.pipe(Schema.pluck('value' as any)) : table.schema
|
|
78
|
+
|
|
73
79
|
return new LiveStoreSQLQuery({
|
|
74
|
-
label: `rowQuery:query:${tableSchema.name}${id === undefined ? '' : `:${id}`}`,
|
|
80
|
+
label: options?.label ?? `rowQuery:query:${tableSchema.name}${id === undefined ? '' : `:${id}`}`,
|
|
75
81
|
genQueryString: queryStr,
|
|
76
82
|
queriedTables: new Set([tableName]),
|
|
77
83
|
reactivityGraph: options?.reactivityGraph,
|
|
@@ -83,18 +89,8 @@ export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
|
|
|
83
89
|
id,
|
|
84
90
|
skipInsertDefaultRow: options?.skipInsertDefaultRow,
|
|
85
91
|
}),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const parseResult = Schema.decodeEither(table.schema)(results[0]!)
|
|
90
|
-
|
|
91
|
-
if (parseResult._tag === 'Left') {
|
|
92
|
-
console.error('decode error', TreeFormatter.formatErrorSync(parseResult.left), 'results', results)
|
|
93
|
-
return shouldNeverHappen(`Error decoding query result for ${queryStr}`)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return table.isSingleColumn === true ? parseResult.right.value : parseResult.right
|
|
97
|
-
},
|
|
92
|
+
schema: rowSchema.pipe(Schema.Array, Schema.headOrElse()),
|
|
93
|
+
map: options?.map,
|
|
98
94
|
queryInfo: { _tag: 'Row', table, id: id ?? 'singleton' },
|
|
99
95
|
})
|
|
100
96
|
}
|
package/src/store-devtools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DebugInfo, StoreAdapter } from '@livestore/common'
|
|
2
|
-
import { Devtools, liveStoreVersion } from '@livestore/common'
|
|
2
|
+
import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
|
|
3
3
|
import { throttle } from '@livestore/utils'
|
|
4
4
|
import { BrowserChannel, Effect, Stream } from '@livestore/utils/effect'
|
|
5
5
|
|
|
@@ -9,29 +9,24 @@ import { NOT_REFRESHED_YET } from './reactive.js'
|
|
|
9
9
|
import type { LiveQuery, ReactivityGraph } from './reactiveQueries/base-class.js'
|
|
10
10
|
import type { ReferenceCountedSet } from './utils/data-structures.js'
|
|
11
11
|
|
|
12
|
-
type Unsub = () => void
|
|
13
|
-
type RequestId = string
|
|
14
|
-
|
|
15
12
|
type IStore = {
|
|
16
13
|
adapter: StoreAdapter
|
|
17
|
-
devtoolsConnectionId: string
|
|
18
14
|
reactivityGraph: ReactivityGraph
|
|
19
15
|
mainDbWrapper: MainDatabaseWrapper
|
|
20
16
|
activeQueries: ReferenceCountedSet<LiveQuery<any>>
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
type Unsub = () => void
|
|
20
|
+
type RequestId = string
|
|
21
|
+
type SubMap = Map<RequestId, Unsub>
|
|
22
|
+
|
|
23
|
+
export const connectDevtoolsToStore = ({ storeMessagePort, store }: { storeMessagePort: MessagePort; store: IStore }) =>
|
|
24
24
|
Effect.gen(function* () {
|
|
25
25
|
const channelId = store.adapter.coordinator.devtools.channelId
|
|
26
26
|
|
|
27
|
-
const reactivityGraphSubcriptions = new Map
|
|
28
|
-
const liveQueriesSubscriptions = new Map
|
|
29
|
-
const debugInfoHistorySubscriptions = new Map
|
|
30
|
-
|
|
31
|
-
const { storeMessagePort } = yield* store.adapter.coordinator.devtools.connect({
|
|
32
|
-
port,
|
|
33
|
-
connectionId: store.devtoolsConnectionId,
|
|
34
|
-
})
|
|
27
|
+
const reactivityGraphSubcriptions: SubMap = new Map()
|
|
28
|
+
const liveQueriesSubscriptions: SubMap = new Map()
|
|
29
|
+
const debugInfoHistorySubscriptions: SubMap = new Map()
|
|
35
30
|
|
|
36
31
|
const storePortChannel = yield* BrowserChannel.messagePortChannel({
|
|
37
32
|
port: storeMessagePort,
|
|
@@ -210,4 +205,4 @@ export const connectStoreToDevtools = ({ port, store }: { port: MessagePort; sto
|
|
|
210
205
|
Stream.runDrain,
|
|
211
206
|
Effect.withSpan('LSD.devtools.onMessage'),
|
|
212
207
|
)
|
|
213
|
-
})
|
|
208
|
+
}).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('LSD.devtools.connectStoreToDevtools'))
|
package/src/store.ts
CHANGED
|
@@ -7,15 +7,15 @@ import type {
|
|
|
7
7
|
StoreAdapter,
|
|
8
8
|
StoreAdapterFactory,
|
|
9
9
|
} from '@livestore/common'
|
|
10
|
-
import {
|
|
10
|
+
import { getExecArgsFromMutation, prepareBindValues, UnexpectedError } from '@livestore/common'
|
|
11
11
|
import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
|
|
12
12
|
import { makeMutationEventSchemaMemo, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE } from '@livestore/common/schema'
|
|
13
13
|
import { assertNever, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
|
|
14
|
-
import {
|
|
14
|
+
import type { Cause } from '@livestore/utils/effect'
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
Deferred,
|
|
17
|
+
Duration,
|
|
17
18
|
Effect,
|
|
18
|
-
Either,
|
|
19
19
|
Exit,
|
|
20
20
|
FiberSet,
|
|
21
21
|
Inspectable,
|
|
@@ -37,7 +37,7 @@ import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
|
|
|
37
37
|
import type { StackInfo } from './react/utils/stack-info.js'
|
|
38
38
|
import type { DebugRefreshReasonBase, Ref } from './reactive.js'
|
|
39
39
|
import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
|
|
40
|
-
import {
|
|
40
|
+
import { connectDevtoolsToStore } from './store-devtools.js'
|
|
41
41
|
import { ReferenceCountedSet } from './utils/data-structures.js'
|
|
42
42
|
import { downloadBlob } from './utils/dev.js'
|
|
43
43
|
import { getDurationMsFromSpan } from './utils/otel.js'
|
|
@@ -58,6 +58,9 @@ export type OtelOptions = {
|
|
|
58
58
|
rootSpanContext: otel.Context
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export class ForceStoreShutdown extends Schema.TaggedError<ForceStoreShutdown>()('LiveStore.ForceStoreShutdown', {}) {}
|
|
62
|
+
export class StoreShutdown extends Schema.TaggedError<StoreShutdown>()('LiveStore.StoreShutdown', {}) {}
|
|
63
|
+
|
|
61
64
|
export type StoreOptions<
|
|
62
65
|
TGraphQLContext extends BaseGraphQLContext,
|
|
63
66
|
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
@@ -126,7 +129,6 @@ export class Store<
|
|
|
126
129
|
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
127
130
|
> extends Inspectable.Class {
|
|
128
131
|
id = uniqueStoreId()
|
|
129
|
-
readonly devtoolsConnectionId = cuid()
|
|
130
132
|
private fiberSet: FiberSet.FiberSet
|
|
131
133
|
reactivityGraph: ReactivityGraph
|
|
132
134
|
mainDbWrapper: MainDatabaseWrapper
|
|
@@ -232,14 +234,11 @@ export class Store<
|
|
|
232
234
|
this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
|
|
233
235
|
}),
|
|
234
236
|
Stream.runDrain,
|
|
237
|
+
Effect.interruptible,
|
|
235
238
|
Effect.withSpan('LiveStore:syncMutations'),
|
|
236
239
|
Effect.forkScoped,
|
|
237
240
|
)
|
|
238
241
|
|
|
239
|
-
if (disableDevtools !== true) {
|
|
240
|
-
yield* this.bootDevtools().pipe(Effect.forkScoped)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
242
|
yield* Effect.addFinalizer(() =>
|
|
244
243
|
Effect.sync(() => {
|
|
245
244
|
for (const tableRef of Object.values(this.tableRefs)) {
|
|
@@ -256,7 +255,6 @@ export class Store<
|
|
|
256
255
|
yield* Effect.never
|
|
257
256
|
}).pipe(Effect.scoped, Effect.withSpan('LiveStore:store-constructor'), FiberSet.run(fiberSet), runEffectFork)
|
|
258
257
|
}
|
|
259
|
-
|
|
260
258
|
// #endregion constructor
|
|
261
259
|
|
|
262
260
|
static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
|
|
@@ -590,52 +588,6 @@ export class Store<
|
|
|
590
588
|
meta: { liveStoreRefType: 'table' },
|
|
591
589
|
})
|
|
592
590
|
|
|
593
|
-
// #region devtools
|
|
594
|
-
private bootDevtools = () =>
|
|
595
|
-
Effect.gen(this, function* () {
|
|
596
|
-
// const webBridgeBroadcastChannel = yield* Devtools.WebBridge.makeBroadcastChannel()
|
|
597
|
-
|
|
598
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
|
599
|
-
const store = this
|
|
600
|
-
const channelId = this.adapter.coordinator.devtools.channelId
|
|
601
|
-
|
|
602
|
-
// Chrome extension bridge
|
|
603
|
-
{
|
|
604
|
-
const windowChannel = yield* BrowserChannel.windowChannel({
|
|
605
|
-
window,
|
|
606
|
-
listenSchema: Devtools.DevtoolsWindowMessage.MessageForStore,
|
|
607
|
-
sendSchema: Devtools.DevtoolsWindowMessage.MessageForContentscript,
|
|
608
|
-
})
|
|
609
|
-
|
|
610
|
-
yield* windowChannel.send(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
|
|
611
|
-
|
|
612
|
-
yield* windowChannel.listen.pipe(
|
|
613
|
-
Stream.filterMap(Either.getRight),
|
|
614
|
-
Stream.tap((message) =>
|
|
615
|
-
Effect.gen(function* () {
|
|
616
|
-
if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
|
|
617
|
-
// Send message to contentscript via window (which the contentscript iframe is listening to)
|
|
618
|
-
yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
|
|
619
|
-
return
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (message.channelId !== channelId) return
|
|
623
|
-
|
|
624
|
-
if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
|
|
625
|
-
yield* connectStoreToDevtools({ port: message.port, store })
|
|
626
|
-
}
|
|
627
|
-
}),
|
|
628
|
-
),
|
|
629
|
-
Stream.runDrain,
|
|
630
|
-
Effect.tapCauseLogPretty,
|
|
631
|
-
Effect.forkScoped,
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
|
|
635
|
-
}
|
|
636
|
-
})
|
|
637
|
-
// #endregion devtools
|
|
638
|
-
|
|
639
591
|
__devDownloadDb = () => {
|
|
640
592
|
const data = this.mainDbWrapper.export()
|
|
641
593
|
downloadBlob(data, `livestore-${Date.now()}.db`)
|
|
@@ -728,21 +680,44 @@ export const createStore = <
|
|
|
728
680
|
|
|
729
681
|
yield* Queue.take(bootStatusQueue).pipe(
|
|
730
682
|
Effect.tapSync((status) => onBootStatus?.(status)),
|
|
683
|
+
Effect.tap((status) => (status.stage === 'done' ? Queue.shutdown(bootStatusQueue) : Effect.void)),
|
|
731
684
|
Effect.forever,
|
|
732
685
|
Effect.tapCauseLogPretty,
|
|
733
686
|
Effect.forkScoped,
|
|
734
687
|
)
|
|
735
688
|
|
|
689
|
+
const storeDeferred = yield* Deferred.make<Store>()
|
|
690
|
+
|
|
691
|
+
const connectDevtoolsToStore_ = ({ storeMessagePort }: { storeMessagePort: MessagePort }) =>
|
|
692
|
+
Effect.gen(function* () {
|
|
693
|
+
const store = yield* Deferred.await(storeDeferred)
|
|
694
|
+
yield* connectDevtoolsToStore({ storeMessagePort, store })
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
// TODO close parent scope? (Needs refactor with Mike A)
|
|
698
|
+
const shutdown = (cause: Cause.Cause<unknown>) =>
|
|
699
|
+
Effect.gen(function* () {
|
|
700
|
+
yield* Effect.logWarning(`Shutting down LiveStore`, cause)
|
|
701
|
+
|
|
702
|
+
FiberSet.clear(fiberSet).pipe(
|
|
703
|
+
Effect.andThen(() => FiberSet.run(fiberSet, Effect.fail(StoreShutdown.make()))),
|
|
704
|
+
Effect.timeout(Duration.seconds(1)),
|
|
705
|
+
Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown:clear-fiber-set', duration: 500 }),
|
|
706
|
+
Effect.catchTag('TimeoutException', () =>
|
|
707
|
+
Effect.logWarning('Store shutdown timed out. Forcing shutdown.').pipe(
|
|
708
|
+
Effect.andThen(FiberSet.run(fiberSet, Effect.fail(ForceStoreShutdown.make()))),
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
runEffectFork, // NOTE we need to fork this separately otherwise it will also be interrupted
|
|
712
|
+
)
|
|
713
|
+
}).pipe(Effect.withSpan('livestore:shutdown'))
|
|
714
|
+
|
|
736
715
|
const adapter: StoreAdapter = yield* adapterFactory({
|
|
737
716
|
schema,
|
|
738
717
|
devtoolsEnabled: disableDevtools !== true,
|
|
739
718
|
bootStatusQueue,
|
|
740
|
-
shutdown
|
|
741
|
-
|
|
742
|
-
yield* Effect.logWarning(`Shutting down LiveStore`, cause)
|
|
743
|
-
// TODO close parent scope? (Needs refactor with Mike A)
|
|
744
|
-
yield* FiberSet.clear(fiberSet)
|
|
745
|
-
}).pipe(Effect.withSpan('livestore:shutdown')),
|
|
719
|
+
shutdown,
|
|
720
|
+
connectDevtoolsToStore: connectDevtoolsToStore_,
|
|
746
721
|
}).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
|
|
747
722
|
|
|
748
723
|
if (batchUpdates !== undefined) {
|
|
@@ -826,7 +801,7 @@ export const createStore = <
|
|
|
826
801
|
)
|
|
827
802
|
}
|
|
828
803
|
|
|
829
|
-
|
|
804
|
+
const store = Store.createStore<TGraphQLContext, TSchema>(
|
|
830
805
|
{
|
|
831
806
|
adapter,
|
|
832
807
|
schema,
|
|
@@ -839,6 +814,10 @@ export const createStore = <
|
|
|
839
814
|
},
|
|
840
815
|
span,
|
|
841
816
|
)
|
|
817
|
+
|
|
818
|
+
yield* Deferred.succeed(storeDeferred, store as any as Store)
|
|
819
|
+
|
|
820
|
+
return store
|
|
842
821
|
}).pipe(
|
|
843
822
|
Effect.withSpan('createStore', {
|
|
844
823
|
parent: otelOptions?.rootSpanContext
|