@livestore/livestore 0.2.0 → 0.3.0-dev.10

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 (100) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SynchronousDatabaseWrapper.d.ts +6 -1
  3. package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
  4. package/dist/SynchronousDatabaseWrapper.js +14 -2
  5. package/dist/SynchronousDatabaseWrapper.js.map +1 -1
  6. package/dist/__tests__/fixture.d.ts +252 -0
  7. package/dist/__tests__/fixture.d.ts.map +1 -0
  8. package/dist/__tests__/fixture.js +18 -0
  9. package/dist/__tests__/fixture.js.map +1 -0
  10. package/dist/effect/LiveStore.d.ts +6 -6
  11. package/dist/effect/LiveStore.d.ts.map +1 -1
  12. package/dist/effect/LiveStore.js +5 -12
  13. package/dist/effect/LiveStore.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/live-queries/db.d.ts.map +1 -1
  18. package/dist/live-queries/db.js +28 -23
  19. package/dist/live-queries/db.js.map +1 -1
  20. package/dist/live-queries/db.test.js +2 -1
  21. package/dist/live-queries/db.test.js.map +1 -1
  22. package/dist/row-query-utils.js +1 -1
  23. package/dist/row-query-utils.js.map +1 -1
  24. package/dist/store/create-store.d.ts +12 -10
  25. package/dist/store/create-store.d.ts.map +1 -1
  26. package/dist/store/create-store.js +22 -28
  27. package/dist/store/create-store.js.map +1 -1
  28. package/dist/store/devtools.d.ts +1 -1
  29. package/dist/store/devtools.d.ts.map +1 -1
  30. package/dist/store/devtools.js +41 -19
  31. package/dist/store/devtools.js.map +1 -1
  32. package/dist/store/store-types.d.ts +9 -14
  33. package/dist/store/store-types.d.ts.map +1 -1
  34. package/dist/store/store.d.ts +29 -28
  35. package/dist/store/store.d.ts.map +1 -1
  36. package/dist/store/store.js +147 -160
  37. package/dist/store/store.js.map +1 -1
  38. package/dist/store/store.test.d.ts +2 -0
  39. package/dist/store/store.test.d.ts.map +1 -0
  40. package/dist/store/store.test.js +27 -0
  41. package/dist/store/store.test.js.map +1 -0
  42. package/dist/utils/dev.d.ts.map +1 -1
  43. package/dist/utils/dev.js +3 -2
  44. package/dist/utils/dev.js.map +1 -1
  45. package/dist/utils/tests/fixture.d.ts +1 -1
  46. package/dist/utils/tests/fixture.d.ts.map +1 -1
  47. package/dist/utils/tests/fixture.js +4 -8
  48. package/dist/utils/tests/fixture.js.map +1 -1
  49. package/dist/utils/tests/otel.d.ts +60 -1
  50. package/dist/utils/tests/otel.d.ts.map +1 -1
  51. package/dist/utils/tests/otel.js +65 -4
  52. package/dist/utils/tests/otel.js.map +1 -1
  53. package/package.json +12 -12
  54. package/src/SynchronousDatabaseWrapper.ts +18 -2
  55. package/src/ambient.d.ts +1 -1
  56. package/src/effect/LiveStore.ts +11 -20
  57. package/src/index.ts +1 -1
  58. package/src/live-queries/__snapshots__/db.test.ts.snap +42 -45
  59. package/src/live-queries/db.test.ts +2 -1
  60. package/src/live-queries/db.ts +28 -23
  61. package/src/row-query-utils.ts +1 -1
  62. package/src/store/create-store.ts +115 -119
  63. package/src/store/devtools.ts +48 -22
  64. package/src/store/store-types.ts +14 -14
  65. package/src/store/store.ts +188 -224
  66. package/src/utils/dev.ts +4 -2
  67. package/src/utils/tests/fixture.ts +4 -9
  68. package/src/utils/tests/otel.ts +71 -5
  69. package/dist/live-queries/sql.d.ts +0 -62
  70. package/dist/live-queries/sql.d.ts.map +0 -1
  71. package/dist/live-queries/sql.js +0 -175
  72. package/dist/live-queries/sql.js.map +0 -1
  73. package/dist/live-queries/sql.test.d.ts +0 -2
  74. package/dist/live-queries/sql.test.d.ts.map +0 -1
  75. package/dist/live-queries/sql.test.js +0 -285
  76. package/dist/live-queries/sql.test.js.map +0 -1
  77. package/dist/reactiveQueries/base-class.d.ts +0 -64
  78. package/dist/reactiveQueries/base-class.d.ts.map +0 -1
  79. package/dist/reactiveQueries/base-class.js +0 -31
  80. package/dist/reactiveQueries/base-class.js.map +0 -1
  81. package/dist/reactiveQueries/computed.d.ts +0 -26
  82. package/dist/reactiveQueries/computed.d.ts.map +0 -1
  83. package/dist/reactiveQueries/computed.js +0 -38
  84. package/dist/reactiveQueries/computed.js.map +0 -1
  85. package/dist/reactiveQueries/graphql.d.ts +0 -49
  86. package/dist/reactiveQueries/graphql.d.ts.map +0 -1
  87. package/dist/reactiveQueries/graphql.js +0 -122
  88. package/dist/reactiveQueries/graphql.js.map +0 -1
  89. package/dist/reactiveQueries/sql.d.ts +0 -62
  90. package/dist/reactiveQueries/sql.d.ts.map +0 -1
  91. package/dist/reactiveQueries/sql.js +0 -175
  92. package/dist/reactiveQueries/sql.js.map +0 -1
  93. package/dist/reactiveQueries/sql.test.d.ts +0 -2
  94. package/dist/reactiveQueries/sql.test.d.ts.map +0 -1
  95. package/dist/reactiveQueries/sql.test.js +0 -285
  96. package/dist/reactiveQueries/sql.test.js.map +0 -1
  97. package/dist/row-query.d.ts +0 -16
  98. package/dist/row-query.d.ts.map +0 -1
  99. package/dist/row-query.js +0 -30
  100. package/dist/row-query.js.map +0 -1
@@ -5,6 +5,7 @@ import {
5
5
  prepareBindValues,
6
6
  QueryBuilderAstSymbol,
7
7
  replaceSessionIdSymbol,
8
+ UnexpectedError,
8
9
  } from '@livestore/common'
9
10
  import { deepEqual, shouldNeverHappen } from '@livestore/utils'
10
11
  import { Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
@@ -144,28 +145,32 @@ export class LiveStoreDbQuery<
144
145
  let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, QueryContext, RefreshReason>
145
146
 
146
147
  const fromQueryBuilder = (qb: QueryBuilder.Any, otelContext: otel.Context | undefined) => {
147
- const qbRes = qb.asSql()
148
- const schema = getResultSchema(qb) as Schema.Schema<TResultSchema, ReadonlyArray<any>>
149
- const ast = qb[QueryBuilderAstSymbol]
150
-
151
- return {
152
- queryInputRaw: {
153
- query: qbRes.query,
154
- schema,
155
- bindValues: qbRes.bindValues,
156
- queriedTables: new Set([ast.tableDef.sqliteDef.name]),
157
- queryInfo: ast._tag === 'RowQuery' ? { _tag: 'Row', table: ast.tableDef, id: ast.id } : { _tag: 'None' },
158
- } satisfies TQueryInputRaw,
159
- label: ast._tag === 'RowQuery' ? rowQueryLabel(ast.tableDef, ast.id) : qb.toString(),
160
- execBeforeFirstRun:
161
- ast._tag === 'RowQuery'
162
- ? makeExecBeforeFirstRun({
163
- table: ast.tableDef,
164
- insertValues: ast.insertValues,
165
- id: ast.id,
166
- otelContext,
167
- })
168
- : undefined,
148
+ try {
149
+ const qbRes = qb.asSql()
150
+ const schema = getResultSchema(qb) as Schema.Schema<TResultSchema, ReadonlyArray<any>>
151
+ const ast = qb[QueryBuilderAstSymbol]
152
+
153
+ return {
154
+ queryInputRaw: {
155
+ query: qbRes.query,
156
+ schema,
157
+ bindValues: qbRes.bindValues,
158
+ queriedTables: new Set([ast.tableDef.sqliteDef.name]),
159
+ queryInfo: ast._tag === 'RowQuery' ? { _tag: 'Row', table: ast.tableDef, id: ast.id } : { _tag: 'None' },
160
+ } satisfies TQueryInputRaw,
161
+ label: ast._tag === 'RowQuery' ? rowQueryLabel(ast.tableDef, ast.id) : qb.toString(),
162
+ execBeforeFirstRun:
163
+ ast._tag === 'RowQuery'
164
+ ? makeExecBeforeFirstRun({
165
+ table: ast.tableDef,
166
+ insertValues: ast.insertValues,
167
+ id: ast.id,
168
+ otelContext,
169
+ })
170
+ : undefined,
171
+ }
172
+ } catch (cause) {
173
+ throw new UnexpectedError({ cause, note: `Error building query for ${qb.toString()}`, payload: { qb } })
169
174
  }
170
175
  }
171
176
 
@@ -274,7 +279,7 @@ export class LiveStoreDbQuery<
274
279
  }
275
280
 
276
281
  if (bindValues !== undefined) {
277
- replaceSessionIdSymbol(bindValues, store.clientSession.coordinator.sessionId)
282
+ replaceSessionIdSymbol(bindValues, store.clientSession.sessionId)
278
283
  }
279
284
 
280
285
  // Establish a reactive dependency on the tables used in the query
@@ -60,6 +60,6 @@ export const makeExecBeforeFirstRun =
60
60
  }
61
61
 
62
62
  // NOTE It's important that we only mutate and don't refresh here, as this function is called during a render
63
- store.mutateWithoutRefresh(table.insert({ id, ...insertValues }), { otelContext, coordinatorMode: 'default' })
63
+ store.mutate({ otelContext, skipRefresh: true }, table.insert({ id, ...insertValues }))
64
64
  }
65
65
  }
@@ -2,21 +2,18 @@ import type {
2
2
  Adapter,
3
3
  BootStatus,
4
4
  ClientSession,
5
- EventId,
6
5
  IntentionalShutdownCause,
7
6
  StoreDevtoolsChannel,
8
7
  } from '@livestore/common'
9
- import { UnexpectedError } from '@livestore/common'
10
- import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
11
- import { makeNoopTracer } from '@livestore/utils'
8
+ import { provideOtel, UnexpectedError } from '@livestore/common'
9
+ import type { EventId, LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
10
+ import { LS_DEV } from '@livestore/utils'
11
+ import type { Cause } from '@livestore/utils/effect'
12
12
  import {
13
- Cause,
14
13
  Deferred,
15
- Duration,
16
14
  Effect,
17
15
  Exit,
18
- FiberSet,
19
- Layer,
16
+ identity,
20
17
  Logger,
21
18
  LogLevel,
22
19
  MutableHashMap,
@@ -24,29 +21,34 @@ import {
24
21
  Queue,
25
22
  Runtime,
26
23
  Scope,
24
+ TaskTracing,
27
25
  } from '@livestore/utils/effect'
26
+ import { nanoid } from '@livestore/utils/nanoid'
28
27
  import * as otel from '@opentelemetry/api'
29
28
 
30
29
  import { globalReactivityGraph } from '../global-state.js'
31
30
  import type { ReactivityGraph } from '../live-queries/base-class.js'
32
31
  import { connectDevtoolsToStore } from './devtools.js'
33
32
  import { Store } from './store.js'
34
- import type { BaseGraphQLContext, GraphQLOptions, OtelOptions } from './store-types.js'
33
+ import type { BaseGraphQLContext, GraphQLOptions, OtelOptions, ShutdownDeferred } from './store-types.js'
35
34
 
36
- export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
35
+ export interface CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> {
37
36
  schema: TSchema
38
37
  adapter: Adapter
39
38
  storeId: string
40
39
  reactivityGraph?: ReactivityGraph
41
40
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
42
- otelOptions?: Partial<OtelOptions>
43
41
  boot?: (
44
42
  store: Store<TGraphQLContext, TSchema>,
45
43
  parentSpan: otel.Span,
46
- ) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
44
+ ) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer>
47
45
  batchUpdates?: (run: () => void) => void
48
46
  disableDevtools?: boolean
49
47
  onBootStatus?: (status: BootStatus) => void
48
+ shutdownDeferred?: ShutdownDeferred
49
+ debug?: {
50
+ instanceId?: string
51
+ }
50
52
  }
51
53
 
52
54
  /** Create a new LiveStore Store */
@@ -55,8 +57,12 @@ export const createStorePromise = async <
55
57
  TSchema extends LiveStoreSchema = LiveStoreSchema,
56
58
  >({
57
59
  signal,
60
+ otelOptions,
58
61
  ...options
59
- }: CreateStoreOptions<TGraphQLContext, TSchema> & { signal?: AbortSignal }): Promise<Store<TGraphQLContext, TSchema>> =>
62
+ }: CreateStoreOptions<TGraphQLContext, TSchema> & {
63
+ signal?: AbortSignal
64
+ otelOptions?: Partial<OtelOptions>
65
+ }): Promise<Store<TGraphQLContext, TSchema>> =>
60
66
  Effect.gen(function* () {
61
67
  const scope = yield* Scope.make()
62
68
  const runtime = yield* Effect.runtime()
@@ -67,12 +73,12 @@ export const createStorePromise = async <
67
73
  })
68
74
  }
69
75
 
70
- return yield* FiberSet.make().pipe(
71
- Effect.andThen((fiberSet) => createStore({ ...options, fiberSet })),
72
- Scope.extend(scope),
73
- )
76
+ return yield* createStore({ ...options }).pipe(Scope.extend(scope))
74
77
  }).pipe(
75
- Effect.withSpan('createStore'),
78
+ Effect.withSpan('createStore', {
79
+ attributes: { storeId: options.storeId, disableDevtools: options.disableDevtools },
80
+ }),
81
+ provideOtel({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer }),
76
82
  Effect.tapCauseLogPretty,
77
83
  Effect.annotateLogs({ thread: 'window' }),
78
84
  Effect.provide(Logger.pretty),
@@ -88,127 +94,117 @@ export const createStore = <
88
94
  adapter,
89
95
  storeId,
90
96
  graphQLOptions,
91
- otelOptions,
92
97
  boot,
93
98
  reactivityGraph = globalReactivityGraph,
94
99
  batchUpdates,
95
100
  disableDevtools,
96
101
  onBootStatus,
97
- fiberSet,
98
- }: CreateStoreOptions<TGraphQLContext, TSchema> & { fiberSet: FiberSet.FiberSet }): Effect.Effect<
102
+ shutdownDeferred,
103
+ debug,
104
+ }: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<
99
105
  Store<TGraphQLContext, TSchema>,
100
106
  UnexpectedError,
101
- Scope.Scope
102
- > => {
103
- const otelTracer = otelOptions?.tracer ?? makeNoopTracer()
104
- const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active()
107
+ Scope.Scope | OtelTracer.OtelTracer
108
+ > =>
109
+ Effect.gen(function* () {
110
+ const lifetimeScope = yield* Scope.make()
105
111
 
106
- const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
107
- Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)),
108
- )
112
+ yield* Effect.addFinalizer((_) => Scope.close(lifetimeScope, _))
109
113
 
110
- return Effect.gen(function* () {
111
- const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
114
+ const debugInstanceId = debug?.instanceId ?? nanoid(10)
112
115
 
113
- const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
116
+ return yield* Effect.gen(function* () {
117
+ const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
118
+ const otelRootSpanContext = otel.trace.setSpan(otel.context.active(), span)
119
+ const otelTracer = yield* OtelTracer.OtelTracer
114
120
 
115
- yield* Queue.take(bootStatusQueue).pipe(
116
- Effect.tapSync((status) => onBootStatus?.(status)),
117
- Effect.tap((status) => (status.stage === 'done' ? Queue.shutdown(bootStatusQueue) : Effect.void)),
118
- Effect.forever,
119
- Effect.tapCauseLogPretty,
120
- Effect.forkScoped,
121
- )
121
+ const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
122
122
 
123
- const storeDeferred = yield* Deferred.make<Store>()
123
+ yield* Queue.take(bootStatusQueue).pipe(
124
+ Effect.tapSync((status) => onBootStatus?.(status)),
125
+ Effect.tap((status) => (status.stage === 'done' ? Queue.shutdown(bootStatusQueue) : Effect.void)),
126
+ Effect.forever,
127
+ Effect.tapCauseLogPretty,
128
+ Effect.forkScoped,
129
+ )
124
130
 
125
- const connectDevtoolsToStore_ = (storeDevtoolsChannel: StoreDevtoolsChannel) =>
126
- Effect.gen(function* () {
127
- const store = yield* Deferred.await(storeDeferred)
128
- yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
129
- })
131
+ const storeDeferred = yield* Deferred.make<Store>()
132
+
133
+ const connectDevtoolsToStore_ = (storeDevtoolsChannel: StoreDevtoolsChannel) =>
134
+ Effect.gen(function* () {
135
+ const store = yield* storeDeferred
136
+ yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
137
+ })
138
+
139
+ const runtime = yield* Effect.runtime<Scope.Scope>()
130
140
 
131
- const runtime = yield* Effect.runtime<Scope.Scope>()
132
-
133
- // TODO close parent scope? (Needs refactor with Mike Arnaldi)
134
- const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
135
- Effect.gen(function* () {
136
- // NOTE we're calling `cause.toString()` here to avoid triggering a `console.error` in the grouped log
137
- const logCause =
138
- Cause.isFailType(cause) && cause.error._tag === 'LiveStore.IntentionalShutdownCause'
139
- ? cause.toString()
140
- : cause
141
- yield* Effect.logDebug(`Shutting down LiveStore`, logCause)
142
-
143
- FiberSet.clear(fiberSet).pipe(
144
- Effect.andThen(() => FiberSet.run(fiberSet, Effect.failCause(cause))),
145
- Effect.timeout(Duration.seconds(1)),
146
- Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown:clear-fiber-set', duration: 500 }),
147
- Effect.catchTag('TimeoutException', (err) =>
148
- Effect.logError('Store shutdown timed out. Forcing shutdown.', err).pipe(
149
- Effect.andThen(FiberSet.run(fiberSet, Effect.failCause(cause))),
150
- ),
141
+ const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
142
+ Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
143
+ Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
144
+ Effect.timeout(1000),
145
+ Effect.catchTag('TimeoutException', () =>
146
+ Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
151
147
  ),
152
- Runtime.runFork(runtime), // NOTE we need to fork this separately otherwise it will also be interrupted
148
+ Effect.tap(() => (shutdownDeferred ? Deferred.failCause(shutdownDeferred, cause) : Effect.void)),
149
+ Effect.tap(() => Effect.logDebug('LiveStore shutdown complete')),
150
+ Effect.withSpan('livestore:shutdown'),
153
151
  )
154
- }).pipe(Effect.withSpan('livestore:shutdown'))
155
-
156
- const clientSession: ClientSession = yield* adapter({
157
- schema,
158
- storeId,
159
- devtoolsEnabled: disableDevtools !== true,
160
- bootStatusQueue,
161
- shutdown,
162
- connectDevtoolsToStore: connectDevtoolsToStore_,
163
- }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
164
-
165
- // TODO fill up with unsynced mutation events from the client session
166
- const unsyncedMutationEvents = MutableHashMap.empty<EventId, MutationEvent.ForSchema<TSchema>>()
167
-
168
- const store = Store.createStore<TGraphQLContext, TSchema>(
169
- {
170
- clientSession,
152
+
153
+ const clientSession: ClientSession = yield* adapter({
171
154
  schema,
172
- graphQLOptions,
173
- otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
174
- reactivityGraph,
175
- disableDevtools,
176
- unsyncedMutationEvents,
177
- fiberSet,
178
- runtime,
179
- // NOTE during boot we're not yet executing mutations in a batched context
180
- // but only set the provided `batchUpdates` function after boot
181
- batchUpdates: (run) => run(),
182
155
  storeId,
183
- },
184
- span,
185
- )
186
-
187
- if (boot !== undefined) {
188
- // TODO also incorporate `boot` function progress into `bootStatusQueue`
189
- yield* Effect.tryAll(() => boot(store, span)).pipe(
190
- UnexpectedError.mapToUnexpectedError,
191
- Effect.withSpan('createStore:boot'),
156
+ devtoolsEnabled: disableDevtools !== true,
157
+ bootStatusQueue,
158
+ shutdown,
159
+ connectDevtoolsToStore: connectDevtoolsToStore_,
160
+ debugInstanceId,
161
+ }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
162
+
163
+ // TODO fill up with unsynced mutation events from the client session
164
+ const unsyncedMutationEvents = MutableHashMap.empty<EventId.EventId, MutationEvent.ForSchema<TSchema>>()
165
+
166
+ const store = Store.createStore<TGraphQLContext, TSchema>(
167
+ {
168
+ clientSession,
169
+ schema,
170
+ graphQLOptions,
171
+ otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
172
+ reactivityGraph,
173
+ disableDevtools,
174
+ unsyncedMutationEvents,
175
+ lifetimeScope,
176
+ runtime,
177
+ // NOTE during boot we're not yet executing mutations in a batched context
178
+ // but only set the provided `batchUpdates` function after boot
179
+ batchUpdates: (run) => run(),
180
+ storeId,
181
+ },
182
+ span,
192
183
  )
193
- }
194
184
 
195
- // NOTE it's important to yield here to allow the forked Effect in the store constructor to run
196
- yield* Effect.yieldNow()
185
+ if (boot !== undefined) {
186
+ // TODO also incorporate `boot` function progress into `bootStatusQueue`
187
+ yield* Effect.tryAll(() => boot(store, span)).pipe(
188
+ UnexpectedError.mapToUnexpectedError,
189
+ Effect.withSpan('createStore:boot'),
190
+ )
191
+ }
197
192
 
198
- if (batchUpdates !== undefined) {
199
- // Replacing the default batchUpdates function with the provided one after boot
200
- store.reactivityGraph.context!.effectsWrapper = batchUpdates
201
- }
193
+ // NOTE it's important to yield here to allow the forked Effect in the store constructor to run
194
+ yield* Effect.yieldNow()
202
195
 
203
- yield* Deferred.succeed(storeDeferred, store as any as Store)
196
+ if (batchUpdates !== undefined) {
197
+ // Replacing the default batchUpdates function with the provided one after boot
198
+ store.reactivityGraph.context!.effectsWrapper = batchUpdates
199
+ }
204
200
 
205
- return store
206
- }).pipe(
207
- Effect.withSpan('createStore', {
208
- parent: otelOptions?.rootSpanContext
209
- ? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext)!)
210
- : undefined,
211
- }),
212
- Effect.provide(TracingLive),
213
- )
214
- }
201
+ yield* Deferred.succeed(storeDeferred, store as any as Store)
202
+
203
+ return store
204
+ }).pipe(
205
+ Effect.withSpan('createStore', { attributes: { debugInstanceId, storeId } }),
206
+ Effect.annotateLogs({ debugInstanceId, storeId }),
207
+ LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
208
+ Scope.extend(lifetimeScope),
209
+ )
210
+ })
@@ -21,20 +21,29 @@ type Unsub = () => void
21
21
  type RequestId = string
22
22
  type SubMap = Map<RequestId, Unsub>
23
23
 
24
+ // When running this code in Node.js, we need to use `setTimeout` instead of `requestAnimationFrame`
25
+ const requestNextTick: (cb: () => void) => number =
26
+ globalThis.requestAnimationFrame === undefined
27
+ ? (cb: () => void) => setTimeout(cb, 1000) as unknown as number
28
+ : globalThis.requestAnimationFrame
29
+
30
+ const cancelTick: (id: number) => void =
31
+ globalThis.cancelAnimationFrame === undefined ? (id: number) => clearTimeout(id) : globalThis.cancelAnimationFrame
32
+
24
33
  export const connectDevtoolsToStore = ({
25
34
  storeDevtoolsChannel,
26
35
  store,
27
36
  }: {
28
- storeDevtoolsChannel: WebChannel.WebChannel<Devtools.MessageToAppHostStore, Devtools.MessageFromAppHostStore>
37
+ storeDevtoolsChannel: WebChannel.WebChannel<Devtools.MessageToAppClientSession, Devtools.MessageFromAppClientSession>
29
38
  store: IStore
30
39
  }) =>
31
40
  Effect.gen(function* () {
32
- const appHostId = store.clientSession.coordinator.devtools.appHostId
33
-
34
41
  const reactivityGraphSubcriptions: SubMap = new Map()
35
42
  const liveQueriesSubscriptions: SubMap = new Map()
36
43
  const debugInfoHistorySubscriptions: SubMap = new Map()
37
44
 
45
+ const { clientId, sessionId } = store.clientSession
46
+
38
47
  yield* Effect.addFinalizer(() =>
39
48
  Effect.sync(() => {
40
49
  reactivityGraphSubcriptions.forEach((unsub) => unsub())
@@ -43,17 +52,22 @@ export const connectDevtoolsToStore = ({
43
52
  }),
44
53
  )
45
54
 
46
- const sendToDevtools = (message: Devtools.MessageFromAppHostStore) =>
47
- storeDevtoolsChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runSync)
55
+ const sendToDevtools = (message: Devtools.MessageFromAppClientSession) =>
56
+ storeDevtoolsChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runFork)
48
57
 
49
- const onMessage = (decodedMessage: typeof Devtools.MessageToAppHostStore.Type) => {
50
- // console.log('storeMessagePort message', decodedMessage)
58
+ const onMessage = (decodedMessage: typeof Devtools.MessageToAppClientSession.Type) => {
59
+ // console.debug('@livestore/livestore:store:devtools:onMessage', decodedMessage)
51
60
 
52
- if (decodedMessage.appHostId !== store.clientSession.coordinator.devtools.appHostId) {
61
+ if (decodedMessage.clientId !== clientId || decodedMessage.sessionId !== sessionId) {
53
62
  // console.log(`Unknown message`, event)
54
63
  return
55
64
  }
56
65
 
66
+ if (decodedMessage._tag === 'LSD.Disconnect') {
67
+ // console.error('TODO handle disconnect properly in store')
68
+ return
69
+ }
70
+
57
71
  const requestId = decodedMessage.requestId
58
72
 
59
73
  const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
@@ -71,7 +85,8 @@ export const connectDevtoolsToStore = ({
71
85
  Devtools.ReactivityGraphRes.make({
72
86
  reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
73
87
  requestId,
74
- appHostId,
88
+ clientId,
89
+ sessionId,
75
90
  liveStoreVersion,
76
91
  }),
77
92
  ),
@@ -94,7 +109,8 @@ export const connectDevtoolsToStore = ({
94
109
  Devtools.DebugInfoRes.make({
95
110
  debugInfo: store.syncDbWrapper.debugInfo,
96
111
  requestId,
97
- appHostId,
112
+ clientId,
113
+ sessionId,
98
114
  liveStoreVersion,
99
115
  }),
100
116
  )
@@ -103,7 +119,7 @@ export const connectDevtoolsToStore = ({
103
119
  case 'LSD.DebugInfoHistorySubscribe': {
104
120
  const buffer: DebugInfo[] = []
105
121
  let hasStopped = false
106
- let rafHandle: number | undefined
122
+ let tickHandle: number | undefined
107
123
 
108
124
  const tick = () => {
109
125
  buffer.push(store.syncDbWrapper.debugInfo)
@@ -119,7 +135,8 @@ export const connectDevtoolsToStore = ({
119
135
  Devtools.DebugInfoHistoryRes.make({
120
136
  debugInfoHistory: buffer,
121
137
  requestId,
122
- appHostId,
138
+ clientId,
139
+ sessionId,
123
140
  liveStoreVersion,
124
141
  }),
125
142
  )
@@ -127,16 +144,17 @@ export const connectDevtoolsToStore = ({
127
144
  }
128
145
 
129
146
  if (hasStopped === false) {
130
- rafHandle = requestAnimationFrame(tick)
147
+ tickHandle = requestNextTick(tick)
131
148
  }
132
149
  }
133
150
 
134
- rafHandle = requestAnimationFrame(tick)
151
+ tickHandle = requestNextTick(tick)
135
152
 
136
153
  const unsub = () => {
137
154
  hasStopped = true
138
- if (rafHandle !== undefined) {
139
- cancelAnimationFrame(rafHandle)
155
+ if (tickHandle !== undefined) {
156
+ cancelTick(tickHandle)
157
+ tickHandle = undefined
140
158
  }
141
159
  }
142
160
 
@@ -145,23 +163,27 @@ export const connectDevtoolsToStore = ({
145
163
  break
146
164
  }
147
165
  case 'LSD.DebugInfoHistoryUnsubscribe': {
148
- debugInfoHistorySubscriptions.get(requestId)!()
166
+ // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
167
+ // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
168
+ debugInfoHistorySubscriptions.get(requestId)?.()
149
169
  debugInfoHistorySubscriptions.delete(requestId)
150
170
  break
151
171
  }
152
172
  case 'LSD.DebugInfoResetReq': {
153
173
  store.syncDbWrapper.debugInfo.slowQueries.clear()
154
- sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, appHostId, liveStoreVersion }))
174
+ sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }))
155
175
  break
156
176
  }
157
177
  case 'LSD.DebugInfoRerunQueryReq': {
158
178
  const { queryStr, bindValues, queriedTables } = decodedMessage
159
179
  store.syncDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
160
- sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, appHostId, liveStoreVersion }))
180
+ sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }))
161
181
  break
162
182
  }
163
183
  case 'LSD.ReactivityGraphUnsubscribe': {
164
- reactivityGraphSubcriptions.get(requestId)!()
184
+ // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
185
+ // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
186
+ reactivityGraphSubcriptions.get(requestId)?.()
165
187
  break
166
188
  }
167
189
  case 'LSD.LiveQueriesSubscribe': {
@@ -184,7 +206,8 @@ export const connectDevtoolsToStore = ({
184
206
  })),
185
207
  requestId,
186
208
  liveStoreVersion,
187
- appHostId,
209
+ clientId,
210
+ sessionId,
188
211
  }),
189
212
  ),
190
213
  { timeout: 500 },
@@ -200,7 +223,9 @@ export const connectDevtoolsToStore = ({
200
223
  break
201
224
  }
202
225
  case 'LSD.LiveQueriesUnsubscribe': {
203
- liveQueriesSubscriptions.get(requestId)!()
226
+ // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
227
+ // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
228
+ liveQueriesSubscriptions.get(requestId)?.()
204
229
  liveQueriesSubscriptions.delete(requestId)
205
230
  break
206
231
  }
@@ -209,6 +234,7 @@ export const connectDevtoolsToStore = ({
209
234
  }
210
235
 
211
236
  yield* storeDevtoolsChannel.listen.pipe(
237
+ // Stream.tapLogWithLabel('@livestore/livestore:store:devtools:onMessage'),
212
238
  Stream.flatten(),
213
239
  Stream.tapSync((message) => onMessage(message)),
214
240
  Stream.runDrain,
@@ -1,6 +1,6 @@
1
- import type { ClientSession, EventId, IntentionalShutdownCause, UnexpectedError } from '@livestore/common'
2
- import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
3
- import type { FiberSet, MutableHashMap, Runtime, Scope } from '@livestore/utils/effect'
1
+ import type { ClientSession, IntentionalShutdownCause, UnexpectedError } from '@livestore/common'
2
+ import type { EventId, LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
3
+ import type { Deferred, MutableHashMap, Runtime, Scope } from '@livestore/utils/effect'
4
4
  import { Schema } from '@livestore/utils/effect'
5
5
  import type * as otel from '@opentelemetry/api'
6
6
  import type { GraphQLSchema } from 'graphql'
@@ -25,6 +25,11 @@ export type LiveStoreContext =
25
25
  export class StoreAbort extends Schema.TaggedError<StoreAbort>()('LiveStore.StoreAbort', {}) {}
26
26
  export class StoreInterrupted extends Schema.TaggedError<StoreInterrupted>()('LiveStore.StoreInterrupted', {}) {}
27
27
 
28
+ export type ShutdownDeferred = Deferred.Deferred<
29
+ void,
30
+ UnexpectedError | IntentionalShutdownCause | StoreInterrupted | StoreAbort
31
+ >
32
+
28
33
  export type LiveStoreContextRunning = {
29
34
  stage: 'running'
30
35
  store: Store
@@ -58,10 +63,11 @@ export type StoreOptions<
58
63
  otelOptions: OtelOptions
59
64
  reactivityGraph: ReactivityGraph
60
65
  disableDevtools?: boolean
61
- fiberSet: FiberSet.FiberSet
66
+ lifetimeScope: Scope.Scope
62
67
  runtime: Runtime.Runtime<Scope.Scope>
63
68
  batchUpdates: (runUpdates: () => void) => void
64
- unsyncedMutationEvents: MutableHashMap.MutableHashMap<EventId, MutationEvent.ForSchema<TSchema>>
69
+ // TODO validate whether we still need this
70
+ unsyncedMutationEvents: MutableHashMap.MutableHashMap<EventId.EventId, MutationEvent.ForSchema<TSchema>>
65
71
  }
66
72
 
67
73
  export type RefreshReason =
@@ -69,7 +75,7 @@ export type RefreshReason =
69
75
  | {
70
76
  _tag: 'mutate'
71
77
  /** The mutations that were applied */
72
- mutations: ReadonlyArray<MutationEvent.Any>
78
+ mutations: ReadonlyArray<MutationEvent.AnyDecoded | MutationEvent.PartialAnyDecoded>
73
79
 
74
80
  /** The tables that were written to by the event */
75
81
  writeTables: ReadonlyArray<string>
@@ -99,12 +105,6 @@ export type StoreOtel = {
99
105
  export type StoreMutateOptions = {
100
106
  label?: string
101
107
  skipRefresh?: boolean
102
- wasSyncMessage?: boolean
103
- /**
104
- * When set to `false` the mutation won't be persisted in the mutation log and sync server (but still synced).
105
- * This can be useful e.g. for fine-granular update events (e.g. position updates during drag & drop)
106
- *
107
- * @default true
108
- */
109
- persisted?: boolean
108
+ spanLinks?: otel.Link[]
109
+ otelContext?: otel.Context
110
110
  }