@livestore/livestore 0.0.54-dev.22 → 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 (43) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/react/fixture.d.ts +0 -8
  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/row-query.d.ts.map +1 -1
  28. package/dist/row-query.js +4 -38
  29. package/dist/row-query.js.map +1 -1
  30. package/dist/store.d.ts +7 -4
  31. package/dist/store.d.ts.map +1 -1
  32. package/dist/store.js +231 -96
  33. package/dist/store.js.map +1 -1
  34. package/package.json +5 -5
  35. package/src/__tests__/react/fixture.tsx +1 -1
  36. package/src/effect/LiveStore.ts +2 -1
  37. package/src/global-state.ts +0 -4
  38. package/src/index.ts +2 -1
  39. package/src/react/LiveStoreContext.ts +4 -0
  40. package/src/react/LiveStoreProvider.test.tsx +9 -4
  41. package/src/react/LiveStoreProvider.tsx +17 -10
  42. package/src/row-query.ts +5 -51
  43. package/src/store.ts +315 -128
package/src/store.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  BootDb,
3
+ BootStatus,
3
4
  ParamsObject,
4
5
  PreparedBindValues,
5
6
  ResetMode,
@@ -7,12 +8,12 @@ import type {
7
8
  StoreAdapterFactory,
8
9
  UnexpectedError,
9
10
  } from '@livestore/common'
10
- import { Devtools, getExecArgsFromMutation, prepareBindValues } from '@livestore/common'
11
- import { version as liveStoreVersion } from '@livestore/common/package.json'
11
+ import { Devtools, getExecArgsFromMutation, liveStoreVersion, prepareBindValues } from '@livestore/common'
12
12
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
13
13
  import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
14
- import { assertNever, isPromise, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
15
- import { Effect, Layer, OtelTracer, 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'
16
17
  import * as otel from '@opentelemetry/api'
17
18
  import type { GraphQLSchema } from 'graphql'
18
19
 
@@ -52,6 +53,8 @@ export type StoreOptions<
52
53
  otelOptions: OtelOptions
53
54
  reactivityGraph: ReactivityGraph
54
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>
55
58
  }
56
59
 
57
60
  export type RefreshReason =
@@ -106,6 +109,7 @@ export class Store<
106
109
  TSchema extends LiveStoreSchema = LiveStoreSchema,
107
110
  > {
108
111
  id = uniqueStoreId()
112
+ readonly devtoolsConnectionId = cuid()
109
113
  reactivityGraph: ReactivityGraph
110
114
  mainDbWrapper: MainDatabaseWrapper
111
115
  adapter: StoreAdapter
@@ -120,8 +124,8 @@ export class Store<
120
124
  tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
121
125
 
122
126
  // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
123
- __processedMutationIds = new Set<string>()
124
- __processedMutationWithoutRefreshIds = new Set<string>()
127
+ private __processedMutationIds
128
+ private __processedMutationWithoutRefreshIds = new Set<string>()
125
129
 
126
130
  /** RC-based set to see which queries are currently subscribed to */
127
131
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
@@ -135,6 +139,7 @@ export class Store<
135
139
  reactivityGraph,
136
140
  otelOptions,
137
141
  disableDevtools,
142
+ __processedMutationIds,
138
143
  }: StoreOptions<TGraphQLContext, TSchema>) {
139
144
  this.mainDbWrapper = new MainDatabaseWrapper({ otel: otelOptions, db: adapter.mainDb })
140
145
  this.adapter = adapter
@@ -143,6 +148,9 @@ export class Store<
143
148
  // TODO refactor
144
149
  this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
145
150
 
151
+ // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
152
+ this.__processedMutationIds = __processedMutationIds
153
+
146
154
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
147
155
  this.tableRefs = {}
148
156
  this.activeQueries = new ReferenceCountedSet()
@@ -165,8 +173,7 @@ export class Store<
165
173
  this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
166
174
  }),
167
175
  Stream.runDrain,
168
- Effect.tapCauseLogPretty,
169
- Effect.runFork,
176
+ runEffectFork,
170
177
  )
171
178
 
172
179
  if (disableDevtools !== true) {
@@ -180,11 +187,7 @@ export class Store<
180
187
  }
181
188
 
182
189
  // Need a set here since `schema.tables` might contain duplicates and some componentStateTables
183
- const allTableNames = new Set(
184
- this.schema.tables.keys(),
185
- // TODO activate dynamic tables
186
- // ...Array.from(dynamicallyRegisteredTables.values()).map((_) => _.sqliteDef.name),
187
- )
190
+ const allTableNames = new Set(this.schema.tables.keys())
188
191
  const existingTableRefs = new Map(
189
192
  Array.from(this.reactivityGraph.atoms.values())
190
193
  .filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
@@ -272,7 +275,7 @@ export class Store<
272
275
  otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
273
276
  otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
274
277
 
275
- await this.adapter.coordinator.shutdown.pipe(Effect.tapCauseLogPretty, Effect.runPromise)
278
+ await this.adapter.coordinator.shutdown.pipe(runEffectPromise)
276
279
  }
277
280
 
278
281
  mutate: {
@@ -491,7 +494,7 @@ export class Store<
491
494
  // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
492
495
  this.adapter.coordinator
493
496
  .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
494
- .pipe(Effect.tapCauseLogPretty, Effect.runFork)
497
+ .pipe(runEffectFork)
495
498
  }
496
499
 
497
500
  // Uncomment to print a list of queries currently registered on the store
@@ -517,9 +520,7 @@ export class Store<
517
520
  ) => {
518
521
  this.mainDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
519
522
 
520
- this.adapter.coordinator
521
- .execute(query, prepareBindValues(params, query))
522
- .pipe(Effect.tapCauseLogPretty, Effect.runFork)
523
+ this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(runEffectFork)
523
524
  }
524
525
 
525
526
  select = (query: string, params: ParamsObject = {}) => {
@@ -533,118 +534,268 @@ export class Store<
533
534
  meta: { liveStoreRefType: 'table' },
534
535
  })
535
536
 
537
+ // TODO shutdown behaviour
536
538
  private bootDevtools = () => {
537
- 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
+ }
538
544
 
539
- type Unsub = () => void
540
- type RequestId = string
545
+ const channelId = this.adapter.coordinator.devtools.channelId
541
546
 
542
- const reactivityGraphSubcriptions = new Map<RequestId, Unsub>()
543
- const liveQueriesSubscriptions = new Map<RequestId, Unsub>()
544
- devtoolsChannel.toAppHost.addEventListener('message', async (event) => {
545
- const decoded = Schema.decodeUnknownOption(Devtools.MessageToAppHost)(event.data)
546
- if (
547
- decoded._tag === 'None' ||
548
- decoded.value._tag === 'LSD.DevtoolsReady' ||
549
- decoded.value._tag === 'LSD.DevtoolsConnected' ||
550
- decoded.value.channelId !== this.adapter.coordinator.devtools.channelId
551
- ) {
552
- // 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 }))
553
555
  return
554
556
  }
555
557
 
556
- const requestId = decoded.value.requestId
557
- const sendToDevtools = (message: Devtools.MessageFromAppHost) =>
558
- devtoolsChannel.fromAppHost.postMessage(Schema.encodeSync(Devtools.MessageFromAppHost)(message))
559
-
560
- switch (decoded.value._tag) {
561
- case 'LSD.ReactivityGraphSubscribe': {
562
- const includeResults = decoded.value.includeResults
563
- const send = () =>
564
- sendToDevtools(
565
- Devtools.ReactivityGraphRes.make({
566
- reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
567
- requestId,
568
- liveStoreVersion,
569
- }),
570
- )
571
-
572
- send()
573
-
574
- reactivityGraphSubcriptions.set(
575
- requestId,
576
- this.reactivityGraph.subscribeToRefresh(() => send()),
577
- )
558
+ if (message.channelId !== channelId) return
578
559
 
579
- break
580
- }
581
- case 'LSD.DebugInfoReq': {
582
- sendToDevtools(
583
- Devtools.DebugInfoRes.make({ debugInfo: this.mainDbWrapper.debugInfo, requestId, liveStoreVersion }),
584
- )
585
- break
586
- }
587
- case 'LSD.DebugInfoResetReq': {
588
- this.mainDbWrapper.debugInfo.slowQueries.clear()
589
- sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
590
- break
591
- }
592
- case 'LSD.DebugInfoRerunQueryReq': {
593
- const { queryStr, bindValues, queriedTables } = decoded.value
594
- this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
595
- sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
596
- break
597
- }
598
- case 'LSD.ReactivityGraphUnsubscribe': {
599
- reactivityGraphSubcriptions.get(requestId)!()
600
- break
601
- }
602
- case 'LSD.LiveQueriesSubscribe': {
603
- const send = () =>
604
- sendToDevtools(
605
- Devtools.LiveQueriesRes.make({
606
- liveQueries: [...this.activeQueries].map((q) => ({
607
- _tag: q._tag,
608
- id: q.id,
609
- label: q.label,
610
- runs: q.runs,
611
- executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
612
- lastestResult:
613
- q.results$.previousResult === NOT_REFRESHED_YET
614
- ? 'SYMBOL_NOT_REFRESHED_YET'
615
- : q.results$.previousResult,
616
- activeSubscriptions: Array.from(q.activeSubscriptions),
617
- })),
618
- requestId,
619
- liveStoreVersion,
620
- }),
621
- )
622
-
623
- send()
624
-
625
- liveQueriesSubscriptions.set(
626
- requestId,
627
- this.reactivityGraph.subscribeToRefresh(() => send()),
628
- )
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)
629
567
 
630
- break
631
- }
632
- case 'LSD.LiveQueriesUnsubscribe': {
633
- liveQueriesSubscriptions.get(requestId)!()
634
- break
635
- }
636
- case 'LSD.ResetAllDataReq': {
637
- await this.adapter.coordinator
638
- .dangerouslyReset(decoded.value.mode)
639
- .pipe(Effect.tapCauseLogPretty, Effect.runPromise)
568
+ if (decodedMessage.channelId !== this.adapter.coordinator.devtools.channelId) {
569
+ // console.log(`Unknown message`, event)
570
+ return
571
+ }
572
+
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
+ })
640
675
 
641
- sendToDevtools(Devtools.ResetAllDataRes.make({ requestId, liveStoreVersion }))
676
+ storeMessagePort.start()
677
+ }),
678
+ runEffectFork,
679
+ )
642
680
 
643
- break
644
- }
645
- // No default
681
+ return
646
682
  }
647
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
+ // })
648
799
  }
649
800
 
650
801
  __devDownloadDb = () => {
@@ -653,13 +804,12 @@ export class Store<
653
804
  }
654
805
 
655
806
  __devDownloadMutationLogDb = async () => {
656
- const data = await this.adapter.coordinator.getMutationLogData.pipe(Effect.tapCauseLogPretty, Effect.runPromise)
807
+ const data = await this.adapter.coordinator.getMutationLogData.pipe(runEffectPromise)
657
808
  downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
658
809
  }
659
810
 
660
811
  // TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
661
- dangerouslyResetStorage = (mode: ResetMode) =>
662
- this.adapter.coordinator.dangerouslyReset(mode).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
812
+ dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
663
813
  }
664
814
 
665
815
  export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
@@ -671,6 +821,7 @@ export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSche
671
821
  boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
672
822
  batchUpdates?: (run: () => void) => void
673
823
  disableDevtools?: boolean
824
+ onBootStatus?: (status: BootStatus) => void
674
825
  }
675
826
 
676
827
  /** Create a new LiveStore Store */
@@ -679,7 +830,7 @@ export const createStore = async <
679
830
  TSchema extends LiveStoreSchema = LiveStoreSchema,
680
831
  >(
681
832
  options: CreateStoreOptions<TGraphQLContext, TSchema>,
682
- ): Promise<Store<TGraphQLContext, TSchema>> => createStoreEff(options).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
833
+ ): Promise<Store<TGraphQLContext, TSchema>> => createStoreEff(options).pipe(runEffectPromise)
683
834
 
684
835
  export const createStoreEff = <
685
836
  TGraphQLContext extends BaseGraphQLContext,
@@ -693,6 +844,7 @@ export const createStoreEff = <
693
844
  reactivityGraph = globalReactivityGraph,
694
845
  batchUpdates,
695
846
  disableDevtools,
847
+ onBootStatus,
696
848
  }: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<Store<TGraphQLContext, TSchema>, UnexpectedError> => {
697
849
  const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
698
850
  const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active()
@@ -704,17 +856,29 @@ export const createStoreEff = <
704
856
  return Effect.gen(function* () {
705
857
  const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
706
858
 
707
- const adapter = yield* adapterFactory({ schema }).pipe(
708
- Effect.withPerformanceMeasure('livestore:makeAdapter'),
709
- Effect.withSpan('createStore:makeAdapter'),
859
+ const bootStatusQueue = yield* Queue.unbounded<BootStatus>()
860
+
861
+ yield* Queue.take(bootStatusQueue).pipe(
862
+ Effect.tapSync((status) => onBootStatus?.(status)),
863
+ Effect.forever,
864
+ Effect.tapCauseLogPretty,
865
+ Effect.forkScoped,
710
866
  )
711
867
 
868
+ const adapter: StoreAdapter = yield* adapterFactory({
869
+ schema,
870
+ devtoolsEnabled: disableDevtools !== true,
871
+ bootStatusQueue,
872
+ }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
873
+
712
874
  if (batchUpdates !== undefined) {
713
875
  reactivityGraph.effectsWrapper = batchUpdates
714
876
  }
715
877
 
716
878
  const mutationEventSchema = makeMutationEventSchemaMemo(schema)
717
879
 
880
+ const __processedMutationIds = new Set<string>()
881
+
718
882
  // TODO consider moving booting into the storage backend
719
883
  if (boot !== undefined) {
720
884
  let isInTxn = false
@@ -729,7 +893,7 @@ export const createStoreEff = <
729
893
  if (isInTxn === true) {
730
894
  txnExecuteStmnts.push([queryStr, bindValues])
731
895
  } else {
732
- adapter.coordinator.execute(queryStr, bindValues).pipe(Effect.tapCauseLogPretty, Effect.runFork)
896
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
733
897
  }
734
898
  },
735
899
  mutate: (...list) => {
@@ -738,6 +902,8 @@ export const createStoreEff = <
738
902
  schema.mutations.get(mutationEventDecoded.mutation) ??
739
903
  shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
740
904
 
905
+ __processedMutationIds.add(mutationEventDecoded.id)
906
+
741
907
  const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
742
908
  // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
743
909
 
@@ -749,7 +915,7 @@ export const createStoreEff = <
749
915
 
750
916
  adapter.coordinator
751
917
  .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
752
- .pipe(Effect.tapCauseLogPretty, Effect.runFork)
918
+ .pipe(runEffectFork)
753
919
  }
754
920
  },
755
921
  select: (queryStr, bindValues) => {
@@ -767,7 +933,7 @@ export const createStoreEff = <
767
933
 
768
934
  // adapter.coordinator.execute('BEGIN', undefined, undefined)
769
935
  for (const [queryStr, bindValues] of txnExecuteStmnts) {
770
- adapter.coordinator.execute(queryStr, bindValues).pipe(Effect.tapCauseLogPretty, Effect.runFork)
936
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
771
937
  }
772
938
  // adapter.coordinator.execute('COMMIT', undefined, undefined)
773
939
  } catch (e: any) {
@@ -798,10 +964,12 @@ export const createStoreEff = <
798
964
  otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
799
965
  reactivityGraph,
800
966
  disableDevtools,
967
+ __processedMutationIds,
801
968
  },
802
969
  span,
803
970
  )
804
971
  }).pipe(
972
+ Effect.scoped,
805
973
  Effect.withSpan('createStore', {
806
974
  parent: otelOptions?.rootSpanContext
807
975
  ? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
@@ -811,6 +979,7 @@ export const createStoreEff = <
811
979
  )
812
980
  }
813
981
 
982
+ // TODO consider replacing with Effect's RC data structures
814
983
  class ReferenceCountedSet<T> {
815
984
  private map: Map<T, number>
816
985
 
@@ -846,3 +1015,21 @@ class ReferenceCountedSet<T> {
846
1015
  }
847
1016
  }
848
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
+ )