@livestore/livestore 0.0.54-dev.5 → 0.0.54

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 (118) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/MainDatabaseWrapper.d.ts +6 -5
  3. package/dist/MainDatabaseWrapper.d.ts.map +1 -1
  4. package/dist/MainDatabaseWrapper.js +3 -3
  5. package/dist/MainDatabaseWrapper.js.map +1 -1
  6. package/dist/QueryCache.d.ts +1 -1
  7. package/dist/QueryCache.d.ts.map +1 -1
  8. package/dist/QueryCache.js.map +1 -1
  9. package/dist/__tests__/react/fixture.d.ts +9 -27
  10. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  11. package/dist/__tests__/react/fixture.js +12 -10
  12. package/dist/__tests__/react/fixture.js.map +1 -1
  13. package/dist/effect/LiveStore.d.ts +18 -11
  14. package/dist/effect/LiveStore.d.ts.map +1 -1
  15. package/dist/effect/LiveStore.js +20 -19
  16. package/dist/effect/LiveStore.js.map +1 -1
  17. package/dist/effect/index.d.ts +1 -1
  18. package/dist/effect/index.d.ts.map +1 -1
  19. package/dist/effect/index.js +1 -1
  20. package/dist/effect/index.js.map +1 -1
  21. package/dist/global-state.d.ts +1 -3
  22. package/dist/global-state.d.ts.map +1 -1
  23. package/dist/global-state.js +2 -3
  24. package/dist/global-state.js.map +1 -1
  25. package/dist/index.d.ts +6 -7
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -6
  28. package/dist/index.js.map +1 -1
  29. package/dist/react/LiveStoreContext.d.ts +5 -2
  30. package/dist/react/LiveStoreContext.d.ts.map +1 -1
  31. package/dist/react/LiveStoreContext.js +3 -0
  32. package/dist/react/LiveStoreContext.js.map +1 -1
  33. package/dist/react/LiveStoreProvider.d.ts +6 -6
  34. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  35. package/dist/react/LiveStoreProvider.js +70 -43
  36. package/dist/react/LiveStoreProvider.js.map +1 -1
  37. package/dist/react/LiveStoreProvider.test.js +33 -12
  38. package/dist/react/LiveStoreProvider.test.js.map +1 -1
  39. package/dist/react/components/LiveList.d.ts.map +1 -1
  40. package/dist/react/useAtom.d.ts +1 -1
  41. package/dist/react/useAtom.d.ts.map +1 -1
  42. package/dist/react/useLocalId.d.ts.map +1 -1
  43. package/dist/react/useQuery.d.ts.map +1 -1
  44. package/dist/react/useQuery.js +2 -2
  45. package/dist/react/useQuery.js.map +1 -1
  46. package/dist/react/useRow.d.ts +2 -2
  47. package/dist/react/useRow.d.ts.map +1 -1
  48. package/dist/react/useRow.js +5 -5
  49. package/dist/react/useRow.js.map +1 -1
  50. package/dist/react/useRow.test.js +22 -22
  51. package/dist/react/useRow.test.js.map +1 -1
  52. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  53. package/dist/react/useTemporaryQuery.js +1 -1
  54. package/dist/react/useTemporaryQuery.js.map +1 -1
  55. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  56. package/dist/reactive.d.ts +1 -1
  57. package/dist/reactive.d.ts.map +1 -1
  58. package/dist/reactive.js +4 -5
  59. package/dist/reactive.js.map +1 -1
  60. package/dist/reactiveQueries/base-class.d.ts +6 -6
  61. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  62. package/dist/reactiveQueries/base-class.js +3 -3
  63. package/dist/reactiveQueries/base-class.js.map +1 -1
  64. package/dist/reactiveQueries/graphql.d.ts +8 -8
  65. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  66. package/dist/reactiveQueries/graphql.js +10 -10
  67. package/dist/reactiveQueries/graphql.js.map +1 -1
  68. package/dist/reactiveQueries/js.d.ts +6 -6
  69. package/dist/reactiveQueries/js.d.ts.map +1 -1
  70. package/dist/reactiveQueries/js.js +8 -8
  71. package/dist/reactiveQueries/js.js.map +1 -1
  72. package/dist/reactiveQueries/sql.d.ts +9 -10
  73. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  74. package/dist/reactiveQueries/sql.js +12 -12
  75. package/dist/reactiveQueries/sql.js.map +1 -1
  76. package/dist/reactiveQueries/sql.test.js +6 -6
  77. package/dist/reactiveQueries/sql.test.js.map +1 -1
  78. package/dist/row-query.d.ts +2 -2
  79. package/dist/row-query.d.ts.map +1 -1
  80. package/dist/row-query.js +4 -38
  81. package/dist/row-query.js.map +1 -1
  82. package/dist/store.d.ts +35 -22
  83. package/dist/store.d.ts.map +1 -1
  84. package/dist/store.js +329 -221
  85. package/dist/store.js.map +1 -1
  86. package/dist/utils/otel.d.ts.map +1 -1
  87. package/package.json +10 -19
  88. package/src/MainDatabaseWrapper.ts +14 -8
  89. package/src/QueryCache.ts +1 -2
  90. package/src/__tests__/react/fixture.tsx +13 -11
  91. package/src/effect/LiveStore.ts +53 -44
  92. package/src/effect/index.ts +2 -1
  93. package/src/global-state.ts +2 -6
  94. package/src/index.ts +25 -7
  95. package/src/react/LiveStoreContext.ts +7 -2
  96. package/src/react/LiveStoreProvider.test.tsx +56 -14
  97. package/src/react/LiveStoreProvider.tsx +107 -51
  98. package/src/react/useQuery.ts +2 -2
  99. package/src/react/useRow.test.tsx +22 -22
  100. package/src/react/useRow.ts +7 -10
  101. package/src/react/useTemporaryQuery.ts +2 -2
  102. package/src/reactive.ts +6 -5
  103. package/src/reactiveQueries/base-class.ts +9 -9
  104. package/src/reactiveQueries/graphql.ts +19 -15
  105. package/src/reactiveQueries/js.ts +12 -12
  106. package/src/reactiveQueries/sql.test.ts +6 -6
  107. package/src/reactiveQueries/sql.ts +19 -21
  108. package/src/row-query.ts +8 -54
  109. package/src/store.ts +514 -284
  110. package/dist/utils/bounded-collections.d.ts +0 -34
  111. package/dist/utils/bounded-collections.d.ts.map +0 -1
  112. package/dist/utils/bounded-collections.js +0 -91
  113. package/dist/utils/bounded-collections.js.map +0 -1
  114. package/dist/utils/util.d.ts +0 -14
  115. package/dist/utils/util.d.ts.map +0 -1
  116. package/dist/utils/util.js +0 -19
  117. package/dist/utils/util.js.map +0 -1
  118. package/src/utils/util.ts +0 -31
package/dist/store.js CHANGED
@@ -1,23 +1,22 @@
1
- import { Devtools, getExecArgsFromMutation } from '@livestore/common';
2
- import { version as liveStoreVersion } from '@livestore/common/package.json';
3
- import { makeMutationEventSchema } from '@livestore/common/schema';
4
- import { assertNever, isPromise, makeNoopTracer, ref, shouldNeverHappen } from '@livestore/utils';
5
- import { Effect, Schema, Stream } from '@livestore/utils/effect';
1
+ import { Devtools, getExecArgsFromMutation, liveStoreVersion, prepareBindValues, UnexpectedError, } from '@livestore/common';
2
+ import { makeMutationEventSchemaMemo } from '@livestore/common/schema';
3
+ import { assertNever, isPromise, makeNoopTracer, shouldNeverHappen, throttle } from '@livestore/utils';
4
+ import { cuid } from '@livestore/utils/cuid';
5
+ import { Effect, Exit, Layer, Logger, LogLevel, OtelTracer, Queue, Runtime, Schema, Scope, Stream, } from '@livestore/utils/effect';
6
6
  import * as otel from '@opentelemetry/api';
7
- import { globalDbGraph } from './global-state.js';
8
- import { MainDatabaseWrapper } from './MainDatabaseWrapper.js';
7
+ import { globalReactivityGraph } from './global-state.js';
8
+ import { emptyDebugInfo as makeEmptyDebugInfo, MainDatabaseWrapper } from './MainDatabaseWrapper.js';
9
+ import { NOT_REFRESHED_YET } from './reactive.js';
9
10
  import { downloadBlob } from './utils/dev.js';
10
11
  import { getDurationMsFromSpan } from './utils/otel.js';
11
- import { prepareBindValues } from './utils/util.js';
12
12
  let storeCount = 0;
13
13
  const uniqueStoreId = () => `store-${++storeCount}`;
14
14
  export class Store {
15
15
  id = uniqueStoreId();
16
- graph;
16
+ devtoolsConnectionId = cuid();
17
+ storeScope;
18
+ reactivityGraph;
17
19
  mainDbWrapper;
18
- // TODO refactor
19
- // _proxyDb: InMemoryDatabase
20
- // TODO
21
20
  adapter;
22
21
  schema;
23
22
  graphQLSchema;
@@ -29,41 +28,41 @@ export class Store {
29
28
  */
30
29
  tableRefs;
31
30
  // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
32
- __processedMutationIds = new Set();
31
+ __processedMutationIds;
33
32
  __processedMutationWithoutRefreshIds = new Set();
34
33
  /** RC-based set to see which queries are currently subscribed to */
35
34
  activeQueries;
36
35
  __mutationEventSchema;
37
- constructor({ adapter, schema, graphQLOptions, dbGraph, otelTracer, otelRootSpanContext, mutationEventSchema, disableDevtools, }) {
38
- this.mainDbWrapper = new MainDatabaseWrapper({ otelTracer, otelRootSpanContext, db: adapter.mainDb });
36
+ constructor({ adapter, schema, graphQLOptions, reactivityGraph, otelOptions, disableDevtools, __processedMutationIds, storeScope, }) {
37
+ this.mainDbWrapper = new MainDatabaseWrapper({ otel: otelOptions, db: adapter.mainDb });
39
38
  this.adapter = adapter;
40
39
  this.schema = schema;
40
+ this.storeScope = storeScope;
41
41
  // TODO refactor
42
- this.__mutationEventSchema = mutationEventSchema;
43
- // this.mutationEventSchema = makeMutationEventSchema(Object.fromEntries(schema.mutations.entries()) as any)
42
+ this.__mutationEventSchema = makeMutationEventSchemaMemo(schema);
43
+ // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
44
+ this.__processedMutationIds = __processedMutationIds;
44
45
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
45
46
  this.tableRefs = {};
46
47
  this.activeQueries = new ReferenceCountedSet();
47
- const mutationsSpan = otelTracer.startSpan('LiveStore:mutations', {}, otelRootSpanContext);
48
+ const mutationsSpan = otelOptions.tracer.startSpan('LiveStore:mutations', {}, otelOptions.rootSpanContext);
48
49
  const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan);
49
- const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext);
50
+ const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext);
50
51
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan);
51
- this.graph = dbGraph;
52
- this.graph.context = { store: this, otelTracer, rootOtelContext: otelQueriesSpanContext };
53
- this.adapter.coordinator.syncMutations.pipe(Stream.tapSync((mutationEventDecoded) => {
54
- this.mutate({ wasSyncMessage: true }, mutationEventDecoded);
55
- }), Stream.runDrain, Effect.tapCauseLogPretty, Effect.runFork);
56
- if (disableDevtools !== true) {
57
- this.bootDevtools();
58
- }
52
+ this.reactivityGraph = reactivityGraph;
53
+ this.reactivityGraph.context = {
54
+ store: this,
55
+ otelTracer: otelOptions.tracer,
56
+ rootOtelContext: otelQueriesSpanContext,
57
+ };
59
58
  this.otel = {
60
- tracer: otelTracer,
59
+ tracer: otelOptions.tracer,
61
60
  mutationsSpanContext: otelMuationsSpanContext,
62
61
  queriesSpanContext: otelQueriesSpanContext,
63
62
  };
64
63
  // Need a set here since `schema.tables` might contain duplicates and some componentStateTables
65
64
  const allTableNames = new Set(this.schema.tables.keys());
66
- const existingTableRefs = new Map(Array.from(this.graph.atoms.values())
65
+ const existingTableRefs = new Map(Array.from(this.reactivityGraph.atoms.values())
67
66
  .filter((_) => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
68
67
  .map((_) => [_.label.slice('tableRef:'.length), _]));
69
68
  for (const tableName of allTableNames) {
@@ -73,10 +72,27 @@ export class Store {
73
72
  this.graphQLSchema = graphQLOptions.schema;
74
73
  this.graphQLContext = graphQLOptions.makeContext(this.mainDbWrapper, this.otel.tracer);
75
74
  }
75
+ Effect.gen(this, function* () {
76
+ yield* this.adapter.coordinator.syncMutations.pipe(Stream.tapSync((mutationEventDecoded) => {
77
+ this.mutate({ wasSyncMessage: true }, mutationEventDecoded);
78
+ }), Stream.runDrain, Effect.withSpan('LiveStore:syncMutations'), Effect.forkScoped);
79
+ if (disableDevtools !== true) {
80
+ yield* this.bootDevtools().pipe(Effect.forkScoped);
81
+ }
82
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
83
+ for (const tableRef of Object.values(this.tableRefs)) {
84
+ for (const superComp of tableRef.super) {
85
+ this.reactivityGraph.removeEdge(superComp, tableRef);
86
+ }
87
+ }
88
+ otel.trace.getSpan(this.otel.mutationsSpanContext).end();
89
+ otel.trace.getSpan(this.otel.queriesSpanContext).end();
90
+ }));
91
+ }).pipe(Scope.extend(storeScope), Effect.forkIn(storeScope), Effect.scoped, runEffectFork);
76
92
  }
77
93
  static createStore = (storeOptions, parentSpan) => {
78
94
  const ctx = otel.trace.setSpan(otel.context.active(), parentSpan);
79
- return storeOptions.otelTracer.startActiveSpan('LiveStore:store-constructor', {}, ctx, (span) => {
95
+ return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:store-constructor', {}, ctx, (span) => {
80
96
  try {
81
97
  return new Store(storeOptions);
82
98
  }
@@ -93,7 +109,7 @@ export class Store {
93
109
  // console.log('store sub', query$.label)
94
110
  const otelContext = otel.trace.setSpan(otel.context.active(), span);
95
111
  const label = `subscribe:${options?.label}`;
96
- const effect = this.graph.makeEffect((get) => onNewValue(get(query$.results$)), { label });
112
+ const effect = this.reactivityGraph.makeEffect((get) => onNewValue(get(query$.results$)), { label });
97
113
  this.activeQueries.add(query$);
98
114
  // Running effect right away to get initial value (unless `skipInitialRun` is set)
99
115
  if (options?.skipInitialRun !== true) {
@@ -102,7 +118,7 @@ export class Store {
102
118
  const unsubscribe = () => {
103
119
  // console.log('store unsub', query$.label)
104
120
  try {
105
- this.graph.destroyNode(effect);
121
+ this.reactivityGraph.destroyNode(effect);
106
122
  this.activeQueries.remove(query$);
107
123
  onUnsubsubscribe?.();
108
124
  }
@@ -118,14 +134,7 @@ export class Store {
118
134
  * Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
119
135
  */
120
136
  destroy = async () => {
121
- for (const tableRef of Object.values(this.tableRefs)) {
122
- for (const superComp of tableRef.super) {
123
- this.graph.removeEdge(superComp, tableRef);
124
- }
125
- }
126
- otel.trace.getSpan(this.otel.mutationsSpanContext).end();
127
- otel.trace.getSpan(this.otel.queriesSpanContext).end();
128
- await this.adapter.coordinator.shutdown();
137
+ await Scope.close(this.storeScope, Exit.void).pipe(Effect.withSpan('Store:destroy'), runEffectPromise);
129
138
  };
130
139
  mutate = (firstMutationOrTxnFnOrOptions, ...restMutations) => {
131
140
  let mutationsEvents;
@@ -164,6 +173,7 @@ export class Store {
164
173
  // console.group('LiveStore.mutate', { skipRefresh, wasSyncMessage, label })
165
174
  // mutationsEvents.forEach((_) => console.log(_.mutation, _.id, _.args))
166
175
  // console.groupEnd()
176
+ let durationMs;
167
177
  return this.otel.tracer.startActiveSpan('LiveStore:mutate', { attributes: { 'livestore.mutateLabel': label } }, this.otel.mutationsSpanContext, (span) => {
168
178
  const otelContext = otel.trace.setSpan(otel.context.active(), span);
169
179
  try {
@@ -184,8 +194,8 @@ export class Store {
184
194
  }
185
195
  }
186
196
  catch (e) {
187
- debugger;
188
197
  console.error(e, mutationEvent);
198
+ throw e;
189
199
  }
190
200
  }
191
201
  };
@@ -200,6 +210,7 @@ export class Store {
200
210
  catch (e) {
201
211
  console.error(e);
202
212
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() });
213
+ throw e;
203
214
  }
204
215
  finally {
205
216
  span.end();
@@ -217,15 +228,18 @@ export class Store {
217
228
  writeTables: Array.from(writeTables),
218
229
  };
219
230
  // Update all table refs together in a batch, to only trigger one reactive update
220
- this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh });
231
+ this.reactivityGraph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh });
221
232
  }
222
233
  catch (e) {
234
+ console.error(e);
223
235
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() });
236
+ throw e;
224
237
  }
225
238
  finally {
226
239
  span.end();
227
- return { durationMs: getDurationMsFromSpan(span) };
240
+ durationMs = getDurationMsFromSpan(span);
228
241
  }
242
+ return { durationMs };
229
243
  });
230
244
  };
231
245
  /**
@@ -236,7 +250,7 @@ export class Store {
236
250
  const { label } = options ?? {};
237
251
  this.otel.tracer.startActiveSpan('LiveStore:manualRefresh', { attributes: { 'livestore.manualRefreshLabel': label } }, this.otel.mutationsSpanContext, (span) => {
238
252
  const otelContext = otel.trace.setSpan(otel.context.active(), span);
239
- this.graph.runDeferredEffects({ otelContext });
253
+ this.reactivityGraph.runDeferredEffects({ otelContext });
240
254
  span.end();
241
255
  });
242
256
  };
@@ -279,10 +293,9 @@ export class Store {
279
293
  const mutationEventEncoded = Schema.encodeUnknownSync(this.__mutationEventSchema)(mutationEventDecoded);
280
294
  if (coordinatorMode !== 'skip-coordinator') {
281
295
  // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
282
- void this.adapter.coordinator.mutate(mutationEventEncoded, {
283
- span,
284
- persisted: coordinatorMode !== 'skip-persist',
285
- });
296
+ this.adapter.coordinator
297
+ .mutate(mutationEventEncoded, { persisted: coordinatorMode !== 'skip-persist' })
298
+ .pipe(runEffectFork);
286
299
  }
287
300
  // Uncomment to print a list of queries currently registered on the store
288
301
  // console.debug(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
@@ -297,206 +310,299 @@ export class Store {
297
310
  */
298
311
  execute = (query, params = {}, writeTables, otelContext) => {
299
312
  this.mainDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext });
300
- const parentSpan = otel.trace.getSpan(otel.context.active());
301
- this.adapter.coordinator.execute(query, prepareBindValues(params, query), parentSpan);
313
+ this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(runEffectFork);
302
314
  };
303
315
  select = (query, params = {}) => {
304
316
  return this.mainDbWrapper.select(query, { bindValues: prepareBindValues(params, query) });
305
317
  };
306
- makeTableRef = (tableName) => this.graph.makeRef(null, {
318
+ makeTableRef = (tableName) => this.reactivityGraph.makeRef(null, {
307
319
  equal: () => false,
308
320
  label: `tableRef:${tableName}`,
309
321
  meta: { liveStoreRefType: 'table' },
310
322
  });
311
- bootDevtools = () => {
312
- const devtoolsChannel = Devtools.makeBroadcastChannels();
313
- const signalsSubcriptionRef = ref(undefined);
314
- // let alreadySubscribedToLiveQueries = false
315
- const liveQueriesSubscriptionRef = ref(undefined);
316
- devtoolsChannel.toAppHost.addEventListener('message', async (event) => {
317
- const decoded = Schema.decodeUnknownOption(Devtools.MessageToAppHost)(event.data);
318
- if (decoded._tag === 'None' ||
319
- decoded.value._tag === 'LSD.DevtoolsReadyBroadcast' ||
320
- decoded.value._tag === 'LSD.DevtoolsConnected' ||
321
- decoded.value.channelId !== this.adapter.coordinator.devtools.channelId) {
322
- // console.log(`Unknown message`, event)
323
+ // TODO shutdown behaviour
324
+ bootDevtools = () => Effect.gen(this, function* () {
325
+ const sendToDevtoolsContentscript = (message) => {
326
+ window.postMessage(Schema.encodeSync(Devtools.DevtoolsWindowMessage.MessageForContentscript)(message), '*');
327
+ };
328
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.LoadIframe.make({}));
329
+ const channelId = this.adapter.coordinator.devtools.channelId;
330
+ window.addEventListener('message', (event) => {
331
+ const decodedMessageRes = Schema.decodeOption(Devtools.DevtoolsWindowMessage.MessageForStore)(event.data);
332
+ if (decodedMessageRes._tag === 'None')
333
+ return;
334
+ const message = decodedMessageRes.value;
335
+ if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
336
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }));
323
337
  return;
324
338
  }
325
- const requestId = decoded.value.requestId;
326
- const sendToDevtools = (message) => devtoolsChannel.fromAppHost.postMessage(Schema.encodeSync(Devtools.MessageFromAppHost)(message));
327
- switch (decoded.value._tag) {
328
- case 'LSD.SignalsSubscribe': {
329
- const includeResults = decoded.value.includeResults;
330
- const send = () => sendToDevtools(Devtools.SignalsRes.make({
331
- signals: this.graph.getSnapshot({ includeResults }),
332
- requestId,
333
- liveStoreVersion,
334
- }));
335
- send();
336
- if (signalsSubcriptionRef.current === undefined) {
337
- signalsSubcriptionRef.current = this.graph.subscribeToRefresh(() => send());
338
- }
339
- break;
340
- }
341
- case 'LSD.DebugInfoReq': {
342
- sendToDevtools(Devtools.DebugInfoRes.make({ debugInfo: this.mainDbWrapper.debugInfo, requestId, liveStoreVersion }));
343
- break;
344
- }
345
- case 'LSD.DebugInfoResetReq': {
346
- this.mainDbWrapper.debugInfo.slowQueries.clear();
347
- sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }));
348
- break;
349
- }
350
- case 'LSD.DebugInfoRerunQueryReq': {
351
- const { queryStr, bindValues, queriedTables } = decoded.value;
352
- this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true });
353
- sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }));
354
- break;
355
- }
356
- case 'LSD.SignalsUnsubscribe': {
357
- signalsSubcriptionRef.current();
358
- signalsSubcriptionRef.current = undefined;
359
- break;
360
- }
361
- case 'LSD.LiveQueriesSubscribe': {
362
- const send = () => sendToDevtools(Devtools.LiveQueriesRes.make({
363
- liveQueries: [...this.activeQueries].map((q) => ({
364
- _tag: q._tag,
365
- id: q.id,
366
- label: q.label,
367
- runs: q.runs,
368
- executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
369
- lastestResult: q.results$.previousResult,
370
- activeSubscriptions: Array.from(q.activeSubscriptions),
371
- })),
372
- requestId,
373
- liveStoreVersion,
374
- }));
375
- send();
376
- if (liveQueriesSubscriptionRef.current === undefined) {
377
- liveQueriesSubscriptionRef.current = this.graph.subscribeToRefresh(() => send());
378
- }
379
- break;
380
- }
381
- case 'LSD.LiveQueriesUnsubscribe': {
382
- liveQueriesSubscriptionRef.current();
383
- liveQueriesSubscriptionRef.current = undefined;
384
- break;
385
- }
386
- case 'LSD.ResetAllDataReq': {
387
- await this.adapter.coordinator.dangerouslyReset(decoded.value.mode);
388
- sendToDevtools(Devtools.ResetAllDataRes.make({ requestId, liveStoreVersion }));
389
- break;
390
- }
391
- // No default
339
+ if (message.channelId !== channelId)
340
+ return;
341
+ if (message._tag === 'LSD.WindowMessage.MessagePortForStore') {
342
+ const reactivityGraphSubcriptions = new Map();
343
+ const liveQueriesSubscriptions = new Map();
344
+ const debugInfoHistorySubscriptions = new Map();
345
+ this.adapter.coordinator.devtools
346
+ .connect({ port: message.port, connectionId: this.devtoolsConnectionId })
347
+ .pipe(Effect.tapSync(({ storeMessagePort }) => {
348
+ // console.log('storeMessagePort', storeMessagePort)
349
+ storeMessagePort.addEventListener('message', (event) => {
350
+ const decodedMessage = Schema.decodeUnknownSync(Devtools.MessageToAppHostStore)(event.data);
351
+ // console.log('storeMessagePort message', decodedMessage)
352
+ if (decodedMessage.channelId !== this.adapter.coordinator.devtools.channelId) {
353
+ // console.log(`Unknown message`, event)
354
+ return;
355
+ }
356
+ const requestId = decodedMessage.requestId;
357
+ const sendToDevtools = (message) => storeMessagePort.postMessage(Schema.encodeSync(Devtools.MessageFromAppHostStore)(message));
358
+ const requestIdleCallback = window.requestIdleCallback ?? ((cb) => cb());
359
+ switch (decodedMessage._tag) {
360
+ case 'LSD.ReactivityGraphSubscribe': {
361
+ const includeResults = decodedMessage.includeResults;
362
+ const send = () =>
363
+ // In order to not add more work to the current tick, we use requestIdleCallback
364
+ // to send the reactivity graph updates to the devtools
365
+ requestIdleCallback(() => sendToDevtools(Devtools.ReactivityGraphRes.make({
366
+ reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults }),
367
+ requestId,
368
+ liveStoreVersion,
369
+ })), { timeout: 500 });
370
+ send();
371
+ // In some cases, there can be A LOT of reactivity graph updates in a short period of time
372
+ // so we throttle the updates to avoid sending too much data
373
+ // This might need to be tweaked further and possibly be exposed to the user in some way.
374
+ const throttledSend = throttle(send, 20);
375
+ reactivityGraphSubcriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend));
376
+ break;
377
+ }
378
+ case 'LSD.DebugInfoReq': {
379
+ sendToDevtools(Devtools.DebugInfoRes.make({
380
+ debugInfo: this.mainDbWrapper.debugInfo,
381
+ requestId,
382
+ liveStoreVersion,
383
+ }));
384
+ break;
385
+ }
386
+ case 'LSD.DebugInfoHistorySubscribe': {
387
+ const buffer = [];
388
+ let hasStopped = false;
389
+ let rafHandle;
390
+ const tick = () => {
391
+ buffer.push(this.mainDbWrapper.debugInfo);
392
+ // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
393
+ // will get the empty debug info
394
+ // TODO We need to come up with a more graceful way to do this. Probably via a single global
395
+ // `requestAnimationFrame` loop that is passed in somehow.
396
+ this.mainDbWrapper.debugInfo = makeEmptyDebugInfo();
397
+ if (buffer.length > 10) {
398
+ sendToDevtools(Devtools.DebugInfoHistoryRes.make({
399
+ debugInfoHistory: buffer,
400
+ requestId,
401
+ liveStoreVersion,
402
+ }));
403
+ buffer.length = 0;
404
+ }
405
+ if (hasStopped === false) {
406
+ rafHandle = requestAnimationFrame(tick);
407
+ }
408
+ };
409
+ rafHandle = requestAnimationFrame(tick);
410
+ const unsub = () => {
411
+ hasStopped = true;
412
+ if (rafHandle !== undefined) {
413
+ cancelAnimationFrame(rafHandle);
414
+ }
415
+ };
416
+ debugInfoHistorySubscriptions.set(requestId, unsub);
417
+ break;
418
+ }
419
+ case 'LSD.DebugInfoHistoryUnsubscribe': {
420
+ debugInfoHistorySubscriptions.get(requestId)();
421
+ debugInfoHistorySubscriptions.delete(requestId);
422
+ break;
423
+ }
424
+ case 'LSD.DebugInfoResetReq': {
425
+ this.mainDbWrapper.debugInfo.slowQueries.clear();
426
+ sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, liveStoreVersion }));
427
+ break;
428
+ }
429
+ case 'LSD.DebugInfoRerunQueryReq': {
430
+ const { queryStr, bindValues, queriedTables } = decodedMessage;
431
+ this.mainDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true });
432
+ sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, liveStoreVersion }));
433
+ break;
434
+ }
435
+ case 'LSD.ReactivityGraphUnsubscribe': {
436
+ reactivityGraphSubcriptions.get(requestId)();
437
+ break;
438
+ }
439
+ case 'LSD.LiveQueriesSubscribe': {
440
+ const send = () => requestIdleCallback(() => sendToDevtools(Devtools.LiveQueriesRes.make({
441
+ liveQueries: [...this.activeQueries].map((q) => ({
442
+ _tag: q._tag,
443
+ id: q.id,
444
+ label: q.label,
445
+ runs: q.runs,
446
+ executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
447
+ lastestResult: q.results$.previousResult === NOT_REFRESHED_YET
448
+ ? 'SYMBOL_NOT_REFRESHED_YET'
449
+ : q.results$.previousResult,
450
+ activeSubscriptions: Array.from(q.activeSubscriptions),
451
+ })),
452
+ requestId,
453
+ liveStoreVersion,
454
+ })), { timeout: 500 });
455
+ send();
456
+ // Same as in the reactivity graph subscription case above, we need to throttle the updates
457
+ const throttledSend = throttle(send, 20);
458
+ liveQueriesSubscriptions.set(requestId, this.reactivityGraph.subscribeToRefresh(throttledSend));
459
+ break;
460
+ }
461
+ case 'LSD.LiveQueriesUnsubscribe': {
462
+ liveQueriesSubscriptions.get(requestId)();
463
+ liveQueriesSubscriptions.delete(requestId);
464
+ break;
465
+ }
466
+ // No default
467
+ }
468
+ });
469
+ storeMessagePort.start();
470
+ }), runEffectFork);
471
+ return;
392
472
  }
393
473
  });
394
- };
474
+ sendToDevtoolsContentscript(Devtools.DevtoolsWindowMessage.StoreReady.make({ channelId }));
475
+ });
395
476
  __devDownloadDb = () => {
396
477
  const data = this.mainDbWrapper.export();
397
478
  downloadBlob(data, `livestore-${Date.now()}.db`);
398
479
  };
399
480
  __devDownloadMutationLogDb = async () => {
400
- const data = await this.adapter.coordinator.getMutationLogData();
481
+ const data = await this.adapter.coordinator.getMutationLogData.pipe(runEffectPromise);
401
482
  downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`);
402
483
  };
403
484
  // TODO allow for graceful store reset without requiring a full page reload (which should also call .boot)
404
- dangerouslyResetStorage = (mode) => this.adapter.coordinator.dangerouslyReset(mode);
485
+ dangerouslyResetStorage = (mode) => this.adapter.coordinator.dangerouslyReset(mode).pipe(runEffectPromise);
405
486
  }
406
487
  /** Create a new LiveStore Store */
407
- export const createStore = async ({ schema, graphQLOptions, otelTracer = makeNoopTracer(), otelRootSpanContext = otel.context.active(), adapter: adapterFactory, boot, dbGraph = globalDbGraph, batchUpdates, disableDevtools, }) => {
408
- return otelTracer.startActiveSpan('createStore', {}, otelRootSpanContext, async (span) => {
409
- try {
410
- performance.mark('livestore:db-creating');
411
- const otelContext = otel.trace.setSpan(otel.context.active(), span);
412
- const adapterPromise = adapterFactory({ otelTracer, otelContext, schema });
413
- const adapter = adapterPromise instanceof Promise ? await adapterPromise : adapterPromise;
414
- performance.mark('livestore:db-created');
415
- performance.measure('livestore:db-create', 'livestore:db-creating', 'livestore:db-created');
416
- if (batchUpdates !== undefined) {
417
- dbGraph.effectsWrapper = batchUpdates;
418
- }
419
- const mutationEventSchema = makeMutationEventSchema(Object.fromEntries(schema.mutations.entries()));
420
- // TODO consider moving booting into the storage backend
421
- if (boot !== undefined) {
422
- let isInTxn = false;
423
- let txnExecuteStmnts = [];
424
- const bootDbImpl = {
425
- _tag: 'BootDb',
426
- execute: (queryStr, bindValues) => {
427
- const stmt = adapter.mainDb.prepare(queryStr);
428
- stmt.execute(bindValues);
429
- if (isInTxn === true) {
430
- txnExecuteStmnts.push([queryStr, bindValues]);
431
- }
432
- else {
433
- void adapter.coordinator.execute(queryStr, bindValues, undefined);
434
- }
435
- },
436
- mutate: (...list) => {
437
- for (const mutationEventDecoded of list) {
438
- const mutationDef = schema.mutations.get(mutationEventDecoded.mutation) ??
439
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`);
440
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded });
441
- // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
442
- for (const { statementSql, bindValues } of execArgsArr) {
443
- adapter.mainDb.execute(statementSql, bindValues);
444
- }
445
- const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded);
446
- void adapter.coordinator.mutate(mutationEventEncoded, { span, persisted: true });
447
- }
448
- },
449
- select: (queryStr, bindValues) => {
450
- const stmt = adapter.mainDb.prepare(queryStr);
451
- return stmt.select(bindValues);
452
- },
453
- txn: (callback) => {
454
- try {
455
- isInTxn = true;
456
- adapter.mainDb.execute('BEGIN', undefined);
457
- callback();
458
- adapter.mainDb.execute('COMMIT', undefined);
459
- // adapter.coordinator.execute('BEGIN', undefined, undefined)
460
- for (const [queryStr, bindValues] of txnExecuteStmnts) {
461
- adapter.coordinator.execute(queryStr, bindValues, undefined);
462
- }
463
- // adapter.coordinator.execute('COMMIT', undefined, undefined)
464
- }
465
- catch (e) {
466
- adapter.mainDb.execute('ROLLBACK', undefined);
467
- throw e;
488
+ export const createStorePromise = async ({ signal, ...options }) => Effect.gen(function* () {
489
+ const scope = yield* Scope.make();
490
+ const runtime = yield* Effect.runtime();
491
+ if (signal !== undefined) {
492
+ signal.addEventListener('abort', () => {
493
+ Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty, Runtime.runFork(runtime));
494
+ });
495
+ }
496
+ return yield* createStore({ ...options, storeScope: scope }).pipe(Scope.extend(scope));
497
+ }).pipe(Effect.withSpan('createStore'), runEffectPromise);
498
+ export const createStore = ({ schema, graphQLOptions, otelOptions, adapter: adapterFactory, boot, reactivityGraph = globalReactivityGraph, batchUpdates, disableDevtools, onBootStatus, storeScope, }) => {
499
+ const otelTracer = otelOptions?.tracer ?? makeNoopTracer();
500
+ const otelRootSpanContext = otelOptions?.rootSpanContext ?? otel.context.active();
501
+ const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(Layer.provide(Layer.sync(OtelTracer.Tracer, () => otelTracer)));
502
+ return Effect.gen(function* () {
503
+ const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie);
504
+ const bootStatusQueue = yield* Queue.unbounded();
505
+ yield* Queue.take(bootStatusQueue).pipe(Effect.tapSync((status) => onBootStatus?.(status)), Effect.forever, Effect.tapCauseLogPretty, Effect.forkScoped);
506
+ const adapter = yield* adapterFactory({
507
+ schema,
508
+ devtoolsEnabled: disableDevtools !== true,
509
+ bootStatusQueue,
510
+ shutdown: (cause) => Scope.close(storeScope, Exit.failCause(cause)),
511
+ }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'));
512
+ if (batchUpdates !== undefined) {
513
+ reactivityGraph.effectsWrapper = batchUpdates;
514
+ }
515
+ const mutationEventSchema = makeMutationEventSchemaMemo(schema);
516
+ const __processedMutationIds = new Set();
517
+ // TODO consider moving booting into the storage backend
518
+ if (boot !== undefined) {
519
+ let isInTxn = false;
520
+ let txnExecuteStmnts = [];
521
+ const bootDbImpl = {
522
+ _tag: 'BootDb',
523
+ execute: (queryStr, bindValues) => {
524
+ const stmt = adapter.mainDb.prepare(queryStr);
525
+ stmt.execute(bindValues);
526
+ if (isInTxn === true) {
527
+ txnExecuteStmnts.push([queryStr, bindValues]);
528
+ }
529
+ else {
530
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork);
531
+ }
532
+ },
533
+ mutate: (...list) => {
534
+ for (const mutationEventDecoded of list) {
535
+ const mutationDef = schema.mutations.get(mutationEventDecoded.mutation) ??
536
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`);
537
+ __processedMutationIds.add(mutationEventDecoded.id);
538
+ const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded });
539
+ // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
540
+ for (const { statementSql, bindValues } of execArgsArr) {
541
+ adapter.mainDb.execute(statementSql, bindValues);
468
542
  }
469
- finally {
470
- isInTxn = false;
471
- txnExecuteStmnts = [];
543
+ const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded);
544
+ adapter.coordinator
545
+ .mutate(mutationEventEncoded, { persisted: true })
546
+ .pipe(runEffectFork);
547
+ }
548
+ },
549
+ select: (queryStr, bindValues) => {
550
+ const stmt = adapter.mainDb.prepare(queryStr);
551
+ return stmt.select(bindValues);
552
+ },
553
+ txn: (callback) => {
554
+ try {
555
+ isInTxn = true;
556
+ adapter.mainDb.execute('BEGIN', undefined);
557
+ callback();
558
+ adapter.mainDb.execute('COMMIT', undefined);
559
+ // adapter.coordinator.execute('BEGIN', undefined, undefined)
560
+ for (const [queryStr, bindValues] of txnExecuteStmnts) {
561
+ adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork);
472
562
  }
473
- },
474
- };
475
- const booting = boot(bootDbImpl, span);
476
- // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
477
- if (isPromise(booting)) {
478
- await booting;
479
- }
563
+ // adapter.coordinator.execute('COMMIT', undefined, undefined)
564
+ }
565
+ catch (e) {
566
+ adapter.mainDb.execute('ROLLBACK', undefined);
567
+ throw e;
568
+ }
569
+ finally {
570
+ isInTxn = false;
571
+ txnExecuteStmnts = [];
572
+ }
573
+ },
574
+ };
575
+ const booting = yield* Effect.try({
576
+ try: () => boot(bootDbImpl, span),
577
+ catch: (cause) => new UnexpectedError({ cause }),
578
+ });
579
+ // NOTE only awaiting if it's actually a promise to avoid unnecessary async/await
580
+ if (isPromise(booting)) {
581
+ yield* Effect.tryPromise({ try: () => booting, catch: (cause) => new UnexpectedError({ cause }) });
480
582
  }
481
- // TODO: we can't apply the schema at this point, we've already loaded persisted data!
482
- // Think about what to do about this case.
483
- // await applySchema(db, schema)
484
- return Store.createStore({
485
- adapter: adapter,
486
- schema,
487
- graphQLOptions,
488
- otelTracer,
489
- otelRootSpanContext,
490
- dbGraph,
491
- mutationEventSchema,
492
- disableDevtools,
493
- }, span);
494
- }
495
- finally {
496
- span.end();
497
583
  }
498
- });
584
+ // TODO: we can't apply the schema at this point, we've already loaded persisted data!
585
+ // Think about what to do about this case.
586
+ // await applySchema(db, schema)
587
+ return Store.createStore({
588
+ adapter,
589
+ schema,
590
+ graphQLOptions,
591
+ otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
592
+ reactivityGraph,
593
+ disableDevtools,
594
+ __processedMutationIds,
595
+ storeScope,
596
+ }, span);
597
+ }).pipe(
598
+ // Effect.scoped,
599
+ Effect.withSpan('createStore', {
600
+ parent: otelOptions?.rootSpanContext
601
+ ? OtelTracer.makeExternalSpan(otel.trace.getSpanContext(otelOptions.rootSpanContext))
602
+ : undefined,
603
+ }), Effect.provide(TracingLive));
499
604
  };
605
+ // TODO consider replacing with Effect's RC data structures
500
606
  class ReferenceCountedSet {
501
607
  map;
502
608
  constructor() {
@@ -527,4 +633,6 @@ class ReferenceCountedSet {
527
633
  }
528
634
  }
529
635
  }
636
+ const runEffectFork = (effect) => effect.pipe(Effect.tapCauseLogPretty, Effect.annotateLogs({ thread: 'window' }), Effect.provide(Logger.pretty), Logger.withMinimumLogLevel(LogLevel.Debug), Effect.runFork);
637
+ const runEffectPromise = (effect) => effect.pipe(Effect.tapCauseLogPretty, Effect.annotateLogs({ thread: 'window' }), Effect.provide(Logger.pretty), Logger.withMinimumLogLevel(LogLevel.Debug), Effect.runPromise);
530
638
  //# sourceMappingURL=store.js.map