@livestore/livestore 0.0.55-dev.0 → 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,218 +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
- liveStoreVersion,
651
- }),
652
- ),
653
- { timeout: 500 },
654
- )
655
-
656
- send()
657
-
658
- // In some cases, there can be A LOT of reactivity graph updates in a short period of time
659
- // so we throttle the updates to avoid sending too much data
660
- // This might need to be tweaked further and possibly be exposed to the user in some way.
661
- const throttledSend = throttle(send, 20)
662
-
663
- reactivityGraphSubcriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
664
-
665
- break
666
- }
667
- case 'LSD.DebugInfoReq': {
668
- sendToDevtools(
669
- Devtools.DebugInfoRes.make({
670
- debugInfo: this.mainDbWrapper.debugInfo,
671
- requestId,
672
- liveStoreVersion,
673
- }),
674
- )
675
- break
676
- }
677
- case 'LSD.DebugInfoHistorySubscribe': {
678
- const buffer: DebugInfo[] = []
679
- let hasStopped = false
680
- let rafHandle: number | undefined
681
-
682
- const tick = () => {
683
- buffer.push(this.mainDbWrapper.debugInfo)
684
-
685
- // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
686
- // will get the empty debug info
687
- // TODO We need to come up with a more graceful way to do this. Probably via a single global
688
- // `requestAnimationFrame` loop that is passed in somehow.
689
- this.mainDbWrapper.debugInfo = makeEmptyDebugInfo()
690
-
691
- if (buffer.length > 10) {
692
- sendToDevtools(
693
- Devtools.DebugInfoHistoryRes.make({
694
- debugInfoHistory: buffer,
695
- requestId,
696
- liveStoreVersion,
697
- }),
698
- )
699
- buffer.length = 0
700
- }
701
-
702
- if (hasStopped === false) {
703
- rafHandle = requestAnimationFrame(tick)
704
- }
705
- }
706
-
707
- rafHandle = requestAnimationFrame(tick)
708
-
709
- const unsub = () => {
710
- hasStopped = true
711
- if (rafHandle !== undefined) {
712
- cancelAnimationFrame(rafHandle)
713
- }
714
- }
715
-
716
- debugInfoHistorySubscriptions.set(requestId, unsub)
717
-
718
- break
719
- }
720
- case 'LSD.DebugInfoHistoryUnsubscribe': {
721
- debugInfoHistorySubscriptions.get(requestId)!()
722
- debugInfoHistorySubscriptions.delete(requestId)
723
- break
724
- }
725
- case 'LSD.DebugInfoResetReq': {
726
- this.mainDbWrapper.debugInfo.slowQueries.clear()
727
- sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
728
- break
729
- }
730
- case 'LSD.DebugInfoRerunQueryReq': {
731
- const { queryStr, bindValues, queriedTables } = decodedMessage
732
- this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
733
- sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
734
- break
735
- }
736
- case 'LSD.ReactivityGraphUnsubscribe': {
737
- reactivityGraphSubcriptions.get(requestId)!()
738
- break
739
- }
740
- case 'LSD.LiveQueriesSubscribe': {
741
- const send = () =>
742
- requestIdleCallback(
743
- () =>
744
- sendToDevtools(
745
- Devtools.LiveQueriesRes.make({
746
- liveQueries: [...this.activeQueries].map((q) => ({
747
- _tag: q._tag,
748
- id: q.id,
749
- label: q.label,
750
- runs: q.runs,
751
- executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
752
- lastestResult:
753
- q.results$.previousResult === NOT_REFRESHED_YET
754
- ? 'SYMBOL_NOT_REFRESHED_YET'
755
- : q.results$.previousResult,
756
- activeSubscriptions: Array.from(q.activeSubscriptions),
757
- })),
758
- requestId,
759
- liveStoreVersion,
760
- }),
761
- ),
762
- { timeout: 500 },
763
- )
764
-
765
- send()
766
-
767
- // Same as in the reactivity graph subscription case above, we need to throttle the updates
768
- const throttledSend = throttle(send, 20)
769
-
770
- liveQueriesSubscriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
771
-
772
- break
773
- }
774
- case 'LSD.LiveQueriesUnsubscribe': {
775
- liveQueriesSubscriptions.get(requestId)!()
776
- liveQueriesSubscriptions.delete(requestId)
777
- break
778
- }
779
- // No default
780
- }
781
- })
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
+ }
782
621
 
783
- storeMessagePort.start()
784
- }),
785
- Runtime.runFork(runtime),
786
- )
622
+ if (message.channelId !== channelId) return
787
623
 
788
- return
789
- }
790
- })
791
-
792
- 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
+ }
793
636
  })
794
637
  // #endregion devtools
795
638
 
@@ -806,6 +649,7 @@ export class Store<
806
649
  // TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
807
650
  dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
808
651
 
652
+ // NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
809
653
  toJSON = () => {
810
654
  return {
811
655
  _tag: 'Store',
@@ -1006,43 +850,7 @@ export const createStore = <
1006
850
  }
1007
851
  // #endregion createStore
1008
852
 
1009
- // TODO consider replacing with Effect's RC data structures
1010
- class ReferenceCountedSet<T> {
1011
- private map: Map<T, number>
1012
-
1013
- constructor() {
1014
- this.map = new Map<T, number>()
1015
- }
1016
-
1017
- add = (key: T) => {
1018
- const count = this.map.get(key) ?? 0
1019
- this.map.set(key, count + 1)
1020
- }
1021
-
1022
- remove = (key: T) => {
1023
- const count = this.map.get(key) ?? 0
1024
- if (count === 1) {
1025
- this.map.delete(key)
1026
- } else {
1027
- this.map.set(key, count - 1)
1028
- }
1029
- }
1030
-
1031
- has = (key: T) => {
1032
- return this.map.has(key)
1033
- }
1034
-
1035
- get size() {
1036
- return this.map.size
1037
- }
1038
-
1039
- *[Symbol.iterator]() {
1040
- for (const key of this.map.keys()) {
1041
- yield key
1042
- }
1043
- }
1044
- }
1045
-
853
+ // TODO propagate runtime
1046
854
  const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
1047
855
  effect.pipe(
1048
856
  Effect.tapCauseLogPretty,
@@ -1052,6 +860,7 @@ const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
1052
860
  Effect.runFork,
1053
861
  )
1054
862
 
863
+ // TODO propagate runtime
1055
864
  const runEffectPromise = <A, E>(effect: Effect.Effect<A, E, never>) =>
1056
865
  effect.pipe(
1057
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
+ }