@livestore/livestore 0.4.0-dev.22 → 0.4.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +8 -8
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +2 -2
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +14 -7
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +0 -15
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +42 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/live-queries/base-class.d.ts +3 -3
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +2 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +1 -1
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +2 -2
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.js +14 -14
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +2 -2
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +9 -9
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +9 -26
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +30 -5
- package/dist/store/StoreRegistry.d.ts.map +1 -1
- package/dist/store/StoreRegistry.js +54 -31
- package/dist/store/StoreRegistry.js.map +1 -1
- package/dist/store/StoreRegistry.test.js +251 -250
- package/dist/store/StoreRegistry.test.js.map +1 -1
- package/dist/store/create-store.d.ts +6 -2
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +13 -7
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +3 -3
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +2 -2
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +70 -5
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store-types.test.js.map +1 -1
- package/dist/store/store.d.ts +81 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +128 -45
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.js +2 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +58 -17
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +4 -2
- package/src/SqliteDbWrapper.ts +12 -11
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +17 -26
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
- package/src/live-queries/base-class.ts +7 -6
- package/src/live-queries/client-document-get-query.ts +4 -2
- package/src/live-queries/computed.ts +3 -2
- package/src/live-queries/db-query.test.ts +3 -2
- package/src/live-queries/db-query.ts +15 -15
- package/src/live-queries/signal.test.ts +3 -2
- package/src/mod.ts +1 -0
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +317 -293
- package/src/store/StoreRegistry.ts +63 -38
- package/src/store/create-store.ts +26 -11
- package/src/store/devtools.ts +5 -6
- package/src/store/store-eventstream.test.ts +4 -2
- package/src/store/store-types.test.ts +3 -1
- package/src/store/store-types.ts +47 -8
- package/src/store/store.ts +172 -55
- package/src/utils/dev.ts +2 -2
- package/src/utils/stack-info.ts +2 -2
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +8 -7
- package/docs/api/index.md +0 -3
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -3
- package/docs/building-with-livestore/crud/index.md +0 -3
- package/docs/building-with-livestore/data-modeling/index.md +0 -30
- package/docs/building-with-livestore/debugging/index.md +0 -17
- package/docs/building-with-livestore/devtools/index.md +0 -79
- package/docs/building-with-livestore/events/index.md +0 -355
- package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -885
- package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
- package/docs/building-with-livestore/opentelemetry/index.md +0 -227
- package/docs/building-with-livestore/production-checklist/index.md +0 -5
- package/docs/building-with-livestore/reactivity-system/index.md +0 -202
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
- package/docs/building-with-livestore/state/materializers/index.md +0 -300
- package/docs/building-with-livestore/state/sql-queries/index.md +0 -94
- package/docs/building-with-livestore/state/sqlite/index.md +0 -45
- package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
- package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
- package/docs/building-with-livestore/store/index.md +0 -625
- package/docs/building-with-livestore/syncing/index.md +0 -136
- package/docs/building-with-livestore/tools/cli/index.md +0 -177
- package/docs/building-with-livestore/tools/mcp/index.md +0 -187
- package/docs/examples/cloudflare-adapter/index.md +0 -44
- package/docs/examples/expo-adapter/index.md +0 -44
- package/docs/examples/index.md +0 -55
- package/docs/examples/node-adapter/index.md +0 -44
- package/docs/examples/web-adapter/index.md +0 -52
- package/docs/framework-integrations/custom-elements/index.md +0 -142
- package/docs/framework-integrations/react-integration/index.md +0 -937
- package/docs/framework-integrations/solid-integration/index.md +0 -293
- package/docs/framework-integrations/svelte-integration/index.md +0 -42
- package/docs/framework-integrations/vue-integration/index.md +0 -294
- package/docs/getting-started/expo/index.md +0 -882
- package/docs/getting-started/node/index.md +0 -115
- package/docs/getting-started/react-web/index.md +0 -626
- package/docs/getting-started/solid/index.md +0 -3
- package/docs/getting-started/vue/index.md +0 -471
- package/docs/index.md +0 -208
- package/docs/llms.txt +0 -146
- package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
- package/docs/misc/FAQ/index.md +0 -37
- package/docs/misc/community/index.md +0 -88
- package/docs/misc/credits/index.md +0 -14
- package/docs/misc/design-partners/index.md +0 -13
- package/docs/misc/package-management/index.md +0 -21
- package/docs/misc/performance/index.md +0 -25
- package/docs/misc/resources/index.md +0 -46
- package/docs/misc/state-of-the-project/index.md +0 -37
- package/docs/misc/troubleshooting/index.md +0 -82
- package/docs/overview/concepts/index.md +0 -78
- package/docs/overview/how-livestore-works/index.md +0 -56
- package/docs/overview/introduction/index.md +0 -413
- package/docs/overview/technology-comparison/index.md +0 -40
- package/docs/overview/when-livestore/index.md +0 -81
- package/docs/overview/why-livestore/index.md +0 -111
- package/docs/patterns/ai/index.md +0 -15
- package/docs/patterns/anonymous-user-transition/index.md +0 -10
- package/docs/patterns/app-evolution/index.md +0 -72
- package/docs/patterns/auth/index.md +0 -377
- package/docs/patterns/effect/index.md +0 -1505
- package/docs/patterns/encryption/index.md +0 -6
- package/docs/patterns/external-data/index.md +0 -5
- package/docs/patterns/file-management/index.md +0 -11
- package/docs/patterns/file-structure/index.md +0 -14
- package/docs/patterns/list-ordering/index.md +0 -369
- package/docs/patterns/offline/index.md +0 -32
- package/docs/patterns/orm/index.md +0 -18
- package/docs/patterns/presence/index.md +0 -11
- package/docs/patterns/rich-text-editing/index.md +0 -11
- package/docs/patterns/server-side-clients/index.md +0 -97
- package/docs/patterns/side-effects/index.md +0 -11
- package/docs/patterns/state-machines/index.md +0 -11
- package/docs/patterns/storybook/index.md +0 -209
- package/docs/patterns/undo-redo/index.md +0 -9
- package/docs/patterns/version-control/index.md +0 -8
- package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
- package/docs/platform-adapters/electron-adapter/index.md +0 -15
- package/docs/platform-adapters/expo-adapter/index.md +0 -262
- package/docs/platform-adapters/node-adapter/index.md +0 -160
- package/docs/platform-adapters/tauri-adapter/index.md +0 -15
- package/docs/platform-adapters/web-adapter/index.md +0 -287
- package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
- package/docs/sustainable-open-source/contributing/info/index.md +0 -63
- package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
- package/docs/sustainable-open-source/sponsoring/index.md +0 -104
- package/docs/sync-providers/cloudflare/index.md +0 -773
- package/docs/sync-providers/custom/index.md +0 -65
- package/docs/sync-providers/electricsql/index.md +0 -159
- package/docs/sync-providers/s2/index.md +0 -230
- package/docs/tutorial/0-welcome/index.md +0 -48
- package/docs/tutorial/1-setup-starter-project/index.md +0 -105
- package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -530
- package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
- package/docs/tutorial/5-expand-business-logic/index.md +0 -174
- package/docs/tutorial/6-persist-ui-state/index.md +0 -453
- package/docs/tutorial/7-next-steps/index.md +0 -22
- package/docs/understanding-livestore/design-decisions/index.md +0 -33
- package/docs/understanding-livestore/event-sourcing/index.md +0 -40
package/src/store/store.ts
CHANGED
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
QueryBuilderAstSymbol,
|
|
17
17
|
replaceSessionIdSymbol,
|
|
18
18
|
type StorageMode,
|
|
19
|
+
type SyncState,
|
|
19
20
|
UnknownError,
|
|
20
21
|
} from '@livestore/common'
|
|
21
22
|
import type { StreamEventsOptions } from '@livestore/common/leader-thread'
|
|
22
23
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
23
|
-
import { LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
|
|
24
|
-
import { assertNever, isDevEnv, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
24
|
+
import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
|
|
25
|
+
import { assertNever, isDevEnv, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
25
26
|
import type { Scope } from '@livestore/utils/effect'
|
|
26
27
|
import {
|
|
27
28
|
Cause,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
StoreInternalsSymbol,
|
|
57
58
|
type StoreOtel,
|
|
58
59
|
type SubscribeOptions,
|
|
60
|
+
type SyncStatus,
|
|
59
61
|
type Unsubscribe,
|
|
60
62
|
} from './store-types.ts'
|
|
61
63
|
|
|
@@ -68,7 +70,7 @@ export type SubscribeFn = {
|
|
|
68
70
|
<TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): AsyncIterable<TResult>
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
if (isDevEnv()) {
|
|
73
|
+
if (isDevEnv() === true) {
|
|
72
74
|
exposeDebugUtils()
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -181,7 +183,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
181
183
|
*/
|
|
182
184
|
readonly [StoreInternalsSymbol]: StoreInternals
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
//#region constructor
|
|
185
187
|
constructor({
|
|
186
188
|
clientSession,
|
|
187
189
|
schema,
|
|
@@ -205,12 +207,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
205
207
|
|
|
206
208
|
const reactivityGraph = makeReactivityGraph()
|
|
207
209
|
|
|
208
|
-
const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
|
|
209
|
-
|
|
210
210
|
const syncProcessor = makeClientSessionSyncProcessor({
|
|
211
211
|
schema,
|
|
212
212
|
clientSession,
|
|
213
|
-
runtime: effectContext.runtime,
|
|
214
213
|
materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
|
|
215
214
|
(eventEncoded, { withChangeset, materializerHashLeader }) =>
|
|
216
215
|
// We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
|
|
@@ -239,7 +238,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
239
238
|
event: { decoded: undefined, encoded: eventEncoded },
|
|
240
239
|
})
|
|
241
240
|
|
|
242
|
-
const materializerHash =
|
|
241
|
+
const materializerHash =
|
|
242
|
+
isDevEnv() === true ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
|
|
243
243
|
|
|
244
244
|
// Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
|
|
245
245
|
// During push path (local commits), materializerHashLeader is always Option.none(), so this condition
|
|
@@ -311,7 +311,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
311
311
|
}
|
|
312
312
|
reactivityGraph.setRefs(tablesToUpdate)
|
|
313
313
|
},
|
|
314
|
-
span: syncSpan,
|
|
315
314
|
params: {
|
|
316
315
|
...omitUndefineds({
|
|
317
316
|
leaderPushBatchSize: params.leaderPushBatchSize,
|
|
@@ -321,7 +320,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
321
320
|
: {}),
|
|
322
321
|
},
|
|
323
322
|
confirmUnsavedChanges,
|
|
324
|
-
})
|
|
323
|
+
}).pipe(Runtime.runSync(effectContext.runtime))
|
|
325
324
|
|
|
326
325
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
|
327
326
|
const tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> } = {}
|
|
@@ -352,7 +351,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
352
351
|
const allTableNames = new Set(
|
|
353
352
|
// NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
|
|
354
353
|
// unless LiveStore is running in the devtools
|
|
355
|
-
__runningInDevtools
|
|
354
|
+
__runningInDevtools === true
|
|
356
355
|
? this.schema.state.sqlite.tables.keys()
|
|
357
356
|
: Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)),
|
|
358
357
|
)
|
|
@@ -366,7 +365,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
366
365
|
existingTableRefs.get(tableName) ??
|
|
367
366
|
reactivityGraph.makeRef(null, {
|
|
368
367
|
equal: () => false,
|
|
369
|
-
label: `tableRef:${tableName}`,
|
|
368
|
+
label: `tableRef:${String(tableName)}`,
|
|
370
369
|
meta: { liveStoreRefType: 'table' },
|
|
371
370
|
})
|
|
372
371
|
}
|
|
@@ -382,7 +381,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
382
381
|
}
|
|
383
382
|
|
|
384
383
|
// End the otel spans
|
|
385
|
-
syncSpan.end()
|
|
386
384
|
commitsSpan.end()
|
|
387
385
|
queriesSpan.end()
|
|
388
386
|
}),
|
|
@@ -415,7 +413,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
415
413
|
// Initialize stable network status property from client session
|
|
416
414
|
this.networkStatus = clientSession.leaderThread.networkStatus
|
|
417
415
|
}
|
|
418
|
-
|
|
416
|
+
//#endregion constructor
|
|
419
417
|
|
|
420
418
|
/**
|
|
421
419
|
* Current session identifier for this Store instance.
|
|
@@ -438,7 +436,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
438
436
|
}
|
|
439
437
|
|
|
440
438
|
private checkShutdown = (operation: string): void => {
|
|
441
|
-
if (this[StoreInternalsSymbol].isShutdown) {
|
|
439
|
+
if (this[StoreInternalsSymbol].isShutdown === true) {
|
|
442
440
|
throw new UnknownError({
|
|
443
441
|
cause: `Store has been shut down (while performing "${operation}").`,
|
|
444
442
|
note: `You cannot perform this operation after the store has been shut down.`,
|
|
@@ -485,19 +483,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
485
483
|
|
|
486
484
|
return this[StoreInternalsSymbol].otel.tracer.startActiveSpan(
|
|
487
485
|
`LiveStore.subscribe`,
|
|
488
|
-
{
|
|
486
|
+
{
|
|
487
|
+
attributes: {
|
|
488
|
+
label: options?.label,
|
|
489
|
+
queryLabel: isQueryBuilder(query) === true ? query.toString() : query.label,
|
|
490
|
+
},
|
|
491
|
+
},
|
|
489
492
|
options?.otelContext ?? this[StoreInternalsSymbol].otel.queriesSpanContext,
|
|
490
493
|
(span) => {
|
|
491
494
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
492
495
|
|
|
493
|
-
const queryRcRef =
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
496
|
+
const queryRcRef =
|
|
497
|
+
isQueryBuilder(query) === true
|
|
498
|
+
? queryDb(query).make(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
499
|
+
: query._tag === 'def' || query._tag === 'signal-def'
|
|
500
|
+
? query.make(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
501
|
+
: {
|
|
502
|
+
value: query,
|
|
503
|
+
deref: () => {},
|
|
504
|
+
}
|
|
501
505
|
const query$ = queryRcRef.value
|
|
502
506
|
|
|
503
507
|
const label = `subscribe:${options?.label}`
|
|
@@ -505,7 +509,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
505
509
|
const effect = this[StoreInternalsSymbol].reactivityGraph.makeEffect(
|
|
506
510
|
(get, _otelContext, debugRefreshReason) => {
|
|
507
511
|
const result = get(query$.results$, otelContext, debugRefreshReason)
|
|
508
|
-
if (suppressCallback) {
|
|
512
|
+
if (suppressCallback === true) {
|
|
509
513
|
return
|
|
510
514
|
}
|
|
511
515
|
onUpdate(result)
|
|
@@ -519,7 +523,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
519
523
|
})
|
|
520
524
|
}
|
|
521
525
|
|
|
522
|
-
if (options?.stackInfo) {
|
|
526
|
+
if (options?.stackInfo !== undefined) {
|
|
523
527
|
query$.activeSubscriptions.add(options.stackInfo)
|
|
524
528
|
}
|
|
525
529
|
|
|
@@ -527,8 +531,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
527
531
|
|
|
528
532
|
this[StoreInternalsSymbol].activeQueries.add(query$ as LiveQuery<TResult>)
|
|
529
533
|
|
|
530
|
-
if (
|
|
531
|
-
if (suppressCallback) {
|
|
534
|
+
if (query$.isDestroyed === false) {
|
|
535
|
+
if (suppressCallback === true) {
|
|
532
536
|
// We still run once to register dependencies in the reactive graph, but suppress the initial callback so the
|
|
533
537
|
// caller truly skips the first emission; subsequent runs (after commits) will call the callback.
|
|
534
538
|
runInitialEffect()
|
|
@@ -543,7 +547,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
543
547
|
this[StoreInternalsSymbol].reactivityGraph.destroyNode(effect)
|
|
544
548
|
this[StoreInternalsSymbol].activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
545
549
|
|
|
546
|
-
if (options?.stackInfo) {
|
|
550
|
+
if (options?.stackInfo !== undefined) {
|
|
547
551
|
query$.activeSubscriptions.delete(options.stackInfo)
|
|
548
552
|
}
|
|
549
553
|
|
|
@@ -575,12 +579,13 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
575
579
|
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
|
|
576
580
|
Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
|
|
577
581
|
)
|
|
578
|
-
const otelContext =
|
|
582
|
+
const otelContext =
|
|
583
|
+
otelSpan !== undefined ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
|
|
579
584
|
|
|
580
585
|
yield* Effect.acquireRelease(
|
|
581
586
|
Effect.sync(() =>
|
|
582
587
|
this.subscribe(query, (result) => emit.single(result), {
|
|
583
|
-
...
|
|
588
|
+
...options,
|
|
584
589
|
otelContext,
|
|
585
590
|
}),
|
|
586
591
|
),
|
|
@@ -617,11 +622,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
617
622
|
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
618
623
|
},
|
|
619
624
|
) as any
|
|
620
|
-
if (query.schema) {
|
|
625
|
+
if (query.schema !== undefined) {
|
|
621
626
|
return Schema.decodeSync(query.schema)(res)
|
|
622
627
|
}
|
|
623
628
|
return res
|
|
624
|
-
} else if (isQueryBuilder(query)) {
|
|
629
|
+
} else if (isQueryBuilder(query) === true) {
|
|
625
630
|
const ast = query[QueryBuilderAstSymbol]
|
|
626
631
|
if (ast._tag === 'RowQuery') {
|
|
627
632
|
makeExecBeforeFirstRun({
|
|
@@ -636,7 +641,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
636
641
|
const schema = getResultSchema(query)
|
|
637
642
|
|
|
638
643
|
// Replace SessionIdSymbol in bind values before executing the query
|
|
639
|
-
if (sqlRes.bindValues) {
|
|
644
|
+
if (sqlRes.bindValues !== undefined) {
|
|
640
645
|
replaceSessionIdSymbol(sqlRes.bindValues, this[StoreInternalsSymbol].clientSession.sessionId)
|
|
641
646
|
}
|
|
642
647
|
|
|
@@ -654,8 +659,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
654
659
|
return decodeResult.right
|
|
655
660
|
} else {
|
|
656
661
|
return shouldNeverHappen(
|
|
657
|
-
|
|
658
|
-
schema
|
|
662
|
+
'Failed to decode query result with for schema:',
|
|
663
|
+
objectToString(schema),
|
|
659
664
|
'raw result:',
|
|
660
665
|
rawRes,
|
|
661
666
|
'decode error:',
|
|
@@ -709,7 +714,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
709
714
|
}
|
|
710
715
|
}
|
|
711
716
|
|
|
712
|
-
|
|
717
|
+
//#region commit
|
|
713
718
|
/**
|
|
714
719
|
* Commit a list of events to the store which will immediately update the local database
|
|
715
720
|
* and sync the events across other clients (similar to a `git commit`).
|
|
@@ -797,23 +802,20 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
797
802
|
|
|
798
803
|
const localRuntime = yield* Effect.runtime()
|
|
799
804
|
|
|
800
|
-
const
|
|
801
|
-
try: () => {
|
|
802
|
-
const runMaterializeEvents = () => {
|
|
803
|
-
return this[StoreInternalsSymbol].syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
|
|
804
|
-
}
|
|
805
|
+
const encodedEvents = yield* this[StoreInternalsSymbol].syncProcessor.encodeEvents(events)
|
|
805
806
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
807
|
+
const { writeTables } = yield* Effect.try({
|
|
808
|
+
try: () => {
|
|
809
|
+
const materialize = () =>
|
|
810
|
+
this[StoreInternalsSymbol].syncProcessor.materializeEvents(encodedEvents).pipe(Runtime.runSync(localRuntime))
|
|
811
|
+
return events.length > 1
|
|
812
|
+
? this[StoreInternalsSymbol].sqliteDbWrapper.txn(materialize)
|
|
813
|
+
: materialize()
|
|
811
814
|
},
|
|
812
815
|
catch: (cause) => UnknownError.make({ cause }),
|
|
813
816
|
})
|
|
814
817
|
|
|
815
|
-
|
|
816
|
-
const { writeTables } = yield* materializeEventsTx
|
|
818
|
+
yield* this[StoreInternalsSymbol].syncProcessor.push(encodedEvents)
|
|
817
819
|
|
|
818
820
|
const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
|
|
819
821
|
for (const tableName of writeTables) {
|
|
@@ -857,7 +859,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
857
859
|
Runtime.runSync(this[StoreInternalsSymbol].effectContext.runtime),
|
|
858
860
|
)
|
|
859
861
|
}
|
|
860
|
-
|
|
862
|
+
//#endregion commit
|
|
861
863
|
|
|
862
864
|
/**
|
|
863
865
|
* Returns an async iterable of events from the eventlog.
|
|
@@ -938,6 +940,113 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
938
940
|
)
|
|
939
941
|
}
|
|
940
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Returns the current synchronization status of the store.
|
|
945
|
+
*
|
|
946
|
+
* This is a synchronous operation that returns the sync state between the
|
|
947
|
+
* client session and the leader thread. Use this to display sync indicators
|
|
948
|
+
* or check if local changes have been pushed to the leader.
|
|
949
|
+
*
|
|
950
|
+
* @example
|
|
951
|
+
* ```ts
|
|
952
|
+
* const status = store.syncStatus()
|
|
953
|
+
* console.log(status.isSynced ? 'Synced' : `${status.pendingCount} pending`)
|
|
954
|
+
* ```
|
|
955
|
+
*
|
|
956
|
+
* @example
|
|
957
|
+
* ```ts
|
|
958
|
+
* // Health check for backend connectivity
|
|
959
|
+
* const status = store.syncStatus()
|
|
960
|
+
* if (!status.isSynced && status.pendingCount > 100) {
|
|
961
|
+
* console.warn('Large backlog of unsynced events')
|
|
962
|
+
* }
|
|
963
|
+
* ```
|
|
964
|
+
*/
|
|
965
|
+
syncStatus = (): SyncStatus => {
|
|
966
|
+
this.checkShutdown('syncStatus')
|
|
967
|
+
|
|
968
|
+
const syncState = this[StoreInternalsSymbol].syncProcessor.syncState.pipe(Effect.runSync)
|
|
969
|
+
const pendingCount = syncState.pending.length
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
localHead: EventSequenceNumber.Client.toString(syncState.localHead),
|
|
973
|
+
upstreamHead: EventSequenceNumber.Client.toString(syncState.upstreamHead),
|
|
974
|
+
pendingCount,
|
|
975
|
+
isSynced: pendingCount === 0,
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Returns an Effect Stream of sync status updates.
|
|
981
|
+
*
|
|
982
|
+
* Emits the current status immediately and then whenever the sync state changes.
|
|
983
|
+
* Use this for Effect-based workflows or when you need more control over the stream.
|
|
984
|
+
*
|
|
985
|
+
* @example
|
|
986
|
+
* ```ts
|
|
987
|
+
* store.syncStatusStream().pipe(
|
|
988
|
+
* Stream.tap((status) => Effect.log(`Sync status: ${status.isSynced}`)),
|
|
989
|
+
* Stream.runDrain,
|
|
990
|
+
* )
|
|
991
|
+
* ```
|
|
992
|
+
*/
|
|
993
|
+
syncStatusStream = (): Stream.Stream<SyncStatus> => {
|
|
994
|
+
const syncStateSubscribable = this[StoreInternalsSymbol].syncProcessor.syncState
|
|
995
|
+
|
|
996
|
+
return Stream.concat(
|
|
997
|
+
Stream.fromEffect(syncStateSubscribable.pipe(Effect.map(this.makeSyncStatus))),
|
|
998
|
+
syncStateSubscribable.changes.pipe(Stream.map(this.makeSyncStatus)),
|
|
999
|
+
)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Subscribes to sync status changes.
|
|
1004
|
+
*
|
|
1005
|
+
* The callback is invoked immediately with the current status and then
|
|
1006
|
+
* whenever the sync state changes (e.g., when events are pushed or confirmed).
|
|
1007
|
+
*
|
|
1008
|
+
* @param onUpdate - Callback invoked with the current sync status
|
|
1009
|
+
* @returns Unsubscribe function to stop receiving updates
|
|
1010
|
+
*
|
|
1011
|
+
* @example
|
|
1012
|
+
* ```ts
|
|
1013
|
+
* const unsubscribe = store.subscribeSyncStatus((status) => {
|
|
1014
|
+
* updateUI(status.isSynced ? 'Synced' : 'Syncing...')
|
|
1015
|
+
* })
|
|
1016
|
+
*
|
|
1017
|
+
* // Later, stop listening
|
|
1018
|
+
* unsubscribe()
|
|
1019
|
+
* ```
|
|
1020
|
+
*/
|
|
1021
|
+
subscribeSyncStatus = (onUpdate: (status: SyncStatus) => void): Unsubscribe => {
|
|
1022
|
+
this.checkShutdown('subscribeSyncStatus')
|
|
1023
|
+
|
|
1024
|
+
const fiber = this.syncStatusStream().pipe(
|
|
1025
|
+
Stream.tap((status) => Effect.sync(() => onUpdate(status))),
|
|
1026
|
+
Stream.runDrain,
|
|
1027
|
+
this.runEffectFork,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
return () => {
|
|
1031
|
+
Fiber.interrupt(fiber).pipe(Runtime.runFork(this[StoreInternalsSymbol].effectContext.runtime))
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private makeSyncStatus = (syncState: {
|
|
1036
|
+
localHead: EventSequenceNumber.Client.Composite
|
|
1037
|
+
upstreamHead: EventSequenceNumber.Client.Composite
|
|
1038
|
+
pending: readonly any[]
|
|
1039
|
+
}): SyncStatus => {
|
|
1040
|
+
const pendingCount = syncState.pending.length
|
|
1041
|
+
|
|
1042
|
+
return {
|
|
1043
|
+
localHead: EventSequenceNumber.Client.toString(syncState.localHead),
|
|
1044
|
+
upstreamHead: EventSequenceNumber.Client.toString(syncState.upstreamHead),
|
|
1045
|
+
pendingCount,
|
|
1046
|
+
isSynced: pendingCount === 0,
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
941
1050
|
/**
|
|
942
1051
|
* This can be used in combination with `skipRefresh` when committing events.
|
|
943
1052
|
* We might need a better solution for this. Let's see.
|
|
@@ -967,7 +1076,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
967
1076
|
this.checkShutdown('shutdownPromise')
|
|
968
1077
|
|
|
969
1078
|
this[StoreInternalsSymbol].isShutdown = true
|
|
970
|
-
await this.shutdown(cause ? Cause.fail(cause) : undefined).pipe(
|
|
1079
|
+
await this.shutdown(cause !== undefined ? Cause.fail(cause) : undefined).pipe(
|
|
1080
|
+
this.runEffectFork,
|
|
1081
|
+
Fiber.join,
|
|
1082
|
+
Effect.runPromise,
|
|
1083
|
+
)
|
|
971
1084
|
}
|
|
972
1085
|
|
|
973
1086
|
/**
|
|
@@ -978,7 +1091,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
978
1091
|
shutdown = (cause?: Cause.Cause<UnknownError | MaterializeError>): Effect.Effect<void> => {
|
|
979
1092
|
this[StoreInternalsSymbol].isShutdown = true
|
|
980
1093
|
return this[StoreInternalsSymbol].clientSession.shutdown(
|
|
981
|
-
cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
|
|
1094
|
+
cause !== undefined ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
|
|
982
1095
|
)
|
|
983
1096
|
}
|
|
984
1097
|
|
|
@@ -1028,7 +1141,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1028
1141
|
.pipe(this.runEffectFork)
|
|
1029
1142
|
},
|
|
1030
1143
|
|
|
1031
|
-
|
|
1144
|
+
// NOTE: Explicit return type needed to avoid TS2742 (inferred type references internal path)
|
|
1145
|
+
syncStates: (): Promise<{ session: SyncState.SyncState; leader: SyncState.SyncState }> =>
|
|
1032
1146
|
Effect.gen(this, function* () {
|
|
1033
1147
|
const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
|
|
1034
1148
|
const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
|
|
@@ -1039,11 +1153,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1039
1153
|
Effect.gen(this, function* () {
|
|
1040
1154
|
const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
|
|
1041
1155
|
yield* Effect.log(
|
|
1042
|
-
`Session sync state: ${session.localHead} (upstream: ${session.upstreamHead})`,
|
|
1156
|
+
`Session sync state: ${objectToString(session.localHead)} (upstream: ${objectToString(session.upstreamHead)})`,
|
|
1043
1157
|
session.toJSON(),
|
|
1044
1158
|
)
|
|
1045
1159
|
const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
|
|
1046
|
-
yield* Effect.log(
|
|
1160
|
+
yield* Effect.log(
|
|
1161
|
+
`Leader sync state: ${objectToString(leader.localHead)} (upstream: ${objectToString(leader.upstreamHead)})`,
|
|
1162
|
+
leader.toJSON(),
|
|
1163
|
+
)
|
|
1047
1164
|
}).pipe(this.runEffectFork)
|
|
1048
1165
|
},
|
|
1049
1166
|
|
package/src/utils/dev.ts
CHANGED
|
@@ -33,8 +33,8 @@ export const downloadURL = (data: string, fileName: string) => {
|
|
|
33
33
|
export const exposeDebugUtils = () => {
|
|
34
34
|
globalThis.__debugLiveStoreUtils = {
|
|
35
35
|
downloadBlob,
|
|
36
|
-
runSync: (effect: Effect.Effect<
|
|
37
|
-
runFork: (effect: Effect.Effect<
|
|
36
|
+
runSync: <A, E>(effect: Effect.Effect<A, E>) => Effect.runSync(effect),
|
|
37
|
+
runFork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runFork(effect),
|
|
38
38
|
dumpDb: (db: SqliteDb) => {
|
|
39
39
|
const tables = db.select<{ name: string }>(`SELECT name FROM sqlite_master WHERE type='table'`)
|
|
40
40
|
for (const table of tables) {
|
package/src/utils/stack-info.ts
CHANGED
|
@@ -41,12 +41,12 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
|
|
|
41
41
|
// console.debug(name, filePath)
|
|
42
42
|
|
|
43
43
|
// NOTE No idea where this `Module.` comes from - possibly a Vite thing?
|
|
44
|
-
if ((name.startsWith('use') || name.startsWith('Module.use')) && name.endsWith('QueryRef') === false) {
|
|
44
|
+
if ((name.startsWith('use') === true || name.startsWith('Module.use') === true) && name.endsWith('QueryRef') === false) {
|
|
45
45
|
hasReachedStart = true
|
|
46
46
|
// console.debug('hasReachedStart. adding one more frame.')
|
|
47
47
|
|
|
48
48
|
frames.unshift({ name: name.replace(/^Module\./, ''), filePath })
|
|
49
|
-
} else if (hasReachedStart) {
|
|
49
|
+
} else if (hasReachedStart === true) {
|
|
50
50
|
// We've reached the end of the `use*` functions, so we're adding the component name and stop
|
|
51
51
|
// Unless it's `react-stack-bottom-frame`, which we skip
|
|
52
52
|
if (name !== 'Object.react-stack-bottom-frame') {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
1
3
|
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
2
4
|
import { provideOtel } from '@livestore/common'
|
|
3
5
|
import { createStore, Events, makeSchema, State } from '@livestore/livestore'
|
|
4
6
|
import { omitUndefineds } from '@livestore/utils'
|
|
5
7
|
import { Effect, Schema } from '@livestore/utils/effect'
|
|
6
|
-
import type * as otel from '@opentelemetry/api'
|
|
7
8
|
|
|
8
9
|
export type Todo = {
|
|
9
10
|
id: string
|
package/src/utils/tests/otel.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { omitUndefineds } from '@livestore/utils'
|
|
2
|
-
import { identity } from '@livestore/utils/effect'
|
|
3
1
|
import type { Attributes } from '@opentelemetry/api'
|
|
4
2
|
import type { InMemorySpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
|
5
3
|
|
|
4
|
+
import { omitUndefineds } from '@livestore/utils'
|
|
5
|
+
import { identity } from '@livestore/utils/effect'
|
|
6
|
+
|
|
6
7
|
type SimplifiedNestedSpan = { _name: string; attributes: any; children: SimplifiedNestedSpan[] }
|
|
7
8
|
|
|
8
9
|
type NestedSpan = { span: ReadableSpan; children: NestedSpan[] }
|
|
@@ -19,8 +20,8 @@ const buildSimplifiedRootSpans = (
|
|
|
19
20
|
|
|
20
21
|
spansMap.forEach((nestedSpan) => {
|
|
21
22
|
const parentId = nestedSpan.span.parentSpanContext?.spanId
|
|
22
|
-
const parentSpan = parentId ? spansMap.get(parentId) : undefined
|
|
23
|
-
if (parentSpan) {
|
|
23
|
+
const parentSpan = parentId !== undefined ? spansMap.get(parentId) : undefined
|
|
24
|
+
if (parentSpan !== undefined) {
|
|
24
25
|
parentSpan.children.push(nestedSpan)
|
|
25
26
|
}
|
|
26
27
|
})
|
|
@@ -55,7 +56,7 @@ export const getSimplifiedRootSpan = (
|
|
|
55
56
|
): SimplifiedNestedSpan => {
|
|
56
57
|
const results = buildSimplifiedRootSpans(exporter, rootSpanName, mapAttributes)
|
|
57
58
|
const firstResult = results[0]
|
|
58
|
-
if (
|
|
59
|
+
if (firstResult == null) throw new Error(`Could not find the root span named '${rootSpanName}'.`)
|
|
59
60
|
return firstResult
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -77,7 +78,7 @@ const omitEmpty = (obj: any) => {
|
|
|
77
78
|
for (const key in obj) {
|
|
78
79
|
if (
|
|
79
80
|
obj[key] !== undefined &&
|
|
80
|
-
!(Array.isArray(obj[key]) && obj[key].length === 0) &&
|
|
81
|
+
!(Array.isArray(obj[key]) === true && obj[key].length === 0) &&
|
|
81
82
|
Object.keys(obj[key]).length > 0
|
|
82
83
|
) {
|
|
83
84
|
result[key] = obj[key]
|
|
@@ -119,7 +120,7 @@ export const toTraceFile = (spans: ReadableSpan[]) => {
|
|
|
119
120
|
typeof value === 'string'
|
|
120
121
|
? { stringValue: value }
|
|
121
122
|
: typeof value === 'number'
|
|
122
|
-
? Number.isInteger(value)
|
|
123
|
+
? Number.isInteger(value) === true
|
|
123
124
|
? { intValue: value }
|
|
124
125
|
: { doubleValue: value }
|
|
125
126
|
: typeof value === 'boolean'
|
package/docs/api/index.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Data modeling
|
|
2
|
-
|
|
3
|
-
## Core idea
|
|
4
|
-
|
|
5
|
-
- Data modeling is probably the most important part of any app and needs to be done carefully.
|
|
6
|
-
- The core idea is to model the read and write model separately.
|
|
7
|
-
- Depending on the use case, you might also want to split up the read/write model into separate "containers" (e.g. for data-sharing/scalability/access control reasons).
|
|
8
|
-
- There is no transactional consistency between containers.
|
|
9
|
-
- Caveat: Event sourcing is not ideal for all use cases - some apps might be better off with another approach (e.g. use CRDTs for rich text editing).
|
|
10
|
-
|
|
11
|
-
## Considerations for data modeling
|
|
12
|
-
|
|
13
|
-
- How much data do you expect to have and what is the shape of the data?
|
|
14
|
-
- Some kind of data needs special handling (e.g. blobs or rich text)
|
|
15
|
-
- Access patterns (performance, ...)
|
|
16
|
-
- Access control
|
|
17
|
-
- Data integrity / consistency
|
|
18
|
-
- Sharing / collaboration
|
|
19
|
-
- Regulatory requirements (e.g. GDPR, audit logs, ...)
|
|
20
|
-
|
|
21
|
-
## TODO
|
|
22
|
-
|
|
23
|
-
- TODO: actually write this section
|
|
24
|
-
- questions to answer
|
|
25
|
-
- When to split things into separate containers?
|
|
26
|
-
- How do migrations work?
|
|
27
|
-
- Read model migrations
|
|
28
|
-
- Write model migrations
|
|
29
|
-
- How to create new write models based on existing ones
|
|
30
|
-
- Example: An app has multiple workspaces and you now want to introduce the concept of "projects" inside a workspace. You might want to pre-populate a "default workspace project" for each workspace.
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Debugging a LiveStore app
|
|
2
|
-
|
|
3
|
-
When working on a LiveStore app you might end up in situations where you need to debug things. LiveStore is built with debuggability in mind and tries to make your life as a developer as easy as possible.
|
|
4
|
-
|
|
5
|
-
Here are a few things that LiveStore offers to help you debug your app:
|
|
6
|
-
|
|
7
|
-
- [OpenTelemetry](/building-with-livestore/opentelemetry) integration for tracing / metrics
|
|
8
|
-
- [Devtools](/building-with-livestore/devtools) for inspecting the state of the store
|
|
9
|
-
- Store helper methods
|
|
10
|
-
|
|
11
|
-
## Debugging helpers on the store
|
|
12
|
-
|
|
13
|
-
The `store` exposes a `_dev` property which contains a few helpers that can help you debug your app.
|
|
14
|
-
|
|
15
|
-
## Other recommended practices and tools
|
|
16
|
-
|
|
17
|
-
- Use the step debugger
|