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

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 (46) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/react/fixture.d.ts +1 -9
  3. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/react/fixture.js +1 -1
  5. package/dist/__tests__/react/fixture.js.map +1 -1
  6. package/dist/effect/LiveStore.d.ts +1 -0
  7. package/dist/effect/LiveStore.d.ts.map +1 -1
  8. package/dist/effect/LiveStore.js +1 -1
  9. package/dist/effect/LiveStore.js.map +1 -1
  10. package/dist/global-state.d.ts +0 -2
  11. package/dist/global-state.d.ts.map +1 -1
  12. package/dist/global-state.js +0 -1
  13. package/dist/global-state.js.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/react/LiveStoreContext.d.ts.map +1 -1
  19. package/dist/react/LiveStoreContext.js +3 -0
  20. package/dist/react/LiveStoreContext.js.map +1 -1
  21. package/dist/react/LiveStoreProvider.d.ts +3 -3
  22. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  23. package/dist/react/LiveStoreProvider.js +16 -8
  24. package/dist/react/LiveStoreProvider.js.map +1 -1
  25. package/dist/react/LiveStoreProvider.test.js +6 -4
  26. package/dist/react/LiveStoreProvider.test.js.map +1 -1
  27. package/dist/reactive.js +1 -1
  28. package/dist/reactive.js.map +1 -1
  29. package/dist/row-query.d.ts.map +1 -1
  30. package/dist/row-query.js +3 -37
  31. package/dist/row-query.js.map +1 -1
  32. package/dist/store.d.ts +12 -6
  33. package/dist/store.d.ts.map +1 -1
  34. package/dist/store.js +312 -176
  35. package/dist/store.js.map +1 -1
  36. package/package.json +5 -5
  37. package/src/__tests__/react/fixture.tsx +1 -1
  38. package/src/effect/LiveStore.ts +2 -1
  39. package/src/global-state.ts +0 -4
  40. package/src/index.ts +2 -1
  41. package/src/react/LiveStoreContext.ts +4 -0
  42. package/src/react/LiveStoreProvider.test.tsx +9 -4
  43. package/src/react/LiveStoreProvider.tsx +17 -10
  44. package/src/reactive.ts +3 -1
  45. package/src/row-query.ts +4 -50
  46. package/src/store.ts +435 -224
package/src/store.ts CHANGED
@@ -1,17 +1,19 @@
1
1
  import type {
2
2
  BootDb,
3
+ BootStatus,
3
4
  ParamsObject,
4
5
  PreparedBindValues,
5
6
  ResetMode,
6
7
  StoreAdapter,
7
8
  StoreAdapterFactory,
9
+ UnexpectedError,
8
10
  } from '@livestore/common'
9
- import { Devtools, getExecArgsFromMutation, prepareBindValues } from '@livestore/common'
10
- import { version as liveStoreVersion } from '@livestore/common/package.json'
11
+ import { Devtools, getExecArgsFromMutation, liveStoreVersion, prepareBindValues } from '@livestore/common'
11
12
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
12
13
  import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
13
- import { assertNever, isPromise, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
14
- import { Effect, Schema, Stream } from '@livestore/utils/effect'
14
+ import { assertNever, isPromise, makeNoopTracer, shouldNeverHappen, throttle } from '@livestore/utils'
15
+ import { cuid } from '@livestore/utils/cuid'
16
+ import { Effect, Layer, Logger, LogLevel, OtelTracer, Queue, Schema, Stream } from '@livestore/utils/effect'
15
17
  import * as otel from '@opentelemetry/api'
16
18
  import type { GraphQLSchema } from 'graphql'
17
19
 
@@ -19,6 +21,7 @@ import { globalReactivityGraph } from './global-state.js'
19
21
  import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
20
22
  import type { StackInfo } from './react/utils/stack-info.js'
21
23
  import type { DebugRefreshReasonBase, Ref } from './reactive.js'
24
+ import { NOT_REFRESHED_YET } from './reactive.js'
22
25
  import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
23
26
  import { downloadBlob } from './utils/dev.js'
24
27
  import { getDurationMsFromSpan } from './utils/otel.js'
@@ -50,6 +53,8 @@ export type StoreOptions<
50
53
  otelOptions: OtelOptions
51
54
  reactivityGraph: ReactivityGraph
52
55
  disableDevtools?: boolean
56
+ // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
57
+ __processedMutationIds: Set<string>
53
58
  }
54
59
 
55
60
  export type RefreshReason =
@@ -104,6 +109,7 @@ export class Store<
104
109
  TSchema extends LiveStoreSchema = LiveStoreSchema,
105
110
  > {
106
111
  id = uniqueStoreId()
112
+ readonly devtoolsConnectionId = cuid()
107
113
  reactivityGraph: ReactivityGraph
108
114
  mainDbWrapper: MainDatabaseWrapper
109
115
  adapter: StoreAdapter
@@ -118,8 +124,8 @@ export class Store<
118
124
  tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
119
125
 
120
126
  // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
121
- __processedMutationIds = new Set<string>()
122
- __processedMutationWithoutRefreshIds = new Set<string>()
127
+ private __processedMutationIds
128
+ private __processedMutationWithoutRefreshIds = new Set<string>()
123
129
 
124
130
  /** RC-based set to see which queries are currently subscribed to */
125
131
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
@@ -133,6 +139,7 @@ export class Store<
133
139
  reactivityGraph,
134
140
  otelOptions,
135
141
  disableDevtools,
142
+ __processedMutationIds,
136
143
  }: StoreOptions<TGraphQLContext, TSchema>) {
137
144
  this.mainDbWrapper = new MainDatabaseWrapper({ otel: otelOptions, db: adapter.mainDb })
138
145
  this.adapter = adapter
@@ -141,6 +148,9 @@ export class Store<
141
148
  // TODO refactor
142
149
  this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
143
150
 
151
+ // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
152
+ this.__processedMutationIds = __processedMutationIds
153
+
144
154
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
145
155
  this.tableRefs = {}
146
156
  this.activeQueries = new ReferenceCountedSet()
@@ -153,7 +163,7 @@ export class Store<
153
163
 
154
164
  this.reactivityGraph = reactivityGraph
155
165
  this.reactivityGraph.context = {
156
- store: this as any,
166
+ store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
157
167
  otelTracer: otelOptions.tracer,
158
168
  rootOtelContext: otelQueriesSpanContext,
159
169
  }
@@ -163,8 +173,7 @@ export class Store<
163
173
  this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
164
174
  }),
165
175
  Stream.runDrain,
166
- Effect.tapCauseLogPretty,
167
- Effect.runFork,
176
+ runEffectFork,
168
177
  )
169
178
 
170
179
  if (disableDevtools !== true) {
@@ -178,11 +187,7 @@ export class Store<
178
187
  }
179
188
 
180
189
  // Need a set here since `schema.tables` might contain duplicates and some componentStateTables
181
- const allTableNames = new Set(
182
- this.schema.tables.keys(),
183
- // TODO activate dynamic tables
184
- // ...Array.from(dynamicallyRegisteredTables.values()).map((_) => _.sqliteDef.name),
185
- )
190
+ const allTableNames = new Set(this.schema.tables.keys())
186
191
  const existingTableRefs = new Map(
187
192
  Array.from(this.reactivityGraph.atoms.values())
188
193
  .filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
@@ -270,7 +275,7 @@ export class Store<
270
275
  otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
271
276
  otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
272
277
 
273
- await this.adapter.coordinator.shutdown()
278
+ await this.adapter.coordinator.shutdown.pipe(runEffectPromise)
274
279
  }
275
280
 
276
281
  mutate: {
@@ -487,10 +492,9 @@ export class Store<
487
492
 
488
493
  if (coordinatorMode !== 'skip-coordinator') {
489
494
  // 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
- })
495
+ this.adapter.coordinator
496
+ .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
497
+ .pipe(runEffectFork)
494
498
  }
495
499
 
496
500
  // Uncomment to print a list of queries currently registered on the store
@@ -516,8 +520,7 @@ export class Store<
516
520
  ) => {
517
521
  this.mainDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
518
522
 
519
- const parentSpan = otel.trace.getSpan(otel.context.active())
520
- this.adapter.coordinator.execute(query, prepareBindValues(params, query), parentSpan)
523
+ this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(runEffectFork)
521
524
  }
522
525
 
523
526
  select = (query: string, params: ParamsObject = {}) => {
@@ -531,112 +534,268 @@ export class Store<
531
534
  meta: { liveStoreRefType: 'table' },
532
535
  })
533
536
 
537
+ // TODO shutdown behaviour
534
538
  private bootDevtools = () => {
535
- const devtoolsChannel = Devtools.makeBroadcastChannels()
539
+ const sendToDevtoolsContentscript = (
540
+ message: typeof Devtools.DevtoolsWindowMessage.MessageForContentscript.Type,
541
+ ) => {
542
+ window.postMessage(Schema.encodeSync(Devtools.DevtoolsWindowMessage.MessageForContentscript)(message), '*')
543
+ }
536
544
 
537
- type Unsub = () => void
538
- type RequestId = string
545
+ const channelId = this.adapter.coordinator.devtools.channelId
539
546
 
540
- const reactivityGraphSubcriptions = new Map<RequestId, Unsub>()
541
- const liveQueriesSubscriptions = new Map<RequestId, Unsub>()
542
- devtoolsChannel.toAppHost.addEventListener('message', async (event) => {
543
- const decoded = Schema.decodeUnknownOption(Devtools.MessageToAppHost)(event.data)
544
- if (
545
- decoded._tag === 'None' ||
546
- decoded.value._tag === 'LSD.DevtoolsReady' ||
547
- decoded.value._tag === 'LSD.DevtoolsConnected' ||
548
- decoded.value.channelId !== this.adapter.coordinator.devtools.channelId
549
- ) {
550
- // console.log(`Unknown message`, event)
547
+ window.addEventListener('message', (event) => {
548
+ const decodedMessageRes = Schema.decodeOption(Devtools.DevtoolsWindowMessage.MessageForStore)(event.data)
549
+ if (decodedMessageRes._tag === 'None') return
550
+
551
+ const message = decodedMessageRes.value
552
+
553
+ if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
554
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
551
555
  return
552
556
  }
553
557
 
554
- const requestId = decoded.value.requestId
555
- const sendToDevtools = (message: Devtools.MessageFromAppHost) =>
556
- devtoolsChannel.fromAppHost.postMessage(Schema.encodeSync(Devtools.MessageFromAppHost)(message))
557
-
558
- switch (decoded.value._tag) {
559
- case 'LSD.ReactivityGraphSubscribe': {
560
- const includeResults = decoded.value.includeResults
561
- const send = () =>
562
- sendToDevtools(
563
- Devtools.ReactivityGraphRes.make({
564
- reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
565
- requestId,
566
- liveStoreVersion,
567
- }),
568
- )
569
-
570
- send()
571
-
572
- reactivityGraphSubcriptions.set(
573
- requestId,
574
- this.reactivityGraph.subscribeToRefresh(() => send()),
575
- )
558
+ if (message.channelId !== channelId) return
576
559
 
577
- break
578
- }
579
- case 'LSD.DebugInfoReq': {
580
- sendToDevtools(
581
- Devtools.DebugInfoRes.make({ debugInfo: this.mainDbWrapper.debugInfo, requestId, liveStoreVersion }),
582
- )
583
- break
584
- }
585
- case 'LSD.DebugInfoResetReq': {
586
- this.mainDbWrapper.debugInfo.slowQueries.clear()
587
- sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
588
- break
589
- }
590
- case 'LSD.DebugInfoRerunQueryReq': {
591
- const { queryStr, bindValues, queriedTables } = decoded.value
592
- this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
593
- sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
594
- break
595
- }
596
- case 'LSD.ReactivityGraphUnsubscribe': {
597
- reactivityGraphSubcriptions.get(requestId)!()
598
- break
599
- }
600
- case 'LSD.LiveQueriesSubscribe': {
601
- const send = () =>
602
- sendToDevtools(
603
- Devtools.LiveQueriesRes.make({
604
- liveQueries: [...this.activeQueries].map((q) => ({
605
- _tag: q._tag,
606
- id: q.id,
607
- label: q.label,
608
- runs: q.runs,
609
- executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
610
- lastestResult: q.results$.previousResult,
611
- activeSubscriptions: Array.from(q.activeSubscriptions),
612
- })),
613
- requestId,
614
- liveStoreVersion,
615
- }),
616
- )
617
-
618
- send()
619
-
620
- liveQueriesSubscriptions.set(
621
- requestId,
622
- this.reactivityGraph.subscribeToRefresh(() => send()),
623
- )
560
+ if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
561
+ this.adapter.coordinator.devtools.connect({ port: message.port, connectionId: this.devtoolsConnectionId }).pipe(
562
+ Effect.tapSync(({ storeMessagePort }) => {
563
+ // console.log('storeMessagePort', storeMessagePort)
564
+ storeMessagePort.addEventListener('message', (event) => {
565
+ const decodedMessage = Schema.decodeUnknownSync(Devtools.MessageToAppHostStore)(event.data)
566
+ // console.log('storeMessagePort message', decodedMessage)
624
567
 
625
- break
626
- }
627
- case 'LSD.LiveQueriesUnsubscribe': {
628
- liveQueriesSubscriptions.get(requestId)!()
629
- break
630
- }
631
- case 'LSD.ResetAllDataReq': {
632
- await this.adapter.coordinator.dangerouslyReset(decoded.value.mode)
633
- sendToDevtools(Devtools.ResetAllDataRes.make({ requestId, liveStoreVersion }))
568
+ if (decodedMessage.channelId !== this.adapter.coordinator.devtools.channelId) {
569
+ // console.log(`Unknown message`, event)
570
+ return
571
+ }
634
572
 
635
- break
636
- }
637
- // No default
573
+ const requestId = decodedMessage.requestId
574
+ const sendToDevtools = (message: Devtools.MessageFromAppHostStore) =>
575
+ storeMessagePort.postMessage(Schema.encodeSync(Devtools.MessageFromAppHostStore)(message))
576
+
577
+ const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
578
+
579
+ switch (decodedMessage._tag) {
580
+ case 'LSD.ReactivityGraphSubscribe': {
581
+ const includeResults = decodedMessage.includeResults
582
+
583
+ const send = () =>
584
+ // In order to not add more work to the current tick, we use requestIdleCallback
585
+ // to send the reactivity graph updates to the devtools
586
+ requestIdleCallback(
587
+ () =>
588
+ sendToDevtools(
589
+ Devtools.ReactivityGraphRes.make({
590
+ reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
591
+ requestId,
592
+ liveStoreVersion,
593
+ }),
594
+ ),
595
+ { timeout: 500 },
596
+ )
597
+
598
+ send()
599
+
600
+ // In some cases, there can be A LOT of reactivity graph updates in a short period of time
601
+ // so we throttle the updates to avoid sending too much data
602
+ // This might need to be tweaked further and possibly be exposed to the user in some way.
603
+ const throttledSend = throttle(send, 20)
604
+
605
+ reactivityGraphSubcriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
606
+
607
+ break
608
+ }
609
+ case 'LSD.DebugInfoReq': {
610
+ sendToDevtools(
611
+ Devtools.DebugInfoRes.make({
612
+ debugInfo: this.mainDbWrapper.debugInfo,
613
+ requestId,
614
+ liveStoreVersion,
615
+ }),
616
+ )
617
+ break
618
+ }
619
+ case 'LSD.DebugInfoResetReq': {
620
+ this.mainDbWrapper.debugInfo.slowQueries.clear()
621
+ sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
622
+ break
623
+ }
624
+ case 'LSD.DebugInfoRerunQueryReq': {
625
+ const { queryStr, bindValues, queriedTables } = decodedMessage
626
+ this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
627
+ sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
628
+ break
629
+ }
630
+ case 'LSD.ReactivityGraphUnsubscribe': {
631
+ reactivityGraphSubcriptions.get(requestId)!()
632
+ break
633
+ }
634
+ case 'LSD.LiveQueriesSubscribe': {
635
+ const send = () =>
636
+ requestIdleCallback(
637
+ () =>
638
+ sendToDevtools(
639
+ Devtools.LiveQueriesRes.make({
640
+ liveQueries: [...this.activeQueries].map((q) => ({
641
+ _tag: q._tag,
642
+ id: q.id,
643
+ label: q.label,
644
+ runs: q.runs,
645
+ executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
646
+ lastestResult:
647
+ q.results$.previousResult === NOT_REFRESHED_YET
648
+ ? 'SYMBOL_NOT_REFRESHED_YET'
649
+ : q.results$.previousResult,
650
+ activeSubscriptions: Array.from(q.activeSubscriptions),
651
+ })),
652
+ requestId,
653
+ liveStoreVersion,
654
+ }),
655
+ ),
656
+ { timeout: 500 },
657
+ )
658
+
659
+ send()
660
+
661
+ // Same as in the reactivity graph subscription case above, we need to throttle the updates
662
+ const throttledSend = throttle(send, 20)
663
+
664
+ liveQueriesSubscriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
665
+
666
+ break
667
+ }
668
+ case 'LSD.LiveQueriesUnsubscribe': {
669
+ liveQueriesSubscriptions.get(requestId)!()
670
+ break
671
+ }
672
+ // No default
673
+ }
674
+ })
675
+
676
+ storeMessagePort.start()
677
+ }),
678
+ runEffectFork,
679
+ )
680
+
681
+ return
638
682
  }
639
683
  })
684
+
685
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
686
+
687
+ // const devtoolsChannel = Devtools.makeBroadcastChannels()
688
+
689
+ type Unsub = () => void
690
+ type RequestId = string
691
+
692
+ const reactivityGraphSubcriptions = new Map<RequestId, Unsub>()
693
+ const liveQueriesSubscriptions = new Map<RequestId, Unsub>()
694
+ // devtoolsChannel.toAppHost.addEventListener('message', async (event) => {
695
+ // const decoded = Schema.decodeUnknownOption(Devtools.MessageToAppHostStore)(event.data)
696
+ // if (decoded._tag === 'None' || decoded.value.channelId !== this.adapter.coordinator.devtools.channelId) {
697
+ // // console.log(`Unknown message`, event)
698
+ // return
699
+ // }
700
+
701
+ // const requestId = decoded.value.requestId
702
+ // const sendToDevtools = (message: Devtools.MessageFromAppHostStore) =>
703
+ // devtoolsChannel.fromAppHost.postMessage(Schema.encodeSync(Devtools.MessageFromAppHostStore)(message))
704
+
705
+ // const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
706
+
707
+ // switch (decoded.value._tag) {
708
+ // case 'LSD.ReactivityGraphSubscribe': {
709
+ // const includeResults = decoded.value.includeResults
710
+
711
+ // const send = () =>
712
+ // // In order to not add more work to the current tick, we use requestIdleCallback
713
+ // // to send the reactivity graph updates to the devtools
714
+ // requestIdleCallback(
715
+ // () =>
716
+ // sendToDevtools(
717
+ // Devtools.ReactivityGraphRes.make({
718
+ // reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
719
+ // requestId,
720
+ // liveStoreVersion,
721
+ // }),
722
+ // ),
723
+ // { timeout: 500 },
724
+ // )
725
+
726
+ // send()
727
+
728
+ // // In some cases, there can be A LOT of reactivity graph updates in a short period of time
729
+ // // so we throttle the updates to avoid sending too much data
730
+ // // This might need to be tweaked further and possibly be exposed to the user in some way.
731
+ // const throttledSend = throttle(send, 20)
732
+
733
+ // reactivityGraphSubcriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
734
+
735
+ // break
736
+ // }
737
+ // case 'LSD.DebugInfoReq': {
738
+ // sendToDevtools(
739
+ // Devtools.DebugInfoRes.make({ debugInfo: this.mainDbWrapper.debugInfo, requestId, liveStoreVersion }),
740
+ // )
741
+ // break
742
+ // }
743
+ // case 'LSD.DebugInfoResetReq': {
744
+ // this.mainDbWrapper.debugInfo.slowQueries.clear()
745
+ // sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
746
+ // break
747
+ // }
748
+ // case 'LSD.DebugInfoRerunQueryReq': {
749
+ // const { queryStr, bindValues, queriedTables } = decoded.value
750
+ // this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
751
+ // sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
752
+ // break
753
+ // }
754
+ // case 'LSD.ReactivityGraphUnsubscribe': {
755
+ // reactivityGraphSubcriptions.get(requestId)!()
756
+ // break
757
+ // }
758
+ // case 'LSD.LiveQueriesSubscribe': {
759
+ // const send = () =>
760
+ // requestIdleCallback(
761
+ // () =>
762
+ // sendToDevtools(
763
+ // Devtools.LiveQueriesRes.make({
764
+ // liveQueries: [...this.activeQueries].map((q) => ({
765
+ // _tag: q._tag,
766
+ // id: q.id,
767
+ // label: q.label,
768
+ // runs: q.runs,
769
+ // executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
770
+ // lastestResult:
771
+ // q.results$.previousResult === NOT_REFRESHED_YET
772
+ // ? 'SYMBOL_NOT_REFRESHED_YET'
773
+ // : q.results$.previousResult,
774
+ // activeSubscriptions: Array.from(q.activeSubscriptions),
775
+ // })),
776
+ // requestId,
777
+ // liveStoreVersion,
778
+ // }),
779
+ // ),
780
+ // { timeout: 500 },
781
+ // )
782
+
783
+ // send()
784
+
785
+ // // Same as in the reactivity graph subscription case above, we need to throttle the updates
786
+ // const throttledSend = throttle(send, 20)
787
+
788
+ // liveQueriesSubscriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
789
+
790
+ // break
791
+ // }
792
+ // case 'LSD.LiveQueriesUnsubscribe': {
793
+ // liveQueriesSubscriptions.get(requestId)!()
794
+ // break
795
+ // }
796
+ // // No default
797
+ // }
798
+ // })
640
799
  }
641
800
 
642
801
  __devDownloadDb = () => {
@@ -645,18 +804,37 @@ export class Store<
645
804
  }
646
805
 
647
806
  __devDownloadMutationLogDb = async () => {
648
- const data = await this.adapter.coordinator.getMutationLogData()
807
+ const data = await this.adapter.coordinator.getMutationLogData.pipe(runEffectPromise)
649
808
  downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
650
809
  }
651
810
 
652
811
  // 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)
812
+ dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
813
+ }
814
+
815
+ export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
816
+ schema: TSchema
817
+ adapter: StoreAdapterFactory
818
+ reactivityGraph?: ReactivityGraph
819
+ graphQLOptions?: GraphQLOptions<TGraphQLContext>
820
+ otelOptions?: Partial<OtelOptions>
821
+ boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
822
+ batchUpdates?: (run: () => void) => void
823
+ disableDevtools?: boolean
824
+ onBootStatus?: (status: BootStatus) => void
654
825
  }
655
826
 
656
827
  /** Create a new LiveStore Store */
657
828
  export const createStore = async <
658
829
  TGraphQLContext extends BaseGraphQLContext,
659
830
  TSchema extends LiveStoreSchema = LiveStoreSchema,
831
+ >(
832
+ options: CreateStoreOptions<TGraphQLContext, TSchema>,
833
+ ): Promise<Store<TGraphQLContext, TSchema>> => createStoreEff(options).pipe(runEffectPromise)
834
+
835
+ export const createStoreEff = <
836
+ TGraphQLContext extends BaseGraphQLContext,
837
+ TSchema extends LiveStoreSchema = LiveStoreSchema,
660
838
  >({
661
839
  schema,
662
840
  graphQLOptions,
@@ -666,127 +844,142 @@ export const createStore = async <
666
844
  reactivityGraph = globalReactivityGraph,
667
845
  batchUpdates,
668
846
  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>> => {
847
+ onBootStatus,
848
+ }: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<Store<TGraphQLContext, TSchema>, UnexpectedError> => {
679
849
  const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
680
850
  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
851
 
695
- const mutationEventSchema = makeMutationEventSchemaMemo(schema)
852
+ const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
853
+ Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)),
854
+ )
696
855
 
697
- // TODO consider moving booting into the storage backend
698
- if (boot !== undefined) {
699
- let isInTxn = false
700
- let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
856
+ return Effect.gen(function* () {
857
+ const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
701
858
 
702
- const bootDbImpl: BootDb = {
703
- _tag: 'BootDb',
704
- execute: (queryStr, bindValues) => {
705
- const stmt = adapter.mainDb.prepare(queryStr)
706
- stmt.execute(bindValues)
859
+ const bootStatusQueue = yield* Queue.unbounded<BootStatus>()
707
860
 
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
- }
861
+ yield* Queue.take(bootStatusQueue).pipe(
862
+ Effect.tapSync((status) => onBootStatus?.(status)),
863
+ Effect.forever,
864
+ Effect.tapCauseLogPretty,
865
+ Effect.forkScoped,
866
+ )
726
867
 
727
- const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
868
+ const adapter: StoreAdapter = yield* adapterFactory({
869
+ schema,
870
+ devtoolsEnabled: disableDevtools !== true,
871
+ bootStatusQueue,
872
+ }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
728
873
 
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 = []
874
+ if (batchUpdates !== undefined) {
875
+ reactivityGraph.effectsWrapper = batchUpdates
876
+ }
877
+
878
+ const mutationEventSchema = makeMutationEventSchemaMemo(schema)
879
+
880
+ const __processedMutationIds = new Set<string>()
881
+
882
+ // TODO consider moving booting into the storage backend
883
+ if (boot !== undefined) {
884
+ let isInTxn = false
885
+ let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
886
+
887
+ const bootDbImpl: BootDb = {
888
+ _tag: 'BootDb',
889
+ execute: (queryStr, bindValues) => {
890
+ const stmt = adapter.mainDb.prepare(queryStr)
891
+ stmt.execute(bindValues)
892
+
893
+ if (isInTxn === true) {
894
+ txnExecuteStmnts.push([queryStr, bindValues])
895
+ } else {
896
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
897
+ }
898
+ },
899
+ mutate: (...list) => {
900
+ for (const mutationEventDecoded of list) {
901
+ const mutationDef =
902
+ schema.mutations.get(mutationEventDecoded.mutation) ??
903
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
904
+
905
+ __processedMutationIds.add(mutationEventDecoded.id)
906
+
907
+ const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
908
+ // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
909
+
910
+ for (const { statementSql, bindValues } of execArgsArr) {
911
+ adapter.mainDb.execute(statementSql, bindValues)
759
912
  }
760
- },
761
- }
762
913
 
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
- }
914
+ const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
769
915
 
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,
916
+ adapter.coordinator
917
+ .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
918
+ .pipe(runEffectFork)
919
+ }
920
+ },
921
+ select: (queryStr, bindValues) => {
922
+ const stmt = adapter.mainDb.prepare(queryStr)
923
+ return stmt.select(bindValues)
781
924
  },
782
- span,
783
- )
784
- } finally {
785
- span.end()
925
+ txn: (callback) => {
926
+ try {
927
+ isInTxn = true
928
+ adapter.mainDb.execute('BEGIN', undefined)
929
+
930
+ callback()
931
+
932
+ adapter.mainDb.execute('COMMIT', undefined)
933
+
934
+ // adapter.coordinator.execute('BEGIN', undefined, undefined)
935
+ for (const [queryStr, bindValues] of txnExecuteStmnts) {
936
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
937
+ }
938
+ // adapter.coordinator.execute('COMMIT', undefined, undefined)
939
+ } catch (e: any) {
940
+ adapter.mainDb.execute('ROLLBACK', undefined)
941
+ throw e
942
+ } finally {
943
+ isInTxn = false
944
+ txnExecuteStmnts = []
945
+ }
946
+ },
947
+ }
948
+
949
+ const booting = boot(bootDbImpl, span)
950
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
951
+ if (isPromise(booting)) {
952
+ yield* Effect.promise(() => booting)
953
+ }
786
954
  }
787
- })
955
+
956
+ // TODO: we can't apply the schema at this point, we've already loaded persisted data!
957
+ // Think about what to do about this case.
958
+ // await applySchema(db, schema)
959
+ return Store.createStore<TGraphQLContext, TSchema>(
960
+ {
961
+ adapter,
962
+ schema,
963
+ graphQLOptions,
964
+ otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
965
+ reactivityGraph,
966
+ disableDevtools,
967
+ __processedMutationIds,
968
+ },
969
+ span,
970
+ )
971
+ }).pipe(
972
+ Effect.scoped,
973
+ Effect.withSpan('createStore', {
974
+ parent: otelOptions?.rootSpanContext
975
+ ? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
976
+ : undefined,
977
+ }),
978
+ Effect.provide(TracingLive),
979
+ )
788
980
  }
789
981
 
982
+ // TODO consider replacing with Effect's RC data structures
790
983
  class ReferenceCountedSet<T> {
791
984
  private map: Map<T, number>
792
985
 
@@ -822,3 +1015,21 @@ class ReferenceCountedSet<T> {
822
1015
  }
823
1016
  }
824
1017
  }
1018
+
1019
+ const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
1020
+ effect.pipe(
1021
+ Effect.tapCauseLogPretty,
1022
+ Effect.annotateLogs({ thread: 'window' }),
1023
+ Effect.provide(Logger.pretty),
1024
+ Logger.withMinimumLogLevel(LogLevel.Debug),
1025
+ Effect.runFork,
1026
+ )
1027
+
1028
+ const runEffectPromise = <A, E>(effect: Effect.Effect<A, E, never>) =>
1029
+ effect.pipe(
1030
+ Effect.tapCauseLogPretty,
1031
+ Effect.annotateLogs({ thread: 'window' }),
1032
+ Effect.provide(Logger.pretty),
1033
+ Logger.withMinimumLogLevel(LogLevel.Debug),
1034
+ Effect.runPromise,
1035
+ )