@livestore/livestore 0.0.54-dev.21 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +1 -9
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +1 -1
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +1 -0
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +1 -1
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/global-state.d.ts +0 -2
- package/dist/global-state.d.ts.map +1 -1
- package/dist/global-state.js +0 -1
- package/dist/global-state.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreContext.d.ts.map +1 -1
- package/dist/react/LiveStoreContext.js +3 -0
- package/dist/react/LiveStoreContext.js.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts +3 -3
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +16 -8
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/LiveStoreProvider.test.js +6 -4
- package/dist/react/LiveStoreProvider.test.js.map +1 -1
- package/dist/reactive.js +1 -1
- package/dist/reactive.js.map +1 -1
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +3 -37
- package/dist/row-query.js.map +1 -1
- package/dist/store.d.ts +12 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +312 -176
- package/dist/store.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/react/fixture.tsx +1 -1
- package/src/effect/LiveStore.ts +2 -1
- package/src/global-state.ts +0 -4
- package/src/index.ts +2 -1
- package/src/react/LiveStoreContext.ts +4 -0
- package/src/react/LiveStoreProvider.test.tsx +9 -4
- package/src/react/LiveStoreProvider.tsx +17 -10
- package/src/reactive.ts +3 -1
- package/src/row-query.ts +4 -50
- package/src/store.ts +435 -224
package/src/store.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BootDb,
|
|
3
|
+
BootStatus,
|
|
3
4
|
ParamsObject,
|
|
4
5
|
PreparedBindValues,
|
|
5
6
|
ResetMode,
|
|
6
7
|
StoreAdapter,
|
|
7
8
|
StoreAdapterFactory,
|
|
9
|
+
UnexpectedError,
|
|
8
10
|
} from '@livestore/common'
|
|
9
|
-
import { Devtools, getExecArgsFromMutation, prepareBindValues } from '@livestore/common'
|
|
10
|
-
import { version as liveStoreVersion } from '@livestore/common/package.json'
|
|
11
|
+
import { Devtools, getExecArgsFromMutation, liveStoreVersion, prepareBindValues } from '@livestore/common'
|
|
11
12
|
import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
|
|
12
13
|
import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
|
|
13
|
-
import { assertNever, isPromise, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
|
|
14
|
-
import {
|
|
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'
|
|
15
17
|
import * as otel from '@opentelemetry/api'
|
|
16
18
|
import type { GraphQLSchema } from 'graphql'
|
|
17
19
|
|
|
@@ -19,6 +21,7 @@ import { globalReactivityGraph } from './global-state.js'
|
|
|
19
21
|
import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
|
|
20
22
|
import type { StackInfo } from './react/utils/stack-info.js'
|
|
21
23
|
import type { DebugRefreshReasonBase, Ref } from './reactive.js'
|
|
24
|
+
import { NOT_REFRESHED_YET } from './reactive.js'
|
|
22
25
|
import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
|
|
23
26
|
import { downloadBlob } from './utils/dev.js'
|
|
24
27
|
import { getDurationMsFromSpan } from './utils/otel.js'
|
|
@@ -50,6 +53,8 @@ export type StoreOptions<
|
|
|
50
53
|
otelOptions: OtelOptions
|
|
51
54
|
reactivityGraph: ReactivityGraph
|
|
52
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>
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export type RefreshReason =
|
|
@@ -104,6 +109,7 @@ export class Store<
|
|
|
104
109
|
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
105
110
|
> {
|
|
106
111
|
id = uniqueStoreId()
|
|
112
|
+
readonly devtoolsConnectionId = cuid()
|
|
107
113
|
reactivityGraph: ReactivityGraph
|
|
108
114
|
mainDbWrapper: MainDatabaseWrapper
|
|
109
115
|
adapter: StoreAdapter
|
|
@@ -118,8 +124,8 @@ export class Store<
|
|
|
118
124
|
tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
|
|
119
125
|
|
|
120
126
|
// TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
|
|
121
|
-
__processedMutationIds
|
|
122
|
-
__processedMutationWithoutRefreshIds = new Set<string>()
|
|
127
|
+
private __processedMutationIds
|
|
128
|
+
private __processedMutationWithoutRefreshIds = new Set<string>()
|
|
123
129
|
|
|
124
130
|
/** RC-based set to see which queries are currently subscribed to */
|
|
125
131
|
activeQueries: ReferenceCountedSet<LiveQuery<any>>
|
|
@@ -133,6 +139,7 @@ export class Store<
|
|
|
133
139
|
reactivityGraph,
|
|
134
140
|
otelOptions,
|
|
135
141
|
disableDevtools,
|
|
142
|
+
__processedMutationIds,
|
|
136
143
|
}: StoreOptions<TGraphQLContext, TSchema>) {
|
|
137
144
|
this.mainDbWrapper = new MainDatabaseWrapper({ otel: otelOptions, db: adapter.mainDb })
|
|
138
145
|
this.adapter = adapter
|
|
@@ -141,6 +148,9 @@ export class Store<
|
|
|
141
148
|
// TODO refactor
|
|
142
149
|
this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
|
|
143
150
|
|
|
151
|
+
// TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
|
|
152
|
+
this.__processedMutationIds = __processedMutationIds
|
|
153
|
+
|
|
144
154
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
|
145
155
|
this.tableRefs = {}
|
|
146
156
|
this.activeQueries = new ReferenceCountedSet()
|
|
@@ -153,7 +163,7 @@ export class Store<
|
|
|
153
163
|
|
|
154
164
|
this.reactivityGraph = reactivityGraph
|
|
155
165
|
this.reactivityGraph.context = {
|
|
156
|
-
store: this as
|
|
166
|
+
store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
|
|
157
167
|
otelTracer: otelOptions.tracer,
|
|
158
168
|
rootOtelContext: otelQueriesSpanContext,
|
|
159
169
|
}
|
|
@@ -163,8 +173,7 @@ export class Store<
|
|
|
163
173
|
this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
|
|
164
174
|
}),
|
|
165
175
|
Stream.runDrain,
|
|
166
|
-
|
|
167
|
-
Effect.runFork,
|
|
176
|
+
runEffectFork,
|
|
168
177
|
)
|
|
169
178
|
|
|
170
179
|
if (disableDevtools !== true) {
|
|
@@ -178,11 +187,7 @@ export class Store<
|
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
// Need a set here since `schema.tables` might contain duplicates and some componentStateTables
|
|
181
|
-
const allTableNames = new Set(
|
|
182
|
-
this.schema.tables.keys(),
|
|
183
|
-
// TODO activate dynamic tables
|
|
184
|
-
// ...Array.from(dynamicallyRegisteredTables.values()).map((_) => _.sqliteDef.name),
|
|
185
|
-
)
|
|
190
|
+
const allTableNames = new Set(this.schema.tables.keys())
|
|
186
191
|
const existingTableRefs = new Map(
|
|
187
192
|
Array.from(this.reactivityGraph.atoms.values())
|
|
188
193
|
.filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
|
|
@@ -270,7 +275,7 @@ export class Store<
|
|
|
270
275
|
otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
|
|
271
276
|
otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
|
|
272
277
|
|
|
273
|
-
await this.adapter.coordinator.shutdown()
|
|
278
|
+
await this.adapter.coordinator.shutdown.pipe(runEffectPromise)
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
mutate: {
|
|
@@ -487,10 +492,9 @@ export class Store<
|
|
|
487
492
|
|
|
488
493
|
if (coordinatorMode !== 'skip-coordinator') {
|
|
489
494
|
// Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
})
|
|
495
|
+
this.adapter.coordinator
|
|
496
|
+
.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
|
|
497
|
+
.pipe(runEffectFork)
|
|
494
498
|
}
|
|
495
499
|
|
|
496
500
|
// Uncomment to print a list of queries currently registered on the store
|
|
@@ -516,8 +520,7 @@ export class Store<
|
|
|
516
520
|
) => {
|
|
517
521
|
this.mainDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
|
|
518
522
|
|
|
519
|
-
|
|
520
|
-
this.adapter.coordinator.execute(query, prepareBindValues(params, query), parentSpan)
|
|
523
|
+
this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(runEffectFork)
|
|
521
524
|
}
|
|
522
525
|
|
|
523
526
|
select = (query: string, params: ParamsObject = {}) => {
|
|
@@ -531,112 +534,268 @@ export class Store<
|
|
|
531
534
|
meta: { liveStoreRefType: 'table' },
|
|
532
535
|
})
|
|
533
536
|
|
|
537
|
+
// TODO shutdown behaviour
|
|
534
538
|
private bootDevtools = () => {
|
|
535
|
-
const
|
|
539
|
+
const sendToDevtoolsContentscript = (
|
|
540
|
+
message: typeof Devtools.DevtoolsWindowMessage.MessageForContentscript.Type,
|
|
541
|
+
) => {
|
|
542
|
+
window.postMessage(Schema.encodeSync(Devtools.DevtoolsWindowMessage.MessageForContentscript)(message), '*')
|
|
543
|
+
}
|
|
536
544
|
|
|
537
|
-
|
|
538
|
-
type RequestId = string
|
|
545
|
+
const channelId = this.adapter.coordinator.devtools.channelId
|
|
539
546
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
decoded.value.channelId !== this.adapter.coordinator.devtools.channelId
|
|
549
|
-
) {
|
|
550
|
-
// 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 }))
|
|
551
555
|
return
|
|
552
556
|
}
|
|
553
557
|
|
|
554
|
-
|
|
555
|
-
const sendToDevtools = (message: Devtools.MessageFromAppHost) =>
|
|
556
|
-
devtoolsChannel.fromAppHost.postMessage(Schema.encodeSync(Devtools.MessageFromAppHost)(message))
|
|
557
|
-
|
|
558
|
-
switch (decoded.value._tag) {
|
|
559
|
-
case 'LSD.ReactivityGraphSubscribe': {
|
|
560
|
-
const includeResults = decoded.value.includeResults
|
|
561
|
-
const send = () =>
|
|
562
|
-
sendToDevtools(
|
|
563
|
-
Devtools.ReactivityGraphRes.make({
|
|
564
|
-
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
|
|
565
|
-
requestId,
|
|
566
|
-
liveStoreVersion,
|
|
567
|
-
}),
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
send()
|
|
571
|
-
|
|
572
|
-
reactivityGraphSubcriptions.set(
|
|
573
|
-
requestId,
|
|
574
|
-
this.reactivityGraph.subscribeToRefresh(() => send()),
|
|
575
|
-
)
|
|
558
|
+
if (message.channelId !== channelId) return
|
|
576
559
|
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
case 'LSD.DebugInfoResetReq': {
|
|
586
|
-
this.mainDbWrapper.debugInfo.slowQueries.clear()
|
|
587
|
-
sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
|
|
588
|
-
break
|
|
589
|
-
}
|
|
590
|
-
case 'LSD.DebugInfoRerunQueryReq': {
|
|
591
|
-
const { queryStr, bindValues, queriedTables } = decoded.value
|
|
592
|
-
this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
|
|
593
|
-
sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
|
|
594
|
-
break
|
|
595
|
-
}
|
|
596
|
-
case 'LSD.ReactivityGraphUnsubscribe': {
|
|
597
|
-
reactivityGraphSubcriptions.get(requestId)!()
|
|
598
|
-
break
|
|
599
|
-
}
|
|
600
|
-
case 'LSD.LiveQueriesSubscribe': {
|
|
601
|
-
const send = () =>
|
|
602
|
-
sendToDevtools(
|
|
603
|
-
Devtools.LiveQueriesRes.make({
|
|
604
|
-
liveQueries: [...this.activeQueries].map((q) => ({
|
|
605
|
-
_tag: q._tag,
|
|
606
|
-
id: q.id,
|
|
607
|
-
label: q.label,
|
|
608
|
-
runs: q.runs,
|
|
609
|
-
executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
|
|
610
|
-
lastestResult: q.results$.previousResult,
|
|
611
|
-
activeSubscriptions: Array.from(q.activeSubscriptions),
|
|
612
|
-
})),
|
|
613
|
-
requestId,
|
|
614
|
-
liveStoreVersion,
|
|
615
|
-
}),
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
send()
|
|
619
|
-
|
|
620
|
-
liveQueriesSubscriptions.set(
|
|
621
|
-
requestId,
|
|
622
|
-
this.reactivityGraph.subscribeToRefresh(() => send()),
|
|
623
|
-
)
|
|
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)
|
|
624
567
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
break
|
|
630
|
-
}
|
|
631
|
-
case 'LSD.ResetAllDataReq': {
|
|
632
|
-
await this.adapter.coordinator.dangerouslyReset(decoded.value.mode)
|
|
633
|
-
sendToDevtools(Devtools.ResetAllDataRes.make({ requestId, liveStoreVersion }))
|
|
568
|
+
if (decodedMessage.channelId !== this.adapter.coordinator.devtools.channelId) {
|
|
569
|
+
// console.log(`Unknown message`, event)
|
|
570
|
+
return
|
|
571
|
+
}
|
|
634
572
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
+
})
|
|
675
|
+
|
|
676
|
+
storeMessagePort.start()
|
|
677
|
+
}),
|
|
678
|
+
runEffectFork,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
return
|
|
638
682
|
}
|
|
639
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
|
+
// })
|
|
640
799
|
}
|
|
641
800
|
|
|
642
801
|
__devDownloadDb = () => {
|
|
@@ -645,18 +804,37 @@ export class Store<
|
|
|
645
804
|
}
|
|
646
805
|
|
|
647
806
|
__devDownloadMutationLogDb = async () => {
|
|
648
|
-
const data = await this.adapter.coordinator.getMutationLogData()
|
|
807
|
+
const data = await this.adapter.coordinator.getMutationLogData.pipe(runEffectPromise)
|
|
649
808
|
downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
|
|
650
809
|
}
|
|
651
810
|
|
|
652
811
|
// TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
|
|
653
|
-
dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode)
|
|
812
|
+
dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
|
|
816
|
+
schema: TSchema
|
|
817
|
+
adapter: StoreAdapterFactory
|
|
818
|
+
reactivityGraph?: ReactivityGraph
|
|
819
|
+
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
|
820
|
+
otelOptions?: Partial<OtelOptions>
|
|
821
|
+
boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
|
|
822
|
+
batchUpdates?: (run: () => void) => void
|
|
823
|
+
disableDevtools?: boolean
|
|
824
|
+
onBootStatus?: (status: BootStatus) => void
|
|
654
825
|
}
|
|
655
826
|
|
|
656
827
|
/** Create a new LiveStore Store */
|
|
657
828
|
export const createStore = async <
|
|
658
829
|
TGraphQLContext extends BaseGraphQLContext,
|
|
659
830
|
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
831
|
+
>(
|
|
832
|
+
options: CreateStoreOptions<TGraphQLContext, TSchema>,
|
|
833
|
+
): Promise<Store<TGraphQLContext, TSchema>> => createStoreEff(options).pipe(runEffectPromise)
|
|
834
|
+
|
|
835
|
+
export const createStoreEff = <
|
|
836
|
+
TGraphQLContext extends BaseGraphQLContext,
|
|
837
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
660
838
|
>({
|
|
661
839
|
schema,
|
|
662
840
|
graphQLOptions,
|
|
@@ -666,127 +844,142 @@ export const createStore = async <
|
|
|
666
844
|
reactivityGraph = globalReactivityGraph,
|
|
667
845
|
batchUpdates,
|
|
668
846
|
disableDevtools,
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
adapter: StoreAdapterFactory
|
|
672
|
-
reactivityGraph?: ReactivityGraph
|
|
673
|
-
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
|
674
|
-
otelOptions?: Partial<OtelOptions>
|
|
675
|
-
boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
|
|
676
|
-
batchUpdates?: (run: () => void) => void
|
|
677
|
-
disableDevtools?: boolean
|
|
678
|
-
}): Promise<Store<TGraphQLContext, TSchema>> => {
|
|
847
|
+
onBootStatus,
|
|
848
|
+
}: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<Store<TGraphQLContext, TSchema>, UnexpectedError> => {
|
|
679
849
|
const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
|
|
680
850
|
const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active()
|
|
681
|
-
return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
|
|
682
|
-
try {
|
|
683
|
-
performance.mark('livestore:db-creating')
|
|
684
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
685
|
-
|
|
686
|
-
const adapterPromise = adapterFactory({ otelTracer, otelContext, schema })
|
|
687
|
-
const adapter = adapterPromise instanceof Promise ? await adapterPromise : adapterPromise
|
|
688
|
-
performance.mark('livestore:db-created')
|
|
689
|
-
performance.measure('livestore:db-create', 'livestore:db-creating', 'livestore:db-created')
|
|
690
|
-
|
|
691
|
-
if (batchUpdates !== undefined) {
|
|
692
|
-
reactivityGraph.effectsWrapper = batchUpdates
|
|
693
|
-
}
|
|
694
851
|
|
|
695
|
-
|
|
852
|
+
const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
|
|
853
|
+
Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)),
|
|
854
|
+
)
|
|
696
855
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
let isInTxn = false
|
|
700
|
-
let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
|
|
856
|
+
return Effect.gen(function* () {
|
|
857
|
+
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
701
858
|
|
|
702
|
-
|
|
703
|
-
_tag: 'BootDb',
|
|
704
|
-
execute: (queryStr, bindValues) => {
|
|
705
|
-
const stmt = adapter.mainDb.prepare(queryStr)
|
|
706
|
-
stmt.execute(bindValues)
|
|
859
|
+
const bootStatusQueue = yield* Queue.unbounded<BootStatus>()
|
|
707
860
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
mutate: (...list) => {
|
|
715
|
-
for (const mutationEventDecoded of list) {
|
|
716
|
-
const mutationDef =
|
|
717
|
-
schema.mutations.get(mutationEventDecoded.mutation) ??
|
|
718
|
-
shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
|
|
719
|
-
|
|
720
|
-
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
721
|
-
// const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
722
|
-
|
|
723
|
-
for (const { statementSql, bindValues } of execArgsArr) {
|
|
724
|
-
adapter.mainDb.execute(statementSql, bindValues)
|
|
725
|
-
}
|
|
861
|
+
yield* Queue.take(bootStatusQueue).pipe(
|
|
862
|
+
Effect.tapSync((status) => onBootStatus?.(status)),
|
|
863
|
+
Effect.forever,
|
|
864
|
+
Effect.tapCauseLogPretty,
|
|
865
|
+
Effect.forkScoped,
|
|
866
|
+
)
|
|
726
867
|
|
|
727
|
-
|
|
868
|
+
const adapter: StoreAdapter = yield* adapterFactory({
|
|
869
|
+
schema,
|
|
870
|
+
devtoolsEnabled: disableDevtools !== true,
|
|
871
|
+
bootStatusQueue,
|
|
872
|
+
}).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
|
|
728
873
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
874
|
+
if (batchUpdates !== undefined) {
|
|
875
|
+
reactivityGraph.effectsWrapper = batchUpdates
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const mutationEventSchema = makeMutationEventSchemaMemo(schema)
|
|
879
|
+
|
|
880
|
+
const __processedMutationIds = new Set<string>()
|
|
881
|
+
|
|
882
|
+
// TODO consider moving booting into the storage backend
|
|
883
|
+
if (boot !== undefined) {
|
|
884
|
+
let isInTxn = false
|
|
885
|
+
let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
|
|
886
|
+
|
|
887
|
+
const bootDbImpl: BootDb = {
|
|
888
|
+
_tag: 'BootDb',
|
|
889
|
+
execute: (queryStr, bindValues) => {
|
|
890
|
+
const stmt = adapter.mainDb.prepare(queryStr)
|
|
891
|
+
stmt.execute(bindValues)
|
|
892
|
+
|
|
893
|
+
if (isInTxn === true) {
|
|
894
|
+
txnExecuteStmnts.push([queryStr, bindValues])
|
|
895
|
+
} else {
|
|
896
|
+
adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
mutate: (...list) => {
|
|
900
|
+
for (const mutationEventDecoded of list) {
|
|
901
|
+
const mutationDef =
|
|
902
|
+
schema.mutations.get(mutationEventDecoded.mutation) ??
|
|
903
|
+
shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
|
|
904
|
+
|
|
905
|
+
__processedMutationIds.add(mutationEventDecoded.id)
|
|
906
|
+
|
|
907
|
+
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
908
|
+
// const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
909
|
+
|
|
910
|
+
for (const { statementSql, bindValues } of execArgsArr) {
|
|
911
|
+
adapter.mainDb.execute(statementSql, bindValues)
|
|
759
912
|
}
|
|
760
|
-
},
|
|
761
|
-
}
|
|
762
913
|
|
|
763
|
-
|
|
764
|
-
// NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
|
|
765
|
-
if (isPromise(booting)) {
|
|
766
|
-
await booting
|
|
767
|
-
}
|
|
768
|
-
}
|
|
914
|
+
const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
|
|
769
915
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
|
|
779
|
-
reactivityGraph,
|
|
780
|
-
disableDevtools,
|
|
916
|
+
adapter.coordinator
|
|
917
|
+
.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
|
|
918
|
+
.pipe(runEffectFork)
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
select: (queryStr, bindValues) => {
|
|
922
|
+
const stmt = adapter.mainDb.prepare(queryStr)
|
|
923
|
+
return stmt.select(bindValues)
|
|
781
924
|
},
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
925
|
+
txn: (callback) => {
|
|
926
|
+
try {
|
|
927
|
+
isInTxn = true
|
|
928
|
+
adapter.mainDb.execute('BEGIN', undefined)
|
|
929
|
+
|
|
930
|
+
callback()
|
|
931
|
+
|
|
932
|
+
adapter.mainDb.execute('COMMIT', undefined)
|
|
933
|
+
|
|
934
|
+
// adapter.coordinator.execute('BEGIN', undefined, undefined)
|
|
935
|
+
for (const [queryStr, bindValues] of txnExecuteStmnts) {
|
|
936
|
+
adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
|
|
937
|
+
}
|
|
938
|
+
// adapter.coordinator.execute('COMMIT', undefined, undefined)
|
|
939
|
+
} catch (e: any) {
|
|
940
|
+
adapter.mainDb.execute('ROLLBACK', undefined)
|
|
941
|
+
throw e
|
|
942
|
+
} finally {
|
|
943
|
+
isInTxn = false
|
|
944
|
+
txnExecuteStmnts = []
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const booting = boot(bootDbImpl, span)
|
|
950
|
+
// NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
|
|
951
|
+
if (isPromise(booting)) {
|
|
952
|
+
yield* Effect.promise(() => booting)
|
|
953
|
+
}
|
|
786
954
|
}
|
|
787
|
-
|
|
955
|
+
|
|
956
|
+
// TODO: we can't apply the schema at this point, we've already loaded persisted data!
|
|
957
|
+
// Think about what to do about this case.
|
|
958
|
+
// await applySchema(db, schema)
|
|
959
|
+
return Store.createStore<TGraphQLContext, TSchema>(
|
|
960
|
+
{
|
|
961
|
+
adapter,
|
|
962
|
+
schema,
|
|
963
|
+
graphQLOptions,
|
|
964
|
+
otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
|
|
965
|
+
reactivityGraph,
|
|
966
|
+
disableDevtools,
|
|
967
|
+
__processedMutationIds,
|
|
968
|
+
},
|
|
969
|
+
span,
|
|
970
|
+
)
|
|
971
|
+
}).pipe(
|
|
972
|
+
Effect.scoped,
|
|
973
|
+
Effect.withSpan('createStore', {
|
|
974
|
+
parent: otelOptions?.rootSpanContext
|
|
975
|
+
? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
|
|
976
|
+
: undefined,
|
|
977
|
+
}),
|
|
978
|
+
Effect.provide(TracingLive),
|
|
979
|
+
)
|
|
788
980
|
}
|
|
789
981
|
|
|
982
|
+
// TODO consider replacing with Effect's RC data structures
|
|
790
983
|
class ReferenceCountedSet<T> {
|
|
791
984
|
private map: Map<T, number>
|
|
792
985
|
|
|
@@ -822,3 +1015,21 @@ class ReferenceCountedSet<T> {
|
|
|
822
1015
|
}
|
|
823
1016
|
}
|
|
824
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
|
+
)
|