@livestore/livestore 0.0.54-dev.4 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/MainDatabaseWrapper.d.ts +6 -5
- package/dist/MainDatabaseWrapper.d.ts.map +1 -1
- package/dist/MainDatabaseWrapper.js +3 -3
- package/dist/MainDatabaseWrapper.js.map +1 -1
- package/dist/QueryCache.d.ts +1 -1
- package/dist/QueryCache.d.ts.map +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/__tests__/react/fixture.d.ts +9 -27
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +12 -10
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +18 -11
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +20 -19
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/index.d.ts +1 -1
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +1 -1
- package/dist/effect/index.js.map +1 -1
- package/dist/global-state.d.ts +1 -3
- package/dist/global-state.d.ts.map +1 -1
- package/dist/global-state.js +2 -3
- package/dist/global-state.js.map +1 -1
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreContext.d.ts +5 -2
- package/dist/react/LiveStoreContext.d.ts.map +1 -1
- package/dist/react/LiveStoreContext.js +3 -0
- package/dist/react/LiveStoreContext.js.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts +6 -6
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +70 -43
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/LiveStoreProvider.test.js +33 -12
- package/dist/react/LiveStoreProvider.test.js.map +1 -1
- package/dist/react/components/LiveList.d.ts.map +1 -1
- package/dist/react/useAtom.d.ts +1 -1
- package/dist/react/useAtom.d.ts.map +1 -1
- package/dist/react/useLocalId.d.ts.map +1 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +2 -2
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useRow.d.ts +2 -2
- package/dist/react/useRow.d.ts.map +1 -1
- package/dist/react/useRow.js +5 -5
- package/dist/react/useRow.js.map +1 -1
- package/dist/react/useRow.test.js +22 -22
- package/dist/react/useRow.test.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js +1 -1
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
- package/dist/reactive.d.ts +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +4 -5
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +6 -6
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +3 -3
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +8 -8
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +10 -10
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +6 -6
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +8 -8
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +9 -10
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +12 -12
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.js +6 -6
- package/dist/reactiveQueries/sql.test.js.map +1 -1
- package/dist/row-query.d.ts +2 -2
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +4 -38
- package/dist/row-query.js.map +1 -1
- package/dist/store.d.ts +35 -22
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +329 -221
- package/dist/store.js.map +1 -1
- package/dist/utils/otel.d.ts.map +1 -1
- package/package.json +10 -19
- package/src/MainDatabaseWrapper.ts +14 -8
- package/src/QueryCache.ts +1 -2
- package/src/__tests__/react/fixture.tsx +13 -11
- package/src/effect/LiveStore.ts +53 -44
- package/src/effect/index.ts +2 -1
- package/src/global-state.ts +2 -6
- package/src/index.ts +25 -7
- package/src/react/LiveStoreContext.ts +7 -2
- package/src/react/LiveStoreProvider.test.tsx +56 -14
- package/src/react/LiveStoreProvider.tsx +107 -51
- package/src/react/useQuery.ts +2 -2
- package/src/react/useRow.test.tsx +22 -22
- package/src/react/useRow.ts +7 -10
- package/src/react/useTemporaryQuery.ts +2 -2
- package/src/reactive.ts +6 -5
- package/src/reactiveQueries/base-class.ts +9 -9
- package/src/reactiveQueries/graphql.ts +19 -15
- package/src/reactiveQueries/js.ts +12 -12
- package/src/reactiveQueries/sql.test.ts +6 -6
- package/src/reactiveQueries/sql.ts +19 -21
- package/src/row-query.ts +8 -54
- package/src/store.ts +514 -284
- package/dist/utils/bounded-collections.d.ts +0 -34
- package/dist/utils/bounded-collections.d.ts.map +0 -1
- package/dist/utils/bounded-collections.js +0 -91
- package/dist/utils/bounded-collections.js.map +0 -1
- package/dist/utils/util.d.ts +0 -14
- package/dist/utils/util.d.ts.map +0 -1
- package/dist/utils/util.js +0 -19
- package/dist/utils/util.js.map +0 -1
- 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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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,
|
|
38
|
-
this.mainDbWrapper = new MainDatabaseWrapper({
|
|
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 =
|
|
43
|
-
// this
|
|
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 =
|
|
48
|
+
const mutationsSpan = otelOptions.tracer.startSpan('LiveStore:mutations', {}, otelOptions.rootSpanContext);
|
|
48
49
|
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan);
|
|
49
|
-
const queriesSpan =
|
|
50
|
+
const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext);
|
|
50
51
|
const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan);
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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.
|
|
318
|
+
makeTableRef = (tableName) => this.reactivityGraph.makeRef(null, {
|
|
307
319
|
equal: () => false,
|
|
308
320
|
label: `tableRef:${tableName}`,
|
|
309
321
|
meta: { liveStoreRefType: 'table' },
|
|
310
322
|
});
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|