@livestore/livestore 0.0.54 → 0.0.55-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.
@@ -1,7 +1,7 @@
1
- import type { BootDb, StoreAdapterFactory, UnexpectedError } from '@livestore/common'
1
+ import type { BootDb, BootStatus, StoreAdapterFactory, UnexpectedError } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
- import type { Cause } from '@livestore/utils/effect'
4
- import { Context, Deferred, Duration, Effect, Layer, OtelTracer, pipe, Runtime, Scope } from '@livestore/utils/effect'
3
+ import type { Cause, Scope } from '@livestore/utils/effect'
4
+ import { Context, Deferred, Duration, Effect, FiberSet, Layer, OtelTracer, pipe } from '@livestore/utils/effect'
5
5
  import * as otel from '@opentelemetry/api'
6
6
  import type { GraphQLSchema } from 'graphql'
7
7
 
@@ -32,7 +32,7 @@ export type LiveStoreCreateStoreOptions<GraphQLContext extends BaseGraphQLContex
32
32
  schema: LiveStoreSchema
33
33
  graphQLOptions?: GraphQLOptions<GraphQLContext>
34
34
  otelOptions?: OtelOptions
35
- boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
35
+ boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
36
36
  adapter: StoreAdapterFactory
37
37
  batchUpdates?: (run: () => void) => void
38
38
  disableDevtools?: boolean
@@ -43,7 +43,7 @@ export const LiveStoreContextRunning = Context.GenericTag<LiveStoreContextRunnin
43
43
  '@livestore/livestore/effect/LiveStoreContextRunning',
44
44
  )
45
45
 
46
- export type DeferredStoreContext = Deferred.Deferred<LiveStoreContextRunning>
46
+ export type DeferredStoreContext = Deferred.Deferred<LiveStoreContextRunning, UnexpectedError>
47
47
  export const DeferredStoreContext = Context.GenericTag<DeferredStoreContext>(
48
48
  '@livestore/livestore/effect/DeferredStoreContext',
49
49
  )
@@ -56,9 +56,10 @@ export type LiveStoreContextProps<GraphQLContext extends BaseGraphQLContext> = {
56
56
  schema: Effect.Effect<GraphQLSchema, never, otel.Tracer>
57
57
  makeContext: (db: MainDatabaseWrapper) => GraphQLContext
58
58
  }
59
- boot?: (db: BootDb) => Effect.Effect<void>
59
+ boot?: (db: BootDb) => Effect.Effect<void, unknown, otel.Tracer>
60
60
  adapter: StoreAdapterFactory
61
61
  disableDevtools?: boolean
62
+ onBootStatus?: (status: BootStatus) => void
62
63
  }
63
64
 
64
65
  export const LiveStoreContextLayer = <GraphQLContext extends BaseGraphQLContext>(
@@ -69,14 +70,18 @@ export const LiveStoreContextLayer = <GraphQLContext extends BaseGraphQLContext>
69
70
  Layer.provide(LiveStoreContextDeferred),
70
71
  )
71
72
 
72
- export const LiveStoreContextDeferred = Layer.effect(DeferredStoreContext, Deferred.make<LiveStoreContextRunning>())
73
+ export const LiveStoreContextDeferred = Layer.effect(
74
+ DeferredStoreContext,
75
+ Deferred.make<LiveStoreContextRunning, UnexpectedError>(),
76
+ )
73
77
 
74
78
  export const makeLiveStoreContext = <GraphQLContext extends BaseGraphQLContext>({
75
79
  schema,
76
80
  graphQLOptions: graphQLOptions_,
77
- boot: boot_,
81
+ boot,
78
82
  adapter,
79
83
  disableDevtools,
84
+ onBootStatus,
80
85
  }: LiveStoreContextProps<GraphQLContext>): Effect.Effect<
81
86
  LiveStoreContextRunning,
82
87
  UnexpectedError | Cause.TimeoutException,
@@ -84,23 +89,16 @@ export const makeLiveStoreContext = <GraphQLContext extends BaseGraphQLContext>(
84
89
  > =>
85
90
  pipe(
86
91
  Effect.gen(function* () {
87
- const runtime = yield* Effect.runtime<never>()
88
-
89
92
  const otelRootSpanContext = otel.context.active()
90
93
 
91
- const storeScope = yield* Scope.make()
92
-
93
- yield* Effect.addFinalizer((ex) => Scope.close(storeScope, ex))
94
-
95
94
  const otelTracer = yield* OtelTracer.Tracer
96
95
 
97
96
  const graphQLOptions = yield* graphQLOptions_
98
97
  ? Effect.all({ schema: graphQLOptions_.schema, makeContext: Effect.succeed(graphQLOptions_.makeContext) })
99
98
  : Effect.succeed(undefined)
100
99
 
101
- const boot = boot_
102
- ? (db: BootDb) => boot_(db).pipe(Effect.withSpan('boot'), Effect.tapCauseLogPretty, Runtime.runPromise(runtime))
103
- : undefined
100
+ // TODO join fiber set and close tear down parent scope in case of error (Needs refactor with Mike A)
101
+ const fiberSet = yield* FiberSet.make()
104
102
 
105
103
  const store = yield* createStore({
106
104
  schema,
@@ -112,14 +110,18 @@ export const makeLiveStoreContext = <GraphQLContext extends BaseGraphQLContext>(
112
110
  boot,
113
111
  adapter,
114
112
  disableDevtools,
115
- storeScope,
113
+ fiberSet,
114
+ onBootStatus,
116
115
  })
117
116
 
118
117
  window.__debugLiveStore = store
119
118
 
120
119
  return { stage: 'running', store } satisfies LiveStoreContextRunning
121
120
  }),
121
+ Effect.tapErrorCause((cause) => Effect.flatMap(DeferredStoreContext, (def) => Deferred.failCause(def, cause))),
122
122
  Effect.tap((storeCtx) => Effect.flatMap(DeferredStoreContext, (def) => Deferred.succeed(def, storeCtx))),
123
- Effect.timeout(Duration.seconds(60)),
123
+ // This can take quite a while.
124
+ // TODO make this configurable
125
+ Effect.timeout(Duration.minutes(5)),
124
126
  Effect.withSpan('@livestore/livestore/effect:makeLiveStoreContext'),
125
127
  )
@@ -1,7 +1,7 @@
1
1
  import { type BootDb, type BootStatus, type StoreAdapterFactory, UnexpectedError } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
3
  import { errorToString } from '@livestore/utils'
4
- import { Effect, Exit, Logger, LogLevel, Schema, Scope } from '@livestore/utils/effect'
4
+ import { Effect, Exit, FiberSet, Logger, LogLevel, Schema, Scope } from '@livestore/utils/effect'
5
5
  import type * as otel from '@opentelemetry/api'
6
6
  import type { ReactElement, ReactNode } from 'react'
7
7
  import React from 'react'
@@ -14,7 +14,7 @@ import { LiveStoreContext } from './LiveStoreContext.js'
14
14
 
15
15
  interface LiveStoreProviderProps<GraphQLContext> {
16
16
  schema: LiveStoreSchema
17
- boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
17
+ boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
18
18
  graphQLOptions?: GraphQLOptions<GraphQLContext>
19
19
  otelOptions?: OtelOptions
20
20
  renderLoading: (status: BootStatus) => ReactElement
@@ -153,20 +153,23 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
153
153
  }
154
154
  })
155
155
 
156
- createStore({
157
- storeScope,
158
- schema,
159
- graphQLOptions,
160
- otelOptions,
161
- boot,
162
- adapter,
163
- batchUpdates,
164
- disableDevtools,
165
- onBootStatus: (status) => {
166
- if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
167
- setContextValue(status)
168
- },
169
- }).pipe(
156
+ FiberSet.make().pipe(
157
+ Effect.andThen((fiberSet) =>
158
+ createStore({
159
+ fiberSet,
160
+ schema,
161
+ graphQLOptions,
162
+ otelOptions,
163
+ boot,
164
+ adapter,
165
+ batchUpdates,
166
+ disableDevtools,
167
+ onBootStatus: (status) => {
168
+ if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
169
+ setContextValue(status)
170
+ },
171
+ }),
172
+ ),
170
173
  Effect.tapSync((store) => setContextValue({ stage: 'running', store })),
171
174
  Effect.tapError((error) => Effect.sync(() => setContextValue({ stage: 'error', error }))),
172
175
  Effect.tapDefect((defect) => Effect.sync(() => setContextValue({ stage: 'error', error: defect }))),
package/src/store.ts CHANGED
@@ -17,11 +17,13 @@ import {
17
17
  } from '@livestore/common'
18
18
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
19
19
  import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
20
- import { assertNever, isPromise, makeNoopTracer, shouldNeverHappen, throttle } from '@livestore/utils'
20
+ import { assertNever, makeNoopTracer, shouldNeverHappen, throttle } from '@livestore/utils'
21
21
  import { cuid } from '@livestore/utils/cuid'
22
22
  import {
23
23
  Effect,
24
24
  Exit,
25
+ FiberSet,
26
+ Inspectable,
25
27
  Layer,
26
28
  Logger,
27
29
  LogLevel,
@@ -71,7 +73,7 @@ export type StoreOptions<
71
73
  otelOptions: OtelOptions
72
74
  reactivityGraph: ReactivityGraph
73
75
  disableDevtools?: boolean
74
- storeScope: Scope.CloseableScope
76
+ fiberSet: FiberSet.FiberSet
75
77
  // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
76
78
  __processedMutationIds: Set<string>
77
79
  }
@@ -126,10 +128,10 @@ export type StoreMutateOptions = {
126
128
  export class Store<
127
129
  TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
128
130
  TSchema extends LiveStoreSchema = LiveStoreSchema,
129
- > {
131
+ > extends Inspectable.Class {
130
132
  id = uniqueStoreId()
131
133
  readonly devtoolsConnectionId = cuid()
132
- private storeScope: Scope.CloseableScope
134
+ private fiberSet: FiberSet.FiberSet
133
135
  reactivityGraph: ReactivityGraph
134
136
  mainDbWrapper: MainDatabaseWrapper
135
137
  adapter: StoreAdapter
@@ -160,13 +162,15 @@ export class Store<
160
162
  otelOptions,
161
163
  disableDevtools,
162
164
  __processedMutationIds,
163
- storeScope,
165
+ fiberSet,
164
166
  }: StoreOptions<TGraphQLContext, TSchema>) {
167
+ super()
168
+
165
169
  this.mainDbWrapper = new MainDatabaseWrapper({ otel: otelOptions, db: adapter.mainDb })
166
170
  this.adapter = adapter
167
171
  this.schema = schema
168
172
 
169
- this.storeScope = storeScope
173
+ this.fiberSet = fiberSet
170
174
 
171
175
  // TODO refactor
172
176
  this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
@@ -239,7 +243,9 @@ export class Store<
239
243
  otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
240
244
  }),
241
245
  )
242
- }).pipe(Scope.extend(storeScope), Effect.forkIn(storeScope), Effect.scoped, runEffectFork)
246
+
247
+ yield* Effect.never
248
+ }).pipe(Effect.scoped, Effect.withSpan('LiveStore:store-constructor'), FiberSet.run(fiberSet), runEffectFork)
243
249
  }
244
250
 
245
251
  static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
@@ -305,7 +311,7 @@ export class Store<
305
311
  * Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
306
312
  */
307
313
  destroy = async () => {
308
- await Scope.close(this.storeScope, Exit.void).pipe(Effect.withSpan('Store:destroy'), runEffectPromise)
314
+ await FiberSet.clear(this.fiberSet).pipe(Effect.withSpan('Store:destroy'), runEffectPromise)
309
315
  }
310
316
 
311
317
  mutate: {
@@ -571,6 +577,7 @@ export class Store<
571
577
  meta: { liveStoreRefType: 'table' },
572
578
  })
573
579
 
580
+ // #region devtools
574
581
  // TODO shutdown behaviour
575
582
  private bootDevtools = () =>
576
583
  Effect.gen(this, function* () {
@@ -584,6 +591,8 @@ export class Store<
584
591
 
585
592
  const channelId = this.adapter.coordinator.devtools.channelId
586
593
 
594
+ const runtime = yield* Effect.runtime()
595
+
587
596
  window.addEventListener('message', (event) => {
588
597
  const decodedMessageRes = Schema.decodeOption(Devtools.DevtoolsWindowMessage.MessageForStore)(event.data)
589
598
  if (decodedMessageRes._tag === 'None') return
@@ -773,7 +782,7 @@ export class Store<
773
782
 
774
783
  storeMessagePort.start()
775
784
  }),
776
- runEffectFork,
785
+ Runtime.runFork(runtime),
777
786
  )
778
787
 
779
788
  return
@@ -782,6 +791,7 @@ export class Store<
782
791
 
783
792
  sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
784
793
  })
794
+ // #endregion devtools
785
795
 
786
796
  __devDownloadDb = () => {
787
797
  const data = this.mainDbWrapper.export()
@@ -795,6 +805,13 @@ export class Store<
795
805
 
796
806
  // TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
797
807
  dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
808
+
809
+ toJSON = () => {
810
+ return {
811
+ _tag: 'Store',
812
+ reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
813
+ }
814
+ }
798
815
  }
799
816
 
800
817
  export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
@@ -803,7 +820,7 @@ export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSche
803
820
  reactivityGraph?: ReactivityGraph
804
821
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
805
822
  otelOptions?: Partial<OtelOptions>
806
- boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
823
+ boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
807
824
  batchUpdates?: (run: () => void) => void
808
825
  disableDevtools?: boolean
809
826
  onBootStatus?: (status: BootStatus) => void
@@ -827,9 +844,13 @@ export const createStorePromise = async <
827
844
  })
828
845
  }
829
846
 
830
- return yield* createStore({ ...options, storeScope: scope }).pipe(Scope.extend(scope))
847
+ return yield* FiberSet.make().pipe(
848
+ Effect.andThen((fiberSet) => createStore({ ...options, fiberSet })),
849
+ Scope.extend(scope),
850
+ )
831
851
  }).pipe(Effect.withSpan('createStore'), runEffectPromise)
832
852
 
853
+ // #region createStore
833
854
  export const createStore = <
834
855
  TGraphQLContext extends BaseGraphQLContext,
835
856
  TSchema extends LiveStoreSchema = LiveStoreSchema,
@@ -843,8 +864,8 @@ export const createStore = <
843
864
  batchUpdates,
844
865
  disableDevtools,
845
866
  onBootStatus,
846
- storeScope,
847
- }: CreateStoreOptions<TGraphQLContext, TSchema> & { storeScope: Scope.CloseableScope }): Effect.Effect<
867
+ fiberSet,
868
+ }: CreateStoreOptions<TGraphQLContext, TSchema> & { fiberSet: FiberSet.FiberSet }): Effect.Effect<
848
869
  Store<TGraphQLContext, TSchema>,
849
870
  UnexpectedError,
850
871
  Scope.Scope
@@ -859,7 +880,7 @@ export const createStore = <
859
880
  return Effect.gen(function* () {
860
881
  const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
861
882
 
862
- const bootStatusQueue = yield* Queue.unbounded<BootStatus>()
883
+ const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
863
884
 
864
885
  yield* Queue.take(bootStatusQueue).pipe(
865
886
  Effect.tapSync((status) => onBootStatus?.(status)),
@@ -872,7 +893,12 @@ export const createStore = <
872
893
  schema,
873
894
  devtoolsEnabled: disableDevtools !== true,
874
895
  bootStatusQueue,
875
- shutdown: (cause) => Scope.close(storeScope, Exit.failCause(cause)),
896
+ shutdown: (cause) =>
897
+ Effect.gen(function* () {
898
+ yield* Effect.logWarning(`Shutting down LiveStore`, cause)
899
+ // TODO close parent scope? (Needs refactor with Mike A)
900
+ yield* FiberSet.clear(fiberSet)
901
+ }).pipe(Effect.withSpan('livestore:shutdown')),
876
902
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
877
903
 
878
904
  if (batchUpdates !== undefined) {
@@ -950,19 +976,12 @@ export const createStore = <
950
976
  },
951
977
  }
952
978
 
953
- const booting = yield* Effect.try({
954
- try: () => boot(bootDbImpl, span),
955
- catch: (cause) => new UnexpectedError({ cause }),
956
- })
957
- // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
958
- if (isPromise(booting)) {
959
- yield* Effect.tryPromise({ try: () => booting, catch: (cause) => new UnexpectedError({ cause }) })
960
- }
979
+ yield* Effect.tryAll(() => boot(bootDbImpl, span)).pipe(
980
+ UnexpectedError.mapToUnexpectedError,
981
+ Effect.withSpan('createStore:boot'),
982
+ )
961
983
  }
962
984
 
963
- // TODO: we can't apply the schema at this point, we've already loaded persisted data!
964
- // Think about what to do about this case.
965
- // await applySchema(db, schema)
966
985
  return Store.createStore<TGraphQLContext, TSchema>(
967
986
  {
968
987
  adapter,
@@ -972,12 +991,11 @@ export const createStore = <
972
991
  reactivityGraph,
973
992
  disableDevtools,
974
993
  __processedMutationIds,
975
- storeScope,
994
+ fiberSet,
976
995
  },
977
996
  span,
978
997
  )
979
998
  }).pipe(
980
- // Effect.scoped,
981
999
  Effect.withSpan('createStore', {
982
1000
  parent: otelOptions?.rootSpanContext
983
1001
  ? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
@@ -986,6 +1004,7 @@ export const createStore = <
986
1004
  Effect.provide(TracingLive),
987
1005
  )
988
1006
  }
1007
+ // #endregion createStore
989
1008
 
990
1009
  // TODO consider replacing with Effect's RC data structures
991
1010
  class ReferenceCountedSet<T> {