@livestore/livestore 0.0.54-dev.5 → 0.0.55-dev.0

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