@livestore/livestore 0.0.55-dev.2 → 0.0.55

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 (54) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/react/fixture.d.ts +2 -5
  3. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/react/fixture.js +1 -2
  5. package/dist/__tests__/react/fixture.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/react/LiveStoreProvider.d.ts +11 -1
  10. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  11. package/dist/react/LiveStoreProvider.js +45 -29
  12. package/dist/react/LiveStoreProvider.js.map +1 -1
  13. package/dist/react/LiveStoreProvider.test.js +3 -2
  14. package/dist/react/LiveStoreProvider.test.js.map +1 -1
  15. package/dist/react/useQuery.test.js +11 -4
  16. package/dist/react/useQuery.test.js.map +1 -1
  17. package/dist/react/useRow.test.js +13 -5
  18. package/dist/react/useRow.test.js.map +1 -1
  19. package/dist/react/useTemporaryQuery.test.js +5 -2
  20. package/dist/react/useTemporaryQuery.test.js.map +1 -1
  21. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  22. package/dist/reactiveQueries/graphql.js.map +1 -1
  23. package/dist/reactiveQueries/sql.d.ts +12 -12
  24. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  25. package/dist/reactiveQueries/sql.js +42 -69
  26. package/dist/reactiveQueries/sql.js.map +1 -1
  27. package/dist/reactiveQueries/sql.test.js +9 -5
  28. package/dist/reactiveQueries/sql.test.js.map +1 -1
  29. package/dist/row-query.d.ts +6 -4
  30. package/dist/row-query.d.ts.map +1 -1
  31. package/dist/row-query.js +5 -12
  32. package/dist/row-query.js.map +1 -1
  33. package/dist/store-devtools.d.ts +4 -4
  34. package/dist/store-devtools.d.ts.map +1 -1
  35. package/dist/store-devtools.js +3 -7
  36. package/dist/store-devtools.js.map +1 -1
  37. package/dist/store.d.ts +12 -3
  38. package/dist/store.d.ts.map +1 -1
  39. package/dist/store.js +24 -46
  40. package/dist/store.js.map +1 -1
  41. package/package.json +5 -5
  42. package/src/__tests__/react/fixture.tsx +1 -3
  43. package/src/index.ts +1 -1
  44. package/src/react/LiveStoreProvider.test.tsx +3 -2
  45. package/src/react/LiveStoreProvider.tsx +52 -32
  46. package/src/react/useQuery.test.tsx +11 -4
  47. package/src/react/useRow.test.tsx +13 -9
  48. package/src/react/useTemporaryQuery.test.tsx +5 -2
  49. package/src/reactiveQueries/graphql.ts +5 -1
  50. package/src/reactiveQueries/sql.test.ts +9 -5
  51. package/src/reactiveQueries/sql.ts +64 -86
  52. package/src/row-query.ts +17 -21
  53. package/src/store-devtools.ts +10 -15
  54. package/src/store.ts +43 -64
@@ -1,23 +1,24 @@
1
1
  import { type Bindable, prepareBindValues, type QueryInfo, type QueryInfoNone } from '@livestore/common'
2
2
  import { shouldNeverHappen } from '@livestore/utils'
3
- import { Schema, TreeFormatter } from '@livestore/utils/effect'
3
+ import { Schema, SchemaEquivalence, TreeFormatter } from '@livestore/utils/effect'
4
4
  import * as otel from '@opentelemetry/api'
5
5
 
6
6
  import { globalReactivityGraph } from '../global-state.js'
7
7
  import type { Thunk } from '../reactive.js'
8
+ import { NOT_REFRESHED_YET } from '../reactive.js'
8
9
  import type { RefreshReason } from '../store.js'
9
10
  import { getDurationMsFromSpan } from '../utils/otel.js'
10
11
  import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
11
12
  import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
12
13
 
13
- export type MapRows<TResult, TRaw = any> =
14
- | ((rows: ReadonlyArray<TRaw>) => TResult)
15
- | Schema.Schema<TResult, ReadonlyArray<TRaw>, unknown>
16
-
17
- export const querySQL = <TResult, TRaw = any>(
14
+ /**
15
+ * NOTE `querySQL` is only supposed to read data. Don't use it to insert/update/delete data but use mutations instead.
16
+ */
17
+ export const querySQL = <TResultSchema, TResult = TResultSchema>(
18
18
  query: string | ((get: GetAtomResult) => string),
19
- options?: {
20
- map?: MapRows<TResult, TRaw>
19
+ options: {
20
+ schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
21
+ map?: (rows: TResultSchema) => TResult
21
22
  /**
22
23
  * Can be provided explicitly to slightly speed up initial query performance
23
24
  *
@@ -29,21 +30,23 @@ export const querySQL = <TResult, TRaw = any>(
29
30
  reactivityGraph?: ReactivityGraph
30
31
  },
31
32
  ): LiveQuery<TResult, QueryInfoNone> =>
32
- new LiveStoreSQLQuery<TResult, QueryInfoNone>({
33
+ new LiveStoreSQLQuery<TResultSchema, TResult, QueryInfoNone>({
33
34
  label: options?.label,
34
35
  genQueryString: query,
35
36
  queriedTables: options?.queriedTables,
36
37
  bindValues: options?.bindValues,
37
38
  reactivityGraph: options?.reactivityGraph,
38
39
  map: options?.map,
40
+ schema: options.schema,
39
41
  queryInfo: { _tag: 'None' },
40
42
  })
41
43
 
42
44
  /* An object encapsulating a reactive SQL query */
43
- export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfoNone> extends LiveStoreQueryBase<
44
- TResult,
45
- TQueryInfo
46
- > {
45
+ export class LiveStoreSQLQuery<
46
+ TResultSchema,
47
+ TResult = TResultSchema,
48
+ TQueryInfo extends QueryInfo = QueryInfoNone,
49
+ > extends LiveStoreQueryBase<TResult, TQueryInfo> {
47
50
  _tag: 'sql' = 'sql'
48
51
 
49
52
  /** A reactive thunk representing the query text */
@@ -59,7 +62,8 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
59
62
  /** Currently only used by `rowQuery` for lazy table migrations and eager default row insertion */
60
63
  private execBeforeFirstRun
61
64
 
62
- private mapRows
65
+ private mapResult: (rows: TResultSchema) => TResult
66
+ private schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
63
67
 
64
68
  queryInfo: TQueryInfo
65
69
 
@@ -67,8 +71,9 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
67
71
  genQueryString,
68
72
  queriedTables,
69
73
  bindValues,
70
- label: label_,
74
+ label = genQueryString.toString(),
71
75
  reactivityGraph,
76
+ schema,
72
77
  map,
73
78
  execBeforeFirstRun,
74
79
  queryInfo,
@@ -78,51 +83,20 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
78
83
  queriedTables?: Set<string>
79
84
  bindValues?: Bindable
80
85
  reactivityGraph?: ReactivityGraph
81
- map?: MapRows<TResult>
86
+ schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
87
+ map?: (rows: TResultSchema) => TResult
82
88
  execBeforeFirstRun?: (ctx: QueryContext) => void
83
89
  queryInfo?: TQueryInfo
84
90
  }) {
85
91
  super()
86
92
 
87
- const label = label_ ?? genQueryString.toString()
88
93
  this.label = `sql(${label})`
89
94
  this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
90
95
  this.execBeforeFirstRun = execBeforeFirstRun
91
96
  this.queryInfo = queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
92
- this.mapRows =
93
- map === undefined
94
- ? (rows: any) => rows as TResult
95
- : Schema.isSchema(map)
96
- ? (rows: any, opts: { sqlString: string }) => {
97
- const parseResult = Schema.decodeEither(map as Schema.Schema<TResult, ReadonlyArray<any>>)(rows)
98
- if (parseResult._tag === 'Left') {
99
- const parseErrorStr = TreeFormatter.formatErrorSync(parseResult.left)
100
- const expectedSchemaStr = String(map.ast)
101
- const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
102
-
103
- console.error(
104
- `\
105
- Error parsing SQL query result.
106
-
107
- Query: ${opts.sqlString}\
108
- ${bindValuesStr}
109
-
110
- Expected schema: ${expectedSchemaStr}
111
97
 
112
- Error: ${parseErrorStr}
113
-
114
- Result:`,
115
- rows,
116
- )
117
- // console.error(`Error parsing SQL query result: ${TreeFormatter.formatErrorSync(parseResult.left)}`)
118
- return shouldNeverHappen(`Error parsing SQL query result: ${parseResult.left}`)
119
- } else {
120
- return parseResult.right as TResult
121
- }
122
- }
123
- : typeof map === 'function'
124
- ? map
125
- : shouldNeverHappen(`Invalid map function ${map}`)
98
+ this.schema = schema
99
+ this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
126
100
 
127
101
  let queryString$OrQueryString: string | Thunk<string, QueryContext, RefreshReason>
128
102
  if (typeof genQueryString === 'function') {
@@ -134,7 +108,11 @@ Result:`,
134
108
  setDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString, durationMs })
135
109
  return queryString
136
110
  },
137
- { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
111
+ {
112
+ label: `${label}:queryString`,
113
+ meta: { liveStoreThunkType: 'sqlQueryString' },
114
+ equal: (a, b) => a === b,
115
+ },
138
116
  )
139
117
 
140
118
  this.queryString$ = queryString$OrQueryString
@@ -146,6 +124,15 @@ Result:`,
146
124
 
147
125
  const queriedTablesRef = { current: queriedTables }
148
126
 
127
+ const schemaEqual = SchemaEquivalence.make(schema)
128
+ // TODO also support derived equality for `map` (probably will depend on having an easy way to transform a schema without an `encode` step)
129
+ // This would mean dropping the `map` option
130
+ const equal =
131
+ map === undefined
132
+ ? (a: TResult, b: TResult) =>
133
+ a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : schemaEqual(a as any, b as any)
134
+ : undefined
135
+
149
136
  const results$ = this.reactivityGraph.makeThunk<TResult>(
150
137
  (get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
151
138
  otelTracer.startActiveSpan(
@@ -186,7 +173,31 @@ Result:`,
186
173
 
187
174
  span.setAttribute('sql.rowsCount', rawResults.length)
188
175
 
189
- const result = this.mapRows(rawResults, { sqlString })
176
+ const parsedResult = Schema.decodeEither(this.schema)(rawResults)
177
+
178
+ if (parsedResult._tag === 'Left') {
179
+ const parseErrorStr = TreeFormatter.formatErrorSync(parsedResult.left)
180
+ const expectedSchemaStr = String(this.schema.ast)
181
+ const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
182
+
183
+ console.error(
184
+ `\
185
+ Error parsing SQL query result.
186
+
187
+ Query: ${sqlString}\
188
+ ${bindValuesStr}
189
+
190
+ Expected schema: ${expectedSchemaStr}
191
+
192
+ Error: ${parseErrorStr}
193
+
194
+ Result:`,
195
+ rawResults,
196
+ )
197
+ return shouldNeverHappen(`Error parsing SQL query result: ${parsedResult.left}`)
198
+ }
199
+
200
+ const result = this.mapResult(parsedResult.right)
190
201
 
191
202
  span.end()
192
203
 
@@ -199,45 +210,12 @@ Result:`,
199
210
  return result
200
211
  },
201
212
  ),
202
- { label: queryLabel },
213
+ { label: queryLabel, equal },
203
214
  )
204
215
 
205
216
  this.results$ = results$
206
217
  }
207
218
 
208
- /**
209
- * Returns a new reactive query that contains the result of
210
- * running an arbitrary JS computation on the results of this SQL query.
211
- */
212
- // pipe = <U>(fn: (result: Result, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
213
- // new LiveStoreJSQuery({
214
- // fn: (get) => {
215
- // const results = get(this.results$!)
216
- // return fn(results, get)
217
- // },
218
- // label: `${this.label}:js`,
219
- // onDestroy: () => this.destroy(),
220
- // reactivityGraph: this.reactivityGraph,
221
- // queryInfo: undefined,
222
- // })
223
-
224
- /** Returns a reactive query */
225
- // getFirstRow = (args?: { defaultValue?: Result }) =>
226
- // new LiveStoreJSQuery({
227
- // fn: (get) => {
228
- // const results = get(this.results$!)
229
- // if (results.length === 0 && args?.defaultValue === undefined) {
230
- // // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
231
- // const queryLabel = this.label
232
- // return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
233
- // }
234
- // return results[0] ?? args!.defaultValue!
235
- // },
236
- // label: `${this.label}:first`,
237
- // onDestroy: () => this.destroy(),
238
- // reactivityGraph: this.reactivityGraph,
239
- // })
240
-
241
219
  destroy = () => {
242
220
  if (this.queryString$ !== undefined) {
243
221
  this.reactivityGraph.destroyNode(this.queryString$)
package/src/row-query.ts CHANGED
@@ -3,7 +3,7 @@ import { sql } from '@livestore/common'
3
3
  import { DbSchema } from '@livestore/common/schema'
4
4
  import type { GetValForKey } from '@livestore/utils'
5
5
  import { shouldNeverHappen } from '@livestore/utils'
6
- import { Schema, TreeFormatter } from '@livestore/utils/effect'
6
+ import { Schema } from '@livestore/utils/effect'
7
7
  import type * as otel from '@opentelemetry/api'
8
8
  import type { SqliteDsl } from 'effect-db-schema'
9
9
 
@@ -12,14 +12,16 @@ import { computed } from './reactiveQueries/js.js'
12
12
  import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
13
13
  import type { Store } from './store.js'
14
14
 
15
- export type RowQueryOptions = {
15
+ export type RowQueryOptions<TTableDef extends DbSchema.TableDef, TResult = RowResult<TTableDef>> = {
16
16
  otelContext?: otel.Context
17
17
  skipInsertDefaultRow?: boolean
18
18
  reactivityGraph?: ReactivityGraph
19
+ map?: (result: RowResult<TTableDef>) => TResult
20
+ label?: string
19
21
  }
20
22
 
21
23
  export type RowQueryOptionsDefaulValues<TTableDef extends DbSchema.TableDef> = {
22
- defaultValues: Partial<RowResult<TTableDef>>
24
+ defaultValues?: Partial<RowResult<TTableDef>>
23
25
  }
24
26
 
25
27
  export type MakeRowQuery = {
@@ -29,9 +31,10 @@ export type MakeRowQuery = {
29
31
  boolean,
30
32
  DbSchema.TableOptions & { isSingleton: true }
31
33
  >,
34
+ TResult = RowResult<TTableDef>,
32
35
  >(
33
36
  table: TTableDef,
34
- options?: RowQueryOptions,
37
+ options?: RowQueryOptions<TTableDef, TResult>,
35
38
  ): LiveQuery<RowResult<TTableDef>, QueryInfoRow<TTableDef>>
36
39
  <
37
40
  TTableDef extends DbSchema.TableDef<
@@ -39,19 +42,20 @@ export type MakeRowQuery = {
39
42
  boolean,
40
43
  DbSchema.TableOptions & { isSingleton: false }
41
44
  >,
45
+ TResult = RowResult<TTableDef>,
42
46
  >(
43
47
  table: TTableDef,
44
48
  // TODO adjust so it works with arbitrary primary keys or unique constraints
45
49
  id: string,
46
- options?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
47
- ): LiveQuery<RowResult<TTableDef>, QueryInfoRow<TTableDef>>
50
+ options?: RowQueryOptions<TTableDef, TResult> & RowQueryOptionsDefaulValues<TTableDef>,
51
+ ): LiveQuery<TResult, QueryInfoRow<TTableDef>>
48
52
  }
49
53
 
50
54
  // TODO also allow other where clauses and multiple rows
51
55
  export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
52
56
  table: TTableDef,
53
- idOrOptions?: string | RowQueryOptions,
54
- options_?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
57
+ idOrOptions?: string | RowQueryOptions<TTableDef, any>,
58
+ options_?: RowQueryOptions<TTableDef, any> & RowQueryOptionsDefaulValues<TTableDef>,
55
59
  ) => {
56
60
  const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
57
61
  const options = typeof idOrOptions === 'string' ? options_ : idOrOptions
@@ -70,8 +74,10 @@ export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
70
74
  const whereClause = id === undefined ? '' : `where id = '${id}'`
71
75
  const queryStr = sql`select * from ${tableName} ${whereClause} limit 1`
72
76
 
77
+ const rowSchema = table.isSingleColumn === true ? table.schema.pipe(Schema.pluck('value' as any)) : table.schema
78
+
73
79
  return new LiveStoreSQLQuery({
74
- label: `rowQuery:query:${tableSchema.name}${id === undefined ? '' : `:${id}`}`,
80
+ label: options?.label ?? `rowQuery:query:${tableSchema.name}${id === undefined ? '' : `:${id}`}`,
75
81
  genQueryString: queryStr,
76
82
  queriedTables: new Set([tableName]),
77
83
  reactivityGraph: options?.reactivityGraph,
@@ -83,18 +89,8 @@ export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
83
89
  id,
84
90
  skipInsertDefaultRow: options?.skipInsertDefaultRow,
85
91
  }),
86
- map: (results): RowResult<TTableDef> => {
87
- if (results.length === 0) return shouldNeverHappen(`No results for query ${queryStr}`)
88
-
89
- const parseResult = Schema.decodeEither(table.schema)(results[0]!)
90
-
91
- if (parseResult._tag === 'Left') {
92
- console.error('decode error', TreeFormatter.formatErrorSync(parseResult.left), 'results', results)
93
- return shouldNeverHappen(`Error decoding query result for ${queryStr}`)
94
- }
95
-
96
- return table.isSingleColumn === true ? parseResult.right.value : parseResult.right
97
- },
92
+ schema: rowSchema.pipe(Schema.Array, Schema.headOrElse()),
93
+ map: options?.map,
98
94
  queryInfo: { _tag: 'Row', table, id: id ?? 'singleton' },
99
95
  })
100
96
  }
@@ -1,5 +1,5 @@
1
1
  import type { DebugInfo, StoreAdapter } from '@livestore/common'
2
- import { Devtools, liveStoreVersion } from '@livestore/common'
2
+ import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
3
3
  import { throttle } from '@livestore/utils'
4
4
  import { BrowserChannel, Effect, Stream } from '@livestore/utils/effect'
5
5
 
@@ -9,29 +9,24 @@ import { NOT_REFRESHED_YET } from './reactive.js'
9
9
  import type { LiveQuery, ReactivityGraph } from './reactiveQueries/base-class.js'
10
10
  import type { ReferenceCountedSet } from './utils/data-structures.js'
11
11
 
12
- type Unsub = () => void
13
- type RequestId = string
14
-
15
12
  type IStore = {
16
13
  adapter: StoreAdapter
17
- devtoolsConnectionId: string
18
14
  reactivityGraph: ReactivityGraph
19
15
  mainDbWrapper: MainDatabaseWrapper
20
16
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
21
17
  }
22
18
 
23
- export const connectStoreToDevtools = ({ port, store }: { port: MessagePort; store: IStore }) =>
19
+ type Unsub = () => void
20
+ type RequestId = string
21
+ type SubMap = Map<RequestId, Unsub>
22
+
23
+ export const connectDevtoolsToStore = ({ storeMessagePort, store }: { storeMessagePort: MessagePort; store: IStore }) =>
24
24
  Effect.gen(function* () {
25
25
  const channelId = store.adapter.coordinator.devtools.channelId
26
26
 
27
- const reactivityGraphSubcriptions = new Map<RequestId, Unsub>()
28
- const liveQueriesSubscriptions = new Map<RequestId, Unsub>()
29
- const debugInfoHistorySubscriptions = new Map<RequestId, Unsub>()
30
-
31
- const { storeMessagePort } = yield* store.adapter.coordinator.devtools.connect({
32
- port,
33
- connectionId: store.devtoolsConnectionId,
34
- })
27
+ const reactivityGraphSubcriptions: SubMap = new Map()
28
+ const liveQueriesSubscriptions: SubMap = new Map()
29
+ const debugInfoHistorySubscriptions: SubMap = new Map()
35
30
 
36
31
  const storePortChannel = yield* BrowserChannel.messagePortChannel({
37
32
  port: storeMessagePort,
@@ -210,4 +205,4 @@ export const connectStoreToDevtools = ({ port, store }: { port: MessagePort; sto
210
205
  Stream.runDrain,
211
206
  Effect.withSpan('LSD.devtools.onMessage'),
212
207
  )
213
- })
208
+ }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('LSD.devtools.connectStoreToDevtools'))
package/src/store.ts CHANGED
@@ -7,15 +7,15 @@ import type {
7
7
  StoreAdapter,
8
8
  StoreAdapterFactory,
9
9
  } from '@livestore/common'
10
- import { Devtools, getExecArgsFromMutation, prepareBindValues, UnexpectedError } from '@livestore/common'
10
+ import { getExecArgsFromMutation, prepareBindValues, UnexpectedError } from '@livestore/common'
11
11
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
12
12
  import { makeMutationEventSchemaMemo, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE } from '@livestore/common/schema'
13
13
  import { assertNever, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
14
- import { cuid } from '@livestore/utils/cuid'
14
+ import type { Cause } from '@livestore/utils/effect'
15
15
  import {
16
- BrowserChannel,
16
+ Deferred,
17
+ Duration,
17
18
  Effect,
18
- Either,
19
19
  Exit,
20
20
  FiberSet,
21
21
  Inspectable,
@@ -37,7 +37,7 @@ import { MainDatabaseWrapper } from './MainDatabaseWrapper.js'
37
37
  import type { StackInfo } from './react/utils/stack-info.js'
38
38
  import type { DebugRefreshReasonBase, Ref } from './reactive.js'
39
39
  import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
40
- import { connectStoreToDevtools } from './store-devtools.js'
40
+ import { connectDevtoolsToStore } from './store-devtools.js'
41
41
  import { ReferenceCountedSet } from './utils/data-structures.js'
42
42
  import { downloadBlob } from './utils/dev.js'
43
43
  import { getDurationMsFromSpan } from './utils/otel.js'
@@ -58,6 +58,9 @@ export type OtelOptions = {
58
58
  rootSpanContext: otel.Context
59
59
  }
60
60
 
61
+ export class ForceStoreShutdown extends Schema.TaggedError<ForceStoreShutdown>()('LiveStore.ForceStoreShutdown', {}) {}
62
+ export class StoreShutdown extends Schema.TaggedError<StoreShutdown>()('LiveStore.StoreShutdown', {}) {}
63
+
61
64
  export type StoreOptions<
62
65
  TGraphQLContext extends BaseGraphQLContext,
63
66
  TSchema extends LiveStoreSchema = LiveStoreSchema,
@@ -126,7 +129,6 @@ export class Store<
126
129
  TSchema extends LiveStoreSchema = LiveStoreSchema,
127
130
  > extends Inspectable.Class {
128
131
  id = uniqueStoreId()
129
- readonly devtoolsConnectionId = cuid()
130
132
  private fiberSet: FiberSet.FiberSet
131
133
  reactivityGraph: ReactivityGraph
132
134
  mainDbWrapper: MainDatabaseWrapper
@@ -232,14 +234,11 @@ export class Store<
232
234
  this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
233
235
  }),
234
236
  Stream.runDrain,
237
+ Effect.interruptible,
235
238
  Effect.withSpan('LiveStore:syncMutations'),
236
239
  Effect.forkScoped,
237
240
  )
238
241
 
239
- if (disableDevtools !== true) {
240
- yield* this.bootDevtools().pipe(Effect.forkScoped)
241
- }
242
-
243
242
  yield* Effect.addFinalizer(() =>
244
243
  Effect.sync(() => {
245
244
  for (const tableRef of Object.values(this.tableRefs)) {
@@ -256,7 +255,6 @@ export class Store<
256
255
  yield* Effect.never
257
256
  }).pipe(Effect.scoped, Effect.withSpan('LiveStore:store-constructor'), FiberSet.run(fiberSet), runEffectFork)
258
257
  }
259
-
260
258
  // #endregion constructor
261
259
 
262
260
  static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
@@ -590,52 +588,6 @@ export class Store<
590
588
  meta: { liveStoreRefType: 'table' },
591
589
  })
592
590
 
593
- // #region devtools
594
- private bootDevtools = () =>
595
- Effect.gen(this, function* () {
596
- // const webBridgeBroadcastChannel = yield* Devtools.WebBridge.makeBroadcastChannel()
597
-
598
- // eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
599
- const store = this
600
- const channelId = this.adapter.coordinator.devtools.channelId
601
-
602
- // Chrome extension bridge
603
- {
604
- const windowChannel = yield* BrowserChannel.windowChannel({
605
- window,
606
- listenSchema: Devtools.DevtoolsWindowMessage.MessageForStore,
607
- sendSchema: Devtools.DevtoolsWindowMessage.MessageForContentscript,
608
- })
609
-
610
- yield* windowChannel.send(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
611
-
612
- yield* windowChannel.listen.pipe(
613
- Stream.filterMap(Either.getRight),
614
- Stream.tap((message) =>
615
- Effect.gen(function* () {
616
- if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
617
- // Send message to contentscript via window (which the contentscript iframe is listening to)
618
- yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
619
- return
620
- }
621
-
622
- if (message.channelId !== channelId) return
623
-
624
- if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
625
- yield* connectStoreToDevtools({ port: message.port, store })
626
- }
627
- }),
628
- ),
629
- Stream.runDrain,
630
- Effect.tapCauseLogPretty,
631
- Effect.forkScoped,
632
- )
633
-
634
- yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }))
635
- }
636
- })
637
- // #endregion devtools
638
-
639
591
  __devDownloadDb = () => {
640
592
  const data = this.mainDbWrapper.export()
641
593
  downloadBlob(data, `livestore-${Date.now()}.db`)
@@ -728,21 +680,44 @@ export const createStore = <
728
680
 
729
681
  yield* Queue.take(bootStatusQueue).pipe(
730
682
  Effect.tapSync((status) => onBootStatus?.(status)),
683
+ Effect.tap((status) => (status.stage === 'done' ? Queue.shutdown(bootStatusQueue) : Effect.void)),
731
684
  Effect.forever,
732
685
  Effect.tapCauseLogPretty,
733
686
  Effect.forkScoped,
734
687
  )
735
688
 
689
+ const storeDeferred = yield* Deferred.make<Store>()
690
+
691
+ const connectDevtoolsToStore_ = ({ storeMessagePort }: { storeMessagePort: MessagePort }) =>
692
+ Effect.gen(function* () {
693
+ const store = yield* Deferred.await(storeDeferred)
694
+ yield* connectDevtoolsToStore({ storeMessagePort, store })
695
+ })
696
+
697
+ // TODO close parent scope? (Needs refactor with Mike A)
698
+ const shutdown = (cause: Cause.Cause<unknown>) =>
699
+ Effect.gen(function* () {
700
+ yield* Effect.logWarning(`Shutting down LiveStore`, cause)
701
+
702
+ FiberSet.clear(fiberSet).pipe(
703
+ Effect.andThen(() => FiberSet.run(fiberSet, Effect.fail(StoreShutdown.make()))),
704
+ Effect.timeout(Duration.seconds(1)),
705
+ Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown:clear-fiber-set', duration: 500 }),
706
+ Effect.catchTag('TimeoutException', () =>
707
+ Effect.logWarning('Store shutdown timed out. Forcing shutdown.').pipe(
708
+ Effect.andThen(FiberSet.run(fiberSet, Effect.fail(ForceStoreShutdown.make()))),
709
+ ),
710
+ ),
711
+ runEffectFork, // NOTE we need to fork this separately otherwise it will also be interrupted
712
+ )
713
+ }).pipe(Effect.withSpan('livestore:shutdown'))
714
+
736
715
  const adapter: StoreAdapter = yield* adapterFactory({
737
716
  schema,
738
717
  devtoolsEnabled: disableDevtools !== true,
739
718
  bootStatusQueue,
740
- shutdown: (cause) =>
741
- Effect.gen(function* () {
742
- yield* Effect.logWarning(`Shutting down LiveStore`, cause)
743
- // TODO close parent scope? (Needs refactor with Mike A)
744
- yield* FiberSet.clear(fiberSet)
745
- }).pipe(Effect.withSpan('livestore:shutdown')),
719
+ shutdown,
720
+ connectDevtoolsToStore: connectDevtoolsToStore_,
746
721
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
747
722
 
748
723
  if (batchUpdates !== undefined) {
@@ -826,7 +801,7 @@ export const createStore = <
826
801
  )
827
802
  }
828
803
 
829
- return Store.createStore<TGraphQLContext, TSchema>(
804
+ const store = Store.createStore<TGraphQLContext, TSchema>(
830
805
  {
831
806
  adapter,
832
807
  schema,
@@ -839,6 +814,10 @@ export const createStore = <
839
814
  },
840
815
  span,
841
816
  )
817
+
818
+ yield* Deferred.succeed(storeDeferred, store as any as Store)
819
+
820
+ return store
842
821
  }).pipe(
843
822
  Effect.withSpan('createStore', {
844
823
  parent: otelOptions?.rootSpanContext