@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +2 -0
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts +11 -1
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +45 -29
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +3 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +3 -0
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/store-devtools.d.ts +18 -0
- package/dist/store-devtools.d.ts.map +1 -0
- package/dist/store-devtools.js +141 -0
- package/dist/store-devtools.js.map +1 -0
- package/dist/store.d.ts +12 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +44 -211
- package/dist/store.js.map +1 -1
- package/dist/utils/data-structures.d.ts +10 -0
- package/dist/utils/data-structures.d.ts.map +1 -0
- package/dist/utils/data-structures.js +32 -0
- package/dist/utils/data-structures.js.map +1 -0
- package/package.json +5 -5
- package/src/react/LiveStoreProvider.tsx +52 -32
- package/src/reactiveQueries/sql.ts +3 -0
- package/src/store-devtools.ts +208 -0
- package/src/store.ts +67 -283
- package/src/utils/data-structures.ts +36 -0
|
@@ -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,
|
|
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
|
-
|
|
87
|
+
fiberSet: FiberSet.FiberSet | undefined
|
|
85
88
|
counter: number
|
|
86
89
|
}>({
|
|
87
90
|
value: { stage: 'loading' },
|
|
88
|
-
|
|
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.
|
|
126
|
-
|
|
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' },
|
|
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.
|
|
151
|
-
|
|
152
|
-
ctxValueRef.current.
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
187
|
-
|
|
188
|
-
ctxValueRef.current.
|
|
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'))
|