@livestore/livestore 0.4.0-dev.16 → 0.4.0-dev.18

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 (40) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SqliteDbWrapper.test.js +2 -1
  3. package/dist/SqliteDbWrapper.test.js.map +1 -1
  4. package/dist/live-queries/client-document-get-query.js +3 -2
  5. package/dist/live-queries/client-document-get-query.js.map +1 -1
  6. package/dist/live-queries/db-query.d.ts.map +1 -1
  7. package/dist/live-queries/db-query.js +6 -4
  8. package/dist/live-queries/db-query.js.map +1 -1
  9. package/dist/live-queries/db-query.test.js +64 -4
  10. package/dist/live-queries/db-query.test.js.map +1 -1
  11. package/dist/mod.d.ts +1 -2
  12. package/dist/mod.d.ts.map +1 -1
  13. package/dist/mod.js +1 -1
  14. package/dist/mod.js.map +1 -1
  15. package/dist/store/create-store.d.ts.map +1 -1
  16. package/dist/store/create-store.js +3 -2
  17. package/dist/store/create-store.js.map +1 -1
  18. package/dist/store/devtools.d.ts +2 -13
  19. package/dist/store/devtools.d.ts.map +1 -1
  20. package/dist/store/devtools.js +34 -13
  21. package/dist/store/devtools.js.map +1 -1
  22. package/dist/store/store-types.d.ts +89 -4
  23. package/dist/store/store-types.d.ts.map +1 -1
  24. package/dist/store/store-types.js +1 -0
  25. package/dist/store/store-types.js.map +1 -1
  26. package/dist/store/store.d.ts +20 -26
  27. package/dist/store/store.d.ts.map +1 -1
  28. package/dist/store/store.js +129 -91
  29. package/dist/store/store.js.map +1 -1
  30. package/package.json +5 -5
  31. package/src/SqliteDbWrapper.test.ts +2 -2
  32. package/src/live-queries/__snapshots__/db-query.test.ts.snap +220 -0
  33. package/src/live-queries/client-document-get-query.ts +3 -3
  34. package/src/live-queries/db-query.test.ts +103 -6
  35. package/src/live-queries/db-query.ts +7 -4
  36. package/src/mod.ts +8 -8
  37. package/src/store/create-store.ts +3 -2
  38. package/src/store/devtools.ts +40 -26
  39. package/src/store/store-types.ts +107 -4
  40. package/src/store/store.ts +166 -123
@@ -1,23 +1,14 @@
1
- import type { ClientSession, ClientSessionSyncProcessor, DebugInfo, SyncState } from '@livestore/common'
1
+ import type { DebugInfo, SyncState } from '@livestore/common'
2
2
  import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
3
3
  import { throttle } from '@livestore/utils'
4
4
  import type { WebChannel } from '@livestore/utils/effect'
5
5
  import { Effect, Stream } from '@livestore/utils/effect'
6
6
  import { nanoid } from '@livestore/utils/nanoid'
7
7
 
8
- import type { LiveQuery, ReactivityGraph } from '../live-queries/base-class.ts'
9
8
  import { NOT_REFRESHED_YET } from '../reactive.ts'
10
- import type { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
11
9
  import { emptyDebugInfo as makeEmptyDebugInfo } from '../SqliteDbWrapper.ts'
12
- import type { ReferenceCountedSet } from '../utils/data-structures.ts'
13
-
14
- type IStore = {
15
- clientSession: ClientSession
16
- reactivityGraph: ReactivityGraph
17
- sqliteDbWrapper: SqliteDbWrapper
18
- activeQueries: ReferenceCountedSet<LiveQuery<any>>
19
- syncProcessor: ClientSessionSyncProcessor
20
- }
10
+ import type { Store } from './store.ts'
11
+ import { StoreInternalsSymbol } from './store-types.ts'
21
12
 
22
13
  type Unsub = () => void
23
14
  type RequestId = string
@@ -40,7 +31,7 @@ export const connectDevtoolsToStore = ({
40
31
  Devtools.ClientSession.MessageToApp,
41
32
  Devtools.ClientSession.MessageFromApp
42
33
  >
43
- store: IStore
34
+ store: Store
44
35
  }) =>
45
36
  Effect.gen(function* () {
46
37
  const reactivityGraphSubcriptions: SubMap = new Map()
@@ -48,7 +39,7 @@ export const connectDevtoolsToStore = ({
48
39
  const debugInfoHistorySubscriptions: SubMap = new Map()
49
40
  const syncHeadClientSessionSubscriptions: SubMap = new Map()
50
41
 
51
- const { clientId, sessionId } = store.clientSession
42
+ const { clientId, sessionId } = store[StoreInternalsSymbol].clientSession
52
43
 
53
44
  yield* Effect.addFinalizer(() =>
54
45
  Effect.sync(() => {
@@ -73,7 +64,21 @@ export const connectDevtoolsToStore = ({
73
64
  }
74
65
 
75
66
  if (decodedMessage._tag === 'LSD.ClientSession.Disconnect') {
76
- // console.error('TODO handle disconnect properly in store')
67
+ // Gracefully tear down all DevTools subscriptions and close the channel.
68
+ // This prevents background fibers from lingering after DevTools closes
69
+ // (e.g. when a window is closed without sending explicit unsubs).
70
+ for (const unsub of reactivityGraphSubcriptions.values()) unsub()
71
+ reactivityGraphSubcriptions.clear()
72
+ for (const unsub of liveQueriesSubscriptions.values()) unsub()
73
+ liveQueriesSubscriptions.clear()
74
+ for (const unsub of debugInfoHistorySubscriptions.values()) unsub()
75
+ debugInfoHistorySubscriptions.clear()
76
+ for (const unsub of syncHeadClientSessionSubscriptions.values()) unsub()
77
+ syncHeadClientSessionSubscriptions.clear()
78
+
79
+ // Signal the WebChannel to shut down; this causes the `listen` stream
80
+ // to complete and allows the surrounding scoped fiber to exit.
81
+ storeDevtoolsChannel.shutdown.pipe(Effect.runFork)
77
82
  return
78
83
  }
79
84
 
@@ -103,7 +108,7 @@ export const connectDevtoolsToStore = ({
103
108
  () =>
104
109
  sendToDevtools(
105
110
  Devtools.ClientSession.ReactivityGraphRes.make({
106
- reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
111
+ reactivityGraph: store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults }),
107
112
  requestId: nanoid(10),
108
113
  clientId,
109
114
  sessionId,
@@ -121,14 +126,17 @@ export const connectDevtoolsToStore = ({
121
126
  // This might need to be tweaked further and possibly be exposed to the user in some way.
122
127
  const throttledSend = throttle(send, 20)
123
128
 
124
- reactivityGraphSubcriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
129
+ reactivityGraphSubcriptions.set(
130
+ subscriptionId,
131
+ store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
132
+ )
125
133
 
126
134
  break
127
135
  }
128
136
  case 'LSD.ClientSession.DebugInfoReq': {
129
137
  sendToDevtools(
130
138
  Devtools.ClientSession.DebugInfoRes.make({
131
- debugInfo: store.sqliteDbWrapper.debugInfo,
139
+ debugInfo: store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo,
132
140
  requestId,
133
141
  clientId,
134
142
  sessionId,
@@ -144,13 +152,13 @@ export const connectDevtoolsToStore = ({
144
152
  let tickHandle: number | undefined
145
153
 
146
154
  const tick = () => {
147
- buffer.push(store.sqliteDbWrapper.debugInfo)
155
+ buffer.push(store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo)
148
156
 
149
157
  // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
150
158
  // will get the empty debug info
151
159
  // TODO We need to come up with a more graceful way to do store. Probably via a single global
152
160
  // `requestAnimationFrame` loop that is passed in somehow.
153
- store.sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
161
+ store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
154
162
 
155
163
  if (buffer.length > 10) {
156
164
  sendToDevtools(
@@ -194,7 +202,7 @@ export const connectDevtoolsToStore = ({
194
202
  break
195
203
  }
196
204
  case 'LSD.ClientSession.DebugInfoResetReq': {
197
- store.sqliteDbWrapper.debugInfo.slowQueries.clear()
205
+ store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo.slowQueries.clear()
198
206
  sendToDevtools(
199
207
  Devtools.ClientSession.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
200
208
  )
@@ -202,7 +210,10 @@ export const connectDevtoolsToStore = ({
202
210
  }
203
211
  case 'LSD.ClientSession.DebugInfoRerunQueryReq': {
204
212
  const { queryStr, bindValues, queriedTables } = decodedMessage
205
- store.sqliteDbWrapper.cachedSelect(queryStr, bindValues, { queriedTables, skipCache: true })
213
+ store[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(queryStr, bindValues, {
214
+ queriedTables,
215
+ skipCache: true,
216
+ })
206
217
  sendToDevtools(
207
218
  Devtools.ClientSession.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
208
219
  )
@@ -223,7 +234,7 @@ export const connectDevtoolsToStore = ({
223
234
  () =>
224
235
  sendToDevtools(
225
236
  Devtools.ClientSession.LiveQueriesRes.make({
226
- liveQueries: [...store.activeQueries].map((q) => ({
237
+ liveQueries: [...store[StoreInternalsSymbol].activeQueries].map((q) => ({
227
238
  _tag: q._tag,
228
239
  id: q.id,
229
240
  label: q.label,
@@ -251,7 +262,10 @@ export const connectDevtoolsToStore = ({
251
262
  // Same as in the reactivity graph subscription case above, we need to throttle the updates
252
263
  const throttledSend = throttle(send, 20)
253
264
 
254
- liveQueriesSubscriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
265
+ liveQueriesSubscriptions.set(
266
+ subscriptionId,
267
+ store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
268
+ )
255
269
 
256
270
  break
257
271
  }
@@ -278,11 +292,11 @@ export const connectDevtoolsToStore = ({
278
292
  }),
279
293
  )
280
294
 
281
- send(store.syncProcessor.syncState.pipe(Effect.runSync))
295
+ send(store[StoreInternalsSymbol].syncProcessor.syncState.pipe(Effect.runSync))
282
296
 
283
297
  syncHeadClientSessionSubscriptions.set(
284
298
  subscriptionId,
285
- store.syncProcessor.syncState.changes.pipe(
299
+ store[StoreInternalsSymbol].syncProcessor.syncState.changes.pipe(
286
300
  Stream.tap((syncState) => send(syncState)),
287
301
  Stream.runDrain,
288
302
  Effect.interruptible,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type ClientSession,
3
+ type ClientSessionSyncProcessor,
3
4
  type ClientSessionSyncProcessorSimulationParams,
4
5
  type IntentionalShutdownCause,
5
6
  type InvalidPullError,
@@ -12,13 +13,20 @@ import {
12
13
  type UnexpectedError,
13
14
  } from '@livestore/common'
14
15
  import type { EventSequenceNumber, LiveStoreEvent, LiveStoreSchema } from '@livestore/common/schema'
15
- import type { Effect, Runtime, Scope } from '@livestore/utils/effect'
16
+ import type { Effect, Runtime, Schema, Scope } from '@livestore/utils/effect'
16
17
  import { Deferred, Predicate } from '@livestore/utils/effect'
17
18
  import type * as otel from '@opentelemetry/api'
18
-
19
- import type { LiveQuery, LiveQueryDef, SignalDef } from '../live-queries/base-class.ts'
19
+ import type {
20
+ LiveQuery,
21
+ LiveQueryDef,
22
+ ReactivityGraph,
23
+ ReactivityGraphContext,
24
+ SignalDef,
25
+ } from '../live-queries/base-class.ts'
20
26
  import { TypeId } from '../live-queries/base-class.ts'
21
- import type { DebugRefreshReasonBase } from '../reactive.ts'
27
+ import type { DebugRefreshReasonBase, Ref } from '../reactive.ts'
28
+ import type { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
29
+ import type { ReferenceCountedSet } from '../utils/data-structures.ts'
22
30
  import type { StackInfo } from '../utils/stack-info.ts'
23
31
  import type { Store } from './store.ts'
24
32
 
@@ -52,6 +60,101 @@ export type OtelOptions = {
52
60
  rootSpanContext: otel.Context
53
61
  }
54
62
 
63
+ export const StoreInternalsSymbol = Symbol.for('livestore.StoreInternals')
64
+ export type StoreInternalsSymbol = typeof StoreInternalsSymbol
65
+
66
+ /**
67
+ * Opaque bag containing the Store's implementation details.
68
+ *
69
+ * Not part of the public API — shapes and semantics may change without notice.
70
+ * Access only from within the @livestore/livestore package (and Devtools) via
71
+ * `StoreInternalsSymbol` to avoid accidental coupling in application code.
72
+ */
73
+ export type StoreInternals = {
74
+ /**
75
+ * Runtime event schema used for encoding/decoding events.
76
+ *
77
+ * Exposed primarily for Devtools (e.g. databrowser) to validate ad‑hoc
78
+ * event payloads. Application code should not depend on it directly.
79
+ */
80
+ readonly eventSchema: Schema.Schema<LiveStoreEvent.AnyDecoded, LiveStoreEvent.AnyEncoded>
81
+
82
+ /**
83
+ * The active client session backing this Store. Provides access to the
84
+ * leader thread, network status, and shutdown signaling.
85
+ *
86
+ * Do not close or mutate directly — use `store.shutdown(...)`.
87
+ */
88
+ readonly clientSession: ClientSession
89
+
90
+ /**
91
+ * Wrapper around the local SQLite state database. Centralizes query
92
+ * planning, caching, and change tracking used by reads and materializers.
93
+ */
94
+ readonly sqliteDbWrapper: SqliteDbWrapper
95
+
96
+ /**
97
+ * Effect runtime and scope used to fork background fibers for the Store.
98
+ *
99
+ * - `runtime` executes effects from imperative Store APIs.
100
+ * - `lifetimeScope` owns forked fibers; closed during Store shutdown.
101
+ */
102
+ readonly effectContext: {
103
+ /** Effect runtime to run Store effects with proper environment. */
104
+ readonly runtime: Runtime.Runtime<Scope.Scope>
105
+ /** Scope that owns all long‑lived fibers spawned by the Store. */
106
+ readonly lifetimeScope: Scope.Scope
107
+ }
108
+
109
+ /**
110
+ * OpenTelemetry primitives used for instrumentation of commits, queries,
111
+ * and Store boot lifecycle.
112
+ */
113
+ readonly otel: StoreOtel
114
+
115
+ /**
116
+ * The Store's reactive graph instance used to model dependencies and
117
+ * propagate updates. Provides APIs to create refs/thunks/effects and to
118
+ * subscribe to refresh cycles.
119
+ */
120
+ readonly reactivityGraph: ReactivityGraph
121
+
122
+ /**
123
+ * Per‑table reactive refs used to broadcast invalidations when materializers
124
+ * write to tables. Values are always `null`; equality is intentionally
125
+ * `false` to force recomputation.
126
+ *
127
+ * Keys are SQLite table names (user tables; some system tables may be
128
+ * intentionally excluded from refresh).
129
+ */
130
+ readonly tableRefs: Readonly<Record<string, Ref<null, ReactivityGraphContext, RefreshReason>>>
131
+
132
+ /**
133
+ * Set of currently subscribed LiveQuery instances (reference‑counted).
134
+ * Used for Devtools and diagnostics.
135
+ */
136
+ readonly activeQueries: ReferenceCountedSet<LiveQuery<any>>
137
+
138
+ /**
139
+ * Client‑session sync processor orchestrating push/pull and materialization
140
+ * of events into local state.
141
+ */
142
+ readonly syncProcessor: ClientSessionSyncProcessor
143
+
144
+ /**
145
+ * Starts background fibers for sync and observation. Must be run exactly
146
+ * once per Store instance. Scoped; installs finalizers to end spans and
147
+ * detach reactive refs.
148
+ */
149
+ readonly boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
150
+
151
+ /**
152
+ * Tracks whether the Store has been shut down. When true, mutating APIs
153
+ * should reject via `checkShutdown`.
154
+ */
155
+ isShutdown: boolean
156
+ }
157
+
55
158
  export type StoreOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> = {
56
159
  clientSession: ClientSession
57
160
  schema: TSchema