@livestore/livestore 0.0.55-dev.1 → 0.0.55-dev.2

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
@@ -1,26 +1,21 @@
1
1
  import type {
2
2
  BootDb,
3
3
  BootStatus,
4
- DebugInfo,
5
4
  ParamsObject,
6
5
  PreparedBindValues,
7
6
  ResetMode,
8
7
  StoreAdapter,
9
8
  StoreAdapterFactory,
10
9
  } from '@livestore/common'
11
- import {
12
- Devtools,
13
- getExecArgsFromMutation,
14
- liveStoreVersion,
15
- prepareBindValues,
16
- UnexpectedError,
17
- } from '@livestore/common'
10
+ import { Devtools, getExecArgsFromMutation, prepareBindValues, UnexpectedError } from '@livestore/common'
18
11
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
19
- import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
20
- import { assertNever, makeNoopTracer, shouldNeverHappen, throttle } from '@livestore/utils'
12
+ import { makeMutationEventSchemaMemo, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE } from '@livestore/common/schema'
13
+ import { assertNever, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
21
14
  import { cuid } from '@livestore/utils/cuid'
22
15
  import {
16
+ BrowserChannel,
23
17
  Effect,
18
+ Either,
24
19
  Exit,
25
20
  FiberSet,
26
21
  Inspectable,
@@ -38,11 +33,12 @@ import * as otel from '@opentelemetry/api'
38
33
  import type { GraphQLSchema } from 'graphql'
39
34
 
40
35
  import { globalReactivityGraph } from './global-state.js'
41
- import { emptyDebugInfo as makeEmptyDebugInfo, MainDatabaseWrapper } from './MainDatabaseWrapper.js'
36
+ import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
42
37
  import type { StackInfo } from './react/utils/stack-info.js'
43
38
  import type { DebugRefreshReasonBase, Ref } from './reactive.js'
44
- import { NOT_REFRESHED_YET } from './reactive.js'
45
39
  import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
40
+ import { connectStoreToDevtools } from './store-devtools.js'
41
+ import { ReferenceCountedSet } from './utils/data-structures.js'
46
42
  import { downloadBlob } from './utils/dev.js'
47
43
  import { getDurationMsFromSpan } from './utils/otel.js'
48
44
 
@@ -154,6 +150,7 @@ export class Store<
154
150
 
155
151
  readonly __mutationEventSchema
156
152
 
153
+ // #region constructor
157
154
  private constructor({
158
155
  adapter,
159
156
  schema,
@@ -201,8 +198,20 @@ export class Store<
201
198
  queriesSpanContext: otelQueriesSpanContext,
202
199
  }
203
200
 
201
+ // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
202
+ // But for now this is a good enough approximation with little downsides
203
+ const isRunningInDevtools = disableDevtools === true
204
+
204
205
  // Need a set here since `schema.tables` might contain duplicates and some componentStateTables
205
- const allTableNames = new Set(this.schema.tables.keys())
206
+ const allTableNames = new Set(
207
+ // NOTE we're excluding the LiveStore schema and mutations tables as they are not user-facing
208
+ // unless LiveStore is running in the devtools
209
+ isRunningInDevtools
210
+ ? this.schema.tables.keys()
211
+ : Array.from(this.schema.tables.keys()).filter(
212
+ (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE,
213
+ ),
214
+ )
206
215
  const existingTableRefs = new Map(
207
216
  Array.from(this.reactivityGraph.atoms.values())
208
217
  .filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
@@ -248,6 +257,8 @@ export class Store<
248
257
  }).pipe(Effect.scoped, Effect.withSpan('LiveStore:store-constructor'), FiberSet.run(fiberSet), runEffectFork)
249
258
  }
250
259
 
260
+ // #endregion constructor
261
+
251
262
  static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
252
263
  storeOptions: StoreOptions<TGraphQLContext, TSchema>,
253
264
  parentSpan: otel.Span,
@@ -314,6 +325,7 @@ export class Store<
314
325
  await FiberSet.clear(this.fiberSet).pipe(Effect.withSpan('Store:destroy'), runEffectPromise)
315
326
  }
316
327
 
328
+ // #region mutate
317
329
  mutate: {
318
330
  <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg): void
319
331
  (
@@ -549,6 +561,7 @@ export class Store<
549
561
  },
550
562
  )
551
563
  }
564
+ // #endregion mutate
552
565
 
553
566
  /**
554
567
  * Directly execute a SQL query on the Store.
@@ -578,222 +591,48 @@ export class Store<
578
591
  })
579
592
 
580
593
  // #region devtools
581
- // TODO shutdown behaviour
582
594
  private bootDevtools = () =>
583
595
  Effect.gen(this, function* () {
584
- const sendToDevtoolsContentscript = (
585
- message: typeof Devtools.DevtoolsWindowMessage.MessageForContentscript.Type,
586
- ) => {
587
- window.postMessage(Schema.encodeSync(Devtools.DevtoolsWindowMessage.MessageForContentscript)(message), '*')
588
- }
589
-
590
- sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
596
+ // const webBridgeBroadcastChannel = yield* Devtools.WebBridge.makeBroadcastChannel()
591
597
 
598
+ // eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
599
+ const store = this
592
600
  const channelId = this.adapter.coordinator.devtools.channelId
593
601
 
594
- const runtime = yield* Effect.runtime()
595
-
596
- window.addEventListener('message', (event) => {
597
- const decodedMessageRes = Schema.decodeOption(Devtools.DevtoolsWindowMessage.MessageForStore)(event.data)
598
- if (decodedMessageRes._tag === 'None') return
599
-
600
- const message = decodedMessageRes.value
601
-
602
- if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
603
- sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
604
- return
605
- }
606
-
607
- if (message.channelId !== channelId) return
608
-
609
- if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
610
- type Unsub = () => void
611
- type RequestId = string
612
-
613
- const reactivityGraphSubcriptions = new Map<RequestId, Unsub>()
614
- const liveQueriesSubscriptions = new Map<RequestId, Unsub>()
615
- const debugInfoHistorySubscriptions = new Map<RequestId, Unsub>()
616
-
617
- this.adapter.coordinator.devtools
618
- .connect({ port: message.port, connectionId: this.devtoolsConnectionId })
619
- .pipe(
620
- Effect.tapSync(({ storeMessagePort }) => {
621
- // console.log('storeMessagePort', storeMessagePort)
622
- storeMessagePort.addEventListener('message', (event) => {
623
- const decodedMessage = Schema.decodeUnknownSync(Devtools.MessageToAppHostStore)(event.data)
624
- // console.log('storeMessagePort message', decodedMessage)
625
-
626
- if (decodedMessage.channelId !== this.adapter.coordinator.devtools.channelId) {
627
- // console.log(`Unknown message`, event)
628
- return
629
- }
630
-
631
- const requestId = decodedMessage.requestId
632
- const sendToDevtools = (message: Devtools.MessageFromAppHostStore) =>
633
- storeMessagePort.postMessage(Schema.encodeSync(Devtools.MessageFromAppHostStore)(message))
634
-
635
- const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
636
-
637
- switch (decodedMessage._tag) {
638
- case 'LSD.ReactivityGraphSubscribe': {
639
- const includeResults = decodedMessage.includeResults
640
-
641
- const send = () =>
642
- // In order to not add more work to the current tick, we use requestIdleCallback
643
- // to send the reactivity graph updates to the devtools
644
- requestIdleCallback(
645
- () =>
646
- sendToDevtools(
647
- Devtools.ReactivityGraphRes.make({
648
- reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
649
- requestId,
650
- channelId,
651
- liveStoreVersion,
652
- }),
653
- ),
654
- { timeout: 500 },
655
- )
656
-
657
- send()
658
-
659
- // In some cases, there can be A LOT of reactivity graph updates in a short period of time
660
- // so we throttle the updates to avoid sending too much data
661
- // This might need to be tweaked further and possibly be exposed to the user in some way.
662
- const throttledSend = throttle(send, 20)
663
-
664
- reactivityGraphSubcriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
665
-
666
- break
667
- }
668
- case 'LSD.DebugInfoReq': {
669
- sendToDevtools(
670
- Devtools.DebugInfoRes.make({
671
- debugInfo: this.mainDbWrapper.debugInfo,
672
- requestId,
673
- channelId,
674
- liveStoreVersion,
675
- }),
676
- )
677
- break
678
- }
679
- case 'LSD.DebugInfoHistorySubscribe': {
680
- const buffer: DebugInfo[] = []
681
- let hasStopped = false
682
- let rafHandle: number | undefined
683
-
684
- const tick = () => {
685
- buffer.push(this.mainDbWrapper.debugInfo)
686
-
687
- // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
688
- // will get the empty debug info
689
- // TODO We need to come up with a more graceful way to do this. Probably via a single global
690
- // `requestAnimationFrame` loop that is passed in somehow.
691
- this.mainDbWrapper.debugInfo = makeEmptyDebugInfo()
692
-
693
- if (buffer.length > 10) {
694
- sendToDevtools(
695
- Devtools.DebugInfoHistoryRes.make({
696
- debugInfoHistory: buffer,
697
- requestId,
698
- channelId,
699
- liveStoreVersion,
700
- }),
701
- )
702
- buffer.length = 0
703
- }
704
-
705
- if (hasStopped === false) {
706
- rafHandle = requestAnimationFrame(tick)
707
- }
708
- }
709
-
710
- rafHandle = requestAnimationFrame(tick)
711
-
712
- const unsub = () => {
713
- hasStopped = true
714
- if (rafHandle !== undefined) {
715
- cancelAnimationFrame(rafHandle)
716
- }
717
- }
718
-
719
- debugInfoHistorySubscriptions.set(requestId, unsub)
720
-
721
- break
722
- }
723
- case 'LSD.DebugInfoHistoryUnsubscribe': {
724
- debugInfoHistorySubscriptions.get(requestId)!()
725
- debugInfoHistorySubscriptions.delete(requestId)
726
- break
727
- }
728
- case 'LSD.DebugInfoResetReq': {
729
- this.mainDbWrapper.debugInfo.slowQueries.clear()
730
- sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, channelId, liveStoreVersion }))
731
- break
732
- }
733
- case 'LSD.DebugInfoRerunQueryReq': {
734
- const { queryStr, bindValues, queriedTables } = decodedMessage
735
- this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
736
- sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, channelId, liveStoreVersion }))
737
- break
738
- }
739
- case 'LSD.ReactivityGraphUnsubscribe': {
740
- reactivityGraphSubcriptions.get(requestId)!()
741
- break
742
- }
743
- case 'LSD.LiveQueriesSubscribe': {
744
- const send = () =>
745
- requestIdleCallback(
746
- () =>
747
- sendToDevtools(
748
- Devtools.LiveQueriesRes.make({
749
- liveQueries: [...this.activeQueries].map((q) => ({
750
- _tag: q._tag,
751
- id: q.id,
752
- label: q.label,
753
- runs: q.runs,
754
- executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
755
- lastestResult:
756
- q.results$.previousResult === NOT_REFRESHED_YET
757
- ? 'SYMBOL_NOT_REFRESHED_YET'
758
- : q.results$.previousResult,
759
- activeSubscriptions: Array.from(q.activeSubscriptions),
760
- })),
761
- requestId,
762
- liveStoreVersion,
763
- channelId,
764
- }),
765
- ),
766
- { timeout: 500 },
767
- )
768
-
769
- send()
770
-
771
- // Same as in the reactivity graph subscription case above, we need to throttle the updates
772
- const throttledSend = throttle(send, 20)
773
-
774
- liveQueriesSubscriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
775
-
776
- break
777
- }
778
- case 'LSD.LiveQueriesUnsubscribe': {
779
- liveQueriesSubscriptions.get(requestId)!()
780
- liveQueriesSubscriptions.delete(requestId)
781
- break
782
- }
783
- // No default
784
- }
785
- })
602
+ // Chrome extension bridge
603
+ {
604
+ const windowChannel = yield* BrowserChannel.windowChannel({
605
+ window,
606
+ listenSchema: Devtools.DevtoolsWindowMessage.MessageForStore,
607
+ sendSchema: Devtools.DevtoolsWindowMessage.MessageForContentscript,
608
+ })
609
+
610
+ yield* windowChannel.send(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
611
+
612
+ yield* windowChannel.listen.pipe(
613
+ Stream.filterMap(Either.getRight),
614
+ Stream.tap((message) =>
615
+ Effect.gen(function* () {
616
+ if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
617
+ // Send message to contentscript via window (which the contentscript iframe is listening to)
618
+ yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
619
+ return
620
+ }
786
621
 
787
- storeMessagePort.start()
788
- }),
789
- Runtime.runFork(runtime),
790
- )
622
+ if (message.channelId !== channelId) return
791
623
 
792
- return
793
- }
794
- })
795
-
796
- sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
624
+ if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
625
+ yield* connectStoreToDevtools({ port: message.port, store })
626
+ }
627
+ }),
628
+ ),
629
+ Stream.runDrain,
630
+ Effect.tapCauseLogPretty,
631
+ Effect.forkScoped,
632
+ )
633
+
634
+ yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
635
+ }
797
636
  })
798
637
  // #endregion devtools
799
638
 
@@ -810,6 +649,7 @@ export class Store<
810
649
  // TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
811
650
  dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
812
651
 
652
+ // NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
813
653
  toJSON = () => {
814
654
  return {
815
655
  _tag: 'Store',
@@ -1010,43 +850,7 @@ export const createStore = <
1010
850
  }
1011
851
  // #endregion createStore
1012
852
 
1013
- // TODO consider replacing with Effect's RC data structures
1014
- class ReferenceCountedSet<T> {
1015
- private map: Map<T, number>
1016
-
1017
- constructor() {
1018
- this.map = new Map<T, number>()
1019
- }
1020
-
1021
- add = (key: T) => {
1022
- const count = this.map.get(key) ?? 0
1023
- this.map.set(key, count + 1)
1024
- }
1025
-
1026
- remove = (key: T) => {
1027
- const count = this.map.get(key) ?? 0
1028
- if (count === 1) {
1029
- this.map.delete(key)
1030
- } else {
1031
- this.map.set(key, count - 1)
1032
- }
1033
- }
1034
-
1035
- has = (key: T) => {
1036
- return this.map.has(key)
1037
- }
1038
-
1039
- get size() {
1040
- return this.map.size
1041
- }
1042
-
1043
- *[Symbol.iterator]() {
1044
- for (const key of this.map.keys()) {
1045
- yield key
1046
- }
1047
- }
1048
- }
1049
-
853
+ // TODO propagate runtime
1050
854
  const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
1051
855
  effect.pipe(
1052
856
  Effect.tapCauseLogPretty,
@@ -1056,6 +860,7 @@ const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
1056
860
  Effect.runFork,
1057
861
  )
1058
862
 
863
+ // TODO propagate runtime
1059
864
  const runEffectPromise = <A, E>(effect: Effect.Effect<A, E, never>) =>
1060
865
  effect.pipe(
1061
866
  Effect.tapCauseLogPretty,
@@ -0,0 +1,36 @@
1
+ // TODO consider replacing with Effect's RC data structures
2
+ export class ReferenceCountedSet<T> {
3
+ private map: Map<T, number>
4
+
5
+ constructor() {
6
+ this.map = new Map<T, number>()
7
+ }
8
+
9
+ add = (key: T) => {
10
+ const count = this.map.get(key) ?? 0
11
+ this.map.set(key, count + 1)
12
+ }
13
+
14
+ remove = (key: T) => {
15
+ const count = this.map.get(key) ?? 0
16
+ if (count === 1) {
17
+ this.map.delete(key)
18
+ } else {
19
+ this.map.set(key, count - 1)
20
+ }
21
+ }
22
+
23
+ has = (key: T) => {
24
+ return this.map.has(key)
25
+ }
26
+
27
+ get size() {
28
+ return this.map.size
29
+ }
30
+
31
+ *[Symbol.iterator]() {
32
+ for (const key of this.map.keys()) {
33
+ yield key
34
+ }
35
+ }
36
+ }