@livestore/livestore 0.2.0 → 0.3.0-dev.10
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/SynchronousDatabaseWrapper.d.ts +6 -1
- package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
- package/dist/SynchronousDatabaseWrapper.js +14 -2
- package/dist/SynchronousDatabaseWrapper.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +252 -0
- package/dist/__tests__/fixture.d.ts.map +1 -0
- package/dist/__tests__/fixture.js +18 -0
- package/dist/__tests__/fixture.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 +5 -12
- package/dist/effect/LiveStore.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/live-queries/db.d.ts.map +1 -1
- package/dist/live-queries/db.js +28 -23
- package/dist/live-queries/db.js.map +1 -1
- package/dist/live-queries/db.test.js +2 -1
- package/dist/live-queries/db.test.js.map +1 -1
- package/dist/row-query-utils.js +1 -1
- package/dist/row-query-utils.js.map +1 -1
- package/dist/store/create-store.d.ts +12 -10
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +22 -28
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +41 -19
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-types.d.ts +9 -14
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store.d.ts +29 -28
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +147 -160
- package/dist/store/store.js.map +1 -1
- package/dist/store/store.test.d.ts +2 -0
- package/dist/store/store.test.d.ts.map +1 -0
- package/dist/store/store.test.js +27 -0
- package/dist/store/store.test.js.map +1 -0
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js +3 -2
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +4 -8
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts +60 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +65 -4
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +12 -12
- package/src/SynchronousDatabaseWrapper.ts +18 -2
- package/src/ambient.d.ts +1 -1
- package/src/effect/LiveStore.ts +11 -20
- package/src/index.ts +1 -1
- package/src/live-queries/__snapshots__/db.test.ts.snap +42 -45
- package/src/live-queries/db.test.ts +2 -1
- package/src/live-queries/db.ts +28 -23
- package/src/row-query-utils.ts +1 -1
- package/src/store/create-store.ts +115 -119
- package/src/store/devtools.ts +48 -22
- package/src/store/store-types.ts +14 -14
- package/src/store/store.ts +188 -224
- package/src/utils/dev.ts +4 -2
- package/src/utils/tests/fixture.ts +4 -9
- package/src/utils/tests/otel.ts +71 -5
- package/dist/live-queries/sql.d.ts +0 -62
- package/dist/live-queries/sql.d.ts.map +0 -1
- package/dist/live-queries/sql.js +0 -175
- package/dist/live-queries/sql.js.map +0 -1
- package/dist/live-queries/sql.test.d.ts +0 -2
- package/dist/live-queries/sql.test.d.ts.map +0 -1
- package/dist/live-queries/sql.test.js +0 -285
- package/dist/live-queries/sql.test.js.map +0 -1
- package/dist/reactiveQueries/base-class.d.ts +0 -64
- package/dist/reactiveQueries/base-class.d.ts.map +0 -1
- package/dist/reactiveQueries/base-class.js +0 -31
- package/dist/reactiveQueries/base-class.js.map +0 -1
- package/dist/reactiveQueries/computed.d.ts +0 -26
- package/dist/reactiveQueries/computed.d.ts.map +0 -1
- package/dist/reactiveQueries/computed.js +0 -38
- package/dist/reactiveQueries/computed.js.map +0 -1
- package/dist/reactiveQueries/graphql.d.ts +0 -49
- package/dist/reactiveQueries/graphql.d.ts.map +0 -1
- package/dist/reactiveQueries/graphql.js +0 -122
- package/dist/reactiveQueries/graphql.js.map +0 -1
- package/dist/reactiveQueries/sql.d.ts +0 -62
- package/dist/reactiveQueries/sql.d.ts.map +0 -1
- package/dist/reactiveQueries/sql.js +0 -175
- package/dist/reactiveQueries/sql.js.map +0 -1
- package/dist/reactiveQueries/sql.test.d.ts +0 -2
- package/dist/reactiveQueries/sql.test.d.ts.map +0 -1
- package/dist/reactiveQueries/sql.test.js +0 -285
- package/dist/reactiveQueries/sql.test.js.map +0 -1
- package/dist/row-query.d.ts +0 -16
- package/dist/row-query.d.ts.map +0 -1
- package/dist/row-query.js +0 -30
- package/dist/row-query.js.map +0 -1
package/src/store/store.ts
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ClientSession,
|
|
3
|
+
ClientSessionSyncProcessor,
|
|
4
|
+
ParamsObject,
|
|
5
|
+
PreparedBindValues,
|
|
6
|
+
QueryBuilder,
|
|
7
|
+
UnexpectedError,
|
|
8
|
+
} from '@livestore/common'
|
|
2
9
|
import {
|
|
10
|
+
Devtools,
|
|
3
11
|
getExecArgsFromMutation,
|
|
4
12
|
getResultSchema,
|
|
13
|
+
IntentionalShutdownCause,
|
|
5
14
|
isQueryBuilder,
|
|
15
|
+
liveStoreVersion,
|
|
16
|
+
makeClientSessionSyncProcessor,
|
|
6
17
|
prepareBindValues,
|
|
7
18
|
QueryBuilderAstSymbol,
|
|
8
19
|
replaceSessionIdSymbol,
|
|
9
20
|
} from '@livestore/common'
|
|
10
|
-
import type { LiveStoreSchema
|
|
21
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
11
22
|
import {
|
|
12
|
-
|
|
13
|
-
makeMutationEventSchemaMemo,
|
|
23
|
+
MutationEvent,
|
|
14
24
|
SCHEMA_META_TABLE,
|
|
15
25
|
SCHEMA_MUTATIONS_META_TABLE,
|
|
16
26
|
SESSION_CHANGESET_META_TABLE,
|
|
17
27
|
} from '@livestore/common/schema'
|
|
18
|
-
import { assertNever,
|
|
28
|
+
import { assertNever, isDevEnv } from '@livestore/utils'
|
|
19
29
|
import type { Scope } from '@livestore/utils/effect'
|
|
20
|
-
import { Data, Effect,
|
|
30
|
+
import { Cause, Data, Effect, Inspectable, MutableHashMap, Runtime, Schema } from '@livestore/utils/effect'
|
|
31
|
+
import { nanoid } from '@livestore/utils/nanoid'
|
|
21
32
|
import * as otel from '@opentelemetry/api'
|
|
22
|
-
import type
|
|
33
|
+
import { type GraphQLSchema } from 'graphql'
|
|
23
34
|
|
|
24
35
|
import type { LiveQuery, QueryContext, ReactivityGraph } from '../live-queries/base-class.js'
|
|
25
36
|
import type { Ref } from '../reactive.js'
|
|
@@ -30,7 +41,7 @@ import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
|
|
|
30
41
|
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
31
42
|
import type { BaseGraphQLContext, RefreshReason, StoreMutateOptions, StoreOptions, StoreOtel } from './store-types.js'
|
|
32
43
|
|
|
33
|
-
if (
|
|
44
|
+
if (isDevEnv()) {
|
|
34
45
|
exposeDebugUtils()
|
|
35
46
|
}
|
|
36
47
|
|
|
@@ -52,7 +63,6 @@ export class Store<
|
|
|
52
63
|
*/
|
|
53
64
|
tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
|
|
54
65
|
|
|
55
|
-
private fiberSet: FiberSet.FiberSet
|
|
56
66
|
private runtime: Runtime.Runtime<Scope.Scope>
|
|
57
67
|
|
|
58
68
|
/** RC-based set to see which queries are currently subscribed to */
|
|
@@ -61,6 +71,8 @@ export class Store<
|
|
|
61
71
|
// NOTE this is currently exposed for the Devtools databrowser to emit mutation events
|
|
62
72
|
readonly __mutationEventSchema
|
|
63
73
|
private unsyncedMutationEvents
|
|
74
|
+
private syncProcessor: ClientSessionSyncProcessor
|
|
75
|
+
readonly lifetimeScope: Scope.Scope
|
|
64
76
|
|
|
65
77
|
// #region constructor
|
|
66
78
|
private constructor({
|
|
@@ -73,24 +85,80 @@ export class Store<
|
|
|
73
85
|
batchUpdates,
|
|
74
86
|
unsyncedMutationEvents,
|
|
75
87
|
storeId,
|
|
76
|
-
|
|
88
|
+
lifetimeScope,
|
|
77
89
|
runtime,
|
|
78
90
|
}: StoreOptions<TGraphQLContext, TSchema>) {
|
|
79
91
|
super()
|
|
80
92
|
|
|
81
93
|
this.storeId = storeId
|
|
82
|
-
|
|
83
94
|
this.unsyncedMutationEvents = unsyncedMutationEvents
|
|
84
95
|
|
|
85
96
|
this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb })
|
|
86
97
|
this.clientSession = clientSession
|
|
87
98
|
this.schema = schema
|
|
88
99
|
|
|
89
|
-
this.
|
|
100
|
+
this.lifetimeScope = lifetimeScope
|
|
90
101
|
this.runtime = runtime
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
|
|
104
|
+
|
|
105
|
+
this.syncProcessor = makeClientSessionSyncProcessor({
|
|
106
|
+
schema,
|
|
107
|
+
initialLeaderHead: clientSession.leaderThread.mutations.initialMutationEventId,
|
|
108
|
+
pushToLeader: (batch) =>
|
|
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,
|
|
115
|
+
applyMutation: (mutationEventDecoded, { otelContext, withChangeset }) => {
|
|
116
|
+
const mutationDef = schema.mutations.get(mutationEventDecoded.mutation)!
|
|
117
|
+
const execArgsArr = getExecArgsFromMutation({
|
|
118
|
+
mutationDef,
|
|
119
|
+
mutationEvent: { decoded: mutationEventDecoded, encoded: undefined },
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const writeTablesForEvent = new Set<string>()
|
|
123
|
+
|
|
124
|
+
const exec = () => {
|
|
125
|
+
for (const {
|
|
126
|
+
statementSql,
|
|
127
|
+
bindValues,
|
|
128
|
+
writeTables = this.syncDbWrapper.getTablesUsed(statementSql),
|
|
129
|
+
} of execArgsArr) {
|
|
130
|
+
this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext })
|
|
131
|
+
|
|
132
|
+
// durationMsTotal += durationMs
|
|
133
|
+
writeTables.forEach((table) => writeTablesForEvent.add(table))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let sessionChangeset: Uint8Array | undefined
|
|
138
|
+
if (withChangeset === true) {
|
|
139
|
+
sessionChangeset = this.syncDbWrapper.withChangeset(exec).changeset
|
|
140
|
+
} else {
|
|
141
|
+
exec()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { writeTables: writeTablesForEvent, sessionChangeset }
|
|
145
|
+
},
|
|
146
|
+
rollback: (changeset) => {
|
|
147
|
+
this.syncDbWrapper.rollback(changeset)
|
|
148
|
+
},
|
|
149
|
+
refreshTables: (tables) => {
|
|
150
|
+
const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
|
|
151
|
+
for (const tableName of tables) {
|
|
152
|
+
const tableRef = this.tableRefs[tableName]
|
|
153
|
+
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
154
|
+
tablesToUpdate.push([tableRef!, null])
|
|
155
|
+
}
|
|
156
|
+
this.reactivityGraph.setRefs(tablesToUpdate)
|
|
157
|
+
},
|
|
158
|
+
span: syncSpan,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
this.__mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
|
|
94
162
|
|
|
95
163
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
|
96
164
|
this.tableRefs = {}
|
|
@@ -141,26 +209,10 @@ export class Store<
|
|
|
141
209
|
|
|
142
210
|
if (graphQLOptions) {
|
|
143
211
|
this.graphQLSchema = graphQLOptions.schema
|
|
144
|
-
this.graphQLContext = graphQLOptions.makeContext(
|
|
145
|
-
this.syncDbWrapper,
|
|
146
|
-
this.otel.tracer,
|
|
147
|
-
clientSession.coordinator.sessionId,
|
|
148
|
-
)
|
|
212
|
+
this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer, clientSession.sessionId)
|
|
149
213
|
}
|
|
150
214
|
|
|
151
215
|
Effect.gen(this, function* () {
|
|
152
|
-
yield* this.clientSession.coordinator.syncMutations.pipe(
|
|
153
|
-
Stream.tapChunk((mutationsEventsDecodedChunk) =>
|
|
154
|
-
Effect.sync(() => {
|
|
155
|
-
this.mutate({ wasSyncMessage: true }, ...mutationsEventsDecodedChunk)
|
|
156
|
-
}),
|
|
157
|
-
),
|
|
158
|
-
Stream.runDrain,
|
|
159
|
-
Effect.interruptible,
|
|
160
|
-
Effect.withSpan('LiveStore:syncMutations'),
|
|
161
|
-
Effect.forkScoped,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
216
|
yield* Effect.addFinalizer(() =>
|
|
165
217
|
Effect.sync(() => {
|
|
166
218
|
// Remove all table refs from the reactivity graph
|
|
@@ -171,13 +223,14 @@ export class Store<
|
|
|
171
223
|
}
|
|
172
224
|
|
|
173
225
|
// End the otel spans
|
|
174
|
-
|
|
175
|
-
|
|
226
|
+
syncSpan.end()
|
|
227
|
+
mutationsSpan.end()
|
|
228
|
+
queriesSpan.end()
|
|
176
229
|
}),
|
|
177
230
|
)
|
|
178
231
|
|
|
179
|
-
yield*
|
|
180
|
-
}).pipe(
|
|
232
|
+
yield* this.syncProcessor.boot
|
|
233
|
+
}).pipe(this.runEffectFork)
|
|
181
234
|
}
|
|
182
235
|
// #endregion constructor
|
|
183
236
|
|
|
@@ -196,7 +249,7 @@ export class Store<
|
|
|
196
249
|
}
|
|
197
250
|
|
|
198
251
|
get sessionId(): string {
|
|
199
|
-
return this.clientSession.
|
|
252
|
+
return this.clientSession.sessionId
|
|
200
253
|
}
|
|
201
254
|
|
|
202
255
|
/**
|
|
@@ -242,6 +295,20 @@ export class Store<
|
|
|
242
295
|
},
|
|
243
296
|
)
|
|
244
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Synchronously queries the database without creating a LiveQuery.
|
|
300
|
+
* This is useful for queries that don't need to be reactive.
|
|
301
|
+
*
|
|
302
|
+
* Example: Query builder
|
|
303
|
+
* ```ts
|
|
304
|
+
* const completedTodos = store.query(tables.todo.where({ complete: true }))
|
|
305
|
+
* ```
|
|
306
|
+
*
|
|
307
|
+
* Example: Raw SQL query
|
|
308
|
+
* ```ts
|
|
309
|
+
* const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
245
312
|
query = <TResult>(
|
|
246
313
|
query: QueryBuilder<TResult, any, any> | LiveQuery<TResult, any> | { query: string; bindValues: ParamsObject },
|
|
247
314
|
options?: { otelContext?: otel.Context },
|
|
@@ -294,39 +361,16 @@ export class Store<
|
|
|
294
361
|
) => void,
|
|
295
362
|
): void
|
|
296
363
|
} = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
|
|
297
|
-
|
|
298
|
-
let options: StoreMutateOptions | undefined
|
|
364
|
+
const { mutationsEvents, options } = this.getMutateArgs(firstMutationOrTxnFnOrOptions, restMutations)
|
|
299
365
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
|
|
303
|
-
} else if (
|
|
304
|
-
firstMutationOrTxnFnOrOptions?.label !== undefined ||
|
|
305
|
-
firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
|
|
306
|
-
firstMutationOrTxnFnOrOptions?.wasSyncMessage !== undefined ||
|
|
307
|
-
firstMutationOrTxnFnOrOptions?.persisted !== undefined
|
|
308
|
-
) {
|
|
309
|
-
options = firstMutationOrTxnFnOrOptions
|
|
310
|
-
mutationsEvents = restMutations
|
|
311
|
-
} else if (firstMutationOrTxnFnOrOptions === undefined) {
|
|
312
|
-
// When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
|
|
313
|
-
mutationsEvents = []
|
|
314
|
-
} else {
|
|
315
|
-
mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
|
|
366
|
+
for (const mutationEvent of mutationsEvents) {
|
|
367
|
+
replaceSessionIdSymbol(mutationEvent.args, this.clientSession.sessionId)
|
|
316
368
|
}
|
|
317
369
|
|
|
318
|
-
mutationsEvents
|
|
319
|
-
(_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
if (mutationsEvents.length === 0) {
|
|
323
|
-
return
|
|
324
|
-
}
|
|
370
|
+
if (mutationsEvents.length === 0) return
|
|
325
371
|
|
|
326
372
|
const label = options?.label ?? 'mutate'
|
|
327
373
|
const skipRefresh = options?.skipRefresh ?? false
|
|
328
|
-
const wasSyncMessage = options?.wasSyncMessage ?? false
|
|
329
|
-
const persisted = options?.persisted ?? true
|
|
330
374
|
|
|
331
375
|
const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
|
|
332
376
|
mutationsSpan.addEvent('mutate')
|
|
@@ -337,47 +381,30 @@ export class Store<
|
|
|
337
381
|
|
|
338
382
|
let durationMs: number
|
|
339
383
|
|
|
340
|
-
|
|
384
|
+
return this.otel.tracer.startActiveSpan(
|
|
341
385
|
'LiveStore:mutate',
|
|
342
|
-
{ attributes: { 'livestore.mutateLabel': label } },
|
|
343
|
-
this.otel.mutationsSpanContext,
|
|
386
|
+
{ attributes: { 'livestore.mutateLabel': label }, links: options?.spanLinks },
|
|
387
|
+
options?.otelContext ?? this.otel.mutationsSpanContext,
|
|
344
388
|
(span) => {
|
|
345
389
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
346
390
|
|
|
347
391
|
try {
|
|
348
|
-
const writeTables
|
|
349
|
-
|
|
350
|
-
this.otel.tracer.startActiveSpan(
|
|
351
|
-
'LiveStore:processWrites',
|
|
392
|
+
const { writeTables } = this.otel.tracer.startActiveSpan(
|
|
393
|
+
'LiveStore:mutate:applyMutations',
|
|
352
394
|
{ attributes: { 'livestore.mutateLabel': label } },
|
|
353
395
|
otel.trace.setSpan(otel.context.active(), span),
|
|
354
396
|
(span) => {
|
|
355
397
|
try {
|
|
356
398
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
399
|
+
// 5
|
|
357
400
|
|
|
358
|
-
const applyMutations = () => {
|
|
359
|
-
for (const mutationEvent of mutationsEvents) {
|
|
360
|
-
try {
|
|
361
|
-
const { writeTables: writeTablesForEvent } = this.mutateWithoutRefresh(mutationEvent, {
|
|
362
|
-
otelContext,
|
|
363
|
-
// NOTE if it was a sync message, it's already coming from the coordinator, so we can skip the coordinator
|
|
364
|
-
coordinatorMode: wasSyncMessage ? 'skip-coordinator' : persisted ? 'default' : 'skip-persist',
|
|
365
|
-
})
|
|
366
|
-
for (const tableName of writeTablesForEvent) {
|
|
367
|
-
writeTables.add(tableName)
|
|
368
|
-
}
|
|
369
|
-
} catch (e: any) {
|
|
370
|
-
console.error(e, mutationEvent)
|
|
371
|
-
throw e
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
401
|
+
const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
|
|
375
402
|
|
|
376
403
|
if (mutationsEvents.length > 1) {
|
|
377
|
-
// TODO: what to do about
|
|
378
|
-
this.syncDbWrapper.txn(applyMutations)
|
|
404
|
+
// TODO: what to do about leader transaction here?
|
|
405
|
+
return this.syncDbWrapper.txn(applyMutations)
|
|
379
406
|
} else {
|
|
380
|
-
applyMutations()
|
|
407
|
+
return applyMutations()
|
|
381
408
|
}
|
|
382
409
|
} catch (e: any) {
|
|
383
410
|
console.error(e)
|
|
@@ -417,16 +444,6 @@ export class Store<
|
|
|
417
444
|
return { durationMs }
|
|
418
445
|
},
|
|
419
446
|
)
|
|
420
|
-
|
|
421
|
-
// NOTE we need to add the mutation events to the unsynced mutation events map only after running the code above
|
|
422
|
-
// so the short-circuiting in `mutateWithoutRefresh` doesn't kick in for those events
|
|
423
|
-
for (const mutationEvent of mutationsEvents) {
|
|
424
|
-
if (mutationEvent.id !== undefined) {
|
|
425
|
-
MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEvent.id), mutationEvent)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return res
|
|
430
447
|
}
|
|
431
448
|
// #endregion mutate
|
|
432
449
|
|
|
@@ -448,117 +465,6 @@ export class Store<
|
|
|
448
465
|
)
|
|
449
466
|
}
|
|
450
467
|
|
|
451
|
-
// #region mutateWithoutRefresh
|
|
452
|
-
/**
|
|
453
|
-
* Apply a mutation to the store.
|
|
454
|
-
* Returns the tables that were affected by the event.
|
|
455
|
-
* This is an internal method that doesn't trigger a refresh;
|
|
456
|
-
* the caller must refresh queries after calling this method.
|
|
457
|
-
*/
|
|
458
|
-
mutateWithoutRefresh = (
|
|
459
|
-
mutationEventDecoded_: MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>,
|
|
460
|
-
options: {
|
|
461
|
-
otelContext: otel.Context
|
|
462
|
-
// TODO adjust `skip-persist` with new rebase sync strategy
|
|
463
|
-
coordinatorMode: 'default' | 'skip-coordinator' | 'skip-persist'
|
|
464
|
-
},
|
|
465
|
-
): { writeTables: ReadonlySet<string>; durationMs: number } => {
|
|
466
|
-
const mutationDef =
|
|
467
|
-
this.schema.mutations.get(mutationEventDecoded_.mutation) ??
|
|
468
|
-
shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
|
|
469
|
-
|
|
470
|
-
// Needs to happen only for partial mutation events (thus a function)
|
|
471
|
-
const nextMutationEventId = () => {
|
|
472
|
-
const { id, parentId } = this.clientSession.coordinator
|
|
473
|
-
.nextMutationEventIdPair({ localOnly: mutationDef.options.localOnly })
|
|
474
|
-
.pipe(Effect.runSync)
|
|
475
|
-
|
|
476
|
-
return { id, parentId }
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const mutationEventDecoded: MutationEvent.ForSchema<TSchema> = isPartialMutationEvent(mutationEventDecoded_)
|
|
480
|
-
? { ...mutationEventDecoded_, ...nextMutationEventId() }
|
|
481
|
-
: mutationEventDecoded_
|
|
482
|
-
|
|
483
|
-
// NOTE we also need this temporary workaround here since some code-paths use `mutateWithoutRefresh` directly
|
|
484
|
-
// e.g. the row-query functionality
|
|
485
|
-
if (MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id))) {
|
|
486
|
-
// NOTE this data should never be used
|
|
487
|
-
return { writeTables: new Set(), durationMs: 0 }
|
|
488
|
-
} else {
|
|
489
|
-
MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const { otelContext, coordinatorMode = 'default' } = options
|
|
493
|
-
|
|
494
|
-
return this.otel.tracer.startActiveSpan(
|
|
495
|
-
'LiveStore:mutateWithoutRefresh',
|
|
496
|
-
{
|
|
497
|
-
attributes: {
|
|
498
|
-
'livestore.mutation': mutationEventDecoded.mutation,
|
|
499
|
-
'livestore.args': JSON.stringify(mutationEventDecoded.args, null, 2),
|
|
500
|
-
},
|
|
501
|
-
},
|
|
502
|
-
otelContext,
|
|
503
|
-
(span) => {
|
|
504
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
505
|
-
|
|
506
|
-
const allWriteTables = new Set<string>()
|
|
507
|
-
let durationMsTotal = 0
|
|
508
|
-
|
|
509
|
-
replaceSessionIdSymbol(mutationEventDecoded.args, this.clientSession.coordinator.sessionId)
|
|
510
|
-
|
|
511
|
-
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
512
|
-
|
|
513
|
-
for (const {
|
|
514
|
-
statementSql,
|
|
515
|
-
bindValues,
|
|
516
|
-
writeTables = this.syncDbWrapper.getTablesUsed(statementSql),
|
|
517
|
-
} of execArgsArr) {
|
|
518
|
-
// TODO when the store doesn't have the lock, we need wait for the coordinator to confirm the mutation
|
|
519
|
-
// before executing the mutation on the main db
|
|
520
|
-
const { durationMs } = this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext })
|
|
521
|
-
|
|
522
|
-
durationMsTotal += durationMs
|
|
523
|
-
writeTables.forEach((table) => allWriteTables.add(table))
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const mutationEventEncoded = Schema.encodeUnknownSync(this.__mutationEventSchema)(mutationEventDecoded)
|
|
527
|
-
|
|
528
|
-
if (coordinatorMode !== 'skip-coordinator') {
|
|
529
|
-
// Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
|
|
530
|
-
this.clientSession.coordinator
|
|
531
|
-
.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
|
|
532
|
-
.pipe(this.runEffectFork)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Uncomment to print a list of queries currently registered on the store
|
|
536
|
-
// console.debug(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
|
|
537
|
-
|
|
538
|
-
span.end()
|
|
539
|
-
|
|
540
|
-
return { writeTables: allWriteTables, durationMs: durationMsTotal }
|
|
541
|
-
},
|
|
542
|
-
)
|
|
543
|
-
}
|
|
544
|
-
// #endregion mutateWithoutRefresh
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Directly execute a SQL query on the Store.
|
|
548
|
-
* This should only be used for framework-internal purposes;
|
|
549
|
-
* all app writes should go through mutate.
|
|
550
|
-
*/
|
|
551
|
-
__execute = (
|
|
552
|
-
query: string,
|
|
553
|
-
params: ParamsObject = {},
|
|
554
|
-
writeTables?: ReadonlySet<string>,
|
|
555
|
-
otelContext?: otel.Context,
|
|
556
|
-
) => {
|
|
557
|
-
this.syncDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
|
|
558
|
-
|
|
559
|
-
this.clientSession.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
|
|
560
|
-
}
|
|
561
|
-
|
|
562
468
|
private makeTableRef = (tableName: string) =>
|
|
563
469
|
this.reactivityGraph.makeRef(null, {
|
|
564
470
|
equal: () => false,
|
|
@@ -566,27 +472,85 @@ export class Store<
|
|
|
566
472
|
meta: { liveStoreRefType: 'table' },
|
|
567
473
|
})
|
|
568
474
|
|
|
569
|
-
__devDownloadDb = () => {
|
|
570
|
-
|
|
571
|
-
|
|
475
|
+
__devDownloadDb = (source: 'local' | 'leader' = 'local') => {
|
|
476
|
+
Effect.gen(this, function* () {
|
|
477
|
+
const data = source === 'local' ? this.syncDbWrapper.export() : yield* this.clientSession.leaderThread.export
|
|
478
|
+
downloadBlob(data, `livestore-${Date.now()}.db`)
|
|
479
|
+
}).pipe(this.runEffectFork)
|
|
572
480
|
}
|
|
573
481
|
|
|
574
|
-
__devDownloadMutationLogDb = () =>
|
|
482
|
+
__devDownloadMutationLogDb = () => {
|
|
575
483
|
Effect.gen(this, function* () {
|
|
576
|
-
const data = yield* this.clientSession.
|
|
484
|
+
const data = yield* this.clientSession.leaderThread.getMutationLogData
|
|
577
485
|
downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
|
|
578
486
|
}).pipe(this.runEffectFork)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
__devHardReset = (mode: 'all-data' | 'only-app-db' = 'all-data') => {
|
|
490
|
+
Effect.gen(this, function* () {
|
|
491
|
+
yield* this.clientSession.leaderThread.sendDevtoolsMessage(
|
|
492
|
+
Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }),
|
|
493
|
+
)
|
|
494
|
+
}).pipe(this.runEffectFork)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
__devSyncStates = () => {
|
|
498
|
+
Effect.gen(this, function* () {
|
|
499
|
+
const session = this.syncProcessor.syncStateRef.current
|
|
500
|
+
console.log('Session sync state:', session.toJSON())
|
|
501
|
+
const leader = yield* this.clientSession.leaderThread.getSyncState
|
|
502
|
+
console.log('Leader sync state:', leader.toJSON())
|
|
503
|
+
}).pipe(this.runEffectFork)
|
|
504
|
+
}
|
|
579
505
|
|
|
580
|
-
|
|
506
|
+
__devShutdown = (cause?: Cause.Cause<UnexpectedError>) => {
|
|
507
|
+
this.clientSession
|
|
508
|
+
.shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
|
|
509
|
+
.pipe(Effect.tapCauseLogPretty, Effect.provide(this.runtime), Effect.runFork)
|
|
510
|
+
}
|
|
581
511
|
|
|
582
512
|
// NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
|
|
583
|
-
toJSON = () => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
513
|
+
toJSON = () => ({
|
|
514
|
+
_tag: 'livestore.Store',
|
|
515
|
+
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
private runEffectFork = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
|
519
|
+
effect.pipe(Effect.forkIn(this.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.runtime))
|
|
520
|
+
|
|
521
|
+
private getMutateArgs = (
|
|
522
|
+
firstMutationOrTxnFnOrOptions: any,
|
|
523
|
+
restMutations: any[],
|
|
524
|
+
): {
|
|
525
|
+
mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
|
|
526
|
+
options: StoreMutateOptions | undefined
|
|
527
|
+
} => {
|
|
528
|
+
let mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
|
|
529
|
+
let options: StoreMutateOptions | undefined
|
|
530
|
+
|
|
531
|
+
if (typeof firstMutationOrTxnFnOrOptions === 'function') {
|
|
532
|
+
// TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
|
|
533
|
+
mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
|
|
534
|
+
} else if (
|
|
535
|
+
firstMutationOrTxnFnOrOptions?.label !== undefined ||
|
|
536
|
+
firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
|
|
537
|
+
firstMutationOrTxnFnOrOptions?.otelContext !== undefined ||
|
|
538
|
+
firstMutationOrTxnFnOrOptions?.spanLinks !== undefined
|
|
539
|
+
) {
|
|
540
|
+
options = firstMutationOrTxnFnOrOptions
|
|
541
|
+
mutationsEvents = restMutations
|
|
542
|
+
} else if (firstMutationOrTxnFnOrOptions === undefined) {
|
|
543
|
+
// When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
|
|
544
|
+
mutationsEvents = []
|
|
545
|
+
} else {
|
|
546
|
+
mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
|
|
587
547
|
}
|
|
588
|
-
}
|
|
589
548
|
|
|
590
|
-
|
|
591
|
-
|
|
549
|
+
mutationsEvents = mutationsEvents.filter(
|
|
550
|
+
// @ts-expect-error TODO
|
|
551
|
+
(_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
return { mutationsEvents, options }
|
|
555
|
+
}
|
|
592
556
|
}
|
package/src/utils/dev.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isDevEnv } from '@livestore/utils'
|
|
2
|
+
|
|
1
3
|
/* eslint-disable unicorn/prefer-global-this */
|
|
2
4
|
export const downloadBlob = (
|
|
3
5
|
data: Uint8Array | Blob | string,
|
|
@@ -24,7 +26,7 @@ export const downloadURL = (data: string, fileName: string) => {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export const exposeDebugUtils = () => {
|
|
27
|
-
if (
|
|
28
|
-
globalThis.
|
|
29
|
+
if (isDevEnv()) {
|
|
30
|
+
globalThis.__debugLiveStoreUtils = { downloadBlob }
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { provideOtel } from '@livestore/common'
|
|
1
2
|
import type { FromInputSchema } from '@livestore/common/schema'
|
|
2
3
|
import type { Store } from '@livestore/livestore'
|
|
3
4
|
import { createStore, DbSchema, globalReactivityGraph, makeReactivityGraph, makeSchema } from '@livestore/livestore'
|
|
4
|
-
import { Effect
|
|
5
|
+
import { Effect } from '@livestore/utils/effect'
|
|
5
6
|
import { makeInMemoryAdapter } from '@livestore/web'
|
|
6
7
|
import type * as otel from '@opentelemetry/api'
|
|
7
8
|
|
|
@@ -55,19 +56,13 @@ export const makeTodoMvc = ({
|
|
|
55
56
|
Effect.gen(function* () {
|
|
56
57
|
const reactivityGraph = useGlobalReactivityGraph ? globalReactivityGraph : makeReactivityGraph()
|
|
57
58
|
|
|
58
|
-
const fiberSet = yield* FiberSet.make()
|
|
59
|
-
|
|
60
59
|
const store: Store<any, FixtureSchema> = yield* createStore({
|
|
61
60
|
schema,
|
|
62
61
|
storeId: 'default',
|
|
63
62
|
adapter: makeInMemoryAdapter(),
|
|
64
63
|
reactivityGraph,
|
|
65
|
-
|
|
66
|
-
tracer: otelTracer,
|
|
67
|
-
rootSpanContext: otelContext,
|
|
68
|
-
},
|
|
69
|
-
fiberSet,
|
|
64
|
+
debug: { instanceId: 'test' },
|
|
70
65
|
})
|
|
71
66
|
|
|
72
67
|
return { store, reactivityGraph }
|
|
73
|
-
})
|
|
68
|
+
}).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer: otelTracer }))
|