@livestore/livestore 0.3.0-dev.4 → 0.3.0-dev.40

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 (170) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts.map +1 -1
  3. package/dist/SqliteDbWrapper.d.ts +60 -0
  4. package/dist/SqliteDbWrapper.d.ts.map +1 -0
  5. package/dist/{SynchronousDatabaseWrapper.js → SqliteDbWrapper.js} +69 -34
  6. package/dist/SqliteDbWrapper.js.map +1 -0
  7. package/dist/effect/LiveStore.d.ts +6 -34
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +10 -12
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/effect/mod.d.ts +3 -0
  12. package/dist/effect/mod.d.ts.map +1 -0
  13. package/dist/effect/mod.js +3 -0
  14. package/dist/effect/mod.js.map +1 -0
  15. package/dist/internal/mod.d.ts +3 -0
  16. package/dist/internal/mod.d.ts.map +1 -0
  17. package/dist/internal/mod.js +3 -0
  18. package/dist/internal/mod.js.map +1 -0
  19. package/dist/live-queries/base-class.d.ts +65 -27
  20. package/dist/live-queries/base-class.d.ts.map +1 -1
  21. package/dist/live-queries/base-class.js +54 -13
  22. package/dist/live-queries/base-class.js.map +1 -1
  23. package/dist/live-queries/client-document-get-query.d.ts +12 -0
  24. package/dist/live-queries/client-document-get-query.d.ts.map +1 -0
  25. package/dist/live-queries/client-document-get-query.js +18 -0
  26. package/dist/live-queries/client-document-get-query.js.map +1 -0
  27. package/dist/live-queries/computed.d.ts +12 -14
  28. package/dist/live-queries/computed.d.ts.map +1 -1
  29. package/dist/live-queries/computed.js +37 -15
  30. package/dist/live-queries/computed.js.map +1 -1
  31. package/dist/live-queries/db-query.d.ts +64 -0
  32. package/dist/live-queries/db-query.d.ts.map +1 -0
  33. package/dist/live-queries/{db.js → db-query.js} +83 -41
  34. package/dist/live-queries/db-query.js.map +1 -0
  35. package/dist/live-queries/db-query.test.d.ts +2 -0
  36. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  37. package/dist/live-queries/db-query.test.js +133 -0
  38. package/dist/live-queries/db-query.test.js.map +1 -0
  39. package/dist/live-queries/mod.d.ts +5 -0
  40. package/dist/live-queries/mod.d.ts.map +1 -0
  41. package/dist/live-queries/mod.js +5 -0
  42. package/dist/live-queries/mod.js.map +1 -0
  43. package/dist/live-queries/signal.d.ts +20 -0
  44. package/dist/live-queries/signal.d.ts.map +1 -0
  45. package/dist/live-queries/signal.js +33 -0
  46. package/dist/live-queries/signal.js.map +1 -0
  47. package/dist/live-queries/signal.test.d.ts +2 -0
  48. package/dist/live-queries/signal.test.d.ts.map +1 -0
  49. package/dist/live-queries/signal.test.js +17 -0
  50. package/dist/live-queries/signal.test.js.map +1 -0
  51. package/dist/mod.d.ts +14 -0
  52. package/dist/mod.d.ts.map +1 -0
  53. package/dist/mod.js +13 -0
  54. package/dist/mod.js.map +1 -0
  55. package/dist/reactive.d.ts +23 -17
  56. package/dist/reactive.d.ts.map +1 -1
  57. package/dist/reactive.js +23 -19
  58. package/dist/reactive.js.map +1 -1
  59. package/dist/reactive.test.js +1 -1
  60. package/dist/reactive.test.js.map +1 -1
  61. package/dist/store/create-store.d.ts +70 -12
  62. package/dist/store/create-store.d.ts.map +1 -1
  63. package/dist/store/create-store.js +69 -19
  64. package/dist/store/create-store.js.map +1 -1
  65. package/dist/store/devtools.d.ts +5 -4
  66. package/dist/store/devtools.d.ts.map +1 -1
  67. package/dist/store/devtools.js +103 -47
  68. package/dist/store/devtools.js.map +1 -1
  69. package/dist/store/store-types.d.ts +32 -42
  70. package/dist/store/store-types.d.ts.map +1 -1
  71. package/dist/store/store-types.js +2 -5
  72. package/dist/store/store-types.js.map +1 -1
  73. package/dist/store/store.d.ts +104 -39
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +261 -214
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/data-structures.d.ts.map +1 -1
  78. package/dist/utils/dev.d.ts.map +1 -1
  79. package/dist/utils/dev.js +6 -1
  80. package/dist/utils/dev.js.map +1 -1
  81. package/dist/utils/function-string.d.ts +7 -0
  82. package/dist/utils/function-string.d.ts.map +1 -0
  83. package/dist/utils/function-string.js +9 -0
  84. package/dist/utils/function-string.js.map +1 -0
  85. package/dist/utils/stack-info.d.ts.map +1 -1
  86. package/dist/utils/stack-info.js +6 -1
  87. package/dist/utils/stack-info.js.map +1 -1
  88. package/dist/utils/stack-info.test.js +54 -1
  89. package/dist/utils/stack-info.test.js.map +1 -1
  90. package/dist/utils/tests/fixture.d.ts +59 -216
  91. package/dist/utils/tests/fixture.d.ts.map +1 -1
  92. package/dist/utils/tests/fixture.js +23 -18
  93. package/dist/utils/tests/fixture.js.map +1 -1
  94. package/dist/utils/tests/mod.d.ts +1 -0
  95. package/dist/utils/tests/mod.d.ts.map +1 -1
  96. package/dist/utils/tests/mod.js +1 -0
  97. package/dist/utils/tests/mod.js.map +1 -1
  98. package/dist/utils/tests/otel.d.ts.map +1 -1
  99. package/dist/utils/tests/otel.js +8 -3
  100. package/dist/utils/tests/otel.js.map +1 -1
  101. package/package.json +29 -26
  102. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +92 -42
  103. package/src/effect/LiveStore.ts +27 -64
  104. package/src/effect/{index.ts → mod.ts} +2 -3
  105. package/src/internal/mod.ts +2 -0
  106. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +220 -45
  107. package/src/live-queries/base-class.ts +152 -50
  108. package/src/live-queries/client-document-get-query.ts +52 -0
  109. package/src/live-queries/computed.ts +51 -33
  110. package/src/live-queries/db-query.test.ts +192 -0
  111. package/src/live-queries/{db.ts → db-query.ts} +140 -82
  112. package/src/live-queries/mod.ts +4 -0
  113. package/src/live-queries/signal.test.ts +25 -0
  114. package/src/live-queries/signal.ts +47 -0
  115. package/src/mod.ts +42 -0
  116. package/src/reactive.test.ts +1 -1
  117. package/src/reactive.ts +66 -43
  118. package/src/store/create-store.ts +187 -59
  119. package/src/store/devtools.ts +136 -54
  120. package/src/store/store-types.ts +31 -43
  121. package/src/store/store.ts +385 -309
  122. package/src/utils/dev.ts +6 -1
  123. package/src/utils/function-string.ts +12 -0
  124. package/src/utils/stack-info.test.ts +58 -1
  125. package/src/utils/stack-info.ts +6 -1
  126. package/src/utils/tests/fixture.ts +22 -31
  127. package/src/utils/tests/mod.ts +1 -0
  128. package/src/utils/tests/otel.ts +10 -3
  129. package/dist/SynchronousDatabaseWrapper.d.ts +0 -41
  130. package/dist/SynchronousDatabaseWrapper.d.ts.map +0 -1
  131. package/dist/SynchronousDatabaseWrapper.js.map +0 -1
  132. package/dist/effect/index.d.ts +0 -2
  133. package/dist/effect/index.d.ts.map +0 -1
  134. package/dist/effect/index.js +0 -2
  135. package/dist/effect/index.js.map +0 -1
  136. package/dist/global-state.d.ts +0 -14
  137. package/dist/global-state.d.ts.map +0 -1
  138. package/dist/global-state.js +0 -16
  139. package/dist/global-state.js.map +0 -1
  140. package/dist/index.d.ts +0 -20
  141. package/dist/index.d.ts.map +0 -1
  142. package/dist/index.js +0 -16
  143. package/dist/index.js.map +0 -1
  144. package/dist/live-queries/db.d.ts +0 -66
  145. package/dist/live-queries/db.d.ts.map +0 -1
  146. package/dist/live-queries/db.js.map +0 -1
  147. package/dist/live-queries/db.test.d.ts +0 -2
  148. package/dist/live-queries/db.test.d.ts.map +0 -1
  149. package/dist/live-queries/db.test.js +0 -118
  150. package/dist/live-queries/db.test.js.map +0 -1
  151. package/dist/live-queries/graphql.d.ts +0 -49
  152. package/dist/live-queries/graphql.d.ts.map +0 -1
  153. package/dist/live-queries/graphql.js +0 -122
  154. package/dist/live-queries/graphql.js.map +0 -1
  155. package/dist/row-query-utils.d.ts +0 -17
  156. package/dist/row-query-utils.d.ts.map +0 -1
  157. package/dist/row-query-utils.js +0 -31
  158. package/dist/row-query-utils.js.map +0 -1
  159. package/dist/utils/otel.d.ts +0 -4
  160. package/dist/utils/otel.d.ts.map +0 -1
  161. package/dist/utils/otel.js +0 -6
  162. package/dist/utils/otel.js.map +0 -1
  163. package/src/global-state.ts +0 -20
  164. package/src/index.ts +0 -66
  165. package/src/live-queries/db.test.ts +0 -154
  166. package/src/live-queries/graphql.ts +0 -219
  167. package/src/row-query-utils.ts +0 -66
  168. package/src/utils/otel.ts +0 -9
  169. package/tsconfig.json +0 -18
  170. package/vitest.config.js +0 -9
package/src/reactive.ts CHANGED
@@ -24,15 +24,19 @@
24
24
  /* eslint-disable prefer-arrow/prefer-arrow-functions */
25
25
 
26
26
  import { BoundArray } from '@livestore/common'
27
- import type { PrettifyFlat } from '@livestore/utils'
28
27
  import { deepEqual, shouldNeverHappen } from '@livestore/utils'
28
+ import type { Types } from '@livestore/utils/effect'
29
29
  import type * as otel from '@opentelemetry/api'
30
30
  // import { getDurationMsFromSpan } from './otel.js'
31
31
 
32
32
  export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
33
33
  export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
34
34
 
35
- export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context) => T
35
+ export type GetAtom = <T>(
36
+ atom: Atom<T, any, any>,
37
+ otelContext?: otel.Context | undefined,
38
+ debugRefreshReason?: TODO | undefined,
39
+ ) => T
36
40
 
37
41
  export type Ref<T, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
38
42
  _tag: 'ref'
@@ -42,7 +46,7 @@ export type Ref<T, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
42
46
  previousResult: T
43
47
  computeResult: () => T
44
48
  sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
45
- super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect>
49
+ super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect<TDebugRefreshReason>>
46
50
  label?: string
47
51
  /** Container for meta information (e.g. the LiveStore Store) */
48
52
  meta?: any
@@ -58,7 +62,7 @@ export type Thunk<TResult, TContext, TDebugRefreshReason extends DebugRefreshRea
58
62
  computeResult: (otelContext?: otel.Context, debugRefreshReason?: TDebugRefreshReason) => TResult
59
63
  previousResult: TResult | NOT_REFRESHED_YET
60
64
  sub: Set<Atom<any, TContext, TDebugRefreshReason>>
61
- super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect>
65
+ super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect<TDebugRefreshReason>>
62
66
  label?: string
63
67
  /** Container for meta information (e.g. the LiveStore Store) */
64
68
  meta?: any
@@ -72,11 +76,11 @@ export type Atom<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
72
76
  | Ref<T, TContext, TDebugRefreshReason>
73
77
  | Thunk<T, TContext, TDebugRefreshReason>
74
78
 
75
- export type Effect = {
79
+ export type Effect<TDebugRefreshReason extends DebugRefreshReason> = {
76
80
  _tag: 'effect'
77
81
  id: string
78
82
  isDestroyed: boolean
79
- doEffect: (otelContext?: otel.Context) => void
83
+ doEffect: (otelContext?: otel.Context | undefined, debugRefreshReason?: TDebugRefreshReason | undefined) => void
80
84
  sub: Set<Atom<any, TODO, TODO>>
81
85
  label?: string
82
86
  invocations: number
@@ -84,7 +88,7 @@ export type Effect = {
84
88
 
85
89
  export type Node<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
86
90
  | Atom<T, TContext, TDebugRefreshReason>
87
- | Effect
91
+ | Effect<TDebugRefreshReason>
88
92
 
89
93
  export const isThunk = <T, TContext, TDebugRefreshReason extends DebugRefreshReason>(
90
94
  obj: unknown,
@@ -98,7 +102,7 @@ export type DebugThunkInfo<T extends string = string> = {
98
102
  }
99
103
 
100
104
  export type DebugRefreshReasonBase =
101
- /** Usually in response to some `mutate` calls with `skipRefresh: true` */
105
+ /** Usually in response to some `commit` calls with `skipRefresh: true` */
102
106
  | {
103
107
  _tag: 'runDeferredEffects'
104
108
  originalRefreshReasons?: ReadonlyArray<DebugRefreshReasonBase>
@@ -140,7 +144,7 @@ const encodedOptionNone = <A>(): EncodedOption<A> => ({ _tag: 'None' })
140
144
  export type SerializedAtom = SerializedRef | SerializedThunk
141
145
 
142
146
  export type SerializedRef = Readonly<
143
- PrettifyFlat<
147
+ Types.Simplify<
144
148
  Pick<Ref<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta' | 'isDirty' | 'isDestroyed' | 'refreshes'> & {
145
149
  /** Is `None` if `getSnapshot` was called with `includeResults: false` which is the default */
146
150
  previousResult: EncodedOption<string>
@@ -151,7 +155,7 @@ export type SerializedRef = Readonly<
151
155
  >
152
156
 
153
157
  export type SerializedThunk = Readonly<
154
- PrettifyFlat<
158
+ Types.Simplify<
155
159
  Pick<
156
160
  Thunk<unknown, unknown, any>,
157
161
  '_tag' | 'id' | 'label' | 'meta' | 'isDirty' | 'isDestroyed' | 'recomputations'
@@ -165,8 +169,8 @@ export type SerializedThunk = Readonly<
165
169
  >
166
170
 
167
171
  export type SerializedEffect = Readonly<
168
- PrettifyFlat<
169
- Pick<Effect, '_tag' | 'id' | 'label' | 'invocations' | 'isDestroyed'> & {
172
+ Types.Simplify<
173
+ Pick<Effect<any>, '_tag' | 'id' | 'label' | 'invocations' | 'isDestroyed'> & {
170
174
  sub: ReadonlyArray<string>
171
175
  }
172
176
  >
@@ -179,14 +183,14 @@ export type ReactiveGraphSnapshot = {
179
183
  readonly deferredEffects: ReadonlyArray<string>
180
184
  }
181
185
 
182
- let nodeIdCounter = 0
183
- const uniqueNodeId = () => `node-${++nodeIdCounter}`
184
- let refreshInfoIdCounter = 0
185
- const uniqueRefreshInfoId = () => `refresh-info-${++refreshInfoIdCounter}`
186
-
187
186
  let globalGraphIdCounter = 0
188
187
  const uniqueGraphId = () => `graph-${++globalGraphIdCounter}`
189
188
 
189
+ /** Used for testing */
190
+ export const __resetIds = () => {
191
+ globalGraphIdCounter = 0
192
+ }
193
+
190
194
  export class ReactiveGraph<
191
195
  TDebugRefreshReason extends DebugRefreshReason,
192
196
  TDebugThunkInfo extends DebugThunkInfo,
@@ -195,27 +199,32 @@ export class ReactiveGraph<
195
199
  id = uniqueGraphId()
196
200
 
197
201
  readonly atoms: Set<Atom<any, TContext, TDebugRefreshReason>> = new Set()
198
- readonly effects: Set<Effect> = new Set()
202
+ readonly effects: Set<Effect<TDebugRefreshReason>> = new Set()
199
203
 
200
204
  context: TContext | undefined
201
205
 
202
- debugRefreshInfos: BoundArray<RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo>> = new BoundArray(5000)
206
+ debugRefreshInfos: BoundArray<RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo>> = new BoundArray(200)
203
207
 
204
208
  private currentDebugRefresh:
205
209
  | { refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]; startMs: DOMHighResTimeStamp }
206
210
  | undefined
207
211
 
208
- private deferredEffects: Map<Effect, Set<TDebugRefreshReason>> = new Map()
212
+ private deferredEffects: Map<Effect<TDebugRefreshReason>, Set<TDebugRefreshReason>> = new Map()
209
213
 
210
214
  private refreshCallbacks: Set<() => void> = new Set()
211
215
 
216
+ private nodeIdCounter = 0
217
+ private uniqueNodeId = () => `node-${++this.nodeIdCounter}`
218
+ private refreshInfoIdCounter = 0
219
+ private uniqueRefreshInfoId = () => `refresh-info-${++this.refreshInfoIdCounter}`
220
+
212
221
  makeRef<T>(
213
222
  val: T,
214
223
  options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean },
215
224
  ): Ref<T, TContext, TDebugRefreshReason> {
216
225
  const ref: Ref<T, TContext, TDebugRefreshReason> = {
217
226
  _tag: 'ref',
218
- id: uniqueNodeId(),
227
+ id: this.uniqueNodeId(),
219
228
  isDirty: false,
220
229
  isDestroyed: false,
221
230
  previousResult: val,
@@ -239,6 +248,7 @@ export class ReactiveGraph<
239
248
  setDebugInfo: (debugInfo: TDebugThunkInfo) => void,
240
249
  ctx: TContext,
241
250
  otelContext: otel.Context | undefined,
251
+ debugRefreshReason: TDebugRefreshReason | undefined,
242
252
  ) => T,
243
253
  options?:
244
254
  | {
@@ -250,7 +260,7 @@ export class ReactiveGraph<
250
260
  ): Thunk<T, TContext, TDebugRefreshReason> {
251
261
  const thunk: Thunk<T, TContext, TDebugRefreshReason> = {
252
262
  _tag: 'thunk',
253
- id: uniqueNodeId(),
263
+ id: this.uniqueNodeId(),
254
264
  previousResult: NOT_REFRESHED_YET,
255
265
  isDirty: true,
256
266
  isDestroyed: false,
@@ -266,7 +276,7 @@ export class ReactiveGraph<
266
276
 
267
277
  const getAtom = (atom: Atom<T, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
268
278
  this.addEdge(thunk, atom)
269
- return compute(atom, otelContext)
279
+ return compute(atom, otelContext, debugRefreshReason)
270
280
  }
271
281
 
272
282
  let debugInfo: TDebugThunkInfo | undefined = undefined
@@ -279,6 +289,7 @@ export class ReactiveGraph<
279
289
  setDebugInfo,
280
290
  this.context ?? throwContextNotSetError(this),
281
291
  otelContext,
292
+ debugRefreshReason,
282
293
  )
283
294
 
284
295
  const resultChanged = thunk.equal(thunk.previousResult as T, result) === false
@@ -301,7 +312,7 @@ export class ReactiveGraph<
301
312
  this.currentDebugRefresh = undefined
302
313
 
303
314
  this.debugRefreshInfos.push({
304
- id: uniqueRefreshInfoId(),
315
+ id: this.uniqueRefreshInfoId(),
305
316
  reason: debugRefreshReason ?? ({ _tag: 'makeThunk', label: options?.label } as TDebugRefreshReason),
306
317
  skippedRefresh: false,
307
318
  refreshedAtoms,
@@ -365,14 +376,18 @@ export class ReactiveGraph<
365
376
  }
366
377
 
367
378
  makeEffect(
368
- doEffect: (get: GetAtom, otelContext?: otel.Context) => void,
379
+ doEffect: (
380
+ get: GetAtom,
381
+ otelContext: otel.Context | undefined,
382
+ debugRefreshReason: DebugRefreshReason | undefined,
383
+ ) => void,
369
384
  options?: { label?: string } | undefined,
370
- ): Effect {
371
- const effect: Effect = {
385
+ ): Effect<TDebugRefreshReason> {
386
+ const effect: Effect<TDebugRefreshReason> = {
372
387
  _tag: 'effect',
373
- id: uniqueNodeId(),
388
+ id: this.uniqueNodeId(),
374
389
  isDestroyed: false,
375
- doEffect: (otelContext) => {
390
+ doEffect: (otelContext, debugRefreshReason) => {
376
391
  effect.invocations++
377
392
 
378
393
  // NOTE we're not tracking any debug refresh info for effects as they're tracked by the thunks they depend on
@@ -380,12 +395,16 @@ export class ReactiveGraph<
380
395
  // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
381
396
  effect.sub = new Set()
382
397
 
383
- const getAtom = (atom: Atom<any, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
398
+ const getAtom = (
399
+ atom: Atom<any, TContext, TDebugRefreshReason>,
400
+ otelContext: otel.Context,
401
+ debugRefreshReason: DebugRefreshReason | undefined,
402
+ ) => {
384
403
  this.addEdge(effect, atom)
385
- return compute(atom, otelContext)
404
+ return compute(atom, otelContext, debugRefreshReason)
386
405
  }
387
406
 
388
- doEffect(getAtom as GetAtom, otelContext)
407
+ doEffect(getAtom as GetAtom, otelContext, debugRefreshReason)
389
408
  },
390
409
  sub: new Set(),
391
410
  label: options?.label,
@@ -421,7 +440,7 @@ export class ReactiveGraph<
421
440
  }
422
441
  | undefined,
423
442
  ) {
424
- const effectsToRefresh = new Set<Effect>()
443
+ const effectsToRefresh = new Set<Effect<TDebugRefreshReason>>()
425
444
  for (const [ref, val] of refs) {
426
445
  ref.previousResult = val
427
446
  ref.refreshes++
@@ -448,7 +467,7 @@ export class ReactiveGraph<
448
467
  }
449
468
 
450
469
  private runEffects = (
451
- effectsToRefresh: Set<Effect>,
470
+ effectsToRefresh: Set<Effect<TDebugRefreshReason>>,
452
471
  options: {
453
472
  debugRefreshReason: TDebugRefreshReason
454
473
  otelContext?: otel.Context
@@ -459,7 +478,7 @@ export class ReactiveGraph<
459
478
  this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
460
479
 
461
480
  for (const effect of effectsToRefresh) {
462
- effect.doEffect(options?.otelContext)
481
+ effect.doEffect(options?.otelContext, options.debugRefreshReason)
463
482
  }
464
483
 
465
484
  const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
@@ -467,7 +486,7 @@ export class ReactiveGraph<
467
486
  this.currentDebugRefresh = undefined
468
487
 
469
488
  const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
470
- id: uniqueRefreshInfoId(),
489
+ id: this.uniqueRefreshInfoId(),
471
490
  reason: options.debugRefreshReason,
472
491
  skippedRefresh: false,
473
492
  refreshedAtoms,
@@ -504,7 +523,7 @@ export class ReactiveGraph<
504
523
  }
505
524
 
506
525
  addEdge(
507
- superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect,
526
+ superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect<TDebugRefreshReason>,
508
527
  subComp: Atom<any, TContext, TDebugRefreshReason>,
509
528
  ) {
510
529
  superComp.sub.add(subComp)
@@ -516,11 +535,11 @@ export class ReactiveGraph<
516
535
  }
517
536
 
518
537
  removeEdge(
519
- superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect,
538
+ superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect<TDebugRefreshReason>,
520
539
  subComp: Atom<any, TContext, TDebugRefreshReason>,
521
540
  ) {
522
541
  superComp.sub.delete(subComp)
523
- const effectsToRefresh = new Set<Effect>()
542
+ const effectsToRefresh = new Set<Effect<TDebugRefreshReason>>()
524
543
  markSuperCompDirtyRec(subComp, effectsToRefresh)
525
544
 
526
545
  for (const effect of effectsToRefresh) {
@@ -563,7 +582,11 @@ export class ReactiveGraph<
563
582
  }
564
583
  }
565
584
 
566
- const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
585
+ const compute = <T>(
586
+ atom: Atom<T, unknown, any>,
587
+ otelContext: otel.Context,
588
+ debugRefreshReason: DebugRefreshReason | undefined,
589
+ ): T => {
567
590
  // const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
568
591
  if (atom.isDestroyed) {
569
592
  shouldNeverHappen(`LiveStore Error: Attempted to compute destroyed ${atom._tag} (${atom.id}): ${atom.label ?? ''}`)
@@ -571,7 +594,7 @@ const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T =
571
594
 
572
595
  if (atom.isDirty) {
573
596
  // console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
574
- const result = atom.computeResult(otelContext)
597
+ const result = atom.computeResult(otelContext, debugRefreshReason)
575
598
  atom.isDirty = false
576
599
  atom.previousResult = result
577
600
  return result
@@ -581,7 +604,7 @@ const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T =
581
604
  }
582
605
  }
583
606
 
584
- const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect>) => {
607
+ const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect<any>>) => {
585
608
  for (const superComp of atom.super) {
586
609
  if (superComp._tag === 'thunk') {
587
610
  superComp.isDirty = true
@@ -644,7 +667,7 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
644
667
  }
645
668
 
646
669
  // NOTE This function is performance-optimized (i.e. not using `pick` and `Array.from`)
647
- const serializeEffect = (effect: Effect): SerializedEffect => {
670
+ const serializeEffect = (effect: Effect<any>): SerializedEffect => {
648
671
  const sub: string[] = []
649
672
  for (const a of effect.sub) {
650
673
  sub.push(a.id)
@@ -2,21 +2,23 @@ import type {
2
2
  Adapter,
3
3
  BootStatus,
4
4
  ClientSession,
5
+ ClientSessionDevtoolsChannel,
5
6
  IntentionalShutdownCause,
6
- StoreDevtoolsChannel,
7
+ MigrationsReport,
7
8
  } from '@livestore/common'
8
9
  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'
10
+ import type { LiveStoreSchema } from '@livestore/common/schema'
11
+ import { isDevEnv, LS_DEV } from '@livestore/utils'
12
+ import type { Cause, Schema } from '@livestore/utils/effect'
12
13
  import {
14
+ Context,
13
15
  Deferred,
14
16
  Effect,
15
17
  Exit,
16
18
  identity,
19
+ Layer,
17
20
  Logger,
18
21
  LogLevel,
19
- MutableHashMap,
20
22
  OtelTracer,
21
23
  Queue,
22
24
  Runtime,
@@ -26,43 +28,112 @@ import {
26
28
  import { nanoid } from '@livestore/utils/nanoid'
27
29
  import * as otel from '@opentelemetry/api'
28
30
 
29
- import { globalReactivityGraph } from '../global-state.js'
30
- import type { ReactivityGraph } from '../live-queries/base-class.js'
31
31
  import { connectDevtoolsToStore } from './devtools.js'
32
32
  import { Store } from './store.js'
33
- import type { BaseGraphQLContext, GraphQLOptions, OtelOptions, ShutdownDeferred } from './store-types.js'
33
+ import type {
34
+ LiveStoreContextRunning as LiveStoreContextRunning_,
35
+ OtelOptions,
36
+ ShutdownDeferred,
37
+ } from './store-types.js'
38
+
39
+ export const DEFAULT_PARAMS = {
40
+ leaderPushBatchSize: 1,
41
+ }
42
+
43
+ export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
44
+ LiveStoreContextRunning,
45
+ LiveStoreContextRunning_
46
+ >() {
47
+ static fromDeferred = Effect.gen(function* () {
48
+ const deferred = yield* DeferredStoreContext
49
+ const ctx = yield* deferred
50
+ return Layer.succeed(LiveStoreContextRunning, ctx)
51
+ }).pipe(Layer.unwrapScoped)
52
+ }
34
53
 
35
- export interface CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> {
54
+ export class DeferredStoreContext extends Context.Tag('@livestore/livestore/effect/DeferredStoreContext')<
55
+ DeferredStoreContext,
56
+ Deferred.Deferred<LiveStoreContextRunning['Type'], UnexpectedError>
57
+ >() {}
58
+
59
+ export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}> = {
60
+ schema: TSchema
61
+ /**
62
+ * The `storeId` can be used to isolate multiple stores from each other.
63
+ * So it can be useful for multi-tenancy scenarios.
64
+ *
65
+ * The `storeId` is also used for persistence.
66
+ *
67
+ * @default 'default'
68
+ */
69
+ storeId?: string
70
+ /** Can be useful for custom live query implementations (e.g. see `@livestore/graphql`) */
71
+ context?: TContext
72
+ boot?: (
73
+ store: Store<TSchema, TContext>,
74
+ ) => Effect.Effect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
75
+ adapter: Adapter
76
+ /**
77
+ * Whether to disable devtools.
78
+ *
79
+ * @default 'auto'
80
+ */
81
+ disableDevtools?: boolean | 'auto'
82
+ onBootStatus?: (status: BootStatus) => void
83
+ batchUpdates: (run: () => void) => void
84
+ }
85
+
86
+ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext = {}> {
36
87
  schema: TSchema
37
88
  adapter: Adapter
38
89
  storeId: string
39
- reactivityGraph?: ReactivityGraph
40
- graphQLOptions?: GraphQLOptions<TGraphQLContext>
90
+ context?: TContext
41
91
  boot?: (
42
- store: Store<TGraphQLContext, TSchema>,
43
- parentSpan: otel.Span,
44
- ) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer>
92
+ store: Store<TSchema, TContext>,
93
+ ctx: {
94
+ migrationsReport: MigrationsReport
95
+ parentSpan: otel.Span
96
+ },
97
+ ) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
45
98
  batchUpdates?: (run: () => void) => void
46
- disableDevtools?: boolean
99
+ /**
100
+ * Whether to disable devtools.
101
+ *
102
+ * @default 'auto'
103
+ */
104
+ disableDevtools?: boolean | 'auto'
47
105
  onBootStatus?: (status: BootStatus) => void
48
106
  shutdownDeferred?: ShutdownDeferred
107
+ /**
108
+ * Currently only used in the web adapter:
109
+ * If true, registers a beforeunload event listener to confirm unsaved changes.
110
+ *
111
+ * @default true
112
+ */
113
+ confirmUnsavedChanges?: boolean
114
+ /**
115
+ * Payload that will be passed to the sync backend when connecting
116
+ *
117
+ * @default undefined
118
+ */
119
+ syncPayload?: Schema.JsonValue
120
+ params?: {
121
+ leaderPushBatchSize?: number
122
+ }
49
123
  debug?: {
50
124
  instanceId?: string
51
125
  }
52
126
  }
53
127
 
54
128
  /** Create a new LiveStore Store */
55
- export const createStorePromise = async <
56
- TGraphQLContext extends BaseGraphQLContext,
57
- TSchema extends LiveStoreSchema = LiveStoreSchema,
58
- >({
129
+ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
59
130
  signal,
60
131
  otelOptions,
61
132
  ...options
62
- }: CreateStoreOptions<TGraphQLContext, TSchema> & {
133
+ }: CreateStoreOptions<TSchema, TContext> & {
63
134
  signal?: AbortSignal
64
135
  otelOptions?: Partial<OtelOptions>
65
- }): Promise<Store<TGraphQLContext, TSchema>> =>
136
+ }): Promise<Store<TSchema, TContext>> =>
66
137
  Effect.gen(function* () {
67
138
  const scope = yield* Scope.make()
68
139
  const runtime = yield* Effect.runtime()
@@ -81,34 +152,35 @@ export const createStorePromise = async <
81
152
  provideOtel({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer }),
82
153
  Effect.tapCauseLogPretty,
83
154
  Effect.annotateLogs({ thread: 'window' }),
84
- Effect.provide(Logger.pretty),
155
+ Effect.provide(Logger.prettyWithThread('window')),
85
156
  Logger.withMinimumLogLevel(LogLevel.Debug),
86
157
  Effect.runPromise,
87
158
  )
88
159
 
89
- export const createStore = <
90
- TGraphQLContext extends BaseGraphQLContext,
91
- TSchema extends LiveStoreSchema = LiveStoreSchema,
92
- >({
160
+ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
93
161
  schema,
94
162
  adapter,
95
163
  storeId,
96
- graphQLOptions,
164
+ context = {} as TContext,
97
165
  boot,
98
- reactivityGraph = globalReactivityGraph,
99
166
  batchUpdates,
100
167
  disableDevtools,
101
168
  onBootStatus,
102
169
  shutdownDeferred,
170
+ params,
103
171
  debug,
104
- }: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<
105
- Store<TGraphQLContext, TSchema>,
172
+ confirmUnsavedChanges = true,
173
+ syncPayload,
174
+ }: CreateStoreOptions<TSchema, TContext>): Effect.Effect<
175
+ Store<TSchema, TContext>,
106
176
  UnexpectedError,
107
177
  Scope.Scope | OtelTracer.OtelTracer
108
178
  > =>
109
179
  Effect.gen(function* () {
110
180
  const lifetimeScope = yield* Scope.make()
111
181
 
182
+ yield* validateStoreId(storeId)
183
+
112
184
  yield* Effect.addFinalizer((_) => Scope.close(lifetimeScope, _))
113
185
 
114
186
  const debugInstanceId = debug?.instanceId ?? nanoid(10)
@@ -130,7 +202,7 @@ export const createStore = <
130
202
 
131
203
  const storeDeferred = yield* Deferred.make<Store>()
132
204
 
133
- const connectDevtoolsToStore_ = (storeDevtoolsChannel: StoreDevtoolsChannel) =>
205
+ const connectDevtoolsToStore_ = (storeDevtoolsChannel: ClientSessionDevtoolsChannel) =>
134
206
  Effect.gen(function* () {
135
207
  const store = yield* storeDeferred
136
208
  yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
@@ -138,50 +210,82 @@ export const createStore = <
138
210
 
139
211
  const runtime = yield* Effect.runtime<Scope.Scope>()
140
212
 
141
- const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
142
- Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
143
- Effect.tap(() => (shutdownDeferred ? Deferred.failCause(shutdownDeferred, cause) : Effect.void)),
144
- Effect.tap(() => Effect.logDebug('LiveStore shutdown complete')),
145
- Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
146
- Effect.withSpan('livestore:shutdown'),
213
+ const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => {
214
+ Effect.gen(function* () {
215
+ yield* Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
216
+ Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
217
+ Effect.timeout(1000),
218
+ Effect.catchTag('TimeoutException', () =>
219
+ Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
220
+ ),
221
+ )
222
+
223
+ if (shutdownDeferred) {
224
+ yield* Deferred.failCause(shutdownDeferred, cause)
225
+ }
226
+
227
+ yield* Effect.logDebug('LiveStore shutdown complete')
228
+ }).pipe(
229
+ Effect.withSpan('@livestore/livestore:shutdown'),
230
+ Effect.provide(runtime),
231
+ Effect.tapCauseLogPretty,
232
+ // Given that the shutdown flow might also interrupt the effect that is calling the shutdown,
233
+ // we want to detach the shutdown effect so it's not interrupted by itself
234
+ Effect.runFork,
147
235
  )
236
+ }
148
237
 
149
238
  const clientSession: ClientSession = yield* adapter({
150
239
  schema,
151
240
  storeId,
152
- devtoolsEnabled: disableDevtools !== true,
241
+ devtoolsEnabled: getDevtoolsEnabled(disableDevtools),
153
242
  bootStatusQueue,
154
243
  shutdown,
155
244
  connectDevtoolsToStore: connectDevtoolsToStore_,
156
245
  debugInstanceId,
246
+ syncPayload,
157
247
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
158
248
 
159
- // TODO fill up with unsynced mutation events from the client session
160
- const unsyncedMutationEvents = MutableHashMap.empty<EventId.EventId, MutationEvent.ForSchema<TSchema>>()
161
-
162
- const store = Store.createStore<TGraphQLContext, TSchema>(
163
- {
164
- clientSession,
165
- schema,
166
- graphQLOptions,
167
- otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
168
- reactivityGraph,
169
- disableDevtools,
170
- unsyncedMutationEvents,
171
- lifetimeScope,
172
- runtime,
173
- // NOTE during boot we're not yet executing mutations in a batched context
174
- // but only set the provided `batchUpdates` function after boot
175
- batchUpdates: (run) => run(),
176
- storeId,
249
+ if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
250
+ yield* Effect.logDebug(
251
+ '[@livestore/livestore:createStore] migrationsReport',
252
+ ...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
253
+ m.hashes.actual === undefined
254
+ ? `Table '${m.tableName}' doesn't exist yet. Creating table...`
255
+ : `Schema hash mismatch for table '${m.tableName}' (DB: ${m.hashes.actual}, expected: ${m.hashes.expected}), migrating table...`,
256
+ ),
257
+ )
258
+ }
259
+
260
+ const store = new Store<TSchema, TContext>({
261
+ clientSession,
262
+ schema,
263
+ context,
264
+ otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
265
+ effectContext: { lifetimeScope, runtime },
266
+ // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
267
+ // But for now this is a good enough approximation with little downsides
268
+ __runningInDevtools: getDevtoolsEnabled(disableDevtools) === false,
269
+ confirmUnsavedChanges,
270
+ // NOTE during boot we're not yet executing events in a batched context
271
+ // but only set the provided `batchUpdates` function after boot
272
+ batchUpdates: (run) => run(),
273
+ storeId,
274
+ params: {
275
+ leaderPushBatchSize: params?.leaderPushBatchSize ?? DEFAULT_PARAMS.leaderPushBatchSize,
177
276
  },
178
- span,
179
- )
277
+ })
278
+
279
+ // Starts background fibers (syncing, event processing, etc) for store
280
+ yield* store.boot
180
281
 
181
282
  if (boot !== undefined) {
182
283
  // TODO also incorporate `boot` function progress into `bootStatusQueue`
183
- yield* Effect.tryAll(() => boot(store, span)).pipe(
284
+ yield* Effect.tryAll(() =>
285
+ boot(store, { migrationsReport: clientSession.leaderThread.initialState.migrationsReport, parentSpan: span }),
286
+ ).pipe(
184
287
  UnexpectedError.mapToUnexpectedError,
288
+ Effect.provide(Layer.succeed(LiveStoreContextRunning, { stage: 'running', store: store as any as Store })),
185
289
  Effect.withSpan('createStore:boot'),
186
290
  )
187
291
  }
@@ -204,3 +308,27 @@ export const createStore = <
204
308
  Scope.extend(lifetimeScope),
205
309
  )
206
310
  })
311
+
312
+ const validateStoreId = (storeId: string) =>
313
+ Effect.gen(function* () {
314
+ const validChars = /^[a-zA-Z0-9_-]+$/
315
+
316
+ if (!validChars.test(storeId)) {
317
+ return yield* UnexpectedError.make({
318
+ cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
319
+ payload: { storeId },
320
+ })
321
+ }
322
+ })
323
+
324
+ const getDevtoolsEnabled = (disableDevtools: boolean | 'auto' | undefined) => {
325
+ if (disableDevtools === true) {
326
+ return false
327
+ }
328
+
329
+ if (isDevEnv() === true) {
330
+ return true
331
+ }
332
+
333
+ return false
334
+ }