@livestore/livestore 0.3.1-dev.0 → 0.3.2-dev.0

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.
Files changed (55) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts.map +1 -1
  3. package/dist/QueryCache.js +8 -3
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +17 -4
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +14 -6
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.d.ts +2 -0
  10. package/dist/SqliteDbWrapper.test.d.ts.map +1 -0
  11. package/dist/SqliteDbWrapper.test.js +25 -0
  12. package/dist/SqliteDbWrapper.test.js.map +1 -0
  13. package/dist/effect/LiveStore.js +1 -1
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/live-queries/client-document-get-query.js +1 -1
  16. package/dist/live-queries/client-document-get-query.js.map +1 -1
  17. package/dist/live-queries/db-query.js +1 -1
  18. package/dist/live-queries/db-query.js.map +1 -1
  19. package/dist/live-queries/db-query.test.js +91 -1
  20. package/dist/live-queries/db-query.test.js.map +1 -1
  21. package/dist/reactive.js +1 -1
  22. package/dist/reactive.js.map +1 -1
  23. package/dist/store/create-store.js +1 -1
  24. package/dist/store/create-store.js.map +1 -1
  25. package/dist/store/devtools.js +9 -5
  26. package/dist/store/devtools.js.map +1 -1
  27. package/dist/store/store.d.ts +5 -4
  28. package/dist/store/store.d.ts.map +1 -1
  29. package/dist/store/store.js +42 -17
  30. package/dist/store/store.js.map +1 -1
  31. package/dist/utils/stack-info.d.ts.map +1 -1
  32. package/dist/utils/stack-info.js +5 -1
  33. package/dist/utils/stack-info.js.map +1 -1
  34. package/dist/utils/stack-info.test.js +6 -2
  35. package/dist/utils/stack-info.test.js.map +1 -1
  36. package/dist/utils/tests/fixture.d.ts +44 -54
  37. package/dist/utils/tests/fixture.d.ts.map +1 -1
  38. package/dist/utils/tests/otel.js +1 -1
  39. package/dist/utils/tests/otel.js.map +1 -1
  40. package/package.json +7 -7
  41. package/src/QueryCache.ts +9 -4
  42. package/src/SqliteDbWrapper.test.ts +38 -0
  43. package/src/SqliteDbWrapper.ts +24 -15
  44. package/src/effect/LiveStore.ts +1 -1
  45. package/src/live-queries/__snapshots__/db-query.test.ts.snap +389 -0
  46. package/src/live-queries/client-document-get-query.ts +1 -1
  47. package/src/live-queries/db-query.test.ts +144 -1
  48. package/src/live-queries/db-query.ts +1 -1
  49. package/src/reactive.ts +1 -1
  50. package/src/store/create-store.ts +1 -1
  51. package/src/store/devtools.ts +5 -5
  52. package/src/store/store.ts +55 -23
  53. package/src/utils/stack-info.test.ts +6 -2
  54. package/src/utils/stack-info.ts +5 -1
  55. package/src/utils/tests/otel.ts +1 -1
package/src/reactive.ts CHANGED
@@ -279,7 +279,7 @@ export class ReactiveGraph<
279
279
  return compute(atom, otelContext, debugRefreshReason)
280
280
  }
281
281
 
282
- let debugInfo: TDebugThunkInfo | undefined = undefined
282
+ let debugInfo: TDebugThunkInfo | undefined
283
283
  const setDebugInfo = (debugInfo_: TDebugThunkInfo) => {
284
284
  debugInfo = debugInfo_
285
285
  }
@@ -38,7 +38,7 @@ import type {
38
38
  } from './store-types.js'
39
39
 
40
40
  export const DEFAULT_PARAMS = {
41
- leaderPushBatchSize: 1,
41
+ leaderPushBatchSize: 100,
42
42
  }
43
43
 
44
44
  export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
@@ -52,10 +52,10 @@ export const connectDevtoolsToStore = ({
52
52
 
53
53
  yield* Effect.addFinalizer(() =>
54
54
  Effect.sync(() => {
55
- reactivityGraphSubcriptions.forEach((unsub) => unsub())
56
- liveQueriesSubscriptions.forEach((unsub) => unsub())
57
- debugInfoHistorySubscriptions.forEach((unsub) => unsub())
58
- syncHeadClientSessionSubscriptions.forEach((unsub) => unsub())
55
+ for (const unsub of reactivityGraphSubcriptions.values()) unsub()
56
+ for (const unsub of liveQueriesSubscriptions.values()) unsub()
57
+ for (const unsub of debugInfoHistorySubscriptions.values()) unsub()
58
+ for (const unsub of syncHeadClientSessionSubscriptions.values()) unsub()
59
59
  }),
60
60
  )
61
61
 
@@ -202,7 +202,7 @@ export const connectDevtoolsToStore = ({
202
202
  }
203
203
  case 'LSD.ClientSession.DebugInfoRerunQueryReq': {
204
204
  const { queryStr, bindValues, queriedTables } = decodedMessage
205
- store.sqliteDbWrapper.select(queryStr, bindValues, { queriedTables, skipCache: true })
205
+ store.sqliteDbWrapper.cachedSelect(queryStr, bindValues, { queriedTables, skipCache: true })
206
206
  sendToDevtools(
207
207
  Devtools.ClientSession.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
208
208
  )
@@ -1,29 +1,28 @@
1
- import type {
2
- ClientSession,
3
- ClientSessionSyncProcessor,
4
- ParamsObject,
5
- PreparedBindValues,
6
- QueryBuilder,
7
- UnexpectedError,
8
- } from '@livestore/common'
9
1
  import {
2
+ type Bindable,
3
+ type ClientSession,
4
+ type ClientSessionSyncProcessor,
10
5
  Devtools,
11
6
  getDurationMsFromSpan,
12
- getExecArgsFromEvent,
7
+ getExecStatementsFromMaterializer,
13
8
  getResultSchema,
9
+ hashMaterializerResults,
14
10
  IntentionalShutdownCause,
15
11
  isQueryBuilder,
16
12
  liveStoreVersion,
17
13
  makeClientSessionSyncProcessor,
14
+ type PreparedBindValues,
18
15
  prepareBindValues,
16
+ type QueryBuilder,
19
17
  QueryBuilderAstSymbol,
20
18
  replaceSessionIdSymbol,
19
+ UnexpectedError,
21
20
  } from '@livestore/common'
22
21
  import type { LiveStoreSchema } from '@livestore/common/schema'
23
22
  import { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
24
23
  import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils'
25
24
  import type { Scope } from '@livestore/utils/effect'
26
- import { Cause, Effect, Fiber, Inspectable, OtelTracer, Runtime, Schema, Stream } from '@livestore/utils/effect'
25
+ import { Cause, Effect, Fiber, Inspectable, Option, OtelTracer, Runtime, Schema, Stream } from '@livestore/utils/effect'
27
26
  import { nanoid } from '@livestore/utils/nanoid'
28
27
  import * as otel from '@opentelemetry/api'
29
28
 
@@ -36,6 +35,7 @@ import type {
36
35
  } from '../live-queries/base-class.js'
37
36
  import { makeReactivityGraph } from '../live-queries/base-class.js'
38
37
  import { makeExecBeforeFirstRun } from '../live-queries/client-document-get-query.js'
38
+ import { queryDb } from '../live-queries/db-query.js'
39
39
  import type { Ref } from '../reactive.js'
40
40
  import { SqliteDbWrapper } from '../SqliteDbWrapper.js'
41
41
  import { ReferenceCountedSet } from '../utils/data-structures.js'
@@ -114,16 +114,33 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
114
114
  schema,
115
115
  clientSession,
116
116
  runtime: effectContext.runtime,
117
- materializeEvent: (eventDecoded, { otelContext, withChangeset }) => {
117
+ materializeEvent: (eventDecoded, { otelContext, withChangeset, materializerHashLeader }) => {
118
118
  const { eventDef, materializer } = getEventDef(schema, eventDecoded.name)
119
119
 
120
- const execArgsArr = getExecArgsFromEvent({
120
+ const execArgsArr = getExecStatementsFromMaterializer({
121
121
  eventDef,
122
122
  materializer,
123
- db: this.sqliteDbWrapper,
123
+ dbState: this.sqliteDbWrapper,
124
124
  event: { decoded: eventDecoded, encoded: undefined },
125
125
  })
126
126
 
127
+ const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
128
+
129
+ if (
130
+ materializerHashLeader._tag === 'Some' &&
131
+ materializerHash._tag === 'Some' &&
132
+ materializerHashLeader.value !== materializerHash.value
133
+ ) {
134
+ void this.shutdown(
135
+ Cause.fail(
136
+ UnexpectedError.make({
137
+ cause: `Materializer hash mismatch detected for event "${eventDecoded.name}".`,
138
+ note: `Please make sure your event materializer is a pure function without side effects.`,
139
+ }),
140
+ ),
141
+ )
142
+ }
143
+
127
144
  const writeTablesForEvent = new Set<string>()
128
145
 
129
146
  const exec = () => {
@@ -132,12 +149,21 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
132
149
  bindValues,
133
150
  writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
134
151
  } of execArgsArr) {
135
- this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables })
152
+ try {
153
+ this.sqliteDbWrapper.cachedExecute(statementSql, bindValues, { otelContext, writeTables })
154
+ } catch (cause) {
155
+ throw UnexpectedError.make({
156
+ cause,
157
+ note: `Error executing materializer for event "${eventDecoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
158
+ })
159
+ }
136
160
 
137
161
  // durationMsTotal += durationMs
138
162
  for (const table of writeTables) {
139
163
  writeTablesForEvent.add(table)
140
164
  }
165
+
166
+ this.sqliteDbWrapper.debug.head = eventDecoded.seqNum
141
167
  }
142
168
  }
143
169
 
@@ -145,13 +171,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
145
171
  | { _tag: 'sessionChangeset'; data: Uint8Array; debug: any }
146
172
  | { _tag: 'no-op' }
147
173
  | { _tag: 'unset' } = { _tag: 'unset' }
174
+
148
175
  if (withChangeset === true) {
149
176
  sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
150
177
  } else {
151
178
  exec()
152
179
  }
153
180
 
154
- return { writeTables: writeTablesForEvent, sessionChangeset }
181
+ return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
155
182
  },
156
183
  rollback: (changeset) => {
157
184
  this.sqliteDbWrapper.rollback(changeset)
@@ -264,7 +291,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
264
291
  * ```
265
292
  */
266
293
  subscribe = <TResult>(
267
- query: LiveQueryDef<TResult, 'def' | 'signal-def'> | LiveQuery<TResult>,
294
+ query: LiveQueryDef<TResult, 'def' | 'signal-def'> | LiveQuery<TResult> | QueryBuilder<TResult, any, any>,
268
295
  options: {
269
296
  /** Called when the query result has changed */
270
297
  onUpdate: (value: TResult) => void
@@ -284,14 +311,15 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
284
311
  ): Unsubscribe =>
285
312
  this.otel.tracer.startActiveSpan(
286
313
  `LiveStore.subscribe`,
287
- { attributes: { label: options?.label, queryLabel: query.label } },
314
+ { attributes: { label: options?.label, queryLabel: isQueryBuilder(query) ? query.toString() : query.label } },
288
315
  options?.otelContext ?? this.otel.queriesSpanContext,
289
316
  (span) => {
290
317
  // console.debug('store sub', query$.id, query$.label)
291
318
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
292
319
 
293
- const queryRcRef =
294
- query._tag === 'def' || query._tag === 'signal-def'
320
+ const queryRcRef = isQueryBuilder(query)
321
+ ? queryDb(query).make(this.reactivityGraph.context!)
322
+ : query._tag === 'def' || query._tag === 'signal-def'
295
323
  ? query.make(this.reactivityGraph.context!)
296
324
  : {
297
325
  value: query as LiveQuery<TResult>,
@@ -385,13 +413,17 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
385
413
  | LiveQuery<TResult>
386
414
  | LiveQueryDef<TResult>
387
415
  | SignalDef<TResult>
388
- | { query: string; bindValues: ParamsObject },
416
+ | { query: string; bindValues: Bindable; schema?: Schema.Schema<TResult> },
389
417
  options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
390
418
  ): TResult => {
391
419
  if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
392
- return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
420
+ const res = this.sqliteDbWrapper.cachedSelect(query.query, prepareBindValues(query.bindValues, query.query), {
393
421
  otelContext: options?.otelContext,
394
422
  }) as any
423
+ if (query.schema) {
424
+ return Schema.decodeSync(query.schema)(res)
425
+ }
426
+ return res
395
427
  } else if (isQueryBuilder(query)) {
396
428
  const ast = query[QueryBuilderAstSymbol]
397
429
  if (ast._tag === 'RowQuery') {
@@ -405,7 +437,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
405
437
 
406
438
  const sqlRes = query.asSql()
407
439
  const schema = getResultSchema(query)
408
- const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
440
+ const rawRes = this.sqliteDbWrapper.cachedSelect(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
409
441
  otelContext: options?.otelContext,
410
442
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
411
443
  })
@@ -558,12 +590,12 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
558
590
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
559
591
 
560
592
  try {
593
+ // Materialize events to state
561
594
  const { writeTables } = (() => {
562
595
  try {
563
596
  const materializeEvents = () => this.syncProcessor.push(events, { otelContext })
564
597
 
565
598
  if (events.length > 1) {
566
- // TODO: what to do about leader transaction here?
567
599
  return this.sqliteDbWrapper.txn(materializeEvents)
568
600
  } else {
569
601
  return materializeEvents()
@@ -19,7 +19,9 @@ Error
19
19
 
20
20
  const stackInfo = extractStackInfoFromStackTrace(stackTrace)
21
21
  // Replacing file paths for snapshot testing as they are not stable
22
- stackInfo.frames.forEach((_) => (_.filePath = '__REPLACED_FOR_SNAPSHOT__'))
22
+ stackInfo.frames.forEach((_) => {
23
+ _.filePath = '__REPLACED_FOR_SNAPSHOT__'
24
+ })
23
25
  expect(stackInfo).toMatchInlineSnapshot(`
24
26
  {
25
27
  "frames": [
@@ -61,7 +63,9 @@ Error
61
63
 
62
64
  const stackInfo = extractStackInfoFromStackTrace(stackTrace)
63
65
  // Replacing file paths for snapshot testing as they are not stable
64
- stackInfo.frames.forEach((_) => (_.filePath = '__REPLACED_FOR_SNAPSHOT__'))
66
+ stackInfo.frames.forEach((_) => {
67
+ _.filePath = '__REPLACED_FOR_SNAPSHOT__'
68
+ })
65
69
  expect(stackInfo).toMatchInlineSnapshot(`
66
70
  {
67
71
  "frames": [
@@ -32,7 +32,11 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
32
32
  const frames: StackFrame[] = []
33
33
  let hasReachedStart = false
34
34
 
35
- while ((match = namePattern.exec(stackTrace)) !== null) {
35
+ while (true) {
36
+ match = namePattern.exec(stackTrace)
37
+ if (match === null) {
38
+ break
39
+ }
36
40
  const [, name, filePath] = match as any as [string, string, string]
37
41
  // console.debug(name, filePath)
38
42
 
@@ -24,7 +24,7 @@ export const getSimplifiedRootSpan = (
24
24
  const createStoreSpanData = spans.find((_) => _.name === 'createStore')
25
25
  if (createStoreSpanData === undefined) {
26
26
  throw new Error(
27
- "Could not find the root span named 'createStore'. Available spans: " + spans.map((s) => s.name).join(', '),
27
+ `Could not find the root span named 'createStore'. Available spans: ${spans.map((s) => s.name).join(', ')}`,
28
28
  )
29
29
  }
30
30
  const rootSpan = spansMap.get(createStoreSpanData.spanContext().spanId)!