@livestore/livestore 0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4
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/README.md +1 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/QueryCache.d.ts +20 -0
- package/dist/QueryCache.d.ts.map +1 -0
- package/dist/QueryCache.js +61 -0
- package/dist/QueryCache.js.map +1 -0
- package/dist/SynchronousDatabaseWrapper.d.ts +36 -0
- package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -0
- package/dist/SynchronousDatabaseWrapper.js +176 -0
- package/dist/SynchronousDatabaseWrapper.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +38 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +38 -0
- package/dist/effect/LiveStore.js.map +1 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +2 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/global-state.d.ts +14 -0
- package/dist/global-state.d.ts.map +1 -0
- package/dist/global-state.js +16 -0
- package/dist/global-state.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/reactive.d.ts +163 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +382 -0
- package/dist/reactive.js.map +1 -0
- package/dist/reactive.test.d.ts +2 -0
- package/dist/reactive.test.d.ts.map +1 -0
- package/dist/reactive.test.js +345 -0
- package/dist/reactive.test.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +59 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +29 -0
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +52 -0
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/reactiveQueries/graphql.js +136 -0
- package/dist/reactiveQueries/graphql.js.map +1 -0
- package/dist/reactiveQueries/js.d.ts +35 -0
- package/dist/reactiveQueries/js.d.ts.map +1 -0
- package/dist/reactiveQueries/js.js +57 -0
- package/dist/reactiveQueries/js.js.map +1 -0
- package/dist/reactiveQueries/sql.d.ts +49 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.js +130 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/reactiveQueries/sql.test.d.ts +2 -0
- package/dist/reactiveQueries/sql.test.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.test.js +284 -0
- package/dist/reactiveQueries/sql.test.js.map +1 -0
- package/dist/row-query.d.ts +33 -0
- package/dist/row-query.d.ts.map +1 -0
- package/dist/row-query.js +84 -0
- package/dist/row-query.js.map +1 -0
- package/dist/store-context.d.ts +26 -0
- package/dist/store-context.d.ts.map +1 -0
- package/dist/store-context.js +6 -0
- package/dist/store-context.js.map +1 -0
- package/dist/store-devtools.d.ts +19 -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 +175 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +507 -0
- package/dist/store.js.map +1 -0
- 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/dist/utils/dev.d.ts +3 -0
- package/dist/utils/dev.d.ts.map +1 -0
- package/dist/utils/dev.js +17 -0
- package/dist/utils/dev.js.map +1 -0
- package/dist/utils/otel.d.ts +4 -0
- package/dist/utils/otel.d.ts.map +1 -0
- package/dist/utils/otel.js +6 -0
- package/dist/utils/otel.js.map +1 -0
- package/dist/utils/stack-info.d.ts +10 -0
- package/dist/utils/stack-info.d.ts.map +1 -0
- package/dist/utils/stack-info.js +41 -0
- package/dist/utils/stack-info.js.map +1 -0
- package/dist/utils/stack-info.test.d.ts +2 -0
- package/dist/utils/stack-info.test.d.ts.map +1 -0
- package/dist/utils/stack-info.test.js +75 -0
- package/dist/utils/stack-info.test.js.map +1 -0
- package/dist/utils/tests/fixture.d.ts +259 -0
- package/dist/utils/tests/fixture.d.ts.map +1 -0
- package/dist/utils/tests/fixture.js +33 -0
- package/dist/utils/tests/fixture.js.map +1 -0
- package/dist/utils/tests/mod.d.ts +3 -0
- package/dist/utils/tests/mod.d.ts.map +1 -0
- package/dist/utils/tests/mod.js +3 -0
- package/dist/utils/tests/mod.js.map +1 -0
- package/dist/utils/tests/otel.d.ts +10 -0
- package/dist/utils/tests/otel.d.ts.map +1 -0
- package/dist/utils/tests/otel.js +42 -0
- package/dist/utils/tests/otel.js.map +1 -0
- package/package.json +60 -0
- package/src/QueryCache.ts +81 -0
- package/src/SynchronousDatabaseWrapper.ts +256 -0
- package/src/ambient.d.ts +10 -0
- package/src/effect/LiveStore.ts +112 -0
- package/src/effect/index.ts +8 -0
- package/src/global-state.ts +20 -0
- package/src/index.ts +64 -0
- package/src/reactive.test.ts +426 -0
- package/src/reactive.ts +661 -0
- package/src/reactiveQueries/base-class.ts +115 -0
- package/src/reactiveQueries/graphql.ts +233 -0
- package/src/reactiveQueries/js.ts +108 -0
- package/src/reactiveQueries/sql.test.ts +308 -0
- package/src/reactiveQueries/sql.ts +226 -0
- package/src/row-query.ts +200 -0
- package/src/store-context.ts +23 -0
- package/src/store-devtools.ts +217 -0
- package/src/store.ts +920 -0
- package/src/utils/data-structures.ts +36 -0
- package/src/utils/dev.ts +24 -0
- package/src/utils/otel.ts +9 -0
- package/src/utils/stack-info.test.ts +79 -0
- package/src/utils/stack-info.ts +54 -0
- package/src/utils/tests/fixture.ts +77 -0
- package/src/utils/tests/mod.ts +2 -0
- package/src/utils/tests/otel.ts +61 -0
- package/tsconfig.json +18 -0
- package/vitest.config.js +9 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Adapter,
|
|
3
|
+
BootDb,
|
|
4
|
+
BootStatus,
|
|
5
|
+
ClientSession,
|
|
6
|
+
EventId,
|
|
7
|
+
IntentionalShutdownCause,
|
|
8
|
+
ParamsObject,
|
|
9
|
+
PreparedBindValues,
|
|
10
|
+
StoreDevtoolsChannel,
|
|
11
|
+
} from '@livestore/common'
|
|
12
|
+
import { getExecArgsFromMutation, prepareBindValues, UnexpectedError } from '@livestore/common'
|
|
13
|
+
import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
|
|
14
|
+
import {
|
|
15
|
+
isPartialMutationEvent,
|
|
16
|
+
makeMutationEventSchemaMemo,
|
|
17
|
+
SCHEMA_META_TABLE,
|
|
18
|
+
SCHEMA_MUTATIONS_META_TABLE,
|
|
19
|
+
SESSION_CHANGESET_META_TABLE,
|
|
20
|
+
} from '@livestore/common/schema'
|
|
21
|
+
import { assertNever, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
|
|
22
|
+
import {
|
|
23
|
+
Cause,
|
|
24
|
+
Data,
|
|
25
|
+
Deferred,
|
|
26
|
+
Duration,
|
|
27
|
+
Effect,
|
|
28
|
+
Exit,
|
|
29
|
+
FiberSet,
|
|
30
|
+
Inspectable,
|
|
31
|
+
Layer,
|
|
32
|
+
Logger,
|
|
33
|
+
LogLevel,
|
|
34
|
+
MutableHashMap,
|
|
35
|
+
OtelTracer,
|
|
36
|
+
Queue,
|
|
37
|
+
Runtime,
|
|
38
|
+
Schema,
|
|
39
|
+
Scope,
|
|
40
|
+
Stream,
|
|
41
|
+
} from '@livestore/utils/effect'
|
|
42
|
+
import * as otel from '@opentelemetry/api'
|
|
43
|
+
import type { GraphQLSchema } from 'graphql'
|
|
44
|
+
|
|
45
|
+
import { globalReactivityGraph } from './global-state.js'
|
|
46
|
+
import type { DebugRefreshReasonBase, Ref } from './reactive.js'
|
|
47
|
+
import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
|
|
48
|
+
import { connectDevtoolsToStore } from './store-devtools.js'
|
|
49
|
+
import { SynchronousDatabaseWrapper } from './SynchronousDatabaseWrapper.js'
|
|
50
|
+
import { ReferenceCountedSet } from './utils/data-structures.js'
|
|
51
|
+
import { downloadBlob } from './utils/dev.js'
|
|
52
|
+
import { getDurationMsFromSpan } from './utils/otel.js'
|
|
53
|
+
import type { StackInfo } from './utils/stack-info.js'
|
|
54
|
+
|
|
55
|
+
export type BaseGraphQLContext = {
|
|
56
|
+
queriedTables: Set<string>
|
|
57
|
+
/** Needed by Pothos Otel plugin for resolver tracing to work */
|
|
58
|
+
otelContext?: otel.Context
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type GraphQLOptions<TContext> = {
|
|
62
|
+
schema: GraphQLSchema
|
|
63
|
+
makeContext: (db: SynchronousDatabaseWrapper, tracer: otel.Tracer, sessionId: string) => TContext
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type OtelOptions = {
|
|
67
|
+
tracer: otel.Tracer
|
|
68
|
+
rootSpanContext: otel.Context
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type StoreOptions<
|
|
72
|
+
TGraphQLContext extends BaseGraphQLContext,
|
|
73
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
74
|
+
> = {
|
|
75
|
+
clientSession: ClientSession
|
|
76
|
+
schema: TSchema
|
|
77
|
+
storeId: string
|
|
78
|
+
// TODO remove graphql-related stuff from store and move to GraphQL query directly
|
|
79
|
+
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
|
80
|
+
otelOptions: OtelOptions
|
|
81
|
+
reactivityGraph: ReactivityGraph
|
|
82
|
+
disableDevtools?: boolean
|
|
83
|
+
fiberSet: FiberSet.FiberSet
|
|
84
|
+
runtime: Runtime.Runtime<Scope.Scope>
|
|
85
|
+
batchUpdates: (runUpdates: () => void) => void
|
|
86
|
+
currentMutationEventIdRef: { current: EventId }
|
|
87
|
+
unsyncedMutationEvents: MutableHashMap.MutableHashMap<EventId, MutationEvent.ForSchema<TSchema>>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type RefreshReason =
|
|
91
|
+
| DebugRefreshReasonBase
|
|
92
|
+
| {
|
|
93
|
+
_tag: 'mutate'
|
|
94
|
+
/** The mutations that were applied */
|
|
95
|
+
mutations: ReadonlyArray<MutationEvent.Any>
|
|
96
|
+
|
|
97
|
+
/** The tables that were written to by the event */
|
|
98
|
+
writeTables: ReadonlyArray<string>
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
// TODO rename to a more appropriate name which is framework-agnostic
|
|
102
|
+
_tag: 'react'
|
|
103
|
+
api: string
|
|
104
|
+
label?: string
|
|
105
|
+
stackInfo?: StackInfo
|
|
106
|
+
}
|
|
107
|
+
| { _tag: 'manual'; label?: string }
|
|
108
|
+
|
|
109
|
+
export type QueryDebugInfo = {
|
|
110
|
+
_tag: 'graphql' | 'sql' | 'js' | 'unknown'
|
|
111
|
+
label: string
|
|
112
|
+
query: string
|
|
113
|
+
durationMs: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type StoreOtel = {
|
|
117
|
+
tracer: otel.Tracer
|
|
118
|
+
mutationsSpanContext: otel.Context
|
|
119
|
+
queriesSpanContext: otel.Context
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type StoreMutateOptions = {
|
|
123
|
+
label?: string
|
|
124
|
+
skipRefresh?: boolean
|
|
125
|
+
wasSyncMessage?: boolean
|
|
126
|
+
/**
|
|
127
|
+
* When set to `false` the mutation won't be persisted in the mutation log and sync server (but still synced).
|
|
128
|
+
* This can be useful e.g. for fine-granular update events (e.g. position updates during drag & drop)
|
|
129
|
+
*
|
|
130
|
+
* @default true
|
|
131
|
+
*/
|
|
132
|
+
persisted?: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
136
|
+
if (import.meta.env.DEV && typeof window !== 'undefined') {
|
|
137
|
+
// eslint-disable-next-line unicorn/prefer-global-this
|
|
138
|
+
window.__debugDownloadBlob = downloadBlob
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class Store<
|
|
142
|
+
TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
|
|
143
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
144
|
+
> extends Inspectable.Class {
|
|
145
|
+
readonly storeId: string
|
|
146
|
+
reactivityGraph: ReactivityGraph
|
|
147
|
+
syncDbWrapper: SynchronousDatabaseWrapper
|
|
148
|
+
clientSession: ClientSession
|
|
149
|
+
schema: LiveStoreSchema
|
|
150
|
+
graphQLSchema?: GraphQLSchema
|
|
151
|
+
graphQLContext?: TGraphQLContext
|
|
152
|
+
otel: StoreOtel
|
|
153
|
+
/**
|
|
154
|
+
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
|
155
|
+
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
|
156
|
+
*/
|
|
157
|
+
tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
|
|
158
|
+
|
|
159
|
+
private fiberSet: FiberSet.FiberSet
|
|
160
|
+
private runtime: Runtime.Runtime<Scope.Scope>
|
|
161
|
+
|
|
162
|
+
/** RC-based set to see which queries are currently subscribed to */
|
|
163
|
+
activeQueries: ReferenceCountedSet<LiveQuery<any>>
|
|
164
|
+
|
|
165
|
+
// NOTE this is currently exposed for the Devtools databrowser to emit mutation events
|
|
166
|
+
readonly __mutationEventSchema
|
|
167
|
+
|
|
168
|
+
private currentMutationEventIdRef
|
|
169
|
+
private unsyncedMutationEvents
|
|
170
|
+
|
|
171
|
+
// #region constructor
|
|
172
|
+
private constructor({
|
|
173
|
+
clientSession,
|
|
174
|
+
schema,
|
|
175
|
+
graphQLOptions,
|
|
176
|
+
reactivityGraph,
|
|
177
|
+
otelOptions,
|
|
178
|
+
disableDevtools,
|
|
179
|
+
batchUpdates,
|
|
180
|
+
currentMutationEventIdRef,
|
|
181
|
+
unsyncedMutationEvents,
|
|
182
|
+
storeId,
|
|
183
|
+
fiberSet,
|
|
184
|
+
runtime,
|
|
185
|
+
}: StoreOptions<TGraphQLContext, TSchema>) {
|
|
186
|
+
super()
|
|
187
|
+
|
|
188
|
+
this.storeId = storeId
|
|
189
|
+
|
|
190
|
+
this.currentMutationEventIdRef = currentMutationEventIdRef
|
|
191
|
+
this.unsyncedMutationEvents = unsyncedMutationEvents
|
|
192
|
+
|
|
193
|
+
this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb })
|
|
194
|
+
this.clientSession = clientSession
|
|
195
|
+
this.schema = schema
|
|
196
|
+
|
|
197
|
+
this.fiberSet = fiberSet
|
|
198
|
+
this.runtime = runtime
|
|
199
|
+
|
|
200
|
+
// TODO refactor
|
|
201
|
+
this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
|
|
202
|
+
|
|
203
|
+
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
|
204
|
+
this.tableRefs = {}
|
|
205
|
+
this.activeQueries = new ReferenceCountedSet()
|
|
206
|
+
|
|
207
|
+
const mutationsSpan = otelOptions.tracer.startSpan('LiveStore:mutations', {}, otelOptions.rootSpanContext)
|
|
208
|
+
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan)
|
|
209
|
+
|
|
210
|
+
const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext)
|
|
211
|
+
const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
|
|
212
|
+
|
|
213
|
+
this.reactivityGraph = reactivityGraph
|
|
214
|
+
this.reactivityGraph.context = {
|
|
215
|
+
store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
|
|
216
|
+
otelTracer: otelOptions.tracer,
|
|
217
|
+
rootOtelContext: otelQueriesSpanContext,
|
|
218
|
+
effectsWrapper: batchUpdates,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.otel = {
|
|
222
|
+
tracer: otelOptions.tracer,
|
|
223
|
+
mutationsSpanContext: otelMuationsSpanContext,
|
|
224
|
+
queriesSpanContext: otelQueriesSpanContext,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
|
|
228
|
+
// But for now this is a good enough approximation with little downsides
|
|
229
|
+
const isRunningInDevtools = disableDevtools === true
|
|
230
|
+
|
|
231
|
+
// Need a set here since `schema.tables` might contain duplicates and some componentStateTables
|
|
232
|
+
const allTableNames = new Set(
|
|
233
|
+
// NOTE we're excluding the LiveStore schema and mutations tables as they are not user-facing
|
|
234
|
+
// unless LiveStore is running in the devtools
|
|
235
|
+
isRunningInDevtools
|
|
236
|
+
? this.schema.tables.keys()
|
|
237
|
+
: Array.from(this.schema.tables.keys()).filter(
|
|
238
|
+
(_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE && _ !== SESSION_CHANGESET_META_TABLE,
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
const existingTableRefs = new Map(
|
|
242
|
+
Array.from(this.reactivityGraph.atoms.values())
|
|
243
|
+
.filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
|
|
244
|
+
.map((_) => [_.label!.slice('tableRef:'.length), _] as const),
|
|
245
|
+
)
|
|
246
|
+
for (const tableName of allTableNames) {
|
|
247
|
+
this.tableRefs[tableName] = existingTableRefs.get(tableName) ?? this.makeTableRef(tableName)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (graphQLOptions) {
|
|
251
|
+
this.graphQLSchema = graphQLOptions.schema
|
|
252
|
+
this.graphQLContext = graphQLOptions.makeContext(
|
|
253
|
+
this.syncDbWrapper,
|
|
254
|
+
this.otel.tracer,
|
|
255
|
+
clientSession.coordinator.sessionId,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
Effect.gen(this, function* () {
|
|
260
|
+
yield* this.clientSession.coordinator.syncMutations.pipe(
|
|
261
|
+
Stream.tapChunk((mutationsEventsDecodedChunk) =>
|
|
262
|
+
Effect.sync(() => {
|
|
263
|
+
this.mutate({ wasSyncMessage: true }, ...mutationsEventsDecodedChunk)
|
|
264
|
+
}),
|
|
265
|
+
),
|
|
266
|
+
Stream.runDrain,
|
|
267
|
+
Effect.interruptible,
|
|
268
|
+
Effect.withSpan('LiveStore:syncMutations'),
|
|
269
|
+
Effect.forkScoped,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
yield* Effect.addFinalizer(() =>
|
|
273
|
+
Effect.sync(() => {
|
|
274
|
+
for (const tableRef of Object.values(this.tableRefs)) {
|
|
275
|
+
for (const superComp of tableRef.super) {
|
|
276
|
+
this.reactivityGraph.removeEdge(superComp, tableRef)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
|
|
281
|
+
otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
|
|
282
|
+
}),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
yield* Effect.never
|
|
286
|
+
}).pipe(Effect.scoped, Effect.withSpan('LiveStore:constructor'), this.runEffectFork)
|
|
287
|
+
}
|
|
288
|
+
// #endregion constructor
|
|
289
|
+
|
|
290
|
+
static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
|
|
291
|
+
storeOptions: StoreOptions<TGraphQLContext, TSchema>,
|
|
292
|
+
parentSpan: otel.Span,
|
|
293
|
+
): Store<TGraphQLContext, TSchema> => {
|
|
294
|
+
const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
|
|
295
|
+
return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
|
|
296
|
+
try {
|
|
297
|
+
return new Store(storeOptions)
|
|
298
|
+
} finally {
|
|
299
|
+
span.end()
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
get sessionId(): string {
|
|
305
|
+
return this.clientSession.coordinator.sessionId
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Subscribe to the results of a query
|
|
310
|
+
* Returns a function to cancel the subscription.
|
|
311
|
+
*/
|
|
312
|
+
subscribe = <TResult>(
|
|
313
|
+
query$: LiveQuery<TResult, any>,
|
|
314
|
+
onNewValue: (value: TResult) => void,
|
|
315
|
+
onUnsubsubscribe?: () => void,
|
|
316
|
+
options?: { label?: string; otelContext?: otel.Context; skipInitialRun?: boolean } | undefined,
|
|
317
|
+
): (() => void) =>
|
|
318
|
+
this.otel.tracer.startActiveSpan(
|
|
319
|
+
`LiveStore.subscribe`,
|
|
320
|
+
{ attributes: { label: options?.label, queryLabel: query$.label } },
|
|
321
|
+
options?.otelContext ?? this.otel.queriesSpanContext,
|
|
322
|
+
(span) => {
|
|
323
|
+
// console.debug('store sub', query$.id, query$.label)
|
|
324
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
325
|
+
|
|
326
|
+
const label = `subscribe:${options?.label}`
|
|
327
|
+
const effect = this.reactivityGraph.makeEffect((get) => onNewValue(get(query$.results$)), { label })
|
|
328
|
+
|
|
329
|
+
this.activeQueries.add(query$ as LiveQuery<TResult>)
|
|
330
|
+
|
|
331
|
+
// Running effect right away to get initial value (unless `skipInitialRun` is set)
|
|
332
|
+
if (options?.skipInitialRun !== true) {
|
|
333
|
+
effect.doEffect(otelContext)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const unsubscribe = () => {
|
|
337
|
+
// console.debug('store unsub', query$.id, query$.label)
|
|
338
|
+
try {
|
|
339
|
+
this.reactivityGraph.destroyNode(effect)
|
|
340
|
+
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
341
|
+
onUnsubsubscribe?.()
|
|
342
|
+
} finally {
|
|
343
|
+
span.end()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return unsubscribe
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
// #region mutate
|
|
352
|
+
mutate: {
|
|
353
|
+
<const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
|
|
354
|
+
(
|
|
355
|
+
txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
|
|
356
|
+
...list: TMutationArg
|
|
357
|
+
) => void,
|
|
358
|
+
): void
|
|
359
|
+
<const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
|
|
360
|
+
options: StoreMutateOptions,
|
|
361
|
+
...list: TMutationArg
|
|
362
|
+
): void
|
|
363
|
+
(
|
|
364
|
+
options: StoreMutateOptions,
|
|
365
|
+
txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
|
|
366
|
+
...list: TMutationArg
|
|
367
|
+
) => void,
|
|
368
|
+
): void
|
|
369
|
+
} = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
|
|
370
|
+
let mutationsEvents: MutationEvent.ForSchema<TSchema>[]
|
|
371
|
+
let options: StoreMutateOptions | undefined
|
|
372
|
+
|
|
373
|
+
if (typeof firstMutationOrTxnFnOrOptions === 'function') {
|
|
374
|
+
// TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
|
|
375
|
+
mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
|
|
376
|
+
} else if (
|
|
377
|
+
firstMutationOrTxnFnOrOptions?.label !== undefined ||
|
|
378
|
+
firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
|
|
379
|
+
firstMutationOrTxnFnOrOptions?.wasSyncMessage !== undefined ||
|
|
380
|
+
firstMutationOrTxnFnOrOptions?.persisted !== undefined
|
|
381
|
+
) {
|
|
382
|
+
options = firstMutationOrTxnFnOrOptions
|
|
383
|
+
mutationsEvents = restMutations
|
|
384
|
+
} else if (firstMutationOrTxnFnOrOptions === undefined) {
|
|
385
|
+
// When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
|
|
386
|
+
mutationsEvents = []
|
|
387
|
+
} else {
|
|
388
|
+
mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
mutationsEvents = mutationsEvents.filter(
|
|
392
|
+
(_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if (mutationsEvents.length === 0) {
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const label = options?.label ?? 'mutate'
|
|
400
|
+
const skipRefresh = options?.skipRefresh ?? false
|
|
401
|
+
const wasSyncMessage = options?.wasSyncMessage ?? false
|
|
402
|
+
const persisted = options?.persisted ?? true
|
|
403
|
+
|
|
404
|
+
const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
|
|
405
|
+
mutationsSpan.addEvent('mutate')
|
|
406
|
+
|
|
407
|
+
// console.group('LiveStore.mutate', { skipRefresh, wasSyncMessage, label })
|
|
408
|
+
// mutationsEvents.forEach((_) => console.debug(_.mutation, _.id, _.args))
|
|
409
|
+
// console.groupEnd()
|
|
410
|
+
|
|
411
|
+
let durationMs: number
|
|
412
|
+
|
|
413
|
+
const res = this.otel.tracer.startActiveSpan(
|
|
414
|
+
'LiveStore:mutate',
|
|
415
|
+
{ attributes: { 'livestore.mutateLabel': label } },
|
|
416
|
+
this.otel.mutationsSpanContext,
|
|
417
|
+
(span) => {
|
|
418
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const writeTables: Set<string> = new Set()
|
|
422
|
+
|
|
423
|
+
this.otel.tracer.startActiveSpan(
|
|
424
|
+
'LiveStore:processWrites',
|
|
425
|
+
{ attributes: { 'livestore.mutateLabel': label } },
|
|
426
|
+
otel.trace.setSpan(otel.context.active(), span),
|
|
427
|
+
(span) => {
|
|
428
|
+
try {
|
|
429
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
430
|
+
|
|
431
|
+
const applyMutations = () => {
|
|
432
|
+
for (const mutationEvent of mutationsEvents) {
|
|
433
|
+
try {
|
|
434
|
+
const { writeTables: writeTablesForEvent } = this.mutateWithoutRefresh(mutationEvent, {
|
|
435
|
+
otelContext,
|
|
436
|
+
// NOTE if it was a sync message, it's already coming from the coordinator, so we can skip the coordinator
|
|
437
|
+
coordinatorMode: wasSyncMessage ? 'skip-coordinator' : persisted ? 'default' : 'skip-persist',
|
|
438
|
+
})
|
|
439
|
+
for (const tableName of writeTablesForEvent) {
|
|
440
|
+
writeTables.add(tableName)
|
|
441
|
+
}
|
|
442
|
+
} catch (e: any) {
|
|
443
|
+
console.error(e, mutationEvent)
|
|
444
|
+
throw e
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (mutationsEvents.length > 1) {
|
|
450
|
+
// TODO: what to do about coordinator transaction here?
|
|
451
|
+
this.syncDbWrapper.txn(applyMutations)
|
|
452
|
+
} else {
|
|
453
|
+
applyMutations()
|
|
454
|
+
}
|
|
455
|
+
} catch (e: any) {
|
|
456
|
+
console.error(e)
|
|
457
|
+
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
458
|
+
throw e
|
|
459
|
+
} finally {
|
|
460
|
+
span.end()
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
|
|
466
|
+
for (const tableName of writeTables) {
|
|
467
|
+
const tableRef = this.tableRefs[tableName]
|
|
468
|
+
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
469
|
+
tablesToUpdate.push([tableRef!, null])
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const debugRefreshReason = {
|
|
473
|
+
_tag: 'mutate' as const,
|
|
474
|
+
mutations: mutationsEvents,
|
|
475
|
+
writeTables: Array.from(writeTables),
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Update all table refs together in a batch, to only trigger one reactive update
|
|
479
|
+
this.reactivityGraph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh })
|
|
480
|
+
} catch (e: any) {
|
|
481
|
+
console.error(e)
|
|
482
|
+
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
|
483
|
+
throw e
|
|
484
|
+
} finally {
|
|
485
|
+
span.end()
|
|
486
|
+
|
|
487
|
+
durationMs = getDurationMsFromSpan(span)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return { durationMs }
|
|
491
|
+
},
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
// NOTE we need to add the mutation events to the unsynced mutation events map only after running the code above
|
|
495
|
+
// so the short-circuiting in `mutateWithoutRefresh` doesn't kick in for those events
|
|
496
|
+
for (const mutationEvent of mutationsEvents) {
|
|
497
|
+
if (mutationEvent.id !== undefined) {
|
|
498
|
+
MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEvent.id), mutationEvent)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return res
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* This can be used in combination with `skipRefresh` when applying mutations.
|
|
507
|
+
* We might need a better solution for this. Let's see.
|
|
508
|
+
*/
|
|
509
|
+
manualRefresh = (options?: { label?: string }) => {
|
|
510
|
+
const { label } = options ?? {}
|
|
511
|
+
this.otel.tracer.startActiveSpan(
|
|
512
|
+
'LiveStore:manualRefresh',
|
|
513
|
+
{ attributes: { 'livestore.manualRefreshLabel': label } },
|
|
514
|
+
this.otel.mutationsSpanContext,
|
|
515
|
+
(span) => {
|
|
516
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
517
|
+
this.reactivityGraph.runDeferredEffects({ otelContext })
|
|
518
|
+
span.end()
|
|
519
|
+
},
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Apply a mutation to the store.
|
|
525
|
+
* Returns the tables that were affected by the event.
|
|
526
|
+
* This is an internal method that doesn't trigger a refresh;
|
|
527
|
+
* the caller must refresh queries after calling this method.
|
|
528
|
+
*/
|
|
529
|
+
mutateWithoutRefresh = (
|
|
530
|
+
mutationEventDecoded_: MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>,
|
|
531
|
+
options: {
|
|
532
|
+
otelContext: otel.Context
|
|
533
|
+
// TODO adjust `skip-persist` with new rebase sync strategy
|
|
534
|
+
coordinatorMode: 'default' | 'skip-coordinator' | 'skip-persist'
|
|
535
|
+
},
|
|
536
|
+
): { writeTables: ReadonlySet<string>; durationMs: number } => {
|
|
537
|
+
const mutationDef =
|
|
538
|
+
this.schema.mutations.get(mutationEventDecoded_.mutation) ??
|
|
539
|
+
shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
|
|
540
|
+
|
|
541
|
+
// Needs to happen only for partial mutation events (thus a function)
|
|
542
|
+
const nextMutationEventId = () => {
|
|
543
|
+
const { id, parentId } = this.clientSession.coordinator
|
|
544
|
+
.nextMutationEventIdPair({ localOnly: mutationDef.options.localOnly })
|
|
545
|
+
.pipe(Effect.runSync)
|
|
546
|
+
|
|
547
|
+
this.currentMutationEventIdRef.current = id
|
|
548
|
+
|
|
549
|
+
return { id, parentId }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const mutationEventDecoded: MutationEvent.ForSchema<TSchema> = isPartialMutationEvent(mutationEventDecoded_)
|
|
553
|
+
? { ...mutationEventDecoded_, ...nextMutationEventId() }
|
|
554
|
+
: mutationEventDecoded_
|
|
555
|
+
|
|
556
|
+
// NOTE we also need this temporary workaround here since some code-paths use `mutateWithoutRefresh` directly
|
|
557
|
+
// e.g. the row-query functionality
|
|
558
|
+
if (MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id))) {
|
|
559
|
+
// NOTE this data should never be used
|
|
560
|
+
return { writeTables: new Set(), durationMs: 0 }
|
|
561
|
+
} else {
|
|
562
|
+
MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const { otelContext, coordinatorMode = 'default' } = options
|
|
566
|
+
|
|
567
|
+
return this.otel.tracer.startActiveSpan(
|
|
568
|
+
'LiveStore:mutateWithoutRefresh',
|
|
569
|
+
{
|
|
570
|
+
attributes: {
|
|
571
|
+
'livestore.mutation': mutationEventDecoded.mutation,
|
|
572
|
+
'livestore.args': JSON.stringify(mutationEventDecoded.args, null, 2),
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
otelContext,
|
|
576
|
+
(span) => {
|
|
577
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
578
|
+
|
|
579
|
+
const allWriteTables = new Set<string>()
|
|
580
|
+
let durationMsTotal = 0
|
|
581
|
+
|
|
582
|
+
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
583
|
+
|
|
584
|
+
for (const {
|
|
585
|
+
statementSql,
|
|
586
|
+
bindValues,
|
|
587
|
+
writeTables = this.syncDbWrapper.getTablesUsed(statementSql),
|
|
588
|
+
} of execArgsArr) {
|
|
589
|
+
// TODO when the store doesn't have the lock, we need wait for the coordinator to confirm the mutation
|
|
590
|
+
// before executing the mutation on the main db
|
|
591
|
+
const { durationMs } = this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext })
|
|
592
|
+
|
|
593
|
+
durationMsTotal += durationMs
|
|
594
|
+
writeTables.forEach((table) => allWriteTables.add(table))
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const mutationEventEncoded = Schema.encodeUnknownSync(this.__mutationEventSchema)(mutationEventDecoded)
|
|
598
|
+
|
|
599
|
+
if (coordinatorMode !== 'skip-coordinator') {
|
|
600
|
+
// Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
|
|
601
|
+
this.clientSession.coordinator
|
|
602
|
+
.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
|
|
603
|
+
.pipe(this.runEffectFork)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Uncomment to print a list of queries currently registered on the store
|
|
607
|
+
// console.debug(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
|
|
608
|
+
|
|
609
|
+
span.end()
|
|
610
|
+
|
|
611
|
+
return { writeTables: allWriteTables, durationMs: durationMsTotal }
|
|
612
|
+
},
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
// #endregion mutate
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Directly execute a SQL query on the Store.
|
|
619
|
+
* This should only be used for framework-internal purposes;
|
|
620
|
+
* all app writes should go through mutate.
|
|
621
|
+
*/
|
|
622
|
+
__execute = (
|
|
623
|
+
query: string,
|
|
624
|
+
params: ParamsObject = {},
|
|
625
|
+
writeTables?: ReadonlySet<string>,
|
|
626
|
+
otelContext?: otel.Context,
|
|
627
|
+
) => {
|
|
628
|
+
this.syncDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
|
|
629
|
+
|
|
630
|
+
this.clientSession.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
__select = (query: string, params: ParamsObject = {}) => {
|
|
634
|
+
return this.syncDbWrapper.select(query, { bindValues: prepareBindValues(params, query) })
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private makeTableRef = (tableName: string) =>
|
|
638
|
+
this.reactivityGraph.makeRef(null, {
|
|
639
|
+
equal: () => false,
|
|
640
|
+
label: `tableRef:${tableName}`,
|
|
641
|
+
meta: { liveStoreRefType: 'table' },
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
__devDownloadDb = () => {
|
|
645
|
+
const data = this.syncDbWrapper.export()
|
|
646
|
+
downloadBlob(data, `livestore-${Date.now()}.db`)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
__devDownloadMutationLogDb = () =>
|
|
650
|
+
Effect.gen(this, function* () {
|
|
651
|
+
const data = yield* this.clientSession.coordinator.getMutationLogData
|
|
652
|
+
downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
|
|
653
|
+
}).pipe(this.runEffectFork)
|
|
654
|
+
|
|
655
|
+
// NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
|
|
656
|
+
toJSON = () => {
|
|
657
|
+
return {
|
|
658
|
+
_tag: 'Store',
|
|
659
|
+
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
|
|
664
|
+
effect.pipe(Effect.tapCauseLogPretty, FiberSet.run(this.fiberSet), Runtime.runFork(this.runtime))
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
|
|
668
|
+
schema: TSchema
|
|
669
|
+
adapter: Adapter
|
|
670
|
+
storeId: string
|
|
671
|
+
reactivityGraph?: ReactivityGraph
|
|
672
|
+
graphQLOptions?: GraphQLOptions<TGraphQLContext>
|
|
673
|
+
otelOptions?: Partial<OtelOptions>
|
|
674
|
+
boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
|
|
675
|
+
batchUpdates?: (run: () => void) => void
|
|
676
|
+
disableDevtools?: boolean
|
|
677
|
+
onBootStatus?: (status: BootStatus) => void
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Create a new LiveStore Store */
|
|
681
|
+
export const createStorePromise = async <
|
|
682
|
+
TGraphQLContext extends BaseGraphQLContext,
|
|
683
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
684
|
+
>({
|
|
685
|
+
signal,
|
|
686
|
+
...options
|
|
687
|
+
}: CreateStoreOptions<TGraphQLContext, TSchema> & { signal?: AbortSignal }): Promise<Store<TGraphQLContext, TSchema>> =>
|
|
688
|
+
Effect.gen(function* () {
|
|
689
|
+
const scope = yield* Scope.make()
|
|
690
|
+
const runtime = yield* Effect.runtime()
|
|
691
|
+
|
|
692
|
+
if (signal !== undefined) {
|
|
693
|
+
signal.addEventListener('abort', () => {
|
|
694
|
+
Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty, Runtime.runFork(runtime))
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return yield* FiberSet.make().pipe(
|
|
699
|
+
Effect.andThen((fiberSet) => createStore({ ...options, fiberSet })),
|
|
700
|
+
Scope.extend(scope),
|
|
701
|
+
)
|
|
702
|
+
}).pipe(
|
|
703
|
+
Effect.withSpan('createStore'),
|
|
704
|
+
Effect.tapCauseLogPretty,
|
|
705
|
+
Effect.annotateLogs({ thread: 'window' }),
|
|
706
|
+
Effect.provide(Logger.pretty),
|
|
707
|
+
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
708
|
+
Effect.runPromise,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
// #region createStore
|
|
712
|
+
export const createStore = <
|
|
713
|
+
TGraphQLContext extends BaseGraphQLContext,
|
|
714
|
+
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
|
715
|
+
>({
|
|
716
|
+
schema,
|
|
717
|
+
adapter,
|
|
718
|
+
storeId,
|
|
719
|
+
graphQLOptions,
|
|
720
|
+
otelOptions,
|
|
721
|
+
boot,
|
|
722
|
+
reactivityGraph = globalReactivityGraph,
|
|
723
|
+
batchUpdates,
|
|
724
|
+
disableDevtools,
|
|
725
|
+
onBootStatus,
|
|
726
|
+
fiberSet,
|
|
727
|
+
}: CreateStoreOptions<TGraphQLContext, TSchema> & { fiberSet: FiberSet.FiberSet }): Effect.Effect<
|
|
728
|
+
Store<TGraphQLContext, TSchema>,
|
|
729
|
+
UnexpectedError,
|
|
730
|
+
Scope.Scope
|
|
731
|
+
> => {
|
|
732
|
+
const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
|
|
733
|
+
const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active()
|
|
734
|
+
|
|
735
|
+
const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
|
|
736
|
+
Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)),
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
return Effect.gen(function* () {
|
|
740
|
+
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
741
|
+
|
|
742
|
+
const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
743
|
+
|
|
744
|
+
yield* Queue.take(bootStatusQueue).pipe(
|
|
745
|
+
Effect.tapSync((status) => onBootStatus?.(status)),
|
|
746
|
+
Effect.tap((status) => (status.stage === 'done' ? Queue.shutdown(bootStatusQueue) : Effect.void)),
|
|
747
|
+
Effect.forever,
|
|
748
|
+
Effect.tapCauseLogPretty,
|
|
749
|
+
Effect.forkScoped,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
const storeDeferred = yield* Deferred.make<Store>()
|
|
753
|
+
|
|
754
|
+
const connectDevtoolsToStore_ = (storeDevtoolsChannel: StoreDevtoolsChannel) =>
|
|
755
|
+
Effect.gen(function* () {
|
|
756
|
+
const store = yield* Deferred.await(storeDeferred)
|
|
757
|
+
yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
const runtime = yield* Effect.runtime<Scope.Scope>()
|
|
761
|
+
|
|
762
|
+
const runEffectFork = (effect: Effect.Effect<any, any, never>) =>
|
|
763
|
+
effect.pipe(Effect.tapCauseLogPretty, FiberSet.run(fiberSet), Runtime.runFork(runtime))
|
|
764
|
+
|
|
765
|
+
// TODO close parent scope? (Needs refactor with Mike A)
|
|
766
|
+
const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
|
|
767
|
+
Effect.gen(function* () {
|
|
768
|
+
// NOTE we're calling `cause.toString()` here to avoid triggering a `console.error` in the grouped log
|
|
769
|
+
const logCause =
|
|
770
|
+
Cause.isFailType(cause) && cause.error._tag === 'LiveStore.IntentionalShutdownCause'
|
|
771
|
+
? cause.toString()
|
|
772
|
+
: cause
|
|
773
|
+
yield* Effect.logDebug(`Shutting down LiveStore`, logCause)
|
|
774
|
+
|
|
775
|
+
FiberSet.clear(fiberSet).pipe(
|
|
776
|
+
Effect.andThen(() => FiberSet.run(fiberSet, Effect.failCause(cause))),
|
|
777
|
+
Effect.timeout(Duration.seconds(1)),
|
|
778
|
+
Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown:clear-fiber-set', duration: 500 }),
|
|
779
|
+
Effect.catchTag('TimeoutException', (err) =>
|
|
780
|
+
Effect.logError('Store shutdown timed out. Forcing shutdown.', err).pipe(
|
|
781
|
+
Effect.andThen(FiberSet.run(fiberSet, Effect.failCause(cause))),
|
|
782
|
+
),
|
|
783
|
+
),
|
|
784
|
+
Runtime.runFork(runtime), // NOTE we need to fork this separately otherwise it will also be interrupted
|
|
785
|
+
)
|
|
786
|
+
}).pipe(Effect.withSpan('livestore:shutdown'))
|
|
787
|
+
|
|
788
|
+
const clientSession: ClientSession = yield* adapter({
|
|
789
|
+
schema,
|
|
790
|
+
storeId,
|
|
791
|
+
devtoolsEnabled: disableDevtools !== true,
|
|
792
|
+
bootStatusQueue,
|
|
793
|
+
shutdown,
|
|
794
|
+
connectDevtoolsToStore: connectDevtoolsToStore_,
|
|
795
|
+
}).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
|
|
796
|
+
|
|
797
|
+
const mutationEventSchema = makeMutationEventSchemaMemo(schema)
|
|
798
|
+
|
|
799
|
+
// TODO get rid of this
|
|
800
|
+
// const __processedMutationIds = new Set<number>()
|
|
801
|
+
|
|
802
|
+
const currentMutationEventIdRef = { current: yield* clientSession.coordinator.getCurrentMutationEventId }
|
|
803
|
+
|
|
804
|
+
// TODO fill up with unsynced mutation events from the coordinator
|
|
805
|
+
const unsyncedMutationEvents = MutableHashMap.empty<EventId, MutationEvent.ForSchema<TSchema>>()
|
|
806
|
+
|
|
807
|
+
// TODO consider moving booting into the storage backend
|
|
808
|
+
if (boot !== undefined) {
|
|
809
|
+
let isInTxn = false
|
|
810
|
+
let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
|
|
811
|
+
|
|
812
|
+
const bootDbImpl: BootDb = {
|
|
813
|
+
_tag: 'BootDb',
|
|
814
|
+
execute: (queryStr, bindValues) => {
|
|
815
|
+
const stmt = clientSession.syncDb.prepare(queryStr)
|
|
816
|
+
stmt.execute(bindValues)
|
|
817
|
+
|
|
818
|
+
if (isInTxn === true) {
|
|
819
|
+
txnExecuteStmnts.push([queryStr, bindValues])
|
|
820
|
+
} else {
|
|
821
|
+
clientSession.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
mutate: (...list) => {
|
|
825
|
+
for (const mutationEventDecoded_ of list) {
|
|
826
|
+
const mutationDef =
|
|
827
|
+
schema.mutations.get(mutationEventDecoded_.mutation) ??
|
|
828
|
+
shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
|
|
829
|
+
|
|
830
|
+
const { id, parentId } = clientSession.coordinator
|
|
831
|
+
.nextMutationEventIdPair({ localOnly: mutationDef.options.localOnly })
|
|
832
|
+
.pipe(Effect.runSync)
|
|
833
|
+
|
|
834
|
+
currentMutationEventIdRef.current = id
|
|
835
|
+
|
|
836
|
+
const mutationEventDecoded = { ...mutationEventDecoded_, id, parentId }
|
|
837
|
+
|
|
838
|
+
MutableHashMap.set(unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
|
|
839
|
+
|
|
840
|
+
// __processedMutationIds.add(mutationEventDecoded.id.global)
|
|
841
|
+
|
|
842
|
+
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
843
|
+
// const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
844
|
+
|
|
845
|
+
for (const { statementSql, bindValues } of execArgsArr) {
|
|
846
|
+
clientSession.syncDb.execute(statementSql, bindValues)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
|
|
850
|
+
|
|
851
|
+
clientSession.coordinator
|
|
852
|
+
.mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
|
|
853
|
+
.pipe(runEffectFork)
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
select: (queryStr, bindValues) => {
|
|
857
|
+
const stmt = clientSession.syncDb.prepare(queryStr)
|
|
858
|
+
return stmt.select(bindValues)
|
|
859
|
+
},
|
|
860
|
+
txn: (callback) => {
|
|
861
|
+
try {
|
|
862
|
+
isInTxn = true
|
|
863
|
+
// clientSession.syncDb.execute('BEGIN TRANSACTION', undefined)
|
|
864
|
+
|
|
865
|
+
callback()
|
|
866
|
+
|
|
867
|
+
// clientSession.syncDb.execute('COMMIT', undefined)
|
|
868
|
+
|
|
869
|
+
// clientSession.coordinator.execute('BEGIN', undefined, undefined)
|
|
870
|
+
for (const [queryStr, bindValues] of txnExecuteStmnts) {
|
|
871
|
+
clientSession.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
|
|
872
|
+
}
|
|
873
|
+
// clientSession.coordinator.execute('COMMIT', undefined, undefined)
|
|
874
|
+
} catch (e: any) {
|
|
875
|
+
// clientSession.syncDb.execute('ROLLBACK', undefined)
|
|
876
|
+
throw e
|
|
877
|
+
} finally {
|
|
878
|
+
isInTxn = false
|
|
879
|
+
txnExecuteStmnts = []
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
yield* Effect.tryAll(() => boot(bootDbImpl, span)).pipe(
|
|
885
|
+
UnexpectedError.mapToUnexpectedError,
|
|
886
|
+
Effect.withSpan('createStore:boot'),
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const store = Store.createStore<TGraphQLContext, TSchema>(
|
|
891
|
+
{
|
|
892
|
+
clientSession,
|
|
893
|
+
schema,
|
|
894
|
+
graphQLOptions,
|
|
895
|
+
otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
|
|
896
|
+
reactivityGraph,
|
|
897
|
+
disableDevtools,
|
|
898
|
+
currentMutationEventIdRef,
|
|
899
|
+
unsyncedMutationEvents,
|
|
900
|
+
fiberSet,
|
|
901
|
+
runtime,
|
|
902
|
+
batchUpdates: batchUpdates ?? ((run) => run()),
|
|
903
|
+
storeId,
|
|
904
|
+
},
|
|
905
|
+
span,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
yield* Deferred.succeed(storeDeferred, store as any as Store)
|
|
909
|
+
|
|
910
|
+
return store
|
|
911
|
+
}).pipe(
|
|
912
|
+
Effect.withSpan('createStore', {
|
|
913
|
+
parent: otelOptions?.rootSpanContext
|
|
914
|
+
? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
|
|
915
|
+
: undefined,
|
|
916
|
+
}),
|
|
917
|
+
Effect.provide(TracingLive),
|
|
918
|
+
)
|
|
919
|
+
}
|
|
920
|
+
// #endregion createStore
|