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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,20 @@
1
1
  import { type BootDb, type BootStatus, type StoreAdapterFactory, UnexpectedError } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
3
  import { errorToString } from '@livestore/utils'
4
- import { Effect, Exit, FiberSet, Logger, LogLevel, Schema, Scope } from '@livestore/utils/effect'
4
+ import { Effect, FiberSet, Logger, LogLevel, Schema } from '@livestore/utils/effect'
5
5
  import type * as otel from '@opentelemetry/api'
6
6
  import type { ReactElement, ReactNode } from 'react'
7
7
  import React from 'react'
8
8
 
9
9
  // TODO refactor so the `react` module doesn't depend on `effect` module
10
10
  import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
11
- import type { BaseGraphQLContext, GraphQLOptions, OtelOptions } from '../store.js'
11
+ import type { BaseGraphQLContext, ForceStoreShutdown, GraphQLOptions, OtelOptions, StoreShutdown } from '../store.js'
12
12
  import { createStore } from '../store.js'
13
13
  import { LiveStoreContext } from './LiveStoreContext.js'
14
14
 
15
+ export class StoreAbort extends Schema.TaggedError<StoreAbort>()('LiveStore.StoreAbort', {}) {}
16
+ export class StoreInterrupted extends Schema.TaggedError<StoreInterrupted>()('LiveStore.StoreInterrupted', {}) {}
17
+
15
18
  interface LiveStoreProviderProps<GraphQLContext> {
16
19
  schema: LiveStoreSchema
17
20
  boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
@@ -81,11 +84,11 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
81
84
  const [_, rerender] = React.useState(0)
82
85
  const ctxValueRef = React.useRef<{
83
86
  value: StoreContext_ | BootStatus
84
- scope: Scope.CloseableScope | undefined
87
+ fiberSet: FiberSet.FiberSet | undefined
85
88
  counter: number
86
89
  }>({
87
90
  value: { stage: 'loading' },
88
- scope: undefined,
91
+ fiberSet: undefined,
89
92
  counter: 0,
90
93
  })
91
94
 
@@ -102,6 +105,12 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
102
105
  signal,
103
106
  })
104
107
 
108
+ const interrupt = (fiberSet: FiberSet.FiberSet, error: StoreAbort | StoreInterrupted) =>
109
+ Effect.gen(function* () {
110
+ yield* FiberSet.clear(fiberSet)
111
+ yield* FiberSet.run(fiberSet, Effect.fail(error))
112
+ }).pipe(Effect.ignoreLogged, Effect.runFork)
113
+
105
114
  if (
106
115
  inputPropsCacheRef.current.schema !== schema ||
107
116
  inputPropsCacheRef.current.graphQLOptions !== graphQLOptions ||
@@ -122,15 +131,14 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
122
131
  disableDevtools,
123
132
  signal,
124
133
  }
125
- if (ctxValueRef.current.scope !== undefined) {
126
- Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
134
+ if (ctxValueRef.current.fiberSet !== undefined) {
135
+ interrupt(ctxValueRef.current.fiberSet, new StoreInterrupted())
136
+ ctxValueRef.current.fiberSet = undefined
127
137
  }
128
- ctxValueRef.current = { value: { stage: 'loading' }, scope: undefined, counter: ctxValueRef.current.counter + 1 }
138
+ ctxValueRef.current = { value: { stage: 'loading' }, fiberSet: undefined, counter: ctxValueRef.current.counter + 1 }
129
139
  }
130
140
 
131
141
  React.useEffect(() => {
132
- const storeScope = Scope.make().pipe(Effect.runSync)
133
-
134
142
  const counter = ctxValueRef.current.counter
135
143
 
136
144
  const setContextValue = (value: StoreContext_ | BootStatus) => {
@@ -139,23 +147,23 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
139
147
  rerender((c) => c + 1)
140
148
  }
141
149
 
142
- Scope.addFinalizer(
143
- storeScope,
144
- Effect.sync(() => setContextValue({ stage: 'shutdown' })),
145
- ).pipe(Effect.runSync)
146
-
147
- ctxValueRef.current.scope = storeScope
148
-
149
150
  signal?.addEventListener('abort', () => {
150
- if (ctxValueRef.current.scope !== undefined && ctxValueRef.current.counter === counter) {
151
- Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
152
- ctxValueRef.current.scope = undefined
151
+ if (ctxValueRef.current.fiberSet !== undefined && ctxValueRef.current.counter === counter) {
152
+ interrupt(ctxValueRef.current.fiberSet, new StoreAbort())
153
+ ctxValueRef.current.fiberSet = undefined
153
154
  }
154
155
  })
155
156
 
156
- FiberSet.make().pipe(
157
- Effect.andThen((fiberSet) =>
158
- createStore({
157
+ Effect.gen(function* () {
158
+ const fiberSet = yield* FiberSet.make<
159
+ unknown,
160
+ UnexpectedError | ForceStoreShutdown | StoreAbort | StoreInterrupted | StoreShutdown
161
+ >()
162
+
163
+ ctxValueRef.current.fiberSet = fiberSet
164
+
165
+ yield* Effect.gen(function* () {
166
+ const store = yield* createStore({
159
167
  fiberSet,
160
168
  schema,
161
169
  graphQLOptions,
@@ -168,13 +176,25 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
168
176
  if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
169
177
  setContextValue(status)
170
178
  },
171
- }),
172
- ),
173
- Effect.tapSync((store) => setContextValue({ stage: 'running', store })),
174
- Effect.tapError((error) => Effect.sync(() => setContextValue({ stage: 'error', error }))),
175
- Effect.tapDefect((defect) => Effect.sync(() => setContextValue({ stage: 'error', error: defect }))),
176
- Scope.extend(storeScope),
177
- Effect.forkIn(storeScope),
179
+ })
180
+
181
+ setContextValue({ stage: 'running', store })
182
+
183
+ yield* Effect.never
184
+ }).pipe(Effect.scoped, FiberSet.run(fiberSet))
185
+
186
+ const shutdownContext = Effect.sync(() => setContextValue({ stage: 'shutdown' }))
187
+
188
+ yield* FiberSet.join(fiberSet).pipe(
189
+ Effect.catchTag('LiveStore.StoreShutdown', () => shutdownContext),
190
+ Effect.catchTag('LiveStore.ForceStoreShutdown', () => shutdownContext),
191
+ Effect.catchTag('LiveStore.StoreAbort', () => shutdownContext),
192
+ Effect.tapError((error) => Effect.sync(() => setContextValue({ stage: 'error', error }))),
193
+ Effect.tapDefect((defect) => Effect.sync(() => setContextValue({ stage: 'error', error: defect }))),
194
+ Effect.exit,
195
+ )
196
+ }).pipe(
197
+ Effect.scoped,
178
198
  Effect.tapCauseLogPretty,
179
199
  Effect.annotateLogs({ thread: 'window' }),
180
200
  Effect.provide(Logger.pretty),
@@ -183,9 +203,9 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
183
203
  )
184
204
 
185
205
  return () => {
186
- if (ctxValueRef.current.scope !== undefined) {
187
- Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
188
- ctxValueRef.current.scope = undefined
206
+ if (ctxValueRef.current.fiberSet !== undefined) {
207
+ interrupt(ctxValueRef.current.fiberSet, new StoreInterrupted())
208
+ ctxValueRef.current.fiberSet = undefined
189
209
  }
190
210
  }
191
211
  }, [schema, graphQLOptions, otelOptions, boot, adapter, batchUpdates, disableDevtools, signal])
@@ -14,6 +14,9 @@ export type MapRows<TResult, TRaw = any> =
14
14
  | ((rows: ReadonlyArray<TRaw>) => TResult)
15
15
  | Schema.Schema<TResult, ReadonlyArray<TRaw>, unknown>
16
16
 
17
+ /**
18
+ * NOTE `querySQL` is only supposed to read data. Don't use it to insert/update/delete data but use mutations instead.
19
+ */
17
20
  export const querySQL = <TResult, TRaw = any>(
18
21
  query: string | ((get: GetAtomResult) => string),
19
22
  options?: {
@@ -0,0 +1,208 @@
1
+ import type { DebugInfo, StoreAdapter } from '@livestore/common'
2
+ import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
3
+ import { throttle } from '@livestore/utils'
4
+ import { BrowserChannel, Effect, Stream } from '@livestore/utils/effect'
5
+
6
+ import type { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
7
+ import { emptyDebugInfo as makeEmptyDebugInfo } from './MainDatabaseWrapper.js'
8
+ import { NOT_REFRESHED_YET } from './reactive.js'
9
+ import type { LiveQuery, ReactivityGraph } from './reactiveQueries/base-class.js'
10
+ import type { ReferenceCountedSet } from './utils/data-structures.js'
11
+
12
+ type IStore = {
13
+ adapter: StoreAdapter
14
+ reactivityGraph: ReactivityGraph
15
+ mainDbWrapper: MainDatabaseWrapper
16
+ activeQueries: ReferenceCountedSet<LiveQuery<any>>
17
+ }
18
+
19
+ type Unsub = () => void
20
+ type RequestId = string
21
+ type SubMap = Map<RequestId, Unsub>
22
+
23
+ export const connectDevtoolsToStore = ({ storeMessagePort, store }: { storeMessagePort: MessagePort; store: IStore }) =>
24
+ Effect.gen(function* () {
25
+ const channelId = store.adapter.coordinator.devtools.channelId
26
+
27
+ const reactivityGraphSubcriptions: SubMap = new Map()
28
+ const liveQueriesSubscriptions: SubMap = new Map()
29
+ const debugInfoHistorySubscriptions: SubMap = new Map()
30
+
31
+ const storePortChannel = yield* BrowserChannel.messagePortChannel({
32
+ port: storeMessagePort,
33
+ listenSchema: Devtools.MessageToAppHostStore,
34
+ sendSchema: Devtools.MessageFromAppHostStore,
35
+ })
36
+
37
+ const sendToDevtools = (message: Devtools.MessageFromAppHostStore) =>
38
+ storePortChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runSync)
39
+
40
+ const onMessage = (decodedMessage: typeof Devtools.MessageToAppHostStore.Type) => {
41
+ // console.log('storeMessagePort message', decodedMessage)
42
+
43
+ if (decodedMessage.channelId !== store.adapter.coordinator.devtools.channelId) {
44
+ // console.log(`Unknown message`, event)
45
+ return
46
+ }
47
+
48
+ const requestId = decodedMessage.requestId
49
+
50
+ const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
51
+
52
+ switch (decodedMessage._tag) {
53
+ case 'LSD.ReactivityGraphSubscribe': {
54
+ const includeResults = decodedMessage.includeResults
55
+
56
+ const send = () =>
57
+ // In order to not add more work to the current tick, we use requestIdleCallback
58
+ // to send the reactivity graph updates to the devtools
59
+ requestIdleCallback(
60
+ () =>
61
+ sendToDevtools(
62
+ Devtools.ReactivityGraphRes.make({
63
+ reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
64
+ requestId,
65
+ channelId,
66
+ liveStoreVersion,
67
+ }),
68
+ ),
69
+ { timeout: 500 },
70
+ )
71
+
72
+ send()
73
+
74
+ // In some cases, there can be A LOT of reactivity graph updates in a short period of time
75
+ // so we throttle the updates to avoid sending too much data
76
+ // This might need to be tweaked further and possibly be exposed to the user in some way.
77
+ const throttledSend = throttle(send, 20)
78
+
79
+ reactivityGraphSubcriptions.set(requestId, store.reactivityGraph.subscribeToRefresh(throttledSend))
80
+
81
+ break
82
+ }
83
+ case 'LSD.DebugInfoReq': {
84
+ sendToDevtools(
85
+ Devtools.DebugInfoRes.make({
86
+ debugInfo: store.mainDbWrapper.debugInfo,
87
+ requestId,
88
+ channelId,
89
+ liveStoreVersion,
90
+ }),
91
+ )
92
+ break
93
+ }
94
+ case 'LSD.DebugInfoHistorySubscribe': {
95
+ const buffer: DebugInfo[] = []
96
+ let hasStopped = false
97
+ let rafHandle: number | undefined
98
+
99
+ const tick = () => {
100
+ buffer.push(store.mainDbWrapper.debugInfo)
101
+
102
+ // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
103
+ // will get the empty debug info
104
+ // TODO We need to come up with a more graceful way to do store. Probably via a single global
105
+ // `requestAnimationFrame` loop that is passed in somehow.
106
+ store.mainDbWrapper.debugInfo = makeEmptyDebugInfo()
107
+
108
+ if (buffer.length > 10) {
109
+ sendToDevtools(
110
+ Devtools.DebugInfoHistoryRes.make({
111
+ debugInfoHistory: buffer,
112
+ requestId,
113
+ channelId,
114
+ liveStoreVersion,
115
+ }),
116
+ )
117
+ buffer.length = 0
118
+ }
119
+
120
+ if (hasStopped === false) {
121
+ rafHandle = requestAnimationFrame(tick)
122
+ }
123
+ }
124
+
125
+ rafHandle = requestAnimationFrame(tick)
126
+
127
+ const unsub = () => {
128
+ hasStopped = true
129
+ if (rafHandle !== undefined) {
130
+ cancelAnimationFrame(rafHandle)
131
+ }
132
+ }
133
+
134
+ debugInfoHistorySubscriptions.set(requestId, unsub)
135
+
136
+ break
137
+ }
138
+ case 'LSD.DebugInfoHistoryUnsubscribe': {
139
+ debugInfoHistorySubscriptions.get(requestId)!()
140
+ debugInfoHistorySubscriptions.delete(requestId)
141
+ break
142
+ }
143
+ case 'LSD.DebugInfoResetReq': {
144
+ store.mainDbWrapper.debugInfo.slowQueries.clear()
145
+ sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, channelId, liveStoreVersion }))
146
+ break
147
+ }
148
+ case 'LSD.DebugInfoRerunQueryReq': {
149
+ const { queryStr, bindValues, queriedTables } = decodedMessage
150
+ store.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
151
+ sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, channelId, liveStoreVersion }))
152
+ break
153
+ }
154
+ case 'LSD.ReactivityGraphUnsubscribe': {
155
+ reactivityGraphSubcriptions.get(requestId)!()
156
+ break
157
+ }
158
+ case 'LSD.LiveQueriesSubscribe': {
159
+ const send = () =>
160
+ requestIdleCallback(
161
+ () =>
162
+ sendToDevtools(
163
+ Devtools.LiveQueriesRes.make({
164
+ liveQueries: [...store.activeQueries].map((q) => ({
165
+ _tag: q._tag,
166
+ id: q.id,
167
+ label: q.label,
168
+ runs: q.runs,
169
+ executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
170
+ lastestResult:
171
+ q.results$.previousResult === NOT_REFRESHED_YET
172
+ ? 'SYMBOL_NOT_REFRESHED_YET'
173
+ : q.results$.previousResult,
174
+ activeSubscriptions: Array.from(q.activeSubscriptions),
175
+ })),
176
+ requestId,
177
+ liveStoreVersion,
178
+ channelId,
179
+ }),
180
+ ),
181
+ { timeout: 500 },
182
+ )
183
+
184
+ send()
185
+
186
+ // Same as in the reactivity graph subscription case above, we need to throttle the updates
187
+ const throttledSend = throttle(send, 20)
188
+
189
+ liveQueriesSubscriptions.set(requestId, store.reactivityGraph.subscribeToRefresh(throttledSend))
190
+
191
+ break
192
+ }
193
+ case 'LSD.LiveQueriesUnsubscribe': {
194
+ liveQueriesSubscriptions.get(requestId)!()
195
+ liveQueriesSubscriptions.delete(requestId)
196
+ break
197
+ }
198
+ // No default
199
+ }
200
+ }
201
+
202
+ yield* storePortChannel.listen.pipe(
203
+ Stream.flatten(),
204
+ Stream.tapSync((message) => onMessage(message)),
205
+ Stream.runDrain,
206
+ Effect.withSpan('LSD.devtools.onMessage'),
207
+ )
208
+ }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('LSD.devtools.connectStoreToDevtools'))