@livestore/livestore 0.0.54-dev.4 → 0.0.54

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 +18 -11
  14. package/dist/effect/LiveStore.d.ts.map +1 -1
  15. package/dist/effect/LiveStore.js +20 -19
  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 +6 -6
  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 +35 -22
  83. package/dist/store.d.ts.map +1 -1
  84. package/dist/store.js +329 -221
  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 +53 -44
  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 +107 -51
  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 +514 -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,48 @@
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, isPromise, makeNoopTracer, shouldNeverHappen, throttle } from '@livestore/utils'
21
+ import { cuid } from '@livestore/utils/cuid'
22
+ import {
23
+ Effect,
24
+ Exit,
25
+ Layer,
26
+ Logger,
27
+ LogLevel,
28
+ OtelTracer,
29
+ Queue,
30
+ Runtime,
31
+ Schema,
32
+ Scope,
33
+ Stream,
34
+ } from '@livestore/utils/effect'
8
35
  import * as otel from '@opentelemetry/api'
9
36
  import type { GraphQLSchema } from 'graphql'
10
37
 
11
- import { globalDbGraph } from './global-state.js'
12
- import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
38
+ import { globalReactivityGraph } from './global-state.js'
39
+ import { emptyDebugInfo as makeEmptyDebugInfo, MainDatabaseWrapper } from './MainDatabaseWrapper.js'
13
40
  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'
41
+ import type { DebugRefreshReasonBase, Ref } from './reactive.js'
42
+ import { NOT_REFRESHED_YET } from './reactive.js'
43
+ import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
16
44
  import { downloadBlob } from './utils/dev.js'
17
45
  import { getDurationMsFromSpan } from './utils/otel.js'
18
- import type { ParamsObject } from './utils/util.js'
19
- import { prepareBindValues } from './utils/util.js'
20
46
 
21
47
  export type BaseGraphQLContext = {
22
48
  queriedTables: Set<string>
@@ -29,18 +55,25 @@ export type GraphQLOptions<TContext> = {
29
55
  makeContext: (db: MainDatabaseWrapper, tracer: otel.Tracer) => TContext
30
56
  }
31
57
 
58
+ export type OtelOptions = {
59
+ tracer: otel.Tracer
60
+ rootSpanContext: otel.Context
61
+ }
62
+
32
63
  export type StoreOptions<
33
64
  TGraphQLContext extends BaseGraphQLContext,
34
65
  TSchema extends LiveStoreSchema = LiveStoreSchema,
35
66
  > = {
36
67
  adapter: StoreAdapter
37
68
  schema: TSchema
69
+ // TODO remove graphql-related stuff from store and move to GraphQL query directly
38
70
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
39
- otelTracer: otel.Tracer
40
- otelRootSpanContext: otel.Context
41
- dbGraph: DbGraph
42
- mutationEventSchema: MutationEventSchema<any>
71
+ otelOptions: OtelOptions
72
+ reactivityGraph: ReactivityGraph
43
73
  disableDevtools?: boolean
74
+ storeScope: Scope.CloseableScope
75
+ // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
76
+ __processedMutationIds: Set<string>
44
77
  }
45
78
 
46
79
  export type RefreshReason =
@@ -95,11 +128,10 @@ export class Store<
95
128
  TSchema extends LiveStoreSchema = LiveStoreSchema,
96
129
  > {
97
130
  id = uniqueStoreId()
98
- graph: ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
131
+ readonly devtoolsConnectionId = cuid()
132
+ private storeScope: Scope.CloseableScope
133
+ reactivityGraph: ReactivityGraph
99
134
  mainDbWrapper: MainDatabaseWrapper
100
- // TODO refactor
101
- // _proxyDb: InMemoryDatabase
102
- // TODO
103
135
  adapter: StoreAdapter
104
136
  schema: LiveStoreSchema
105
137
  graphQLSchema?: GraphQLSchema
@@ -109,11 +141,11 @@ export class Store<
109
141
  * Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
110
142
  * This only works in combination with `equal: () => false` which will always trigger a refresh.
111
143
  */
112
- tableRefs: { [key: string]: Ref<null, DbContext, RefreshReason> }
144
+ tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
113
145
 
114
146
  // 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>()
147
+ private __processedMutationIds
148
+ private __processedMutationWithoutRefreshIds = new Set<string>()
117
149
 
118
150
  /** RC-based set to see which queries are currently subscribed to */
119
151
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
@@ -124,60 +156,51 @@ export class Store<
124
156
  adapter,
125
157
  schema,
126
158
  graphQLOptions,
127
- dbGraph,
128
- otelTracer,
129
- otelRootSpanContext,
130
- mutationEventSchema,
159
+ reactivityGraph,
160
+ otelOptions,
131
161
  disableDevtools,
162
+ __processedMutationIds,
163
+ storeScope,
132
164
  }: StoreOptions<TGraphQLContext, TSchema>) {
133
- this.mainDbWrapper = new MainDatabaseWrapper({ otelTracer, otelRootSpanContext, db: adapter.mainDb })
165
+ this.mainDbWrapper = new MainDatabaseWrapper({ otel: otelOptions, db: adapter.mainDb })
134
166
  this.adapter = adapter
135
167
  this.schema = schema
136
168
 
169
+ this.storeScope = storeScope
170
+
137
171
  // TODO refactor
138
- this.__mutationEventSchema = mutationEventSchema
139
- // this.mutationEventSchema = makeMutationEventSchema(Object.fromEntries(schema.mutations.entries()) as any)
172
+ this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
173
+
174
+ // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
175
+ this.__processedMutationIds = __processedMutationIds
140
176
 
141
177
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
142
178
  this.tableRefs = {}
143
179
  this.activeQueries = new ReferenceCountedSet()
144
180
 
145
- const mutationsSpan = otelTracer.startSpan('LiveStore:mutations', {}, otelRootSpanContext)
181
+ const mutationsSpan = otelOptions.tracer.startSpan('LiveStore:mutations', {}, otelOptions.rootSpanContext)
146
182
  const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan)
147
183
 
148
- const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext)
184
+ const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext)
149
185
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
150
186
 
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()
187
+ this.reactivityGraph = reactivityGraph
188
+ this.reactivityGraph.context = {
189
+ store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
190
+ otelTracer: otelOptions.tracer,
191
+ rootOtelContext: otelQueriesSpanContext,
165
192
  }
166
193
 
167
194
  this.otel = {
168
- tracer: otelTracer,
195
+ tracer: otelOptions.tracer,
169
196
  mutationsSpanContext: otelMuationsSpanContext,
170
197
  queriesSpanContext: otelQueriesSpanContext,
171
198
  }
172
199
 
173
200
  // 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
- )
201
+ const allTableNames = new Set(this.schema.tables.keys())
179
202
  const existingTableRefs = new Map(
180
- Array.from(this.graph.atoms.values())
203
+ Array.from(this.reactivityGraph.atoms.values())
181
204
  .filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
182
205
  .map((_) => [_.label!.slice('tableRef:'.length), _] as const),
183
206
  )
@@ -189,6 +212,34 @@ export class Store<
189
212
  this.graphQLSchema = graphQLOptions.schema
190
213
  this.graphQLContext = graphQLOptions.makeContext(this.mainDbWrapper, this.otel.tracer)
191
214
  }
215
+
216
+ Effect.gen(this, function* () {
217
+ yield* this.adapter.coordinator.syncMutations.pipe(
218
+ Stream.tapSync((mutationEventDecoded) => {
219
+ this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
220
+ }),
221
+ Stream.runDrain,
222
+ Effect.withSpan('LiveStore:syncMutations'),
223
+ Effect.forkScoped,
224
+ )
225
+
226
+ if (disableDevtools !== true) {
227
+ yield* this.bootDevtools().pipe(Effect.forkScoped)
228
+ }
229
+
230
+ yield* Effect.addFinalizer(() =>
231
+ Effect.sync(() => {
232
+ for (const tableRef of Object.values(this.tableRefs)) {
233
+ for (const superComp of tableRef.super) {
234
+ this.reactivityGraph.removeEdge(superComp, tableRef)
235
+ }
236
+ }
237
+
238
+ otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
239
+ otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
240
+ }),
241
+ )
242
+ }).pipe(Scope.extend(storeScope), Effect.forkIn(storeScope), Effect.scoped, runEffectFork)
192
243
  }
193
244
 
194
245
  static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
@@ -196,7 +247,7 @@ export class Store<
196
247
  parentSpan: otel.Span,
197
248
  ): Store<TGraphQLContext, TSchema> => {
198
249
  const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
199
- return storeOptions.otelTracer.startActiveSpan('LiveStore:store-constructor', {}, ctx, (span) => {
250
+ return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:store-constructor', {}, ctx, (span) => {
200
251
  try {
201
252
  return new Store(storeOptions)
202
253
  } finally {
@@ -224,7 +275,7 @@ export class Store<
224
275
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
225
276
 
226
277
  const label = `subscribe:${options?.label}`
227
- const effect = this.graph.makeEffect((get) => onNewValue(get(query$.results$)), { label })
278
+ const effect = this.reactivityGraph.makeEffect((get) => onNewValue(get(query$.results$)), { label })
228
279
 
229
280
  this.activeQueries.add(query$ as LiveQuery<TResult>)
230
281
 
@@ -236,7 +287,7 @@ export class Store<
236
287
  const unsubscribe = () => {
237
288
  // console.log('store unsub', query$.label)
238
289
  try {
239
- this.graph.destroyNode(effect)
290
+ this.reactivityGraph.destroyNode(effect)
240
291
  this.activeQueries.remove(query$ as LiveQuery<TResult>)
241
292
  onUnsubsubscribe?.()
242
293
  } finally {
@@ -254,16 +305,7 @@ export class Store<
254
305
  * Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
255
306
  */
256
307
  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()
308
+ await Scope.close(this.storeScope, Exit.void).pipe(Effect.withSpan('Store:destroy'), runEffectPromise)
267
309
  }
268
310
 
269
311
  mutate: {
@@ -323,6 +365,8 @@ export class Store<
323
365
  // mutationsEvents.forEach((_) => console.log(_.mutation, _.id, _.args))
324
366
  // console.groupEnd()
325
367
 
368
+ let durationMs: number
369
+
326
370
  return this.otel.tracer.startActiveSpan(
327
371
  'LiveStore:mutate',
328
372
  { attributes: { 'livestore.mutateLabel': label } },
@@ -353,8 +397,8 @@ export class Store<
353
397
  writeTables.add(tableName)
354
398
  }
355
399
  } catch (e: any) {
356
- debugger
357
400
  console.error(e, mutationEvent)
401
+ throw e
358
402
  }
359
403
  }
360
404
  }
@@ -368,13 +412,14 @@ export class Store<
368
412
  } catch (e: any) {
369
413
  console.error(e)
370
414
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
415
+ throw e
371
416
  } finally {
372
417
  span.end()
373
418
  }
374
419
  },
375
420
  )
376
421
 
377
- const tablesToUpdate = [] as [Ref<null, DbContext, RefreshReason>, null][]
422
+ const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
378
423
  for (const tableName of writeTables) {
379
424
  const tableRef = this.tableRefs[tableName]
380
425
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
@@ -388,14 +433,18 @@ export class Store<
388
433
  }
389
434
 
390
435
  // Update all table refs together in a batch, to only trigger one reactive update
391
- this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh })
436
+ this.reactivityGraph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh })
392
437
  } catch (e: any) {
438
+ console.error(e)
393
439
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
440
+ throw e
394
441
  } finally {
395
442
  span.end()
396
443
 
397
- return { durationMs: getDurationMsFromSpan(span) }
444
+ durationMs = getDurationMsFromSpan(span)
398
445
  }
446
+
447
+ return { durationMs }
399
448
  },
400
449
  )
401
450
  }
@@ -412,7 +461,7 @@ export class Store<
412
461
  this.otel.mutationsSpanContext,
413
462
  (span) => {
414
463
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
415
- this.graph.runDeferredEffects({ otelContext })
464
+ this.reactivityGraph.runDeferredEffects({ otelContext })
416
465
  span.end()
417
466
  },
418
467
  )
@@ -480,10 +529,9 @@ export class Store<
480
529
 
481
530
  if (coordinatorMode !== 'skip-coordinator') {
482
531
  // 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
- })
532
+ this.adapter.coordinator
533
+ .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
534
+ .pipe(runEffectFork)
487
535
  }
488
536
 
489
537
  // Uncomment to print a list of queries currently registered on the store
@@ -509,8 +557,7 @@ export class Store<
509
557
  ) => {
510
558
  this.mainDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
511
559
 
512
- const parentSpan = otel.trace.getSpan(otel.context.active())
513
- this.adapter.coordinator.execute(query, prepareBindValues(params, query), parentSpan)
560
+ this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(runEffectFork)
514
561
  }
515
562
 
516
563
  select = (query: string, params: ParamsObject = {}) => {
@@ -518,117 +565,223 @@ export class Store<
518
565
  }
519
566
 
520
567
  makeTableRef = (tableName: string) =>
521
- this.graph.makeRef(null, {
568
+ this.reactivityGraph.makeRef(null, {
522
569
  equal: () => false,
523
570
  label: `tableRef:${tableName}`,
524
571
  meta: { liveStoreRefType: 'table' },
525
572
  })
526
573
 
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
574
+ // TODO shutdown behaviour
575
+ private bootDevtools = () =>
576
+ Effect.gen(this, function* () {
577
+ const sendToDevtoolsContentscript = (
578
+ message: typeof Devtools.DevtoolsWindowMessage.MessageForContentscript.Type,
579
+ ) => {
580
+ window.postMessage(Schema.encodeSync(Devtools.DevtoolsWindowMessage.MessageForContentscript)(message), '*')
543
581
  }
544
582
 
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
- )
583
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
560
584
 
561
- send()
585
+ const channelId = this.adapter.coordinator.devtools.channelId
562
586
 
563
- if (signalsSubcriptionRef.current === undefined) {
564
- signalsSubcriptionRef.current = this.graph.subscribeToRefresh(() => send())
565
- }
587
+ window.addEventListener('message', (event) => {
588
+ const decodedMessageRes = Schema.decodeOption(Devtools.DevtoolsWindowMessage.MessageForStore)(event.data)
589
+ if (decodedMessageRes._tag === 'None') return
566
590
 
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
591
+ const message = decodedMessageRes.value
592
+
593
+ if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
594
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
595
+ return
590
596
  }
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
597
 
609
- send()
598
+ if (message.channelId !== channelId) return
610
599
 
611
- if (liveQueriesSubscriptionRef.current === undefined) {
612
- liveQueriesSubscriptionRef.current = this.graph.subscribeToRefresh(() => send())
613
- }
600
+ if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
601
+ type Unsub = () => void
602
+ type RequestId = string
614
603
 
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 }))
604
+ const reactivityGraphSubcriptions = new Map<RequestId, Unsub>()
605
+ const liveQueriesSubscriptions = new Map<RequestId, Unsub>()
606
+ const debugInfoHistorySubscriptions = new Map<RequestId, Unsub>()
607
+
608
+ this.adapter.coordinator.devtools
609
+ .connect({ port: message.port, connectionId: this.devtoolsConnectionId })
610
+ .pipe(
611
+ Effect.tapSync(({ storeMessagePort }) => {
612
+ // console.log('storeMessagePort', storeMessagePort)
613
+ storeMessagePort.addEventListener('message', (event) => {
614
+ const decodedMessage = Schema.decodeUnknownSync(Devtools.MessageToAppHostStore)(event.data)
615
+ // console.log('storeMessagePort message', decodedMessage)
616
+
617
+ if (decodedMessage.channelId !== this.adapter.coordinator.devtools.channelId) {
618
+ // console.log(`Unknown message`, event)
619
+ return
620
+ }
621
+
622
+ const requestId = decodedMessage.requestId
623
+ const sendToDevtools = (message: Devtools.MessageFromAppHostStore) =>
624
+ storeMessagePort.postMessage(Schema.encodeSync(Devtools.MessageFromAppHostStore)(message))
625
+
626
+ const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
627
+
628
+ switch (decodedMessage._tag) {
629
+ case 'LSD.ReactivityGraphSubscribe': {
630
+ const includeResults = decodedMessage.includeResults
631
+
632
+ const send = () =>
633
+ // In order to not add more work to the current tick, we use requestIdleCallback
634
+ // to send the reactivity graph updates to the devtools
635
+ requestIdleCallback(
636
+ () =>
637
+ sendToDevtools(
638
+ Devtools.ReactivityGraphRes.make({
639
+ reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
640
+ requestId,
641
+ liveStoreVersion,
642
+ }),
643
+ ),
644
+ { timeout: 500 },
645
+ )
646
+
647
+ send()
648
+
649
+ // In some cases, there can be A LOT of reactivity graph updates in a short period of time
650
+ // so we throttle the updates to avoid sending too much data
651
+ // This might need to be tweaked further and possibly be exposed to the user in some way.
652
+ const throttledSend = throttle(send, 20)
653
+
654
+ reactivityGraphSubcriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
655
+
656
+ break
657
+ }
658
+ case 'LSD.DebugInfoReq': {
659
+ sendToDevtools(
660
+ Devtools.DebugInfoRes.make({
661
+ debugInfo: this.mainDbWrapper.debugInfo,
662
+ requestId,
663
+ liveStoreVersion,
664
+ }),
665
+ )
666
+ break
667
+ }
668
+ case 'LSD.DebugInfoHistorySubscribe': {
669
+ const buffer: DebugInfo[] = []
670
+ let hasStopped = false
671
+ let rafHandle: number | undefined
672
+
673
+ const tick = () => {
674
+ buffer.push(this.mainDbWrapper.debugInfo)
675
+
676
+ // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
677
+ // will get the empty debug info
678
+ // TODO We need to come up with a more graceful way to do this. Probably via a single global
679
+ // `requestAnimationFrame` loop that is passed in somehow.
680
+ this.mainDbWrapper.debugInfo = makeEmptyDebugInfo()
681
+
682
+ if (buffer.length > 10) {
683
+ sendToDevtools(
684
+ Devtools.DebugInfoHistoryRes.make({
685
+ debugInfoHistory: buffer,
686
+ requestId,
687
+ liveStoreVersion,
688
+ }),
689
+ )
690
+ buffer.length = 0
691
+ }
692
+
693
+ if (hasStopped === false) {
694
+ rafHandle = requestAnimationFrame(tick)
695
+ }
696
+ }
625
697
 
626
- break
698
+ rafHandle = requestAnimationFrame(tick)
699
+
700
+ const unsub = () => {
701
+ hasStopped = true
702
+ if (rafHandle !== undefined) {
703
+ cancelAnimationFrame(rafHandle)
704
+ }
705
+ }
706
+
707
+ debugInfoHistorySubscriptions.set(requestId, unsub)
708
+
709
+ break
710
+ }
711
+ case 'LSD.DebugInfoHistoryUnsubscribe': {
712
+ debugInfoHistorySubscriptions.get(requestId)!()
713
+ debugInfoHistorySubscriptions.delete(requestId)
714
+ break
715
+ }
716
+ case 'LSD.DebugInfoResetReq': {
717
+ this.mainDbWrapper.debugInfo.slowQueries.clear()
718
+ sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }))
719
+ break
720
+ }
721
+ case 'LSD.DebugInfoRerunQueryReq': {
722
+ const { queryStr, bindValues, queriedTables } = decodedMessage
723
+ this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
724
+ sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }))
725
+ break
726
+ }
727
+ case 'LSD.ReactivityGraphUnsubscribe': {
728
+ reactivityGraphSubcriptions.get(requestId)!()
729
+ break
730
+ }
731
+ case 'LSD.LiveQueriesSubscribe': {
732
+ const send = () =>
733
+ requestIdleCallback(
734
+ () =>
735
+ sendToDevtools(
736
+ Devtools.LiveQueriesRes.make({
737
+ liveQueries: [...this.activeQueries].map((q) => ({
738
+ _tag: q._tag,
739
+ id: q.id,
740
+ label: q.label,
741
+ runs: q.runs,
742
+ executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
743
+ lastestResult:
744
+ q.results$.previousResult === NOT_REFRESHED_YET
745
+ ? 'SYMBOL_NOT_REFRESHED_YET'
746
+ : q.results$.previousResult,
747
+ activeSubscriptions: Array.from(q.activeSubscriptions),
748
+ })),
749
+ requestId,
750
+ liveStoreVersion,
751
+ }),
752
+ ),
753
+ { timeout: 500 },
754
+ )
755
+
756
+ send()
757
+
758
+ // Same as in the reactivity graph subscription case above, we need to throttle the updates
759
+ const throttledSend = throttle(send, 20)
760
+
761
+ liveQueriesSubscriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend))
762
+
763
+ break
764
+ }
765
+ case 'LSD.LiveQueriesUnsubscribe': {
766
+ liveQueriesSubscriptions.get(requestId)!()
767
+ liveQueriesSubscriptions.delete(requestId)
768
+ break
769
+ }
770
+ // No default
771
+ }
772
+ })
773
+
774
+ storeMessagePort.start()
775
+ }),
776
+ runEffectFork,
777
+ )
778
+
779
+ return
627
780
  }
628
- // No default
629
- }
781
+ })
782
+
783
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
630
784
  })
631
- }
632
785
 
633
786
  __devDownloadDb = () => {
634
787
  const data = this.mainDbWrapper.export()
@@ -636,146 +789,205 @@ export class Store<
636
789
  }
637
790
 
638
791
  __devDownloadMutationLogDb = async () => {
639
- const data = await this.adapter.coordinator.getMutationLogData()
792
+ const data = await this.adapter.coordinator.getMutationLogData.pipe(runEffectPromise)
640
793
  downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
641
794
  }
642
795
 
643
796
  // 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)
797
+ dangerouslyResetStorage = (mode: ResetMode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise)
798
+ }
799
+
800
+ export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
801
+ schema: TSchema
802
+ adapter: StoreAdapterFactory
803
+ reactivityGraph?: ReactivityGraph
804
+ graphQLOptions?: GraphQLOptions<TGraphQLContext>
805
+ otelOptions?: Partial<OtelOptions>
806
+ boot?: (db: BootDb, parentSpan: otel.Span) => unknown | Promise<unknown>
807
+ batchUpdates?: (run: () => void) => void
808
+ disableDevtools?: boolean
809
+ onBootStatus?: (status: BootStatus) => void
645
810
  }
646
811
 
647
812
  /** Create a new LiveStore Store */
648
- export const createStore = async <
813
+ export const createStorePromise = async <
814
+ TGraphQLContext extends BaseGraphQLContext,
815
+ TSchema extends LiveStoreSchema = LiveStoreSchema,
816
+ >({
817
+ signal,
818
+ ...options
819
+ }: CreateStoreOptions<TGraphQLContext, TSchema> & { signal?: AbortSignal }): Promise<Store<TGraphQLContext, TSchema>> =>
820
+ Effect.gen(function* () {
821
+ const scope = yield* Scope.make()
822
+ const runtime = yield* Effect.runtime()
823
+
824
+ if (signal !== undefined) {
825
+ signal.addEventListener('abort', () => {
826
+ Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty, Runtime.runFork(runtime))
827
+ })
828
+ }
829
+
830
+ return yield* createStore({ ...options, storeScope: scope }).pipe(Scope.extend(scope))
831
+ }).pipe(Effect.withSpan('createStore'), runEffectPromise)
832
+
833
+ export const createStore = <
649
834
  TGraphQLContext extends BaseGraphQLContext,
650
835
  TSchema extends LiveStoreSchema = LiveStoreSchema,
651
836
  >({
652
837
  schema,
653
838
  graphQLOptions,
654
- otelTracer = makeNoopTracer(),
655
- otelRootSpanContext = otel.context.active(),
839
+ otelOptions,
656
840
  adapter: adapterFactory,
657
841
  boot,
658
- dbGraph = globalDbGraph,
842
+ reactivityGraph = globalReactivityGraph,
659
843
  batchUpdates,
660
844
  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
- }
845
+ onBootStatus,
846
+ storeScope,
847
+ }: CreateStoreOptions<TGraphQLContext, TSchema> & { storeScope: Scope.CloseableScope }): Effect.Effect<
848
+ Store<TGraphQLContext, TSchema>,
849
+ UnexpectedError,
850
+ Scope.Scope
851
+ > => {
852
+ const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
853
+ const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active()
854
+
855
+ const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
856
+ Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)),
857
+ )
858
+
859
+ return Effect.gen(function* () {
860
+ const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
861
+
862
+ const bootStatusQueue = yield* Queue.unbounded<BootStatus>()
863
+
864
+ yield* Queue.take(bootStatusQueue).pipe(
865
+ Effect.tapSync((status) => onBootStatus?.(status)),
866
+ Effect.forever,
867
+ Effect.tapCauseLogPretty,
868
+ Effect.forkScoped,
869
+ )
685
870
 
686
- const mutationEventSchema = makeMutationEventSchema(Object.fromEntries(schema.mutations.entries()) as any)
871
+ const adapter: StoreAdapter = yield* adapterFactory({
872
+ schema,
873
+ devtoolsEnabled: disableDevtools !== true,
874
+ bootStatusQueue,
875
+ shutdown: (cause) => Scope.close(storeScope, Exit.failCause(cause)),
876
+ }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
687
877
 
688
- // TODO consider moving booting into the storage backend
689
- if (boot !== undefined) {
690
- let isInTxn = false
691
- let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
878
+ if (batchUpdates !== undefined) {
879
+ reactivityGraph.effectsWrapper = batchUpdates
880
+ }
692
881
 
693
- const bootDbImpl: BootDb = {
694
- _tag: 'BootDb',
695
- execute: (queryStr, bindValues) => {
696
- const stmt = adapter.mainDb.prepare(queryStr)
697
- stmt.execute(bindValues)
882
+ const mutationEventSchema = makeMutationEventSchemaMemo(schema)
698
883
 
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
- }
884
+ const __processedMutationIds = new Set<string>()
717
885
 
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 = []
886
+ // TODO consider moving booting into the storage backend
887
+ if (boot !== undefined) {
888
+ let isInTxn = false
889
+ let txnExecuteStmnts: [string, PreparedBindValues | undefined][] = []
890
+
891
+ const bootDbImpl: BootDb = {
892
+ _tag: 'BootDb',
893
+ execute: (queryStr, bindValues) => {
894
+ const stmt = adapter.mainDb.prepare(queryStr)
895
+ stmt.execute(bindValues)
896
+
897
+ if (isInTxn === true) {
898
+ txnExecuteStmnts.push([queryStr, bindValues])
899
+ } else {
900
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
901
+ }
902
+ },
903
+ mutate: (...list) => {
904
+ for (const mutationEventDecoded of list) {
905
+ const mutationDef =
906
+ schema.mutations.get(mutationEventDecoded.mutation) ??
907
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
908
+
909
+ __processedMutationIds.add(mutationEventDecoded.id)
910
+
911
+ const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
912
+ // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
913
+
914
+ for (const { statementSql, bindValues } of execArgsArr) {
915
+ adapter.mainDb.execute(statementSql, bindValues)
746
916
  }
747
- },
748
- }
749
917
 
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
- }
918
+ const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
756
919
 
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,
920
+ adapter.coordinator
921
+ .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
922
+ .pipe(runEffectFork)
923
+ }
770
924
  },
771
- span,
772
- )
773
- } finally {
774
- span.end()
925
+ select: (queryStr, bindValues) => {
926
+ const stmt = adapter.mainDb.prepare(queryStr)
927
+ return stmt.select(bindValues)
928
+ },
929
+ txn: (callback) => {
930
+ try {
931
+ isInTxn = true
932
+ adapter.mainDb.execute('BEGIN', undefined)
933
+
934
+ callback()
935
+
936
+ adapter.mainDb.execute('COMMIT', undefined)
937
+
938
+ // adapter.coordinator.execute('BEGIN', undefined, undefined)
939
+ for (const [queryStr, bindValues] of txnExecuteStmnts) {
940
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
941
+ }
942
+ // adapter.coordinator.execute('COMMIT', undefined, undefined)
943
+ } catch (e: any) {
944
+ adapter.mainDb.execute('ROLLBACK', undefined)
945
+ throw e
946
+ } finally {
947
+ isInTxn = false
948
+ txnExecuteStmnts = []
949
+ }
950
+ },
951
+ }
952
+
953
+ const booting = yield* Effect.try({
954
+ try: () => boot(bootDbImpl, span),
955
+ catch: (cause) => new UnexpectedError({ cause }),
956
+ })
957
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
958
+ if (isPromise(booting)) {
959
+ yield* Effect.tryPromise({ try: () => booting, catch: (cause) => new UnexpectedError({ cause }) })
960
+ }
775
961
  }
776
- })
962
+
963
+ // TODO: we can't apply the schema at this point, we've already loaded persisted data!
964
+ // Think about what to do about this case.
965
+ // await applySchema(db, schema)
966
+ return Store.createStore<TGraphQLContext, TSchema>(
967
+ {
968
+ adapter,
969
+ schema,
970
+ graphQLOptions,
971
+ otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
972
+ reactivityGraph,
973
+ disableDevtools,
974
+ __processedMutationIds,
975
+ storeScope,
976
+ },
977
+ span,
978
+ )
979
+ }).pipe(
980
+ // Effect.scoped,
981
+ Effect.withSpan('createStore', {
982
+ parent: otelOptions?.rootSpanContext
983
+ ? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
984
+ : undefined,
985
+ }),
986
+ Effect.provide(TracingLive),
987
+ )
777
988
  }
778
989
 
990
+ // TODO consider replacing with Effect's RC data structures
779
991
  class ReferenceCountedSet<T> {
780
992
  private map: Map<T, number>
781
993
 
@@ -811,3 +1023,21 @@ class ReferenceCountedSet<T> {
811
1023
  }
812
1024
  }
813
1025
  }
1026
+
1027
+ const runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
1028
+ effect.pipe(
1029
+ Effect.tapCauseLogPretty,
1030
+ Effect.annotateLogs({ thread: 'window' }),
1031
+ Effect.provide(Logger.pretty),
1032
+ Logger.withMinimumLogLevel(LogLevel.Debug),
1033
+ Effect.runFork,
1034
+ )
1035
+
1036
+ const runEffectPromise = <A, E>(effect: Effect.Effect<A, E, never>) =>
1037
+ effect.pipe(
1038
+ Effect.tapCauseLogPretty,
1039
+ Effect.annotateLogs({ thread: 'window' }),
1040
+ Effect.provide(Logger.pretty),
1041
+ Logger.withMinimumLogLevel(LogLevel.Debug),
1042
+ Effect.runPromise,
1043
+ )