@livestore/livestore 0.0.0-snapshot-abe9ae4963ab9d3948906a6642c39bc33295e9f6 → 0.0.0-snapshot-484c9684bac8056d764aa460fd025c45f5856aa5

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.
@@ -9,7 +9,7 @@ import type {
9
9
  SqliteDbChangeset,
10
10
  SqliteDbSession,
11
11
  } from '@livestore/common'
12
- import { BoundArray, BoundMap, sql } from '@livestore/common'
12
+ import { BoundArray, BoundMap, sql, SqliteError } from '@livestore/common'
13
13
  import { isDevEnv } from '@livestore/utils'
14
14
  import type * as otel from '@opentelemetry/api'
15
15
 
@@ -163,39 +163,45 @@ export class SqliteDbWrapper implements SqliteDb {
163
163
  { attributes: { 'sql.query': queryStr } },
164
164
  options?.otelContext ?? this.otelRootSpanContext,
165
165
  (span) => {
166
- let stmt = this.cachedStmts.get(queryStr)
167
- if (stmt === undefined) {
168
- stmt = this.db.prepare(queryStr)
169
- this.cachedStmts.set(queryStr, stmt)
170
- }
166
+ try {
167
+ let stmt = this.cachedStmts.get(queryStr)
168
+ if (stmt === undefined) {
169
+ stmt = this.db.prepare(queryStr)
170
+ this.cachedStmts.set(queryStr, stmt)
171
+ }
171
172
 
172
- stmt.execute(bindValues)
173
+ stmt.execute(bindValues)
173
174
 
174
- if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(queryStr)) {
175
- // TODO use write tables instead
176
- // check what queries actually end up here.
177
- this.resultCache.invalidate(options?.writeTables ?? this.getTablesUsed(queryStr))
178
- }
175
+ if (options?.hasNoEffects !== true && !this.resultCache.ignoreQuery(queryStr)) {
176
+ // TODO use write tables instead
177
+ // check what queries actually end up here.
178
+ this.resultCache.invalidate(options?.writeTables ?? this.getTablesUsed(queryStr))
179
+ }
179
180
 
180
- span.end()
181
+ span.end()
182
+
183
+ const durationMs = getDurationMsFromSpan(span)
181
184
 
182
- const durationMs = getDurationMsFromSpan(span)
185
+ this.debugInfo.queryFrameDuration += durationMs
186
+ this.debugInfo.queryFrameCount++
183
187
 
184
- this.debugInfo.queryFrameDuration += durationMs
185
- this.debugInfo.queryFrameCount++
188
+ if (durationMs > 5 && isDevEnv()) {
189
+ this.debugInfo.slowQueries.push({
190
+ queryStr,
191
+ bindValues,
192
+ durationMs,
193
+ rowsCount: undefined,
194
+ queriedTables: new Set(),
195
+ startTimePerfNow: getStartTimeHighResFromSpan(span),
196
+ })
197
+ }
186
198
 
187
- if (durationMs > 5 && isDevEnv()) {
188
- this.debugInfo.slowQueries.push({
189
- queryStr,
190
- bindValues,
191
- durationMs,
192
- rowsCount: undefined,
193
- queriedTables: new Set(),
194
- startTimePerfNow: getStartTimeHighResFromSpan(span),
195
- })
199
+ return { durationMs }
200
+ } catch (cause: any) {
201
+ span.recordException(cause)
202
+ span.end()
203
+ throw new SqliteError({ cause, query: { bindValues: bindValues ?? {}, sql: queryStr } })
196
204
  }
197
-
198
- return { durationMs }
199
205
  },
200
206
  )
201
207
  }
@@ -20,6 +20,14 @@ exports[`otel > otel 3`] = `
20
20
  ",
21
21
  },
22
22
  },
23
+ {
24
+ "_name": "client-session-sync-processor:pull",
25
+ "attributes": {
26
+ "code.stacktrace": "<STACKTRACE>",
27
+ "span.label": "⚠︎ Interrupted",
28
+ "status.interrupted": true,
29
+ },
30
+ },
23
31
  {
24
32
  "_name": "LiveStore:sync",
25
33
  },
@@ -280,6 +288,14 @@ exports[`otel > with thunks 7`] = `
280
288
  ",
281
289
  },
282
290
  },
291
+ {
292
+ "_name": "client-session-sync-processor:pull",
293
+ "attributes": {
294
+ "code.stacktrace": "<STACKTRACE>",
295
+ "span.label": "⚠︎ Interrupted",
296
+ "status.interrupted": true,
297
+ },
298
+ },
283
299
  {
284
300
  "_name": "LiveStore:sync",
285
301
  },
@@ -374,6 +390,14 @@ exports[`otel > with thunks with query builder and without labels 3`] = `
374
390
  ",
375
391
  },
376
392
  },
393
+ {
394
+ "_name": "client-session-sync-processor:pull",
395
+ "attributes": {
396
+ "code.stacktrace": "<STACKTRACE>",
397
+ "span.label": "⚠︎ Interrupted",
398
+ "status.interrupted": true,
399
+ },
400
+ },
377
401
  {
378
402
  "_name": "LiveStore:sync",
379
403
  },
@@ -1,4 +1,4 @@
1
- import { Effect, Schema } from '@livestore/utils/effect'
1
+ import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
2
2
  import { Vitest } from '@livestore/utils/node-vitest'
3
3
  import * as otel from '@opentelemetry/api'
4
4
  import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
@@ -19,6 +19,15 @@ TODO write tests for:
19
19
  Vitest.describe('otel', () => {
20
20
  let cachedProvider: BasicTracerProvider | undefined
21
21
 
22
+ const mapAttributes = (attributes: otel.Attributes) => {
23
+ return ReadonlyRecord.map(attributes, (val, key) => {
24
+ if (key === 'code.stacktrace') {
25
+ return '<STACKTRACE>'
26
+ }
27
+ return val
28
+ })
29
+ }
30
+
22
31
  const makeQuery = Effect.gen(function* () {
23
32
  const exporter = new InMemorySpanExporter()
24
33
 
@@ -74,7 +83,7 @@ Vitest.describe('otel', () => {
74
83
  return { exporter }
75
84
  }).pipe(
76
85
  Effect.scoped,
77
- Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()),
86
+ Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()),
78
87
  ),
79
88
  )
80
89
 
@@ -124,7 +133,7 @@ Vitest.describe('otel', () => {
124
133
  return { exporter }
125
134
  }).pipe(
126
135
  Effect.scoped,
127
- Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()),
136
+ Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()),
128
137
  ),
129
138
  )
130
139
 
@@ -160,7 +169,7 @@ Vitest.describe('otel', () => {
160
169
  return { exporter }
161
170
  }).pipe(
162
171
  Effect.scoped,
163
- Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()),
172
+ Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()),
164
173
  ),
165
174
  )
166
175
  })
@@ -33,6 +33,10 @@ import { connectDevtoolsToStore } from './devtools.js'
33
33
  import { Store } from './store.js'
34
34
  import type { BaseGraphQLContext, GraphQLOptions, OtelOptions, ShutdownDeferred } from './store-types.js'
35
35
 
36
+ export const DEFAULT_PARAMS = {
37
+ leaderPushBatchSize: 1,
38
+ }
39
+
36
40
  export interface CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> {
37
41
  schema: TSchema
38
42
  adapter: Adapter
@@ -49,6 +53,9 @@ export interface CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext,
49
53
  disableDevtools?: boolean
50
54
  onBootStatus?: (status: BootStatus) => void
51
55
  shutdownDeferred?: ShutdownDeferred
56
+ params?: {
57
+ leaderPushBatchSize?: number
58
+ }
52
59
  debug?: {
53
60
  instanceId?: string
54
61
  }
@@ -102,6 +109,7 @@ export const createStore = <
102
109
  disableDevtools,
103
110
  onBootStatus,
104
111
  shutdownDeferred,
112
+ params,
105
113
  debug,
106
114
  }: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<
107
115
  Store<TGraphQLContext, TSchema>,
@@ -142,17 +150,30 @@ export const createStore = <
142
150
 
143
151
  const runtime = yield* Effect.runtime<Scope.Scope>()
144
152
 
145
- const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
146
- Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
147
- Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
148
- Effect.timeout(1000),
149
- Effect.catchTag('TimeoutException', () =>
150
- Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
151
- ),
152
- Effect.tap(() => (shutdownDeferred ? Deferred.failCause(shutdownDeferred, cause) : Effect.void)),
153
- Effect.tap(() => Effect.logDebug('LiveStore shutdown complete')),
154
- Effect.withSpan('livestore:shutdown'),
153
+ const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => {
154
+ Effect.gen(function* () {
155
+ yield* Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
156
+ Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
157
+ Effect.timeout(1000),
158
+ Effect.catchTag('TimeoutException', () =>
159
+ Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
160
+ ),
161
+ )
162
+
163
+ if (shutdownDeferred) {
164
+ yield* Deferred.failCause(shutdownDeferred, cause)
165
+ }
166
+
167
+ yield* Effect.logDebug('LiveStore shutdown complete')
168
+ }).pipe(
169
+ Effect.withSpan('@livestore/livestore:shutdown'),
170
+ Effect.provide(runtime),
171
+ Effect.tapCauseLogPretty,
172
+ // Given that the shutdown flow might also interrupt the effect that is calling the shutdown,
173
+ // we want to detach the shutdown effect so it's not interrupted by itself
174
+ Effect.runFork,
155
175
  )
176
+ }
156
177
 
157
178
  const clientSession: ClientSession = yield* adapter({
158
179
  schema,
@@ -167,9 +188,10 @@ export const createStore = <
167
188
  if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
168
189
  yield* Effect.logDebug(
169
190
  '[@livestore/livestore:createStore] migrationsReport',
170
- ...clientSession.leaderThread.initialState.migrationsReport.migrations.map(
171
- (m) =>
172
- `Schema hash mismatch for table '${m.tableName}' (DB: ${m.hashes.actual}, expected: ${m.hashes.expected}), migrating table...`,
191
+ ...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
192
+ m.hashes.actual === undefined
193
+ ? `Table '${m.tableName}' doesn't exist yet. Creating table...`
194
+ : `Schema hash mismatch for table '${m.tableName}' (DB: ${m.hashes.actual}, expected: ${m.hashes.expected}), migrating table...`,
173
195
  ),
174
196
  )
175
197
  }
@@ -190,6 +212,9 @@ export const createStore = <
190
212
  // but only set the provided `batchUpdates` function after boot
191
213
  batchUpdates: (run) => run(),
192
214
  storeId,
215
+ params: {
216
+ leaderPushBatchSize: params?.leaderPushBatchSize ?? DEFAULT_PARAMS.leaderPushBatchSize,
217
+ },
193
218
  })
194
219
 
195
220
  // Starts background fibers (syncing, mutation processing, etc) for store
@@ -66,6 +66,9 @@ export type StoreOptions<
66
66
  batchUpdates: (runUpdates: () => void) => void
67
67
  // TODO validate whether we still need this
68
68
  unsyncedMutationEvents: MutableHashMap.MutableHashMap<EventId.EventId, MutationEvent.ForSchema<TSchema>>
69
+ params: {
70
+ leaderPushBatchSize: number
71
+ }
69
72
  }
70
73
 
71
74
  export type RefreshReason =
@@ -113,6 +113,7 @@ export class Store<
113
113
  storeId,
114
114
  lifetimeScope,
115
115
  runtime,
116
+ params,
116
117
  }: StoreOptions<TGraphQLContext, TSchema>) {
117
118
  super()
118
119
 
@@ -178,6 +179,9 @@ export class Store<
178
179
  reactivityGraph.setRefs(tablesToUpdate)
179
180
  },
180
181
  span: syncSpan,
182
+ params: {
183
+ leaderPushBatchSize: params.leaderPushBatchSize,
184
+ },
181
185
  })
182
186
 
183
187
  this.__mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
@@ -614,11 +618,8 @@ export class Store<
614
618
  }).pipe(this.runEffectFork)
615
619
  },
616
620
 
617
- shutdown: (cause?: Cause.Cause<UnexpectedError>) => {
618
- this.clientSession
619
- .shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
620
- .pipe(Effect.tapCauseLogPretty, Effect.provide(this.runtime), Effect.runFork)
621
- },
621
+ shutdown: (cause?: Cause.Cause<UnexpectedError>) =>
622
+ this.clientSession.shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' }))),
622
623
 
623
624
  version: liveStoreVersion,
624
625
  }
package/src/utils/dev.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isDevEnv } from '@livestore/utils'
2
+ import { Effect } from '@livestore/utils/effect'
2
3
 
3
4
  /* eslint-disable unicorn/prefer-global-this */
4
5
  export const downloadBlob = (
@@ -27,6 +28,10 @@ export const downloadURL = (data: string, fileName: string) => {
27
28
 
28
29
  export const exposeDebugUtils = () => {
29
30
  if (isDevEnv()) {
30
- globalThis.__debugLiveStoreUtils = { downloadBlob }
31
+ globalThis.__debugLiveStoreUtils = {
32
+ downloadBlob,
33
+ runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
34
+ runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
35
+ }
31
36
  }
32
37
  }