@livestore/livestore 0.3.2-dev.10 → 0.3.2-dev.11

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 (52) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/live-queries/base-class.d.ts +5 -0
  3. package/dist/live-queries/base-class.d.ts.map +1 -1
  4. package/dist/live-queries/base-class.js +1 -1
  5. package/dist/live-queries/base-class.js.map +1 -1
  6. package/dist/live-queries/computed.d.ts.map +1 -1
  7. package/dist/live-queries/computed.js +7 -0
  8. package/dist/live-queries/computed.js.map +1 -1
  9. package/dist/live-queries/db-query.d.ts.map +1 -1
  10. package/dist/live-queries/db-query.js +12 -2
  11. package/dist/live-queries/db-query.js.map +1 -1
  12. package/dist/live-queries/signal.d.ts.map +1 -1
  13. package/dist/live-queries/signal.js +7 -0
  14. package/dist/live-queries/signal.js.map +1 -1
  15. package/dist/mod.d.ts +15 -56
  16. package/dist/mod.d.ts.map +1 -1
  17. package/dist/mod.js +1 -0
  18. package/dist/mod.js.map +1 -1
  19. package/dist/reactive.d.ts.map +1 -1
  20. package/dist/reactive.js +2 -0
  21. package/dist/reactive.js.map +1 -1
  22. package/dist/store/create-store.d.ts +5 -7
  23. package/dist/store/create-store.d.ts.map +1 -1
  24. package/dist/store/create-store.js +4 -4
  25. package/dist/store/create-store.js.map +1 -1
  26. package/dist/store/store-shutdown.test.d.ts +2 -0
  27. package/dist/store/store-shutdown.test.d.ts.map +1 -0
  28. package/dist/store/store-shutdown.test.js +103 -0
  29. package/dist/store/store-shutdown.test.js.map +1 -0
  30. package/dist/store/store-types.d.ts +4 -4
  31. package/dist/store/store-types.d.ts.map +1 -1
  32. package/dist/store/store-types.js.map +1 -1
  33. package/dist/store/store.d.ts +11 -2
  34. package/dist/store/store.d.ts.map +1 -1
  35. package/dist/store/store.js +76 -43
  36. package/dist/store/store.js.map +1 -1
  37. package/dist/utils/dev.d.ts.map +1 -1
  38. package/dist/utils/dev.js +14 -8
  39. package/dist/utils/dev.js.map +1 -1
  40. package/dist/utils/tests/fixture.d.ts +8 -46
  41. package/dist/utils/tests/fixture.d.ts.map +1 -1
  42. package/package.json +8 -8
  43. package/src/live-queries/base-class.ts +5 -1
  44. package/src/live-queries/computed.ts +7 -0
  45. package/src/live-queries/db-query.ts +12 -3
  46. package/src/live-queries/signal.ts +7 -0
  47. package/src/mod.ts +2 -1
  48. package/src/reactive.ts +2 -0
  49. package/src/store/create-store.ts +18 -16
  50. package/src/store/store-types.ts +9 -5
  51. package/src/store/store.ts +67 -8
  52. package/src/utils/dev.ts +14 -7
@@ -33,29 +33,10 @@ export declare const todos: State.SQLite.TableDef<State.SQLite.SqliteTableDefFor
33
33
  nullable: false;
34
34
  primaryKey: false;
35
35
  };
36
- }>, State.SQLite.WithDefaults<{
37
- readonly id: {
38
- columnType: "text";
39
- schema: Schema.Schema<string, string, never>;
40
- default: import("effect/Option").None<never>;
41
- nullable: false;
42
- primaryKey: true;
43
- };
44
- readonly text: {
45
- columnType: "text";
46
- schema: Schema.Schema<string, string, never>;
47
- default: import("effect/Option").Some<"">;
48
- nullable: false;
49
- primaryKey: false;
50
- };
51
- readonly completed: {
52
- columnType: "integer";
53
- schema: Schema.Schema<boolean, number, never>;
54
- default: import("effect/Option").Some<false>;
55
- nullable: false;
56
- primaryKey: false;
57
- };
58
- }>, Schema.Schema<{
36
+ }>, {
37
+ isClientDocumentTable: false;
38
+ requiredInsertColumnNames: "id";
39
+ }, Schema.Schema<{
59
40
  readonly id: string;
60
41
  readonly text: string;
61
42
  readonly completed: boolean;
@@ -103,29 +84,10 @@ export declare const tables: {
103
84
  nullable: false;
104
85
  primaryKey: false;
105
86
  };
106
- }>, State.SQLite.WithDefaults<{
107
- readonly id: {
108
- columnType: "text";
109
- schema: Schema.Schema<string, string, never>;
110
- default: import("effect/Option").None<never>;
111
- nullable: false;
112
- primaryKey: true;
113
- };
114
- readonly text: {
115
- columnType: "text";
116
- schema: Schema.Schema<string, string, never>;
117
- default: import("effect/Option").Some<"">;
118
- nullable: false;
119
- primaryKey: false;
120
- };
121
- readonly completed: {
122
- columnType: "integer";
123
- schema: Schema.Schema<boolean, number, never>;
124
- default: import("effect/Option").Some<false>;
125
- nullable: false;
126
- primaryKey: false;
127
- };
128
- }>, Schema.Schema<{
87
+ }>, {
88
+ isClientDocumentTable: false;
89
+ requiredInsertColumnNames: "id";
90
+ }, Schema.Schema<{
129
91
  readonly id: string;
130
92
  readonly text: string;
131
93
  readonly completed: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../../../src/utils/tests/fixture.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmC,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,KAAK,KAAK,IAAI,MAAM,oBAAoB,CAAA;AAE/C,MAAM,MAAM,IAAI,GAAG;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,WAAW,CAAA;AAEnD,MAAM,MAAM,QAAQ,GAAG;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAOhB,CAAA;AAEF,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;EAOd,CAAA;AAEF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAiB,CAAA;AAEpC,eAAO,MAAM,MAAM;;;;;;;;;;CASlB,CAAA;AAMD,eAAO,MAAM,KAAK,8CAAoD,CAAA;AACtE,eAAO,MAAM,MAAM;;;;;;;;;;;;;EAAgC,CAAA;AAEnD,eAAO,MAAM,WAAW,GAAI,+BAGzB;IACD,UAAU,CAAC,EAAE,IAAI,CAAC,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,IAAI,CAAC,OAAO,CAAA;CACtB;;;;;;;;;;;;;mFAU4E,CAAA"}
1
+ {"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../../../src/utils/tests/fixture.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmC,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,KAAK,KAAK,IAAI,MAAM,oBAAoB,CAAA;AAE/C,MAAM,MAAM,IAAI,GAAG;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,WAAW,CAAA;AAEnD,MAAM,MAAM,QAAQ,GAAG;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAOhB,CAAA;AAEF,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;EAOd,CAAA;AAEF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAiB,CAAA;AAEpC,eAAO,MAAM,MAAM;;;;;;;;;;CASlB,CAAA;AAMD,eAAO,MAAM,KAAK,8CAAoD,CAAA;AACtE,eAAO,MAAM,MAAM;;;;;;;;;;;;;EAAgC,CAAA;AAEnD,eAAO,MAAM,WAAW,GAAI,+BAGzB;IACD,UAAU,CAAC,EAAE,IAAI,CAAC,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,IAAI,CAAC,OAAO,CAAA;CACtB;;;;;;;;;;;;;mFAU4E,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/livestore",
3
- "version": "0.3.2-dev.10",
3
+ "version": "0.3.2-dev.11",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -11,17 +11,17 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@opentelemetry/api": "1.9.0",
14
- "@livestore/common": "0.3.2-dev.10",
15
- "@livestore/utils": "0.3.2-dev.10"
14
+ "@livestore/common": "0.3.2-dev.11",
15
+ "@livestore/utils": "0.3.2-dev.11"
16
16
  },
17
17
  "devDependencies": {
18
- "@opentelemetry/sdk-trace-base": "^2.0.0",
19
- "jsdom": "^26.0.0",
18
+ "@opentelemetry/sdk-trace-base": "^2.0.1",
19
+ "jsdom": "^26.1.0",
20
20
  "typescript": "^5.8.3",
21
- "vite": "^7.0.0",
21
+ "vite": "^7.0.6",
22
22
  "vitest": "3.2.4",
23
- "@livestore/adapter-web": "0.3.2-dev.10",
24
- "@livestore/utils-dev": "0.3.2-dev.10"
23
+ "@livestore/adapter-web": "0.3.2-dev.11",
24
+ "@livestore/utils-dev": "0.3.2-dev.11"
25
25
  },
26
26
  "files": [
27
27
  "package.json",
@@ -1,5 +1,5 @@
1
1
  import { isNotNil } from '@livestore/utils'
2
- import { Predicate } from '@livestore/utils/effect'
2
+ import { Equal, Hash, Predicate } from '@livestore/utils/effect'
3
3
  import type * as otel from '@opentelemetry/api'
4
4
 
5
5
  import * as RG from '../reactive.ts'
@@ -41,6 +41,8 @@ export interface SignalDef<T> extends LiveQueryDef<T, 'signal-def'> {
41
41
  hash: string
42
42
  label: string
43
43
  make: (ctx: ReactivityGraphContext) => RcRef<ISignal<T>>
44
+ [Equal.symbol](that: SignalDef<T>): boolean
45
+ [Hash.symbol](): number
44
46
  }
45
47
 
46
48
  export interface ISignal<T> extends LiveQuery<T> {
@@ -77,6 +79,8 @@ export interface LiveQueryDef<TResult, TTag extends string = 'def'> {
77
79
  make: (ctx: ReactivityGraphContext, otelContext?: otel.Context) => RcRef<LiveQuery<TResult> | ISignal<TResult>>
78
80
  label: string
79
81
  hash: string
82
+ [Equal.symbol](that: LiveQueryDef<TResult, TTag>): boolean
83
+ [Hash.symbol](): number
80
84
  }
81
85
 
82
86
  export namespace LiveQueryDef {
@@ -1,4 +1,5 @@
1
1
  import { getDurationMsFromSpan } from '@livestore/common'
2
+ import { Equal, Hash } from '@livestore/utils/effect'
2
3
  import * as otel from '@opentelemetry/api'
3
4
 
4
5
  import type { Thunk } from '../reactive.ts'
@@ -35,6 +36,12 @@ export const computed = <TResult>(
35
36
  // TODO we should figure out whether this could cause some problems and/or if there's a better way to do this
36
37
  // NOTE `fn.toString()` doesn't work in Expo as it always produces `[native code]`
37
38
  hash,
39
+ [Equal.symbol](that: LiveQueryDef<any>): boolean {
40
+ return this.hash === that.hash
41
+ },
42
+ [Hash.symbol](): number {
43
+ return Hash.string(this.hash)
44
+ },
38
45
  }
39
46
 
40
47
  return def
@@ -10,7 +10,7 @@ import {
10
10
  UnexpectedError,
11
11
  } from '@livestore/common'
12
12
  import { deepEqual, shouldNeverHappen } from '@livestore/utils'
13
- import { Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
13
+ import { Equal, Hash, Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
14
14
  import * as otel from '@opentelemetry/api'
15
15
 
16
16
  import type { Thunk } from '../reactive.ts'
@@ -131,6 +131,12 @@ export const queryDb: {
131
131
  }),
132
132
  label,
133
133
  hash,
134
+ [Equal.symbol](that: LiveQueryDef<any>): boolean {
135
+ return this.hash === that.hash
136
+ },
137
+ [Hash.symbol](): number {
138
+ return Hash.string(this.hash)
139
+ },
134
140
  }
135
141
 
136
142
  return def
@@ -314,8 +320,11 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
314
320
 
315
321
  const queriedTablesRef: { current: Set<string> | undefined } = { current: undefined }
316
322
 
317
- const makeResultsEqual = (resultSchema: Schema.Schema<any, any>) => (a: TResult, b: TResult) =>
318
- a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : Schema.equivalence(resultSchema)(a, b)
323
+ const makeResultsEqual = (resultSchema: Schema.Schema<any, any>) => {
324
+ // Creating the equivalence function eagerly in outer scope as it might be expensive
325
+ const eq = Schema.equivalence(resultSchema)
326
+ return (a: TResult, b: TResult) => a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : eq(a, b)
327
+ }
319
328
 
320
329
  // NOTE we try to create the equality function eagerly as it might be expensive
321
330
  // TODO also support derived equality for `map` (probably will depend on having an easy way to transform a schema without an `encode` step)
@@ -1,3 +1,4 @@
1
+ import { Equal, Hash } from '@livestore/utils/effect'
1
2
  import { nanoid } from '@livestore/utils/nanoid'
2
3
 
3
4
  import type * as RG from '../reactive.ts'
@@ -27,6 +28,12 @@ export const signal = <T>(
27
28
  def,
28
29
  }),
29
30
  ),
31
+ [Equal.symbol](that: SignalDef<T>): boolean {
32
+ return this.hash === that.hash
33
+ },
34
+ [Hash.symbol](): number {
35
+ return Hash.string(this.hash)
36
+ },
30
37
  }
31
38
 
32
39
  return def
package/src/mod.ts CHANGED
@@ -35,11 +35,12 @@ export {
35
35
  export { emptyDebugInfo, SqliteDbWrapper } from './SqliteDbWrapper.ts'
36
36
  export { type CreateStoreOptions, createStore, createStorePromise } from './store/create-store.ts'
37
37
  export { Store } from './store/store.ts'
38
- export type { OtelOptions, QueryDebugInfo, RefreshReason } from './store/store-types.ts'
38
+ export type { OtelOptions, QueryDebugInfo, RefreshReason, Unsubscribe } from './store/store-types.ts'
39
39
  export {
40
40
  type LiveStoreContext,
41
41
  type LiveStoreContextRunning,
42
42
  makeShutdownDeferred,
43
43
  type ShutdownDeferred,
44
44
  } from './store/store-types.ts'
45
+ export { exposeDebugUtils } from './utils/dev.ts'
45
46
  export * from './utils/stack-info.ts'
package/src/reactive.ts CHANGED
@@ -211,8 +211,10 @@ export class ReactiveGraph<
211
211
 
212
212
  private refreshCallbacks: Set<() => void> = new Set()
213
213
 
214
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for debugging
214
215
  private nodeIdCounter = 0
215
216
  private uniqueNodeId = () => `node-${++this.nodeIdCounter}`
217
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for debugging
216
218
  private refreshInfoIdCounter = 0
217
219
  private uniqueRefreshInfoId = () => `refresh-info-${++this.refreshInfoIdCounter}`
218
220
 
@@ -1,16 +1,17 @@
1
- import type {
2
- Adapter,
3
- BootStatus,
4
- ClientSession,
5
- ClientSessionDevtoolsChannel,
6
- ClientSessionSyncProcessorSimulationParams,
7
- IntentionalShutdownCause,
8
- MigrationsReport,
1
+ import {
2
+ type Adapter,
3
+ type BootStatus,
4
+ type ClientSession,
5
+ type ClientSessionDevtoolsChannel,
6
+ type ClientSessionSyncProcessorSimulationParams,
7
+ type IntentionalShutdownCause,
8
+ type MigrationsReport,
9
+ provideOtel,
10
+ type SyncError,
11
+ UnexpectedError,
9
12
  } from '@livestore/common'
10
- import { provideOtel, UnexpectedError } from '@livestore/common'
11
13
  import type { LiveStoreSchema } from '@livestore/common/schema'
12
14
  import { isDevEnv, LS_DEV } from '@livestore/utils'
13
- import type { Cause, Schema } from '@livestore/utils/effect'
14
15
  import {
15
16
  Context,
16
17
  Deferred,
@@ -24,6 +25,7 @@ import {
24
25
  OtelTracer,
25
26
  Queue,
26
27
  Runtime,
28
+ type Schema,
27
29
  Scope,
28
30
  TaskTracing,
29
31
  } from '@livestore/utils/effect'
@@ -96,7 +98,7 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
96
98
  migrationsReport: MigrationsReport
97
99
  parentSpan: otel.Span
98
100
  },
99
- ) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
101
+ ) => Effect.SyncOrPromiseOrEffect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
100
102
  batchUpdates?: (run: () => void) => void
101
103
  /**
102
104
  * Whether to disable devtools.
@@ -131,7 +133,7 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
131
133
  }
132
134
 
133
135
  /** Create a new LiveStore Store */
134
- export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
136
+ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>({
135
137
  signal,
136
138
  otelOptions,
137
139
  ...options
@@ -162,7 +164,7 @@ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveS
162
164
  Effect.runPromise,
163
165
  )
164
166
 
165
- export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
167
+ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>({
166
168
  schema,
167
169
  adapter,
168
170
  storeId,
@@ -215,9 +217,9 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema, T
215
217
 
216
218
  const runtime = yield* Effect.runtime<Scope.Scope>()
217
219
 
218
- const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
220
+ const shutdown = (exit: Exit.Exit<IntentionalShutdownCause, UnexpectedError | SyncError>) =>
219
221
  Effect.gen(function* () {
220
- yield* Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
222
+ yield* Scope.close(lifetimeScope, exit).pipe(
221
223
  Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
222
224
  Effect.timeout(1000),
223
225
  Effect.catchTag('TimeoutException', () =>
@@ -226,7 +228,7 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema, T
226
228
  )
227
229
 
228
230
  if (shutdownDeferred) {
229
- yield* Deferred.failCause(shutdownDeferred, cause)
231
+ yield* Deferred.done(shutdownDeferred, exit)
230
232
  }
231
233
 
232
234
  yield* Effect.logDebug('LiveStore shutdown complete')
@@ -3,6 +3,7 @@ import type {
3
3
  ClientSessionSyncProcessorSimulationParams,
4
4
  IntentionalShutdownCause,
5
5
  StoreInterrupted,
6
+ SyncError,
6
7
  UnexpectedError,
7
8
  } from '@livestore/common'
8
9
  import type { EventSequenceNumber, LiveStoreEvent, LiveStoreSchema } from '@livestore/common/schema'
@@ -22,13 +23,16 @@ export type LiveStoreContext =
22
23
  }
23
24
  | {
24
25
  stage: 'shutdown'
25
- cause: IntentionalShutdownCause | StoreInterrupted
26
+ cause: IntentionalShutdownCause | StoreInterrupted | SyncError
26
27
  }
27
28
 
28
- export type ShutdownDeferred = Deferred.Deferred<void, UnexpectedError | IntentionalShutdownCause | StoreInterrupted>
29
+ export type ShutdownDeferred = Deferred.Deferred<
30
+ IntentionalShutdownCause,
31
+ UnexpectedError | SyncError | StoreInterrupted
32
+ >
29
33
  export const makeShutdownDeferred: Effect.Effect<ShutdownDeferred> = Deferred.make<
30
- void,
31
- UnexpectedError | IntentionalShutdownCause | StoreInterrupted
34
+ IntentionalShutdownCause,
35
+ UnexpectedError | SyncError | StoreInterrupted
32
36
  >()
33
37
 
34
38
  export type LiveStoreContextRunning = {
@@ -41,7 +45,7 @@ export type OtelOptions = {
41
45
  rootSpanContext: otel.Context
42
46
  }
43
47
 
44
- export type StoreOptions<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}> = {
48
+ export type StoreOptions<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> = {
45
49
  clientSession: ClientSession
46
50
  schema: TSchema
47
51
  storeId: string
@@ -22,7 +22,18 @@ import type { LiveStoreSchema } from '@livestore/common/schema'
22
22
  import { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
23
23
  import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils'
24
24
  import type { Scope } from '@livestore/utils/effect'
25
- import { Cause, Effect, Fiber, Inspectable, Option, OtelTracer, Runtime, Schema, Stream } from '@livestore/utils/effect'
25
+ import {
26
+ Cause,
27
+ Effect,
28
+ Exit,
29
+ Fiber,
30
+ Inspectable,
31
+ Option,
32
+ OtelTracer,
33
+ Runtime,
34
+ Schema,
35
+ Stream,
36
+ } from '@livestore/utils/effect'
26
37
  import { nanoid } from '@livestore/utils/nanoid'
27
38
  import * as otel from '@opentelemetry/api'
28
39
 
@@ -54,7 +65,7 @@ if (isDevEnv()) {
54
65
  exposeDebugUtils()
55
66
  }
56
67
 
57
- export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}> extends Inspectable.Class {
68
+ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> extends Inspectable.Class {
58
69
  readonly storeId: string
59
70
  reactivityGraph: ReactivityGraph
60
71
  sqliteDbWrapper: SqliteDbWrapper
@@ -68,6 +79,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
68
79
  */
69
80
  tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> }
70
81
 
82
+ /** Tracks whether the store has been shut down */
83
+ private isShutdown = false
84
+
71
85
  private effectContext: {
72
86
  runtime: Runtime.Runtime<Scope.Scope>
73
87
  lifetimeScope: Scope.Scope
@@ -282,6 +296,15 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
282
296
  return this.clientSession.clientId
283
297
  }
284
298
 
299
+ private checkShutdown = (operation: string): void => {
300
+ if (this.isShutdown) {
301
+ throw new UnexpectedError({
302
+ cause: `Store has been shut down (while performing "${operation}").`,
303
+ note: `You cannot perform this operation after the store has been shut down.`,
304
+ })
305
+ }
306
+ }
307
+
285
308
  /**
286
309
  * Subscribe to the results of a query
287
310
  * Returns a function to cancel the subscription.
@@ -309,8 +332,10 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
309
332
  /** If provided, the stack info will be added to the `activeSubscriptions` set of the query */
310
333
  stackInfo?: StackInfo
311
334
  },
312
- ): Unsubscribe =>
313
- this.otel.tracer.startActiveSpan(
335
+ ): Unsubscribe => {
336
+ this.checkShutdown('subscribe')
337
+
338
+ return this.otel.tracer.startActiveSpan(
314
339
  `LiveStore.subscribe`,
315
340
  { attributes: { label: options?.label, queryLabel: isQueryBuilder(query) ? query.toString() : query.label } },
316
341
  options?.otelContext ?? this.otel.queriesSpanContext,
@@ -369,6 +394,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
369
394
  return unsubscribe
370
395
  },
371
396
  )
397
+ }
372
398
 
373
399
  subscribeStream = <TResult>(
374
400
  query$: LiveQueryDef<TResult>,
@@ -417,6 +443,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
417
443
  | { query: string; bindValues: Bindable; schema?: Schema.Schema<TResult> },
418
444
  options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
419
445
  ): TResult => {
446
+ this.checkShutdown('query')
447
+
420
448
  if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
421
449
  const res = this.sqliteDbWrapper.cachedSelect(query.query, prepareBindValues(query.bindValues, query.query), {
422
450
  otelContext: options?.otelContext,
@@ -438,6 +466,12 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
438
466
 
439
467
  const sqlRes = query.asSql()
440
468
  const schema = getResultSchema(query)
469
+
470
+ // Replace SessionIdSymbol in bind values before executing the query
471
+ if (sqlRes.bindValues) {
472
+ replaceSessionIdSymbol(sqlRes.bindValues, this.clientSession.sessionId)
473
+ }
474
+
441
475
  const rawRes = this.sqliteDbWrapper.cachedSelect(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
442
476
  otelContext: options?.otelContext,
443
477
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
@@ -473,6 +507,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
473
507
  * ```
474
508
  */
475
509
  setSignal = <T>(signalDef: SignalDef<T>, value: T | ((prev: T) => T)): void => {
510
+ this.checkShutdown('setSignal')
511
+
476
512
  const signalRef = signalDef.make(this.reactivityGraph.context!)
477
513
  const newValue: T = typeof value === 'function' ? (value as any)(signalRef.value.get()) : value
478
514
  signalRef.value.set(newValue)
@@ -557,6 +593,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
557
593
  ) => void,
558
594
  ): void
559
595
  } = (firstEventOrTxnFnOrOptions: any, ...restEvents: any[]) => {
596
+ this.checkShutdown('commit')
597
+
560
598
  const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
561
599
 
562
600
  for (const event of events) {
@@ -660,10 +698,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
660
698
  * ```
661
699
  */
662
700
  events = (_options?: StoreEventsOptions<TSchema>): AsyncIterable<LiveStoreEvent.ForSchema<TSchema>> => {
701
+ this.checkShutdown('events')
702
+
663
703
  return notYetImplemented(`store.events() is not yet implemented but planned soon`)
664
704
  }
665
705
 
666
706
  eventsStream = (_options?: StoreEventsOptions<TSchema>): Stream.Stream<LiveStoreEvent.ForSchema<TSchema>> => {
707
+ this.checkShutdown('eventsStream')
708
+
667
709
  return notYetImplemented(`store.eventsStream() is not yet implemented but planned soon`)
668
710
  }
669
711
 
@@ -672,6 +714,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
672
714
  * We might need a better solution for this. Let's see.
673
715
  */
674
716
  manualRefresh = (options?: { label?: string }) => {
717
+ this.checkShutdown('manualRefresh')
718
+
675
719
  const { label } = options ?? {}
676
720
  this.otel.tracer.startActiveSpan(
677
721
  'LiveStore:manualRefresh',
@@ -690,10 +734,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext =
690
734
  *
691
735
  * This is called automatically when the store was created using the React or Effect API.
692
736
  */
693
- shutdown = async (cause?: Cause.Cause<UnexpectedError>) => {
694
- await this.clientSession
695
- .shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
696
- .pipe(this.runEffectFork, Fiber.join, Effect.runPromise)
737
+ shutdownPromise = async (cause?: UnexpectedError) => {
738
+ this.checkShutdown('shutdownPromise')
739
+
740
+ this.isShutdown = true
741
+ await this.shutdown(cause ? Cause.fail(cause) : undefined).pipe(this.runEffectFork, Fiber.join, Effect.runPromise)
742
+ }
743
+
744
+ /**
745
+ * Shuts down the store and closes the client session.
746
+ *
747
+ * This is called automatically when the store was created using the React or Effect API.
748
+ */
749
+ shutdown = (cause?: Cause.Cause<UnexpectedError>): Effect.Effect<void> => {
750
+ this.checkShutdown('shutdown')
751
+
752
+ this.isShutdown = true
753
+ return this.clientSession.shutdown(
754
+ cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
755
+ )
697
756
  }
698
757
 
699
758
  /**
package/src/utils/dev.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { isDevEnv } from '@livestore/utils'
1
+ import type { SqliteDb } from '@livestore/common'
2
+ import { prettyBytes } from '@livestore/utils'
2
3
  import { Effect } from '@livestore/utils/effect'
3
4
 
4
5
  export const downloadBlob = (
@@ -26,11 +27,17 @@ export const downloadURL = (data: string, fileName: string) => {
26
27
  }
27
28
 
28
29
  export const exposeDebugUtils = () => {
29
- if (isDevEnv()) {
30
- globalThis.__debugLiveStoreUtils = {
31
- downloadBlob,
32
- runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
33
- runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
34
- }
30
+ globalThis.__debugLiveStoreUtils = {
31
+ downloadBlob,
32
+ runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
33
+ runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
34
+ dumpDb: (db: SqliteDb) => {
35
+ const tables = db.select<{ name: string }>(`SELECT name FROM sqlite_master WHERE type='table'`)
36
+ for (const table of tables) {
37
+ const rows = db.select<any>(`SELECT * FROM ${table.name}`)
38
+ console.log(`Table: ${table.name} (${prettyBytes(table.name.length)}, ${rows.length} rows)`)
39
+ console.table(rows)
40
+ }
41
+ },
35
42
  }
36
43
  }