@livestore/livestore 0.0.54-dev.21 → 0.0.54-dev.22

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/src/store.ts CHANGED
@@ -5,13 +5,14 @@ import type {
5
5
  ResetMode,
6
6
  StoreAdapter,
7
7
  StoreAdapterFactory,
8
+ UnexpectedError,
8
9
  } from '@livestore/common'
9
10
  import { Devtools, getExecArgsFromMutation, prepareBindValues } from '@livestore/common'
10
11
  import { version as liveStoreVersion } from '@livestore/common/package.json'
11
12
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
12
13
  import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
13
14
  import { assertNever, isPromise, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
14
- import { Effect, Schema, Stream } from '@livestore/utils/effect'
15
+ import { Effect, Layer, OtelTracer, Schema, Stream } from '@livestore/utils/effect'
15
16
  import * as otel from '@opentelemetry/api'
16
17
  import type { GraphQLSchema } from 'graphql'
17
18
 
@@ -19,6 +20,7 @@ import { globalReactivityGraph } from './global-state.js'
19
20
  import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
20
21
  import type { StackInfo } from './react/utils/stack-info.js'
21
22
  import type { DebugRefreshReasonBase, Ref } from './reactive.js'
23
+ import { NOT_REFRESHED_YET } from './reactive.js'
22
24
  import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
23
25
  import { downloadBlob } from './utils/dev.js'
24
26
  import { getDurationMsFromSpan } from './utils/otel.js'
@@ -153,7 +155,7 @@ export class Store<
153
155
 
154
156
  this.reactivityGraph = reactivityGraph
155
157
  this.reactivityGraph.context = {
156
- store: this as any,
158
+ store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
157
159
  otelTracer: otelOptions.tracer,
158
160
  rootOtelContext: otelQueriesSpanContext,
159
161
  }
@@ -270,7 +272,7 @@ export class Store<
270
272
  otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
271
273
  otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
272
274
 
273
- await this.adapter.coordinator.shutdown()
275
+ await this.adapter.coordinator.shutdown.pipe(Effect.tapCauseLogPretty, Effect.runPromise)
274
276
  }
275
277
 
276
278
  mutate: {
@@ -487,10 +489,9 @@ export class Store<
487
489
 
488
490
  if (coordinatorMode !== 'skip-coordinator') {
489
491
  // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
490
- void this.adapter.coordinator.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, {
491
- span,
492
- persisted: coordinatorMode !== 'skip-persist',
493
- })
492
+ this.adapter.coordinator
493
+ .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
494
+ .pipe(Effect.tapCauseLogPretty, Effect.runFork)
494
495
  }
495
496
 
496
497
  // Uncomment to print a list of queries currently registered on the store
@@ -516,8 +517,9 @@ export class Store<
516
517
  ) => {
517
518
  this.mainDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
518
519
 
519
- const parentSpan = otel.trace.getSpan(otel.context.active())
520
- this.adapter.coordinator.execute(query, prepareBindValues(params, query), parentSpan)
520
+ this.adapter.coordinator
521
+ .execute(query, prepareBindValues(params, query))
522
+ .pipe(Effect.tapCauseLogPretty, Effect.runFork)
521
523
  }
522
524
 
523
525
  select = (query: string, params: ParamsObject = {}) => {
@@ -607,7 +609,10 @@ export class Store<
607
609
  label: q.label,
608
610
  runs: q.runs,
609
611
  executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
610
- lastestResult: q.results$.previousResult,
612
+ lastestResult:
613
+ q.results$.previousResult === NOT_REFRESHED_YET
614
+ ? 'SYMBOL_NOT_REFRESHED_YET'
615
+ : q.results$.previousResult,
611
616
  activeSubscriptions: Array.from(q.activeSubscriptions),
612
617
  })),
613
618
  requestId,
@@ -629,7 +634,10 @@ export class Store<
629
634
  break
630
635
  }
631
636
  case 'LSD.ResetAllDataReq': {
632
- await this.adapter.coordinator.dangerouslyReset(decoded.value.mode)
637
+ await this.adapter.coordinator
638
+ .dangerouslyReset(decoded.value.mode)
639
+ .pipe(Effect.tapCauseLogPretty, Effect.runPromise)
640
+
633
641
  sendToDevtools(Devtools.ResetAllDataRes.make({ requestId, liveStoreVersion }))
634
642
 
635
643
  break
@@ -645,18 +653,37 @@ export class Store<
645
653
  }
646
654
 
647
655
  __devDownloadMutationLogDb = async () => {
648
- const data = await this.adapter.coordinator.getMutationLogData()
656
+ const data = await this.adapter.coordinator.getMutationLogData.pipe(Effect.tapCauseLogPretty, Effect.runPromise)
649
657
  downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
650
658
  }
651
659
 
652
660
  // TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
653
- dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode)
661
+ dangerouslyResetStorage = (mode: ResetMode) =>
662
+ this.adapter.coordinator.dangerouslyReset(mode).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
663
+ }
664
+
665
+ export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
666
+ schema: TSchema
667
+ adapter: StoreAdapterFactory
668
+ reactivityGraph?: ReactivityGraph
669
+ graphQLOptions?: GraphQLOptions<TGraphQLContext>
670
+ otelOptions?: Partial<OtelOptions>
671
+ boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
672
+ batchUpdates?: (run: () => void) => void
673
+ disableDevtools?: boolean
654
674
  }
655
675
 
656
676
  /** Create a new LiveStore Store */
657
677
  export const createStore = async <
658
678
  TGraphQLContext extends BaseGraphQLContext,
659
679
  TSchema extends LiveStoreSchema = LiveStoreSchema,
680
+ >(
681
+ options: CreateStoreOptions<TGraphQLContext, TSchema>,
682
+ ): Promise<Store<TGraphQLContext, TSchema>> => createStoreEff(options).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
683
+
684
+ export const createStoreEff = <
685
+ TGraphQLContext extends BaseGraphQLContext,
686
+ TSchema extends LiveStoreSchema = LiveStoreSchema,
660
687
  >({
661
688
  schema,
662
689
  graphQLOptions,
@@ -666,125 +693,122 @@ export const createStore = async <
666
693
  reactivityGraph = globalReactivityGraph,
667
694
  batchUpdates,
668
695
  disableDevtools,
669
- }: {
670
- schema: TSchema
671
- adapter: StoreAdapterFactory
672
- reactivityGraph?: ReactivityGraph
673
- graphQLOptions?: GraphQLOptions<TGraphQLContext>
674
- otelOptions?: Partial<OtelOptions>
675
- boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
676
- batchUpdates?: (run: () => void) => void
677
- disableDevtools?: boolean
678
- }): Promise<Store<TGraphQLContext, TSchema>> => {
696
+ }: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<Store<TGraphQLContext, TSchema>, UnexpectedError> => {
679
697
  const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
680
698
  const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active()
681
- return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
682
- try {
683
- performance.mark('livestore:db-creating')
684
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
685
-
686
- const adapterPromise = adapterFactory({ otelTracer, otelContext, schema })
687
- const adapter = adapterPromise instanceof Promise ? await adapterPromise : adapterPromise
688
- performance.mark('livestore:db-created')
689
- performance.measure('livestore:db-create', 'livestore:db-creating', 'livestore:db-created')
690
-
691
- if (batchUpdates !== undefined) {
692
- reactivityGraph.effectsWrapper = batchUpdates
693
- }
694
699
 
695
- const mutationEventSchema = makeMutationEventSchemaMemo(schema)
700
+ const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
701
+ Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)),
702
+ )
696
703
 
697
- // TODO consider moving booting into the storage backend
698
- if (boot !== undefined) {
699
- let isInTxn = false
700
- let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
704
+ return Effect.gen(function* () {
705
+ const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
701
706
 
702
- const bootDbImpl: BootDb = {
703
- _tag: 'BootDb',
704
- execute: (queryStr, bindValues) => {
705
- const stmt = adapter.mainDb.prepare(queryStr)
706
- stmt.execute(bindValues)
707
+ const adapter = yield* adapterFactory({ schema }).pipe(
708
+ Effect.withPerformanceMeasure('livestore:makeAdapter'),
709
+ Effect.withSpan('createStore:makeAdapter'),
710
+ )
707
711
 
708
- if (isInTxn === true) {
709
- txnExecuteStmnts.push([queryStr, bindValues])
710
- } else {
711
- void adapter.coordinator.execute(queryStr, bindValues, undefined)
712
- }
713
- },
714
- mutate: (...list) => {
715
- for (const mutationEventDecoded of list) {
716
- const mutationDef =
717
- schema.mutations.get(mutationEventDecoded.mutation) ??
718
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
719
-
720
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
721
- // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
722
-
723
- for (const { statementSql, bindValues } of execArgsArr) {
724
- adapter.mainDb.execute(statementSql, bindValues)
725
- }
712
+ if (batchUpdates !== undefined) {
713
+ reactivityGraph.effectsWrapper = batchUpdates
714
+ }
726
715
 
727
- const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
716
+ const mutationEventSchema = makeMutationEventSchemaMemo(schema)
728
717
 
729
- void adapter.coordinator.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, {
730
- span,
731
- persisted: true,
732
- })
733
- }
734
- },
735
- select: (queryStr, bindValues) => {
736
- const stmt = adapter.mainDb.prepare(queryStr)
737
- return stmt.select(bindValues)
738
- },
739
- txn: (callback) => {
740
- try {
741
- isInTxn = true
742
- adapter.mainDb.execute('BEGIN', undefined)
743
-
744
- callback()
745
-
746
- adapter.mainDb.execute('COMMIT', undefined)
747
-
748
- // adapter.coordinator.execute('BEGIN', undefined, undefined)
749
- for (const [queryStr, bindValues] of txnExecuteStmnts) {
750
- adapter.coordinator.execute(queryStr, bindValues, undefined)
751
- }
752
- // adapter.coordinator.execute('COMMIT', undefined, undefined)
753
- } catch (e: any) {
754
- adapter.mainDb.execute('ROLLBACK', undefined)
755
- throw e
756
- } finally {
757
- isInTxn = false
758
- txnExecuteStmnts = []
718
+ // TODO consider moving booting into the storage backend
719
+ if (boot !== undefined) {
720
+ let isInTxn = false
721
+ let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
722
+
723
+ const bootDbImpl: BootDb = {
724
+ _tag: 'BootDb',
725
+ execute: (queryStr, bindValues) => {
726
+ const stmt = adapter.mainDb.prepare(queryStr)
727
+ stmt.execute(bindValues)
728
+
729
+ if (isInTxn === true) {
730
+ txnExecuteStmnts.push([queryStr, bindValues])
731
+ } else {
732
+ adapter.coordinator.execute(queryStr, bindValues).pipe(Effect.tapCauseLogPretty, Effect.runFork)
733
+ }
734
+ },
735
+ mutate: (...list) => {
736
+ for (const mutationEventDecoded of list) {
737
+ const mutationDef =
738
+ schema.mutations.get(mutationEventDecoded.mutation) ??
739
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
740
+
741
+ const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
742
+ // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
743
+
744
+ for (const { statementSql, bindValues } of execArgsArr) {
745
+ adapter.mainDb.execute(statementSql, bindValues)
759
746
  }
760
- },
761
- }
762
747
 
763
- const booting = boot(bootDbImpl, span)
764
- // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
765
- if (isPromise(booting)) {
766
- await booting
767
- }
768
- }
748
+ const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
769
749
 
770
- // TODO: we can't apply the schema at this point, we've already loaded persisted data!
771
- // Think about what to do about this case.
772
- // await applySchema(db, schema)
773
- return Store.createStore<TGraphQLContext, TSchema>(
774
- {
775
- adapter,
776
- schema,
777
- graphQLOptions,
778
- otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
779
- reactivityGraph,
780
- disableDevtools,
750
+ adapter.coordinator
751
+ .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
752
+ .pipe(Effect.tapCauseLogPretty, Effect.runFork)
753
+ }
781
754
  },
782
- span,
783
- )
784
- } finally {
785
- span.end()
755
+ select: (queryStr, bindValues) => {
756
+ const stmt = adapter.mainDb.prepare(queryStr)
757
+ return stmt.select(bindValues)
758
+ },
759
+ txn: (callback) => {
760
+ try {
761
+ isInTxn = true
762
+ adapter.mainDb.execute('BEGIN', undefined)
763
+
764
+ callback()
765
+
766
+ adapter.mainDb.execute('COMMIT', undefined)
767
+
768
+ // adapter.coordinator.execute('BEGIN', undefined, undefined)
769
+ for (const [queryStr, bindValues] of txnExecuteStmnts) {
770
+ adapter.coordinator.execute(queryStr, bindValues).pipe(Effect.tapCauseLogPretty, Effect.runFork)
771
+ }
772
+ // adapter.coordinator.execute('COMMIT', undefined, undefined)
773
+ } catch (e: any) {
774
+ adapter.mainDb.execute('ROLLBACK', undefined)
775
+ throw e
776
+ } finally {
777
+ isInTxn = false
778
+ txnExecuteStmnts = []
779
+ }
780
+ },
781
+ }
782
+
783
+ const booting = boot(bootDbImpl, span)
784
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
785
+ if (isPromise(booting)) {
786
+ yield* Effect.promise(() => booting)
787
+ }
786
788
  }
787
- })
789
+
790
+ // TODO: we can't apply the schema at this point, we've already loaded persisted data!
791
+ // Think about what to do about this case.
792
+ // await applySchema(db, schema)
793
+ return Store.createStore<TGraphQLContext, TSchema>(
794
+ {
795
+ adapter,
796
+ schema,
797
+ graphQLOptions,
798
+ otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
799
+ reactivityGraph,
800
+ disableDevtools,
801
+ },
802
+ span,
803
+ )
804
+ }).pipe(
805
+ Effect.withSpan('createStore', {
806
+ parent: otelOptions?.rootSpanContext
807
+ ? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
808
+ : undefined,
809
+ }),
810
+ Effect.provide(TracingLive),
811
+ )
788
812
  }
789
813
 
790
814
  class ReferenceCountedSet<T> {