@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.
Files changed (118) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/MainDatabaseWrapper.d.ts +6 -5
  3. package/dist/MainDatabaseWrapper.d.ts.map +1 -1
  4. package/dist/MainDatabaseWrapper.js +3 -3
  5. package/dist/MainDatabaseWrapper.js.map +1 -1
  6. package/dist/QueryCache.d.ts +1 -1
  7. package/dist/QueryCache.d.ts.map +1 -1
  8. package/dist/QueryCache.js.map +1 -1
  9. package/dist/__tests__/react/fixture.d.ts +9 -27
  10. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  11. package/dist/__tests__/react/fixture.js +12 -10
  12. package/dist/__tests__/react/fixture.js.map +1 -1
  13. package/dist/effect/LiveStore.d.ts +20 -12
  14. package/dist/effect/LiveStore.d.ts.map +1 -1
  15. package/dist/effect/LiveStore.js +23 -22
  16. package/dist/effect/LiveStore.js.map +1 -1
  17. package/dist/effect/index.d.ts +1 -1
  18. package/dist/effect/index.d.ts.map +1 -1
  19. package/dist/effect/index.js +1 -1
  20. package/dist/effect/index.js.map +1 -1
  21. package/dist/global-state.d.ts +1 -3
  22. package/dist/global-state.d.ts.map +1 -1
  23. package/dist/global-state.js +2 -3
  24. package/dist/global-state.js.map +1 -1
  25. package/dist/index.d.ts +6 -7
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -6
  28. package/dist/index.js.map +1 -1
  29. package/dist/react/LiveStoreContext.d.ts +5 -2
  30. package/dist/react/LiveStoreContext.d.ts.map +1 -1
  31. package/dist/react/LiveStoreContext.js +3 -0
  32. package/dist/react/LiveStoreContext.js.map +1 -1
  33. package/dist/react/LiveStoreProvider.d.ts +8 -7
  34. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  35. package/dist/react/LiveStoreProvider.js +70 -43
  36. package/dist/react/LiveStoreProvider.js.map +1 -1
  37. package/dist/react/LiveStoreProvider.test.js +33 -12
  38. package/dist/react/LiveStoreProvider.test.js.map +1 -1
  39. package/dist/react/components/LiveList.d.ts.map +1 -1
  40. package/dist/react/useAtom.d.ts +1 -1
  41. package/dist/react/useAtom.d.ts.map +1 -1
  42. package/dist/react/useLocalId.d.ts.map +1 -1
  43. package/dist/react/useQuery.d.ts.map +1 -1
  44. package/dist/react/useQuery.js +2 -2
  45. package/dist/react/useQuery.js.map +1 -1
  46. package/dist/react/useRow.d.ts +2 -2
  47. package/dist/react/useRow.d.ts.map +1 -1
  48. package/dist/react/useRow.js +5 -5
  49. package/dist/react/useRow.js.map +1 -1
  50. package/dist/react/useRow.test.js +22 -22
  51. package/dist/react/useRow.test.js.map +1 -1
  52. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  53. package/dist/react/useTemporaryQuery.js +1 -1
  54. package/dist/react/useTemporaryQuery.js.map +1 -1
  55. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  56. package/dist/reactive.d.ts +1 -1
  57. package/dist/reactive.d.ts.map +1 -1
  58. package/dist/reactive.js +4 -5
  59. package/dist/reactive.js.map +1 -1
  60. package/dist/reactiveQueries/base-class.d.ts +6 -6
  61. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  62. package/dist/reactiveQueries/base-class.js +3 -3
  63. package/dist/reactiveQueries/base-class.js.map +1 -1
  64. package/dist/reactiveQueries/graphql.d.ts +8 -8
  65. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  66. package/dist/reactiveQueries/graphql.js +10 -10
  67. package/dist/reactiveQueries/graphql.js.map +1 -1
  68. package/dist/reactiveQueries/js.d.ts +6 -6
  69. package/dist/reactiveQueries/js.d.ts.map +1 -1
  70. package/dist/reactiveQueries/js.js +8 -8
  71. package/dist/reactiveQueries/js.js.map +1 -1
  72. package/dist/reactiveQueries/sql.d.ts +9 -10
  73. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  74. package/dist/reactiveQueries/sql.js +12 -12
  75. package/dist/reactiveQueries/sql.js.map +1 -1
  76. package/dist/reactiveQueries/sql.test.js +6 -6
  77. package/dist/reactiveQueries/sql.test.js.map +1 -1
  78. package/dist/row-query.d.ts +2 -2
  79. package/dist/row-query.d.ts.map +1 -1
  80. package/dist/row-query.js +4 -38
  81. package/dist/row-query.js.map +1 -1
  82. package/dist/store.d.ts +41 -24
  83. package/dist/store.d.ts.map +1 -1
  84. package/dist/store.js +336 -223
  85. package/dist/store.js.map +1 -1
  86. package/dist/utils/otel.d.ts.map +1 -1
  87. package/package.json +10 -19
  88. package/src/MainDatabaseWrapper.ts +14 -8
  89. package/src/QueryCache.ts +1 -2
  90. package/src/__tests__/react/fixture.tsx +13 -11
  91. package/src/effect/LiveStore.ts +65 -54
  92. package/src/effect/index.ts +2 -1
  93. package/src/global-state.ts +2 -6
  94. package/src/index.ts +25 -7
  95. package/src/react/LiveStoreContext.ts +7 -2
  96. package/src/react/LiveStoreProvider.test.tsx +56 -14
  97. package/src/react/LiveStoreProvider.tsx +105 -46
  98. package/src/react/useQuery.ts +2 -2
  99. package/src/react/useRow.test.tsx +22 -22
  100. package/src/react/useRow.ts +7 -10
  101. package/src/react/useTemporaryQuery.ts +2 -2
  102. package/src/reactive.ts +6 -5
  103. package/src/reactiveQueries/base-class.ts +9 -9
  104. package/src/reactiveQueries/graphql.ts +19 -15
  105. package/src/reactiveQueries/js.ts +12 -12
  106. package/src/reactiveQueries/sql.test.ts +6 -6
  107. package/src/reactiveQueries/sql.ts +19 -21
  108. package/src/row-query.ts +8 -54
  109. package/src/store.ts +533 -284
  110. package/dist/utils/bounded-collections.d.ts +0 -34
  111. package/dist/utils/bounded-collections.d.ts.map +0 -1
  112. package/dist/utils/bounded-collections.js +0 -91
  113. package/dist/utils/bounded-collections.js.map +0 -1
  114. package/dist/utils/util.d.ts +0 -14
  115. package/dist/utils/util.d.ts.map +0 -1
  116. package/dist/utils/util.js +0 -19
  117. package/dist/utils/util.js.map +0 -1
  118. package/src/utils/util.ts +0 -31
package/src/store.ts CHANGED
@@ -1,22 +1,50 @@
1
- import type { BootDb, PreparedBindValues, ResetMode, StoreAdapter, StoreAdapterFactory } from '@livestore/common'
2
- import { Devtools, getExecArgsFromMutation } from '@livestore/common'
3
- import { version as liveStoreVersion } from '@livestore/common/package.json'
4
- import type { LiveStoreSchema, MutationEvent, MutationEventSchema } from '@livestore/common/schema'
5
- import { makeMutationEventSchema } from '@livestore/common/schema'
6
- import { assertNever, isPromise, makeNoopTracer, ref, shouldNeverHappen } from '@livestore/utils'
7
- import { Effect, Schema, Stream } from '@livestore/utils/effect'
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 { globalDbGraph } from './global-state.js'
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, ReactiveGraph, Ref } from './reactive.js'
15
- import type { DbContext, DbGraph, LiveQuery } from './reactiveQueries/base-class.js'
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
- otelTracer: otel.Tracer
40
- otelRootSpanContext: otel.Context
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
- graph: ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
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, DbContext, RefreshReason> }
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 = new Set<string>()
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
- dbGraph,
128
- otelTracer,
129
- otelRootSpanContext,
130
- mutationEventSchema,
161
+ reactivityGraph,
162
+ otelOptions,
131
163
  disableDevtools,
164
+ __processedMutationIds,
165
+ fiberSet,
132
166
  }: StoreOptions<TGraphQLContext, TSchema>) {
133
- this.mainDbWrapper = new MainDatabaseWrapper({ otelTracer, otelRootSpanContext, db: adapter.mainDb })
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 = mutationEventSchema
139
- // this.mutationEventSchema = makeMutationEventSchema(Object.fromEntries(schema.mutations.entries()) as any)
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 = otelTracer.startSpan('LiveStore:mutations', {}, otelRootSpanContext)
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 = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext)
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.graph = dbGraph
152
- this.graph.context = { store: this as any, otelTracer, rootOtelContext: otelQueriesSpanContext }
153
-
154
- this.adapter.coordinator.syncMutations.pipe(
155
- Stream.tapSync((mutationEventDecoded) => {
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: otelTracer,
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.graph.atoms.values())
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.otelTracer.startActiveSpan('LiveStore:store-constructor', {}, ctx, (span) => {
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.graph.makeEffect((get) => onNewValue(get(query$.results$)), { label })
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.graph.destroyNode(effect)
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
- for (const tableRef of Object.values(this.tableRefs)) {
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, DbContext, RefreshReason>, 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.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh })
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
- return { durationMs: getDurationMsFromSpan(span) }
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.graph.runDeferredEffects({ otelContext })
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
- void this.adapter.coordinator.mutate(mutationEventEncoded, {
484
- span,
485
- persisted: coordinatorMode !== 'skip-persist',
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
- const parentSpan = otel.trace.getSpan(otel.context.active())
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.graph.makeRef(null, {
574
+ this.reactivityGraph.makeRef(null, {
522
575
  equal: () => false,
523
576
  label: `tableRef:${tableName}`,
524
577
  meta: { liveStoreRefType: 'table' },
525
578
  })
526
579
 
527
- private bootDevtools = () => {
528
- const devtoolsChannel = Devtools.makeBroadcastChannels()
529
-
530
- const signalsSubcriptionRef = ref<undefined | (() => void)>(undefined)
531
- // let alreadySubscribedToLiveQueries = false
532
- const liveQueriesSubscriptionRef = ref<undefined | (() => void)>(undefined)
533
- devtoolsChannel.toAppHost.addEventListener('message', async (event) => {
534
- const decoded = Schema.decodeUnknownOption(Devtools.MessageToAppHost)(event.data)
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
- const requestId = decoded.value.requestId
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
- send()
592
+ const channelId = this.adapter.coordinator.devtools.channelId
562
593
 
563
- if (signalsSubcriptionRef.current === undefined) {
564
- signalsSubcriptionRef.current = this.graph.subscribeToRefresh(() => send())
565
- }
594
+ const runtime = yield* Effect.runtime()
566
595
 
567
- break
568
- }
569
- case 'LSD.DebugInfoReq': {
570
- sendToDevtools(
571
- Devtools.DebugInfoRes.make({ debugInfo: this.mainDbWrapper.debugInfo, requestId, liveStoreVersion }),
572
- )
573
- break
574
- }
575
- case 'LSD.DebugInfoResetReq': {
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
- send()
607
+ if (message.channelId !== channelId) return
610
608
 
611
- if (liveQueriesSubscriptionRef.current === undefined) {
612
- liveQueriesSubscriptionRef.current = this.graph.subscribeToRefresh(() => send())
613
- }
609
+ if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
610
+ type Unsub = () => void
611
+ type RequestId = string
614
612
 
615
- break
616
- }
617
- case 'LSD.LiveQueriesUnsubscribe': {
618
- liveQueriesSubscriptionRef.current!()
619
- liveQueriesSubscriptionRef.current = undefined
620
- break
621
- }
622
- case 'LSD.ResetAllDataReq': {
623
- await this.adapter.coordinator.dangerouslyReset(decoded.value.mode)
624
- sendToDevtools(Devtools.ResetAllDataRes.make({ requestId, liveStoreVersion }))
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
- break
783
+ storeMessagePort.start()
784
+ }),
785
+ Runtime.runFork(runtime),
786
+ )
787
+
788
+ return
627
789
  }
628
- // No default
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 createStore = async <
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
- otelTracer = makeNoopTracer(),
655
- otelRootSpanContext = otel.context.active(),
860
+ otelOptions,
656
861
  adapter: adapterFactory,
657
862
  boot,
658
- dbGraph = globalDbGraph,
863
+ reactivityGraph = globalReactivityGraph,
659
864
  batchUpdates,
660
865
  disableDevtools,
661
- }: {
662
- schema: TSchema
663
- graphQLOptions?: GraphQLOptions<TGraphQLContext>
664
- otelTracer?: otel.Tracer
665
- otelRootSpanContext?: otel.Context
666
- adapter: StoreAdapterFactory
667
- boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
668
- dbGraph?: DbGraph
669
- batchUpdates?: (run: () => void) => void
670
- disableDevtools?: boolean
671
- }): Promise<Store<TGraphQLContext, TSchema>> => {
672
- return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
673
- try {
674
- performance.mark('livestore:db-creating')
675
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
676
-
677
- const adapterPromise = adapterFactory({ otelTracer, otelContext, schema })
678
- const adapter = adapterPromise instanceof Promise ? await adapterPromise : adapterPromise
679
- performance.mark('livestore:db-created')
680
- performance.measure('livestore:db-create', 'livestore:db-creating', 'livestore:db-created')
681
-
682
- if (batchUpdates !== undefined) {
683
- dbGraph.effectsWrapper = batchUpdates
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
- const mutationEventSchema = makeMutationEventSchema(Object.fromEntries(schema.mutations.entries()) as any)
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
- // TODO consider moving booting into the storage backend
689
- if (boot !== undefined) {
690
- let isInTxn = false
691
- let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
908
+ const mutationEventSchema = makeMutationEventSchemaMemo(schema)
692
909
 
693
- const bootDbImpl: BootDb = {
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
- if (isInTxn === true) {
700
- txnExecuteStmnts.push([queryStr, bindValues])
701
- } else {
702
- void adapter.coordinator.execute(queryStr, bindValues, undefined)
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
- const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
719
- void adapter.coordinator.mutate(mutationEventEncoded, { span, persisted: true })
720
- }
721
- },
722
- select: (queryStr, bindValues) => {
723
- const stmt = adapter.mainDb.prepare(queryStr)
724
- return stmt.select(bindValues)
725
- },
726
- txn: (callback) => {
727
- try {
728
- isInTxn = true
729
- adapter.mainDb.execute('BEGIN', undefined)
730
-
731
- callback()
732
-
733
- adapter.mainDb.execute('COMMIT', undefined)
734
-
735
- // adapter.coordinator.execute('BEGIN', undefined, undefined)
736
- for (const [queryStr, bindValues] of txnExecuteStmnts) {
737
- adapter.coordinator.execute(queryStr, bindValues, undefined)
738
- }
739
- // adapter.coordinator.execute('COMMIT', undefined, undefined)
740
- } catch (e: any) {
741
- adapter.mainDb.execute('ROLLBACK', undefined)
742
- throw e
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
- const booting = boot(bootDbImpl, span)
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
- // TODO: we can't apply the schema at this point, we've already loaded persisted data!
758
- // Think about what to do about this case.
759
- // await applySchema(db, schema)
760
- return Store.createStore<TGraphQLContext, TSchema>(
761
- {
762
- adapter: adapter,
763
- schema,
764
- graphQLOptions,
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
- span,
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
+ )