@livestore/livestore 0.3.0-dev.10 → 0.3.0-dev.11
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/SqliteDbWrapper.d.ts +54 -0
- package/dist/SqliteDbWrapper.d.ts.map +1 -0
- package/dist/SqliteDbWrapper.js +212 -0
- package/dist/SqliteDbWrapper.js.map +1 -0
- package/dist/SynchronousDatabaseWrapper.d.ts +14 -5
- package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
- package/dist/SynchronousDatabaseWrapper.js +24 -4
- package/dist/SynchronousDatabaseWrapper.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +12 -8
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +9 -2
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +64 -21
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +56 -13
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/computed.d.ts +7 -7
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +35 -11
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts +67 -0
- package/dist/live-queries/db-query.d.ts.map +1 -0
- package/dist/live-queries/db-query.js +244 -0
- package/dist/live-queries/db-query.js.map +1 -0
- package/dist/live-queries/db-query.test.d.ts +2 -0
- package/dist/live-queries/db-query.test.d.ts.map +1 -0
- package/dist/live-queries/db-query.test.js +123 -0
- package/dist/live-queries/db-query.test.js.map +1 -0
- package/dist/live-queries/db.d.ts +12 -15
- package/dist/live-queries/db.d.ts.map +1 -1
- package/dist/live-queries/db.js +44 -25
- package/dist/live-queries/db.js.map +1 -1
- package/dist/live-queries/db.test.js +16 -14
- package/dist/live-queries/db.test.js.map +1 -1
- package/dist/live-queries/graphql.d.ts +8 -8
- package/dist/live-queries/graphql.d.ts.map +1 -1
- package/dist/live-queries/graphql.js +35 -9
- package/dist/live-queries/graphql.js.map +1 -1
- package/dist/live-queries/make-ref.d.ts +20 -0
- package/dist/live-queries/make-ref.d.ts.map +1 -0
- package/dist/live-queries/make-ref.js +33 -0
- package/dist/live-queries/make-ref.js.map +1 -0
- package/dist/reactive.d.ts +15 -13
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +15 -9
- package/dist/reactive.js.map +1 -1
- package/dist/row-query-utils.d.ts +4 -4
- package/dist/row-query-utils.d.ts.map +1 -1
- package/dist/row-query-utils.js +14 -10
- package/dist/row-query-utils.js.map +1 -1
- package/dist/store/create-store.d.ts +3 -4
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +7 -7
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +2 -2
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +15 -15
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-types.d.ts +9 -4
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store.d.ts +34 -16
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +125 -75
- package/dist/store/store.js.map +1 -1
- package/dist/utils/expo.d.ts +2 -0
- package/dist/utils/expo.d.ts.map +1 -0
- package/dist/utils/expo.js +8 -0
- package/dist/utils/expo.js.map +1 -0
- package/dist/utils/function-string.d.ts +7 -0
- package/dist/utils/function-string.d.ts.map +1 -0
- package/dist/utils/function-string.js +9 -0
- package/dist/utils/function-string.js.map +1 -0
- package/dist/utils/stack-info.d.ts.map +1 -1
- package/dist/utils/stack-info.js +6 -1
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/stack-info.test.js +54 -1
- package/dist/utils/stack-info.test.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +2 -6
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +3 -5
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/mod.d.ts +1 -0
- package/dist/utils/tests/mod.d.ts.map +1 -1
- package/dist/utils/tests/mod.js +1 -0
- package/dist/utils/tests/mod.js.map +1 -1
- package/package.json +5 -5
- package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +41 -11
- package/src/effect/LiveStore.ts +22 -14
- package/src/index.ts +14 -7
- package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +196 -42
- package/src/live-queries/base-class.ts +160 -40
- package/src/live-queries/computed.ts +45 -19
- package/src/live-queries/{db.test.ts → db-query.test.ts} +21 -11
- package/src/live-queries/{db.ts → db-query.ts} +97 -39
- package/src/live-queries/graphql.ts +47 -21
- package/src/live-queries/make-ref.ts +47 -0
- package/src/reactive.ts +52 -27
- package/src/row-query-utils.ts +29 -18
- package/src/store/create-store.ts +20 -23
- package/src/store/devtools.ts +17 -17
- package/src/store/store-types.ts +6 -4
- package/src/store/store.ts +227 -120
- package/src/utils/function-string.ts +12 -0
- package/src/utils/stack-info.test.ts +58 -1
- package/src/utils/stack-info.ts +6 -1
- package/src/utils/tests/fixture.ts +2 -7
- package/src/utils/tests/mod.ts +1 -0
- package/src/global-state.ts +0 -20
package/src/store/store.ts
CHANGED
|
@@ -27,19 +27,44 @@ import {
|
|
|
27
27
|
} from '@livestore/common/schema'
|
|
28
28
|
import { assertNever, isDevEnv } from '@livestore/utils'
|
|
29
29
|
import type { Scope } from '@livestore/utils/effect'
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
Cause,
|
|
32
|
+
Data,
|
|
33
|
+
Effect,
|
|
34
|
+
Inspectable,
|
|
35
|
+
MutableHashMap,
|
|
36
|
+
OtelTracer,
|
|
37
|
+
Runtime,
|
|
38
|
+
Schema,
|
|
39
|
+
Stream,
|
|
40
|
+
} from '@livestore/utils/effect'
|
|
31
41
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
32
42
|
import * as otel from '@opentelemetry/api'
|
|
33
43
|
import { type GraphQLSchema } from 'graphql'
|
|
34
44
|
|
|
35
|
-
import type {
|
|
45
|
+
import type {
|
|
46
|
+
ILiveQueryRefDef,
|
|
47
|
+
LiveQuery,
|
|
48
|
+
LiveQueryDef,
|
|
49
|
+
ReactivityGraph,
|
|
50
|
+
ReactivityGraphContext,
|
|
51
|
+
} from '../live-queries/base-class.js'
|
|
52
|
+
import { makeReactivityGraph } from '../live-queries/base-class.js'
|
|
36
53
|
import type { Ref } from '../reactive.js'
|
|
37
54
|
import { makeExecBeforeFirstRun } from '../row-query-utils.js'
|
|
38
|
-
import {
|
|
55
|
+
import { SqliteDbWrapper } from '../SqliteDbWrapper.js'
|
|
39
56
|
import { ReferenceCountedSet } from '../utils/data-structures.js'
|
|
40
57
|
import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
|
|
41
58
|
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
42
|
-
import type {
|
|
59
|
+
import type { StackInfo } from '../utils/stack-info.js'
|
|
60
|
+
import type {
|
|
61
|
+
BaseGraphQLContext,
|
|
62
|
+
RefreshReason,
|
|
63
|
+
StoreMutateOptions,
|
|
64
|
+
StoreOptions,
|
|
65
|
+
StoreOtel,
|
|
66
|
+
Unsubscribe,
|
|
67
|
+
} from './store-types.js'
|
|
43
68
|
|
|
44
69
|
if (isDevEnv()) {
|
|
45
70
|
exposeDebugUtils()
|
|
@@ -51,7 +76,7 @@ export class Store<
|
|
|
51
76
|
> extends Inspectable.Class {
|
|
52
77
|
readonly storeId: string
|
|
53
78
|
reactivityGraph: ReactivityGraph
|
|
54
|
-
|
|
79
|
+
sqliteDbWrapper: SqliteDbWrapper
|
|
55
80
|
clientSession: ClientSession
|
|
56
81
|
schema: LiveStoreSchema
|
|
57
82
|
graphQLSchema?: GraphQLSchema
|
|
@@ -61,7 +86,7 @@ export class Store<
|
|
|
61
86
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
|
62
87
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
|
63
88
|
*/
|
|
64
|
-
tableRefs: { [key: string]: Ref<null,
|
|
89
|
+
tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> }
|
|
65
90
|
|
|
66
91
|
private runtime: Runtime.Runtime<Scope.Scope>
|
|
67
92
|
|
|
@@ -74,12 +99,13 @@ export class Store<
|
|
|
74
99
|
private syncProcessor: ClientSessionSyncProcessor
|
|
75
100
|
readonly lifetimeScope: Scope.Scope
|
|
76
101
|
|
|
102
|
+
readonly boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
|
|
103
|
+
|
|
77
104
|
// #region constructor
|
|
78
|
-
|
|
105
|
+
constructor({
|
|
79
106
|
clientSession,
|
|
80
107
|
schema,
|
|
81
108
|
graphQLOptions,
|
|
82
|
-
reactivityGraph,
|
|
83
109
|
otelOptions,
|
|
84
110
|
disableDevtools,
|
|
85
111
|
batchUpdates,
|
|
@@ -93,25 +119,21 @@ export class Store<
|
|
|
93
119
|
this.storeId = storeId
|
|
94
120
|
this.unsyncedMutationEvents = unsyncedMutationEvents
|
|
95
121
|
|
|
96
|
-
this.
|
|
122
|
+
this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb })
|
|
97
123
|
this.clientSession = clientSession
|
|
98
124
|
this.schema = schema
|
|
99
125
|
|
|
100
126
|
this.lifetimeScope = lifetimeScope
|
|
101
127
|
this.runtime = runtime
|
|
102
128
|
|
|
129
|
+
const reactivityGraph = makeReactivityGraph()
|
|
130
|
+
|
|
103
131
|
const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
|
|
104
132
|
|
|
105
133
|
this.syncProcessor = makeClientSessionSyncProcessor({
|
|
106
134
|
schema,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
clientSession.leaderThread.mutations.push(batch).pipe(
|
|
110
|
-
// NOTE we don't want to shutdown in case of an invalid push error, since it will be retried
|
|
111
|
-
Effect.catchTag('InvalidPushError', Effect.ignoreLogged),
|
|
112
|
-
this.runEffectFork,
|
|
113
|
-
),
|
|
114
|
-
pullFromLeader: clientSession.leaderThread.mutations.pull,
|
|
135
|
+
clientSession,
|
|
136
|
+
runtime,
|
|
115
137
|
applyMutation: (mutationEventDecoded, { otelContext, withChangeset }) => {
|
|
116
138
|
const mutationDef = schema.mutations.get(mutationEventDecoded.mutation)!
|
|
117
139
|
const execArgsArr = getExecArgsFromMutation({
|
|
@@ -125,9 +147,9 @@ export class Store<
|
|
|
125
147
|
for (const {
|
|
126
148
|
statementSql,
|
|
127
149
|
bindValues,
|
|
128
|
-
writeTables = this.
|
|
150
|
+
writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
|
|
129
151
|
} of execArgsArr) {
|
|
130
|
-
this.
|
|
152
|
+
this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables })
|
|
131
153
|
|
|
132
154
|
// durationMsTotal += durationMs
|
|
133
155
|
writeTables.forEach((table) => writeTablesForEvent.add(table))
|
|
@@ -136,7 +158,7 @@ export class Store<
|
|
|
136
158
|
|
|
137
159
|
let sessionChangeset: Uint8Array | undefined
|
|
138
160
|
if (withChangeset === true) {
|
|
139
|
-
sessionChangeset = this.
|
|
161
|
+
sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
|
|
140
162
|
} else {
|
|
141
163
|
exec()
|
|
142
164
|
}
|
|
@@ -144,16 +166,16 @@ export class Store<
|
|
|
144
166
|
return { writeTables: writeTablesForEvent, sessionChangeset }
|
|
145
167
|
},
|
|
146
168
|
rollback: (changeset) => {
|
|
147
|
-
this.
|
|
169
|
+
this.sqliteDbWrapper.rollback(changeset)
|
|
148
170
|
},
|
|
149
171
|
refreshTables: (tables) => {
|
|
150
|
-
const tablesToUpdate = [] as [Ref<null,
|
|
172
|
+
const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
|
|
151
173
|
for (const tableName of tables) {
|
|
152
174
|
const tableRef = this.tableRefs[tableName]
|
|
153
175
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
154
176
|
tablesToUpdate.push([tableRef!, null])
|
|
155
177
|
}
|
|
156
|
-
|
|
178
|
+
reactivityGraph.setRefs(tablesToUpdate)
|
|
157
179
|
},
|
|
158
180
|
span: syncSpan,
|
|
159
181
|
})
|
|
@@ -173,6 +195,8 @@ export class Store<
|
|
|
173
195
|
this.reactivityGraph = reactivityGraph
|
|
174
196
|
this.reactivityGraph.context = {
|
|
175
197
|
store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
|
|
198
|
+
liveQueryRCMap: new Map(),
|
|
199
|
+
reactivityGraph: new WeakRef(reactivityGraph),
|
|
176
200
|
otelTracer: otelOptions.tracer,
|
|
177
201
|
rootOtelContext: otelQueriesSpanContext,
|
|
178
202
|
effectsWrapper: batchUpdates,
|
|
@@ -209,10 +233,10 @@ export class Store<
|
|
|
209
233
|
|
|
210
234
|
if (graphQLOptions) {
|
|
211
235
|
this.graphQLSchema = graphQLOptions.schema
|
|
212
|
-
this.graphQLContext = graphQLOptions.makeContext(this.
|
|
236
|
+
this.graphQLContext = graphQLOptions.makeContext(this.sqliteDbWrapper, this.otel.tracer, clientSession.sessionId)
|
|
213
237
|
}
|
|
214
238
|
|
|
215
|
-
Effect.gen(this, function* () {
|
|
239
|
+
this.boot = Effect.gen(this, function* () {
|
|
216
240
|
yield* Effect.addFinalizer(() =>
|
|
217
241
|
Effect.sync(() => {
|
|
218
242
|
// Remove all table refs from the reactivity graph
|
|
@@ -230,23 +254,9 @@ export class Store<
|
|
|
230
254
|
)
|
|
231
255
|
|
|
232
256
|
yield* this.syncProcessor.boot
|
|
233
|
-
}).pipe(this.runEffectFork)
|
|
234
|
-
}
|
|
235
|
-
// #endregion constructor
|
|
236
|
-
|
|
237
|
-
static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
|
|
238
|
-
storeOptions: StoreOptions<TGraphQLContext, TSchema>,
|
|
239
|
-
parentSpan: otel.Span,
|
|
240
|
-
): Store<TGraphQLContext, TSchema> => {
|
|
241
|
-
const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
|
|
242
|
-
return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
|
|
243
|
-
try {
|
|
244
|
-
return new Store(storeOptions)
|
|
245
|
-
} finally {
|
|
246
|
-
span.end()
|
|
247
|
-
}
|
|
248
257
|
})
|
|
249
258
|
}
|
|
259
|
+
// #endregion constructor
|
|
250
260
|
|
|
251
261
|
get sessionId(): string {
|
|
252
262
|
return this.clientSession.sessionId
|
|
@@ -255,29 +265,66 @@ export class Store<
|
|
|
255
265
|
/**
|
|
256
266
|
* Subscribe to the results of a query
|
|
257
267
|
* Returns a function to cancel the subscription.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* const unsubscribe = store.subscribe(query$, { onUpdate: (result) => console.log(result) })
|
|
272
|
+
* ```
|
|
258
273
|
*/
|
|
259
274
|
subscribe = <TResult>(
|
|
260
|
-
query
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
275
|
+
query: LiveQueryDef<TResult, any> | LiveQuery<TResult, any>,
|
|
276
|
+
options: {
|
|
277
|
+
/** Called when the query result has changed */
|
|
278
|
+
onUpdate: (value: TResult) => void
|
|
279
|
+
onSubscribe?: (query$: LiveQuery<TResult, any>) => void
|
|
280
|
+
/** Gets called after the query subscription has been removed */
|
|
281
|
+
onUnsubsubscribe?: () => void
|
|
282
|
+
label?: string
|
|
283
|
+
/**
|
|
284
|
+
* Skips the initial `onUpdate` callback
|
|
285
|
+
* @default false
|
|
286
|
+
*/
|
|
287
|
+
skipInitialRun?: boolean
|
|
288
|
+
otelContext?: otel.Context
|
|
289
|
+
/** If provided, the stack info will be added to the `activeSubscriptions` set of the query */
|
|
290
|
+
stackInfo?: StackInfo
|
|
291
|
+
},
|
|
292
|
+
): Unsubscribe =>
|
|
265
293
|
this.otel.tracer.startActiveSpan(
|
|
266
294
|
`LiveStore.subscribe`,
|
|
267
|
-
{ attributes: { label: options?.label, queryLabel: query
|
|
295
|
+
{ attributes: { label: options?.label, queryLabel: query.label } },
|
|
268
296
|
options?.otelContext ?? this.otel.queriesSpanContext,
|
|
269
297
|
(span) => {
|
|
270
298
|
// console.debug('store sub', query$.id, query$.label)
|
|
271
299
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
272
300
|
|
|
301
|
+
const queryRcRef =
|
|
302
|
+
query._tag === 'def'
|
|
303
|
+
? query.make(this.reactivityGraph.context!)
|
|
304
|
+
: {
|
|
305
|
+
value: query,
|
|
306
|
+
deref: () => {},
|
|
307
|
+
}
|
|
308
|
+
const query$ = queryRcRef.value
|
|
309
|
+
|
|
273
310
|
const label = `subscribe:${options?.label}`
|
|
274
|
-
const effect = this.reactivityGraph.makeEffect(
|
|
311
|
+
const effect = this.reactivityGraph.makeEffect(
|
|
312
|
+
(get, _otelContext, debugRefreshReason) =>
|
|
313
|
+
options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
|
|
314
|
+
{ label },
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if (options?.stackInfo) {
|
|
318
|
+
query$.activeSubscriptions.add(options.stackInfo)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
options?.onSubscribe?.(query$)
|
|
275
322
|
|
|
276
323
|
this.activeQueries.add(query$ as LiveQuery<TResult>)
|
|
277
324
|
|
|
278
325
|
// Running effect right away to get initial value (unless `skipInitialRun` is set)
|
|
279
|
-
if (options?.skipInitialRun !== true) {
|
|
280
|
-
effect.doEffect(otelContext)
|
|
326
|
+
if (options?.skipInitialRun !== true && !query$.isDestroyed) {
|
|
327
|
+
effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` })
|
|
281
328
|
}
|
|
282
329
|
|
|
283
330
|
const unsubscribe = () => {
|
|
@@ -285,7 +332,14 @@ export class Store<
|
|
|
285
332
|
try {
|
|
286
333
|
this.reactivityGraph.destroyNode(effect)
|
|
287
334
|
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
288
|
-
|
|
335
|
+
|
|
336
|
+
if (options?.stackInfo) {
|
|
337
|
+
query$.activeSubscriptions.delete(options.stackInfo)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
queryRcRef.deref()
|
|
341
|
+
|
|
342
|
+
options?.onUnsubsubscribe?.()
|
|
289
343
|
} finally {
|
|
290
344
|
span.end()
|
|
291
345
|
}
|
|
@@ -295,6 +349,30 @@ export class Store<
|
|
|
295
349
|
},
|
|
296
350
|
)
|
|
297
351
|
|
|
352
|
+
subscribeStream = <TResult>(
|
|
353
|
+
query$: LiveQueryDef<TResult, any>,
|
|
354
|
+
options?: { label?: string; skipInitialRun?: boolean } | undefined,
|
|
355
|
+
): Stream.Stream<TResult> =>
|
|
356
|
+
Stream.asyncPush<TResult>((emit) =>
|
|
357
|
+
Effect.gen(this, function* () {
|
|
358
|
+
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
|
|
359
|
+
Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
|
|
360
|
+
)
|
|
361
|
+
const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
|
|
362
|
+
|
|
363
|
+
yield* Effect.acquireRelease(
|
|
364
|
+
Effect.sync(() =>
|
|
365
|
+
this.subscribe(query$, {
|
|
366
|
+
onUpdate: (result) => emit.single(result),
|
|
367
|
+
otelContext,
|
|
368
|
+
label: options?.label,
|
|
369
|
+
}),
|
|
370
|
+
),
|
|
371
|
+
(unsub) => Effect.sync(() => unsub()),
|
|
372
|
+
)
|
|
373
|
+
}),
|
|
374
|
+
)
|
|
375
|
+
|
|
298
376
|
/**
|
|
299
377
|
* Synchronously queries the database without creating a LiveQuery.
|
|
300
378
|
* This is useful for queries that don't need to be reactive.
|
|
@@ -310,12 +388,15 @@ export class Store<
|
|
|
310
388
|
* ```
|
|
311
389
|
*/
|
|
312
390
|
query = <TResult>(
|
|
313
|
-
query:
|
|
314
|
-
|
|
391
|
+
query:
|
|
392
|
+
| QueryBuilder<TResult, any, any>
|
|
393
|
+
| LiveQuery<TResult, any>
|
|
394
|
+
| LiveQueryDef<TResult, any>
|
|
395
|
+
| { query: string; bindValues: ParamsObject },
|
|
396
|
+
options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
|
|
315
397
|
): TResult => {
|
|
316
398
|
if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
|
|
317
|
-
return this.
|
|
318
|
-
bindValues: prepareBindValues(query.bindValues, query.query),
|
|
399
|
+
return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
|
|
319
400
|
otelContext: options?.otelContext,
|
|
320
401
|
}) as any
|
|
321
402
|
} else if (isQueryBuilder(query)) {
|
|
@@ -331,17 +412,38 @@ export class Store<
|
|
|
331
412
|
|
|
332
413
|
const sqlRes = query.asSql()
|
|
333
414
|
const schema = getResultSchema(query)
|
|
334
|
-
const rawRes = this.
|
|
335
|
-
bindValues: sqlRes.bindValues as any as PreparedBindValues,
|
|
415
|
+
const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
|
|
336
416
|
otelContext: options?.otelContext,
|
|
337
417
|
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
|
338
418
|
})
|
|
339
419
|
return Schema.decodeSync(schema)(rawRes)
|
|
420
|
+
} else if (query._tag === 'def') {
|
|
421
|
+
const query$ = query.make(this.reactivityGraph.context!)
|
|
422
|
+
const result = this.query(query$.value, options)
|
|
423
|
+
query$.deref()
|
|
424
|
+
return result
|
|
340
425
|
} else {
|
|
341
|
-
return query.run(options?.otelContext)
|
|
426
|
+
return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason })
|
|
342
427
|
}
|
|
343
428
|
}
|
|
344
429
|
|
|
430
|
+
// makeLive: {
|
|
431
|
+
// <T>(def: LiveQueryDef<T, any>): LiveQuery<T, any>
|
|
432
|
+
// <T>(def: ILiveQueryRefDef<T>): ILiveQueryRef<T>
|
|
433
|
+
// } = (def: any) => {
|
|
434
|
+
// if (def._tag === 'live-ref-def') {
|
|
435
|
+
// return (def as ILiveQueryRefDef<any>).make(this.reactivityGraph.context!)
|
|
436
|
+
// } else {
|
|
437
|
+
// return (def as LiveQueryDef<any, any>).make(this.reactivityGraph.context!) as any
|
|
438
|
+
// }
|
|
439
|
+
// }
|
|
440
|
+
|
|
441
|
+
setRef = <T>(refDef: ILiveQueryRefDef<T>, value: T): void => {
|
|
442
|
+
const ref = refDef.make(this.reactivityGraph.context!)
|
|
443
|
+
ref.value.set(value)
|
|
444
|
+
ref.deref()
|
|
445
|
+
}
|
|
446
|
+
|
|
345
447
|
// #region mutate
|
|
346
448
|
mutate: {
|
|
347
449
|
<const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
|
|
@@ -369,7 +471,6 @@ export class Store<
|
|
|
369
471
|
|
|
370
472
|
if (mutationsEvents.length === 0) return
|
|
371
473
|
|
|
372
|
-
const label = options?.label ?? 'mutate'
|
|
373
474
|
const skipRefresh = options?.skipRefresh ?? false
|
|
374
475
|
|
|
375
476
|
const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
|
|
@@ -383,40 +484,39 @@ export class Store<
|
|
|
383
484
|
|
|
384
485
|
return this.otel.tracer.startActiveSpan(
|
|
385
486
|
'LiveStore:mutate',
|
|
386
|
-
{
|
|
487
|
+
{
|
|
488
|
+
attributes: {
|
|
489
|
+
'livestore.mutationEventsCount': mutationsEvents.length,
|
|
490
|
+
'livestore.mutationEventTags': mutationsEvents.map((_) => _.mutation),
|
|
491
|
+
'livestore.mutateLabel': options?.label,
|
|
492
|
+
},
|
|
493
|
+
links: options?.spanLinks,
|
|
494
|
+
},
|
|
387
495
|
options?.otelContext ?? this.otel.mutationsSpanContext,
|
|
388
496
|
(span) => {
|
|
389
497
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
390
498
|
|
|
391
499
|
try {
|
|
392
|
-
const { writeTables } =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
|
|
402
|
-
|
|
403
|
-
if (mutationsEvents.length > 1) {
|
|
404
|
-
// TODO: what to do about leader transaction here?
|
|
405
|
-
return this.syncDbWrapper.txn(applyMutations)
|
|
406
|
-
} else {
|
|
407
|
-
return applyMutations()
|
|
408
|
-
}
|
|
409
|
-
} catch (e: any) {
|
|
410
|
-
console.error(e)
|
|
411
|
-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
412
|
-
throw e
|
|
413
|
-
} finally {
|
|
414
|
-
span.end()
|
|
500
|
+
const { writeTables } = (() => {
|
|
501
|
+
try {
|
|
502
|
+
const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
|
|
503
|
+
|
|
504
|
+
if (mutationsEvents.length > 1) {
|
|
505
|
+
// TODO: what to do about leader transaction here?
|
|
506
|
+
return this.sqliteDbWrapper.txn(applyMutations)
|
|
507
|
+
} else {
|
|
508
|
+
return applyMutations()
|
|
415
509
|
}
|
|
416
|
-
}
|
|
417
|
-
|
|
510
|
+
} catch (e: any) {
|
|
511
|
+
console.error(e)
|
|
512
|
+
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
513
|
+
throw e
|
|
514
|
+
} finally {
|
|
515
|
+
span.end()
|
|
516
|
+
}
|
|
517
|
+
})()
|
|
418
518
|
|
|
419
|
-
const tablesToUpdate = [] as [Ref<null,
|
|
519
|
+
const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
|
|
420
520
|
for (const tableName of writeTables) {
|
|
421
521
|
const tableRef = this.tableRefs[tableName]
|
|
422
522
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
@@ -472,41 +572,48 @@ export class Store<
|
|
|
472
572
|
meta: { liveStoreRefType: 'table' },
|
|
473
573
|
})
|
|
474
574
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.pipe(
|
|
575
|
+
/**
|
|
576
|
+
* Helper methods useful during development
|
|
577
|
+
*
|
|
578
|
+
* @internal
|
|
579
|
+
*/
|
|
580
|
+
_dev = {
|
|
581
|
+
downloadDb: (source: 'local' | 'leader' = 'local') => {
|
|
582
|
+
Effect.gen(this, function* () {
|
|
583
|
+
const data = source === 'local' ? this.sqliteDbWrapper.export() : yield* this.clientSession.leaderThread.export
|
|
584
|
+
downloadBlob(data, `livestore-${Date.now()}.db`)
|
|
585
|
+
}).pipe(this.runEffectFork)
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
downloadMutationLogDb: () => {
|
|
589
|
+
Effect.gen(this, function* () {
|
|
590
|
+
const data = yield* this.clientSession.leaderThread.getMutationLogData
|
|
591
|
+
downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
|
|
592
|
+
}).pipe(this.runEffectFork)
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
hardReset: (mode: 'all-data' | 'only-app-db' = 'all-data') => {
|
|
596
|
+
Effect.gen(this, function* () {
|
|
597
|
+
yield* this.clientSession.leaderThread.sendDevtoolsMessage(
|
|
598
|
+
Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }),
|
|
599
|
+
)
|
|
600
|
+
}).pipe(this.runEffectFork)
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
syncStates: () => {
|
|
604
|
+
Effect.gen(this, function* () {
|
|
605
|
+
const session = this.syncProcessor.syncStateRef.current
|
|
606
|
+
console.log('Session sync state:', session.toJSON())
|
|
607
|
+
const leader = yield* this.clientSession.leaderThread.getSyncState
|
|
608
|
+
console.log('Leader sync state:', leader.toJSON())
|
|
609
|
+
}).pipe(this.runEffectFork)
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
shutdown: (cause?: Cause.Cause<UnexpectedError>) => {
|
|
613
|
+
this.clientSession
|
|
614
|
+
.shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
|
|
615
|
+
.pipe(Effect.tapCauseLogPretty, Effect.provide(this.runtime), Effect.runFork)
|
|
616
|
+
},
|
|
510
617
|
}
|
|
511
618
|
|
|
512
619
|
// NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Related https://github.com/facebook/hermes/issues/612#issuecomment-2549404649
|
|
2
|
+
const REACT_NATIVE_BAD_FUNCTION_STRING = 'function() { [bytecode] }'
|
|
3
|
+
|
|
4
|
+
export const isValidFunctionString = (
|
|
5
|
+
fnStr: string,
|
|
6
|
+
): { _tag: 'valid' } | { _tag: 'invalid'; reason: 'react-native' } => {
|
|
7
|
+
if (fnStr === REACT_NATIVE_BAD_FUNCTION_STRING) {
|
|
8
|
+
return { _tag: 'invalid', reason: 'react-native' }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return { _tag: 'valid' }
|
|
12
|
+
}
|
|
@@ -46,7 +46,7 @@ Error
|
|
|
46
46
|
|
|
47
47
|
it('Tracklist_ stacktrace', async () => {
|
|
48
48
|
const stackTrace = `\
|
|
49
|
-
|
|
49
|
+
Error
|
|
50
50
|
at https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1701368568351:19:23
|
|
51
51
|
at mountMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-YKTDXTVC.js?v=86daed82:12817:27)
|
|
52
52
|
at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-YKTDXTVC.js?v=86daed82:13141:24)
|
|
@@ -77,3 +77,60 @@ stack Error
|
|
|
77
77
|
}
|
|
78
78
|
`)
|
|
79
79
|
})
|
|
80
|
+
|
|
81
|
+
it('React 19', async () => {
|
|
82
|
+
const stackTrace = `\
|
|
83
|
+
Error:
|
|
84
|
+
at /Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:57:19
|
|
85
|
+
at mountMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:6816:23)
|
|
86
|
+
at Object.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22757:18)
|
|
87
|
+
at Object.process.env.NODE_ENV.exports.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react@19.0.0/node_modules/react/cjs/react.development.js:1488:34)
|
|
88
|
+
at Module.useQueryRef (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:54:27)
|
|
89
|
+
at Module.useRow (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20)
|
|
90
|
+
at TestComponent (/Users/schickling/Code/overtone/node_modules/.pnpm/@testing-library+react@16.1.0_@testing-library+dom@10.4.0_@types+react-dom@19.0.3_@types+reac_2jaiibiag2sxou3wtzbuqx3r5a/node_modules/@testing-library/react/dist/pure.js:309:27)
|
|
91
|
+
at Object.react-stack-bottom-frame (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22428:20)
|
|
92
|
+
at renderWithHooks (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:5757:22)
|
|
93
|
+
`
|
|
94
|
+
|
|
95
|
+
const stackInfo = extractStackInfoFromStackTrace(stackTrace)
|
|
96
|
+
expect(stackInfo).toMatchInlineSnapshot(`
|
|
97
|
+
{
|
|
98
|
+
"frames": [
|
|
99
|
+
{
|
|
100
|
+
"filePath": "/Users/schickling/Code/overtone/node_modules/.pnpm/@testing-library+react@16.1.0_@testing-library+dom@10.4.0_@types+react-dom@19.0.3_@types+reac_2jaiibiag2sxou3wtzbuqx3r5a/node_modules/@testing-library/react/dist/pure.js:309:27",
|
|
101
|
+
"name": "TestComponent",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"filePath": "/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20",
|
|
105
|
+
"name": "useRow",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
}
|
|
109
|
+
`)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('React 19 - skip react-stack-bottom-frame', async () => {
|
|
113
|
+
const stackTrace = `\
|
|
114
|
+
Error:
|
|
115
|
+
at /Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:57:19
|
|
116
|
+
at mountMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:6816:23)
|
|
117
|
+
at Object.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22757:18)
|
|
118
|
+
at Object.process.env.NODE_ENV.exports.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react@19.0.0/node_modules/react/cjs/react.development.js:1488:34)
|
|
119
|
+
at Module.useQueryRef (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:54:27)
|
|
120
|
+
at Module.useRow (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20)
|
|
121
|
+
at Object.react-stack-bottom-frame (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22428:20)
|
|
122
|
+
at renderWithHooks (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:5757:22)
|
|
123
|
+
`
|
|
124
|
+
|
|
125
|
+
const stackInfo = extractStackInfoFromStackTrace(stackTrace)
|
|
126
|
+
expect(stackInfo).toMatchInlineSnapshot(`
|
|
127
|
+
{
|
|
128
|
+
"frames": [
|
|
129
|
+
{
|
|
130
|
+
"filePath": "/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20",
|
|
131
|
+
"name": "useRow",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
}
|
|
135
|
+
`)
|
|
136
|
+
})
|
package/src/utils/stack-info.ts
CHANGED
|
@@ -34,15 +34,20 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
|
|
|
34
34
|
|
|
35
35
|
while ((match = namePattern.exec(stackTrace)) !== null) {
|
|
36
36
|
const [, name, filePath] = match as any as [string, string, string]
|
|
37
|
+
// console.debug(name, filePath)
|
|
37
38
|
|
|
38
39
|
// NOTE No idea where this `Module.` comes from - possibly a Vite thing?
|
|
39
40
|
if ((name.startsWith('use') || name.startsWith('Module.use')) && name.endsWith('QueryRef') === false) {
|
|
40
41
|
hasReachedStart = true
|
|
42
|
+
// console.debug('hasReachedStart. adding one more frame.')
|
|
41
43
|
|
|
42
44
|
frames.unshift({ name: name.replace(/^Module\./, ''), filePath })
|
|
43
45
|
} else if (hasReachedStart) {
|
|
44
46
|
// We've reached the end of the `use*` functions, so we're adding the component name and stop
|
|
45
|
-
|
|
47
|
+
// Unless it's `react-stack-bottom-frame`, which we skip
|
|
48
|
+
if (name !== 'Object.react-stack-bottom-frame') {
|
|
49
|
+
frames.unshift({ name, filePath })
|
|
50
|
+
}
|
|
46
51
|
break
|
|
47
52
|
}
|
|
48
53
|
}
|