@livestore/livestore 0.0.54-dev.5 → 0.0.55-dev.0
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/MainDatabaseWrapper.d.ts +6 -5
- package/dist/MainDatabaseWrapper.d.ts.map +1 -1
- package/dist/MainDatabaseWrapper.js +3 -3
- package/dist/MainDatabaseWrapper.js.map +1 -1
- package/dist/QueryCache.d.ts +1 -1
- package/dist/QueryCache.d.ts.map +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/__tests__/react/fixture.d.ts +9 -27
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +12 -10
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +20 -12
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +23 -22
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/index.d.ts +1 -1
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +1 -1
- package/dist/effect/index.js.map +1 -1
- package/dist/global-state.d.ts +1 -3
- package/dist/global-state.d.ts.map +1 -1
- package/dist/global-state.js +2 -3
- package/dist/global-state.js.map +1 -1
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreContext.d.ts +5 -2
- 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 +8 -7
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +70 -43
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/LiveStoreProvider.test.js +33 -12
- package/dist/react/LiveStoreProvider.test.js.map +1 -1
- package/dist/react/components/LiveList.d.ts.map +1 -1
- package/dist/react/useAtom.d.ts +1 -1
- package/dist/react/useAtom.d.ts.map +1 -1
- package/dist/react/useLocalId.d.ts.map +1 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +2 -2
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useRow.d.ts +2 -2
- package/dist/react/useRow.d.ts.map +1 -1
- package/dist/react/useRow.js +5 -5
- package/dist/react/useRow.js.map +1 -1
- package/dist/react/useRow.test.js +22 -22
- package/dist/react/useRow.test.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js +1 -1
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
- package/dist/reactive.d.ts +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +4 -5
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +6 -6
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +3 -3
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +8 -8
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +10 -10
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +6 -6
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +8 -8
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +9 -10
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +12 -12
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.js +6 -6
- package/dist/reactiveQueries/sql.test.js.map +1 -1
- package/dist/row-query.d.ts +2 -2
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +4 -38
- package/dist/row-query.js.map +1 -1
- package/dist/store.d.ts +41 -24
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +336 -223
- package/dist/store.js.map +1 -1
- package/dist/utils/otel.d.ts.map +1 -1
- package/package.json +10 -19
- package/src/MainDatabaseWrapper.ts +14 -8
- package/src/QueryCache.ts +1 -2
- package/src/__tests__/react/fixture.tsx +13 -11
- package/src/effect/LiveStore.ts +65 -54
- package/src/effect/index.ts +2 -1
- package/src/global-state.ts +2 -6
- package/src/index.ts +25 -7
- package/src/react/LiveStoreContext.ts +7 -2
- package/src/react/LiveStoreProvider.test.tsx +56 -14
- package/src/react/LiveStoreProvider.tsx +105 -46
- package/src/react/useQuery.ts +2 -2
- package/src/react/useRow.test.tsx +22 -22
- package/src/react/useRow.ts +7 -10
- package/src/react/useTemporaryQuery.ts +2 -2
- package/src/reactive.ts +6 -5
- package/src/reactiveQueries/base-class.ts +9 -9
- package/src/reactiveQueries/graphql.ts +19 -15
- package/src/reactiveQueries/js.ts +12 -12
- package/src/reactiveQueries/sql.test.ts +6 -6
- package/src/reactiveQueries/sql.ts +19 -21
- package/src/row-query.ts +8 -54
- package/src/store.ts +533 -284
- package/dist/utils/bounded-collections.d.ts +0 -34
- package/dist/utils/bounded-collections.d.ts.map +0 -1
- package/dist/utils/bounded-collections.js +0 -91
- package/dist/utils/bounded-collections.js.map +0 -1
- package/dist/utils/util.d.ts +0 -14
- package/dist/utils/util.d.ts.map +0 -1
- package/dist/utils/util.js +0 -19
- package/dist/utils/util.js.map +0 -1
- package/src/utils/util.ts +0 -31
package/src/store.ts
CHANGED
|
@@ -1,22 +1,50 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import type {
|
|
2
|
+
BootDb,
|
|
3
|
+
BootStatus,
|
|
4
|
+
DebugInfo,
|
|
5
|
+
ParamsObject,
|
|
6
|
+
PreparedBindValues,
|
|
7
|
+
ResetMode,
|
|
8
|
+
StoreAdapter,
|
|
9
|
+
StoreAdapterFactory,
|
|
10
|
+
} from '@livestore/common'
|
|
11
|
+
import {
|
|
12
|
+
Devtools,
|
|
13
|
+
getExecArgsFromMutation,
|
|
14
|
+
liveStoreVersion,
|
|
15
|
+
prepareBindValues,
|
|
16
|
+
UnexpectedError,
|
|
17
|
+
} from '@livestore/common'
|
|
18
|
+
import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
|
|
19
|
+
import { makeMutationEventSchemaMemo } from '@livestore/common/schema'
|
|
20
|
+
import { assertNever, makeNoopTracer, shouldNeverHappen, throttle } from '@livestore/utils'
|
|
21
|
+
import { cuid } from '@livestore/utils/cuid'
|
|
22
|
+
import {
|
|
23
|
+
Effect,
|
|
24
|
+
Exit,
|
|
25
|
+
FiberSet,
|
|
26
|
+
Inspectable,
|
|
27
|
+
Layer,
|
|
28
|
+
Logger,
|
|
29
|
+
LogLevel,
|
|
30
|
+
OtelTracer,
|
|
31
|
+
Queue,
|
|
32
|
+
Runtime,
|
|
33
|
+
Schema,
|
|
34
|
+
Scope,
|
|
35
|
+
Stream,
|
|
36
|
+
} from '@livestore/utils/effect'
|
|
8
37
|
import * as otel from '@opentelemetry/api'
|
|
9
38
|
import type { GraphQLSchema } from 'graphql'
|
|
10
39
|
|
|
11
|
-
import {
|
|
12
|
-
import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
|
|
40
|
+
import { globalReactivityGraph } from './global-state.js'
|
|
41
|
+
import { emptyDebugInfo as makeEmptyDebugInfo, MainDatabaseWrapper } from './MainDatabaseWrapper.js'
|
|
13
42
|
import type { StackInfo } from './react/utils/stack-info.js'
|
|
14
|
-
import type { DebugRefreshReasonBase,
|
|
15
|
-
import
|
|
43
|
+
import type { DebugRefreshReasonBase, Ref } from './reactive.js'
|
|
44
|
+
import { NOT_REFRESHED_YET } from './reactive.js'
|
|
45
|
+
import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
|
|
16
46
|
import { downloadBlob } from './utils/dev.js'
|
|
17
47
|
import { getDurationMsFromSpan } from './utils/otel.js'
|
|
18
|
-
import type { ParamsObject } from './utils/util.js'
|
|
19
|
-
import { prepareBindValues } from './utils/util.js'
|
|
20
48
|
|
|
21
49
|
export type BaseGraphQLContext = {
|
|
22
50
|
queriedTables: Set<string>
|
|
@@ -29,18 +57,25 @@ export type GraphQLOptions<TContext> = {
|
|
|
29
57
|
makeContext: (db: MainDatabaseWrapper, tracer: otel.Tracer) => TContext
|
|
30
58
|
}
|
|
31
59
|
|
|
60
|
+
export type OtelOptions = {
|
|
61
|
+
tracer: otel.Tracer
|
|
62
|
+
rootSpanContext: otel.Context
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
export type StoreOptions<
|
|
33
66
|
TGraphQLContext extends BaseGraphQLContext,
|
|
34
67
|
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
35
68
|
> = {
|
|
36
69
|
adapter: StoreAdapter
|
|
37
70
|
schema: TSchema
|
|
71
|
+
// TODO remove graphql-related stuff from store and move to GraphQL query directly
|
|
38
72
|
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
dbGraph: DbGraph
|
|
42
|
-
mutationEventSchema: MutationEventSchema<any>
|
|
73
|
+
otelOptions: OtelOptions
|
|
74
|
+
reactivityGraph: ReactivityGraph
|
|
43
75
|
disableDevtools?: boolean
|
|
76
|
+
fiberSet: FiberSet.FiberSet
|
|
77
|
+
// TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
|
|
78
|
+
__processedMutationIds: Set<string>
|
|
44
79
|
}
|
|
45
80
|
|
|
46
81
|
export type RefreshReason =
|
|
@@ -93,13 +128,12 @@ export type StoreMutateOptions = {
|
|
|
93
128
|
export class Store<
|
|
94
129
|
TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
|
|
95
130
|
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
96
|
-
> {
|
|
131
|
+
> extends Inspectable.Class {
|
|
97
132
|
id = uniqueStoreId()
|
|
98
|
-
|
|
133
|
+
readonly devtoolsConnectionId = cuid()
|
|
134
|
+
private fiberSet: FiberSet.FiberSet
|
|
135
|
+
reactivityGraph: ReactivityGraph
|
|
99
136
|
mainDbWrapper: MainDatabaseWrapper
|
|
100
|
-
// TODO refactor
|
|
101
|
-
// _proxyDb: InMemoryDatabase
|
|
102
|
-
// TODO
|
|
103
137
|
adapter: StoreAdapter
|
|
104
138
|
schema: LiveStoreSchema
|
|
105
139
|
graphQLSchema?: GraphQLSchema
|
|
@@ -109,11 +143,11 @@ export class Store<
|
|
|
109
143
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
|
110
144
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
|
111
145
|
*/
|
|
112
|
-
tableRefs: { [key: string]: Ref<null,
|
|
146
|
+
tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
|
|
113
147
|
|
|
114
148
|
// TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
|
|
115
|
-
__processedMutationIds
|
|
116
|
-
__processedMutationWithoutRefreshIds = new Set<string>()
|
|
149
|
+
private __processedMutationIds
|
|
150
|
+
private __processedMutationWithoutRefreshIds = new Set<string>()
|
|
117
151
|
|
|
118
152
|
/** RC-based set to see which queries are currently subscribed to */
|
|
119
153
|
activeQueries: ReferenceCountedSet<LiveQuery<any>>
|
|
@@ -124,60 +158,53 @@ export class Store<
|
|
|
124
158
|
adapter,
|
|
125
159
|
schema,
|
|
126
160
|
graphQLOptions,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
otelRootSpanContext,
|
|
130
|
-
mutationEventSchema,
|
|
161
|
+
reactivityGraph,
|
|
162
|
+
otelOptions,
|
|
131
163
|
disableDevtools,
|
|
164
|
+
__processedMutationIds,
|
|
165
|
+
fiberSet,
|
|
132
166
|
}: StoreOptions<TGraphQLContext, TSchema>) {
|
|
133
|
-
|
|
167
|
+
super()
|
|
168
|
+
|
|
169
|
+
this.mainDbWrapper = new MainDatabaseWrapper({ otel: otelOptions, db: adapter.mainDb })
|
|
134
170
|
this.adapter = adapter
|
|
135
171
|
this.schema = schema
|
|
136
172
|
|
|
173
|
+
this.fiberSet = fiberSet
|
|
174
|
+
|
|
137
175
|
// TODO refactor
|
|
138
|
-
this.__mutationEventSchema =
|
|
139
|
-
|
|
176
|
+
this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
|
|
177
|
+
|
|
178
|
+
// TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
|
|
179
|
+
this.__processedMutationIds = __processedMutationIds
|
|
140
180
|
|
|
141
181
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
|
142
182
|
this.tableRefs = {}
|
|
143
183
|
this.activeQueries = new ReferenceCountedSet()
|
|
144
184
|
|
|
145
|
-
const mutationsSpan =
|
|
185
|
+
const mutationsSpan = otelOptions.tracer.startSpan('LiveStore:mutations', {}, otelOptions.rootSpanContext)
|
|
146
186
|
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan)
|
|
147
187
|
|
|
148
|
-
const queriesSpan =
|
|
188
|
+
const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext)
|
|
149
189
|
const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
|
|
150
190
|
|
|
151
|
-
this.
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
|
|
157
|
-
}),
|
|
158
|
-
Stream.runDrain,
|
|
159
|
-
Effect.tapCauseLogPretty,
|
|
160
|
-
Effect.runFork,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
if (disableDevtools !== true) {
|
|
164
|
-
this.bootDevtools()
|
|
191
|
+
this.reactivityGraph = reactivityGraph
|
|
192
|
+
this.reactivityGraph.context = {
|
|
193
|
+
store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
|
|
194
|
+
otelTracer: otelOptions.tracer,
|
|
195
|
+
rootOtelContext: otelQueriesSpanContext,
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
this.otel = {
|
|
168
|
-
tracer:
|
|
199
|
+
tracer: otelOptions.tracer,
|
|
169
200
|
mutationsSpanContext: otelMuationsSpanContext,
|
|
170
201
|
queriesSpanContext: otelQueriesSpanContext,
|
|
171
202
|
}
|
|
172
203
|
|
|
173
204
|
// Need a set here since `schema.tables` might contain duplicates and some componentStateTables
|
|
174
|
-
const allTableNames = new Set(
|
|
175
|
-
this.schema.tables.keys(),
|
|
176
|
-
// TODO activate dynamic tables
|
|
177
|
-
// ...Array.from(dynamicallyRegisteredTables.values()).map((_) => _.sqliteDef.name),
|
|
178
|
-
)
|
|
205
|
+
const allTableNames = new Set(this.schema.tables.keys())
|
|
179
206
|
const existingTableRefs = new Map(
|
|
180
|
-
Array.from(this.
|
|
207
|
+
Array.from(this.reactivityGraph.atoms.values())
|
|
181
208
|
.filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
|
|
182
209
|
.map((_) => [_.label!.slice('tableRef:'.length), _] as const),
|
|
183
210
|
)
|
|
@@ -189,6 +216,36 @@ export class Store<
|
|
|
189
216
|
this.graphQLSchema = graphQLOptions.schema
|
|
190
217
|
this.graphQLContext = graphQLOptions.makeContext(this.mainDbWrapper, this.otel.tracer)
|
|
191
218
|
}
|
|
219
|
+
|
|
220
|
+
Effect.gen(this, function* () {
|
|
221
|
+
yield* this.adapter.coordinator.syncMutations.pipe(
|
|
222
|
+
Stream.tapSync((mutationEventDecoded) => {
|
|
223
|
+
this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
|
|
224
|
+
}),
|
|
225
|
+
Stream.runDrain,
|
|
226
|
+
Effect.withSpan('LiveStore:syncMutations'),
|
|
227
|
+
Effect.forkScoped,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if (disableDevtools !== true) {
|
|
231
|
+
yield* this.bootDevtools().pipe(Effect.forkScoped)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
yield* Effect.addFinalizer(() =>
|
|
235
|
+
Effect.sync(() => {
|
|
236
|
+
for (const tableRef of Object.values(this.tableRefs)) {
|
|
237
|
+
for (const superComp of tableRef.super) {
|
|
238
|
+
this.reactivityGraph.removeEdge(superComp, tableRef)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
|
|
243
|
+
otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
yield* Effect.never
|
|
248
|
+
}).pipe(Effect.scoped, Effect.withSpan('LiveStore:store-constructor'), FiberSet.run(fiberSet), runEffectFork)
|
|
192
249
|
}
|
|
193
250
|
|
|
194
251
|
static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
|
|
@@ -196,7 +253,7 @@ export class Store<
|
|
|
196
253
|
parentSpan: otel.Span,
|
|
197
254
|
): Store<TGraphQLContext, TSchema> => {
|
|
198
255
|
const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
|
|
199
|
-
return storeOptions.
|
|
256
|
+
return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:store-constructor', {}, ctx, (span) => {
|
|
200
257
|
try {
|
|
201
258
|
return new Store(storeOptions)
|
|
202
259
|
} finally {
|
|
@@ -224,7 +281,7 @@ export class Store<
|
|
|
224
281
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
225
282
|
|
|
226
283
|
const label = `subscribe:${options?.label}`
|
|
227
|
-
const effect = this.
|
|
284
|
+
const effect = this.reactivityGraph.makeEffect((get) => onNewValue(get(query$.results$)), { label })
|
|
228
285
|
|
|
229
286
|
this.activeQueries.add(query$ as LiveQuery<TResult>)
|
|
230
287
|
|
|
@@ -236,7 +293,7 @@ export class Store<
|
|
|
236
293
|
const unsubscribe = () => {
|
|
237
294
|
// console.log('store unsub', query$.label)
|
|
238
295
|
try {
|
|
239
|
-
this.
|
|
296
|
+
this.reactivityGraph.destroyNode(effect)
|
|
240
297
|
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
241
298
|
onUnsubsubscribe?.()
|
|
242
299
|
} finally {
|
|
@@ -254,16 +311,7 @@ export class Store<
|
|
|
254
311
|
* Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
|
|
255
312
|
*/
|
|
256
313
|
destroy = async () => {
|
|
257
|
-
|
|
258
|
-
for (const superComp of tableRef.super) {
|
|
259
|
-
this.graph.removeEdge(superComp, tableRef)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
|
|
264
|
-
otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
|
|
265
|
-
|
|
266
|
-
await this.adapter.coordinator.shutdown()
|
|
314
|
+
await FiberSet.clear(this.fiberSet).pipe(Effect.withSpan('Store:destroy'), runEffectPromise)
|
|
267
315
|
}
|
|
268
316
|
|
|
269
317
|
mutate: {
|
|
@@ -323,6 +371,8 @@ export class Store<
|
|
|
323
371
|
// mutationsEvents.forEach((_) => console.log(_.mutation, _.id, _.args))
|
|
324
372
|
// console.groupEnd()
|
|
325
373
|
|
|
374
|
+
let durationMs: number
|
|
375
|
+
|
|
326
376
|
return this.otel.tracer.startActiveSpan(
|
|
327
377
|
'LiveStore:mutate',
|
|
328
378
|
{ attributes: { 'livestore.mutateLabel': label } },
|
|
@@ -353,8 +403,8 @@ export class Store<
|
|
|
353
403
|
writeTables.add(tableName)
|
|
354
404
|
}
|
|
355
405
|
} catch (e: any) {
|
|
356
|
-
debugger
|
|
357
406
|
console.error(e, mutationEvent)
|
|
407
|
+
throw e
|
|
358
408
|
}
|
|
359
409
|
}
|
|
360
410
|
}
|
|
@@ -368,13 +418,14 @@ export class Store<
|
|
|
368
418
|
} catch (e: any) {
|
|
369
419
|
console.error(e)
|
|
370
420
|
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
421
|
+
throw e
|
|
371
422
|
} finally {
|
|
372
423
|
span.end()
|
|
373
424
|
}
|
|
374
425
|
},
|
|
375
426
|
)
|
|
376
427
|
|
|
377
|
-
const tablesToUpdate = [] as [Ref<null,
|
|
428
|
+
const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
|
|
378
429
|
for (const tableName of writeTables) {
|
|
379
430
|
const tableRef = this.tableRefs[tableName]
|
|
380
431
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
@@ -388,14 +439,18 @@ export class Store<
|
|
|
388
439
|
}
|
|
389
440
|
|
|
390
441
|
// Update all table refs together in a batch, to only trigger one reactive update
|
|
391
|
-
this.
|
|
442
|
+
this.reactivityGraph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh })
|
|
392
443
|
} catch (e: any) {
|
|
444
|
+
console.error(e)
|
|
393
445
|
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
446
|
+
throw e
|
|
394
447
|
} finally {
|
|
395
448
|
span.end()
|
|
396
449
|
|
|
397
|
-
|
|
450
|
+
durationMs = getDurationMsFromSpan(span)
|
|
398
451
|
}
|
|
452
|
+
|
|
453
|
+
return { durationMs }
|
|
399
454
|
},
|
|
400
455
|
)
|
|
401
456
|
}
|
|
@@ -412,7 +467,7 @@ export class Store<
|
|
|
412
467
|
this.otel.mutationsSpanContext,
|
|
413
468
|
(span) => {
|
|
414
469
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
415
|
-
this.
|
|
470
|
+
this.reactivityGraph.runDeferredEffects({ otelContext })
|
|
416
471
|
span.end()
|
|
417
472
|
},
|
|
418
473
|
)
|
|
@@ -480,10 +535,9 @@ export class Store<
|
|
|
480
535
|
|
|
481
536
|
if (coordinatorMode !== 'skip-coordinator') {
|
|
482
537
|
// Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
})
|
|
538
|
+
this.adapter.coordinator
|
|
539
|
+
.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
|
|
540
|
+
.pipe(runEffectFork)
|
|
487
541
|
}
|
|
488
542
|
|
|
489
543
|
// Uncomment to print a list of queries currently registered on the store
|
|
@@ -509,8 +563,7 @@ export class Store<
|
|
|
509
563
|
) => {
|
|
510
564
|
this.mainDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
|
|
511
565
|
|
|
512
|
-
|
|
513
|
-
this.adapter.coordinator.execute(query, prepareBindValues(params, query), parentSpan)
|
|
566
|
+
this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(runEffectFork)
|
|
514
567
|
}
|
|
515
568
|
|
|
516
569
|
select = (query: string, params: ParamsObject = {}) => {
|
|
@@ -518,117 +571,227 @@ export class Store<
|
|
|
518
571
|
}
|
|
519
572
|
|
|
520
573
|
makeTableRef = (tableName: string) =>
|
|
521
|
-
this.
|
|
574
|
+
this.reactivityGraph.makeRef(null, {
|
|
522
575
|
equal: () => false,
|
|
523
576
|
label: `tableRef:${tableName}`,
|
|
524
577
|
meta: { liveStoreRefType: 'table' },
|
|
525
578
|
})
|
|
526
579
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (
|
|
536
|
-
decoded._tag === 'None' ||
|
|
537
|
-
decoded.value._tag === 'LSD.DevtoolsReadyBroadcast' ||
|
|
538
|
-
decoded.value._tag === 'LSD.DevtoolsConnected' ||
|
|
539
|
-
decoded.value.channelId !== this.adapter.coordinator.devtools.channelId
|
|
540
|
-
) {
|
|
541
|
-
// console.log(`Unknown message`, event)
|
|
542
|
-
return
|
|
580
|
+
// #region devtools
|
|
581
|
+
// TODO shutdown behaviour
|
|
582
|
+
private bootDevtools = () =>
|
|
583
|
+
Effect.gen(this, function* () {
|
|
584
|
+
const sendToDevtoolsContentscript = (
|
|
585
|
+
message: typeof Devtools.DevtoolsWindowMessage.MessageForContentscript.Type,
|
|
586
|
+
) => {
|
|
587
|
+
window.postMessage(Schema.encodeSync(Devtools.DevtoolsWindowMessage.MessageForContentscript)(message), '*')
|
|
543
588
|
}
|
|
544
589
|
|
|
545
|
-
|
|
546
|
-
const sendToDevtools = (message: Devtools.MessageFromAppHost) =>
|
|
547
|
-
devtoolsChannel.fromAppHost.postMessage(Schema.encodeSync(Devtools.MessageFromAppHost)(message))
|
|
548
|
-
|
|
549
|
-
switch (decoded.value._tag) {
|
|
550
|
-
case 'LSD.SignalsSubscribe': {
|
|
551
|
-
const includeResults = decoded.value.includeResults
|
|
552
|
-
const send = () =>
|
|
553
|
-
sendToDevtools(
|
|
554
|
-
Devtools.SignalsRes.make({
|
|
555
|
-
signals: this.graph.getSnapshot({ includeResults }),
|
|
556
|
-
requestId,
|
|
557
|
-
liveStoreVersion,
|
|
558
|
-
}),
|
|
559
|
-
)
|
|
590
|
+
sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
|
|
560
591
|
|
|
561
|
-
|
|
592
|
+
const channelId = this.adapter.coordinator.devtools.channelId
|
|
562
593
|
|
|
563
|
-
|
|
564
|
-
signalsSubcriptionRef.current = this.graph.subscribeToRefresh(() => send())
|
|
565
|
-
}
|
|
594
|
+
const runtime = yield* Effect.runtime()
|
|
566
595
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
this.mainDbWrapper.debugInfo.slowQueries.clear()
|
|
577
|
-
sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
|
|
578
|
-
break
|
|
579
|
-
}
|
|
580
|
-
case 'LSD.DebugInfoRerunQueryReq': {
|
|
581
|
-
const { queryStr, bindValues, queriedTables } = decoded.value
|
|
582
|
-
this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
|
|
583
|
-
sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
|
|
584
|
-
break
|
|
585
|
-
}
|
|
586
|
-
case 'LSD.SignalsUnsubscribe': {
|
|
587
|
-
signalsSubcriptionRef.current!()
|
|
588
|
-
signalsSubcriptionRef.current = undefined
|
|
589
|
-
break
|
|
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
|
|
590
605
|
}
|
|
591
|
-
case 'LSD.LiveQueriesSubscribe': {
|
|
592
|
-
const send = () =>
|
|
593
|
-
sendToDevtools(
|
|
594
|
-
Devtools.LiveQueriesRes.make({
|
|
595
|
-
liveQueries: [...this.activeQueries].map((q) => ({
|
|
596
|
-
_tag: q._tag,
|
|
597
|
-
id: q.id,
|
|
598
|
-
label: q.label,
|
|
599
|
-
runs: q.runs,
|
|
600
|
-
executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
|
|
601
|
-
lastestResult: q.results$.previousResult,
|
|
602
|
-
activeSubscriptions: Array.from(q.activeSubscriptions),
|
|
603
|
-
})),
|
|
604
|
-
requestId,
|
|
605
|
-
liveStoreVersion,
|
|
606
|
-
}),
|
|
607
|
-
)
|
|
608
606
|
|
|
609
|
-
|
|
607
|
+
if (message.channelId !== channelId) return
|
|
610
608
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
609
|
+
if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
|
|
610
|
+
type Unsub = () => void
|
|
611
|
+
type RequestId = string
|
|
614
612
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
+
})
|
|
625
782
|
|
|
626
|
-
|
|
783
|
+
storeMessagePort.start()
|
|
784
|
+
}),
|
|
785
|
+
Runtime.runFork(runtime),
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
return
|
|
627
789
|
}
|
|
628
|
-
|
|
629
|
-
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
|
|
630
793
|
})
|
|
631
|
-
|
|
794
|
+
// #endregion devtools
|
|
632
795
|
|
|
633
796
|
__devDownloadDb = () => {
|
|
634
797
|
const data = this.mainDbWrapper.export()
|
|
@@ -636,146 +799,214 @@ export class Store<
|
|
|
636
799
|
}
|
|
637
800
|
|
|
638
801
|
__devDownloadMutationLogDb = async () => {
|
|
639
|
-
const data = await this.adapter.coordinator.getMutationLogData()
|
|
802
|
+
const data = await this.adapter.coordinator.getMutationLogData.pipe(runEffectPromise)
|
|
640
803
|
downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
|
|
641
804
|
}
|
|
642
805
|
|
|
643
806
|
// TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
|
|
644
|
-
dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode)
|
|
807
|
+
dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
|
|
808
|
+
|
|
809
|
+
toJSON = () => {
|
|
810
|
+
return {
|
|
811
|
+
_tag: 'Store',
|
|
812
|
+
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
|
|
818
|
+
schema: TSchema
|
|
819
|
+
adapter: StoreAdapterFactory
|
|
820
|
+
reactivityGraph?: ReactivityGraph
|
|
821
|
+
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
|
822
|
+
otelOptions?: Partial<OtelOptions>
|
|
823
|
+
boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
|
|
824
|
+
batchUpdates?: (run: () => void) => void
|
|
825
|
+
disableDevtools?: boolean
|
|
826
|
+
onBootStatus?: (status: BootStatus) => void
|
|
645
827
|
}
|
|
646
828
|
|
|
647
829
|
/** Create a new LiveStore Store */
|
|
648
|
-
export const
|
|
830
|
+
export const createStorePromise = async <
|
|
831
|
+
TGraphQLContext extends BaseGraphQLContext,
|
|
832
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
833
|
+
>({
|
|
834
|
+
signal,
|
|
835
|
+
...options
|
|
836
|
+
}: CreateStoreOptions<TGraphQLContext, TSchema> & { signal?: AbortSignal }): Promise<Store<TGraphQLContext, TSchema>> =>
|
|
837
|
+
Effect.gen(function* () {
|
|
838
|
+
const scope = yield* Scope.make()
|
|
839
|
+
const runtime = yield* Effect.runtime()
|
|
840
|
+
|
|
841
|
+
if (signal !== undefined) {
|
|
842
|
+
signal.addEventListener('abort', () => {
|
|
843
|
+
Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty, Runtime.runFork(runtime))
|
|
844
|
+
})
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return yield* FiberSet.make().pipe(
|
|
848
|
+
Effect.andThen((fiberSet) => createStore({ ...options, fiberSet })),
|
|
849
|
+
Scope.extend(scope),
|
|
850
|
+
)
|
|
851
|
+
}).pipe(Effect.withSpan('createStore'), runEffectPromise)
|
|
852
|
+
|
|
853
|
+
// #region createStore
|
|
854
|
+
export const createStore = <
|
|
649
855
|
TGraphQLContext extends BaseGraphQLContext,
|
|
650
856
|
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
651
857
|
>({
|
|
652
858
|
schema,
|
|
653
859
|
graphQLOptions,
|
|
654
|
-
|
|
655
|
-
otelRootSpanContext = otel.context.active(),
|
|
860
|
+
otelOptions,
|
|
656
861
|
adapter: adapterFactory,
|
|
657
862
|
boot,
|
|
658
|
-
|
|
863
|
+
reactivityGraph = globalReactivityGraph,
|
|
659
864
|
batchUpdates,
|
|
660
865
|
disableDevtools,
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
866
|
+
onBootStatus,
|
|
867
|
+
fiberSet,
|
|
868
|
+
}: CreateStoreOptions<TGraphQLContext, TSchema> & { fiberSet: FiberSet.FiberSet }): Effect.Effect<
|
|
869
|
+
Store<TGraphQLContext, TSchema>,
|
|
870
|
+
UnexpectedError,
|
|
871
|
+
Scope.Scope
|
|
872
|
+
> => {
|
|
873
|
+
const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
|
|
874
|
+
const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active()
|
|
875
|
+
|
|
876
|
+
const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
|
|
877
|
+
Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)),
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
return Effect.gen(function* () {
|
|
881
|
+
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
882
|
+
|
|
883
|
+
const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
884
|
+
|
|
885
|
+
yield* Queue.take(bootStatusQueue).pipe(
|
|
886
|
+
Effect.tapSync((status) => onBootStatus?.(status)),
|
|
887
|
+
Effect.forever,
|
|
888
|
+
Effect.tapCauseLogPretty,
|
|
889
|
+
Effect.forkScoped,
|
|
890
|
+
)
|
|
685
891
|
|
|
686
|
-
|
|
892
|
+
const adapter: StoreAdapter = yield* adapterFactory({
|
|
893
|
+
schema,
|
|
894
|
+
devtoolsEnabled: disableDevtools !== true,
|
|
895
|
+
bootStatusQueue,
|
|
896
|
+
shutdown: (cause) =>
|
|
897
|
+
Effect.gen(function* () {
|
|
898
|
+
yield* Effect.logWarning(`Shutting down LiveStore`, cause)
|
|
899
|
+
// TODO close parent scope? (Needs refactor with Mike A)
|
|
900
|
+
yield* FiberSet.clear(fiberSet)
|
|
901
|
+
}).pipe(Effect.withSpan('livestore:shutdown')),
|
|
902
|
+
}).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
|
|
903
|
+
|
|
904
|
+
if (batchUpdates !== undefined) {
|
|
905
|
+
reactivityGraph.effectsWrapper = batchUpdates
|
|
906
|
+
}
|
|
687
907
|
|
|
688
|
-
|
|
689
|
-
if (boot !== undefined) {
|
|
690
|
-
let isInTxn = false
|
|
691
|
-
let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
|
|
908
|
+
const mutationEventSchema = makeMutationEventSchemaMemo(schema)
|
|
692
909
|
|
|
693
|
-
|
|
694
|
-
_tag: 'BootDb',
|
|
695
|
-
execute: (queryStr, bindValues) => {
|
|
696
|
-
const stmt = adapter.mainDb.prepare(queryStr)
|
|
697
|
-
stmt.execute(bindValues)
|
|
910
|
+
const __processedMutationIds = new Set<string>()
|
|
698
911
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
},
|
|
705
|
-
mutate: (...list) => {
|
|
706
|
-
for (const mutationEventDecoded of list) {
|
|
707
|
-
const mutationDef =
|
|
708
|
-
schema.mutations.get(mutationEventDecoded.mutation) ??
|
|
709
|
-
shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
|
|
710
|
-
|
|
711
|
-
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
712
|
-
// const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
713
|
-
|
|
714
|
-
for (const { statementSql, bindValues } of execArgsArr) {
|
|
715
|
-
adapter.mainDb.execute(statementSql, bindValues)
|
|
716
|
-
}
|
|
912
|
+
// TODO consider moving booting into the storage backend
|
|
913
|
+
if (boot !== undefined) {
|
|
914
|
+
let isInTxn = false
|
|
915
|
+
let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
|
|
717
916
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
} finally {
|
|
744
|
-
isInTxn = false
|
|
745
|
-
txnExecuteStmnts = []
|
|
917
|
+
const bootDbImpl: BootDb = {
|
|
918
|
+
_tag: 'BootDb',
|
|
919
|
+
execute: (queryStr, bindValues) => {
|
|
920
|
+
const stmt = adapter.mainDb.prepare(queryStr)
|
|
921
|
+
stmt.execute(bindValues)
|
|
922
|
+
|
|
923
|
+
if (isInTxn === true) {
|
|
924
|
+
txnExecuteStmnts.push([queryStr, bindValues])
|
|
925
|
+
} else {
|
|
926
|
+
adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
mutate: (...list) => {
|
|
930
|
+
for (const mutationEventDecoded of list) {
|
|
931
|
+
const mutationDef =
|
|
932
|
+
schema.mutations.get(mutationEventDecoded.mutation) ??
|
|
933
|
+
shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
|
|
934
|
+
|
|
935
|
+
__processedMutationIds.add(mutationEventDecoded.id)
|
|
936
|
+
|
|
937
|
+
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
938
|
+
// const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
939
|
+
|
|
940
|
+
for (const { statementSql, bindValues } of execArgsArr) {
|
|
941
|
+
adapter.mainDb.execute(statementSql, bindValues)
|
|
746
942
|
}
|
|
747
|
-
},
|
|
748
|
-
}
|
|
749
943
|
|
|
750
|
-
|
|
751
|
-
// NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
|
|
752
|
-
if (isPromise(booting)) {
|
|
753
|
-
await booting
|
|
754
|
-
}
|
|
755
|
-
}
|
|
944
|
+
const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
|
|
756
945
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
otelTracer,
|
|
766
|
-
otelRootSpanContext,
|
|
767
|
-
dbGraph,
|
|
768
|
-
mutationEventSchema,
|
|
769
|
-
disableDevtools,
|
|
946
|
+
adapter.coordinator
|
|
947
|
+
.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
|
|
948
|
+
.pipe(runEffectFork)
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
select: (queryStr, bindValues) => {
|
|
952
|
+
const stmt = adapter.mainDb.prepare(queryStr)
|
|
953
|
+
return stmt.select(bindValues)
|
|
770
954
|
},
|
|
771
|
-
|
|
955
|
+
txn: (callback) => {
|
|
956
|
+
try {
|
|
957
|
+
isInTxn = true
|
|
958
|
+
adapter.mainDb.execute('BEGIN', undefined)
|
|
959
|
+
|
|
960
|
+
callback()
|
|
961
|
+
|
|
962
|
+
adapter.mainDb.execute('COMMIT', undefined)
|
|
963
|
+
|
|
964
|
+
// adapter.coordinator.execute('BEGIN', undefined, undefined)
|
|
965
|
+
for (const [queryStr, bindValues] of txnExecuteStmnts) {
|
|
966
|
+
adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
|
|
967
|
+
}
|
|
968
|
+
// adapter.coordinator.execute('COMMIT', undefined, undefined)
|
|
969
|
+
} catch (e: any) {
|
|
970
|
+
adapter.mainDb.execute('ROLLBACK', undefined)
|
|
971
|
+
throw e
|
|
972
|
+
} finally {
|
|
973
|
+
isInTxn = false
|
|
974
|
+
txnExecuteStmnts = []
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
yield* Effect.tryAll(() => boot(bootDbImpl, span)).pipe(
|
|
980
|
+
UnexpectedError.mapToUnexpectedError,
|
|
981
|
+
Effect.withSpan('createStore:boot'),
|
|
772
982
|
)
|
|
773
|
-
} finally {
|
|
774
|
-
span.end()
|
|
775
983
|
}
|
|
776
|
-
|
|
984
|
+
|
|
985
|
+
return Store.createStore<TGraphQLContext, TSchema>(
|
|
986
|
+
{
|
|
987
|
+
adapter,
|
|
988
|
+
schema,
|
|
989
|
+
graphQLOptions,
|
|
990
|
+
otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
|
|
991
|
+
reactivityGraph,
|
|
992
|
+
disableDevtools,
|
|
993
|
+
__processedMutationIds,
|
|
994
|
+
fiberSet,
|
|
995
|
+
},
|
|
996
|
+
span,
|
|
997
|
+
)
|
|
998
|
+
}).pipe(
|
|
999
|
+
Effect.withSpan('createStore', {
|
|
1000
|
+
parent: otelOptions?.rootSpanContext
|
|
1001
|
+
? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
|
|
1002
|
+
: undefined,
|
|
1003
|
+
}),
|
|
1004
|
+
Effect.provide(TracingLive),
|
|
1005
|
+
)
|
|
777
1006
|
}
|
|
1007
|
+
// #endregion createStore
|
|
778
1008
|
|
|
1009
|
+
// TODO consider replacing with Effect's RC data structures
|
|
779
1010
|
class ReferenceCountedSet<T> {
|
|
780
1011
|
private map: Map<T, number>
|
|
781
1012
|
|
|
@@ -811,3 +1042,21 @@ class ReferenceCountedSet<T> {
|
|
|
811
1042
|
}
|
|
812
1043
|
}
|
|
813
1044
|
}
|
|
1045
|
+
|
|
1046
|
+
const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
|
|
1047
|
+
effect.pipe(
|
|
1048
|
+
Effect.tapCauseLogPretty,
|
|
1049
|
+
Effect.annotateLogs({ thread: 'window' }),
|
|
1050
|
+
Effect.provide(Logger.pretty),
|
|
1051
|
+
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
1052
|
+
Effect.runFork,
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
const runEffectPromise = <A, E>(effect: Effect.Effect<A, E, never>) =>
|
|
1056
|
+
effect.pipe(
|
|
1057
|
+
Effect.tapCauseLogPretty,
|
|
1058
|
+
Effect.annotateLogs({ thread: 'window' }),
|
|
1059
|
+
Effect.provide(Logger.pretty),
|
|
1060
|
+
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
1061
|
+
Effect.runPromise,
|
|
1062
|
+
)
|