@livestore/livestore 0.3.0-dev.5 → 0.3.0-dev.51
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/QueryCache.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +60 -0
- package/dist/SqliteDbWrapper.d.ts.map +1 -0
- package/dist/{SynchronousDatabaseWrapper.js → SqliteDbWrapper.js} +69 -34
- package/dist/SqliteDbWrapper.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +6 -34
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +10 -12
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/mod.d.ts +3 -0
- package/dist/effect/mod.d.ts.map +1 -0
- package/dist/effect/mod.js +3 -0
- package/dist/effect/mod.js.map +1 -0
- package/dist/internal/mod.d.ts +3 -0
- package/dist/internal/mod.d.ts.map +1 -0
- package/dist/internal/mod.js +3 -0
- package/dist/internal/mod.js.map +1 -0
- package/dist/live-queries/base-class.d.ts +69 -29
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +60 -14
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +12 -0
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -0
- package/dist/live-queries/client-document-get-query.js +18 -0
- package/dist/live-queries/client-document-get-query.js.map +1 -0
- package/dist/live-queries/computed.d.ts +13 -15
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +37 -15
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts +93 -0
- package/dist/live-queries/db-query.d.ts.map +1 -0
- package/dist/live-queries/{db.js → db-query.js} +113 -40
- package/dist/live-queries/db-query.js.map +1 -0
- package/dist/live-queries/db-query.test.d.ts +2 -0
- package/dist/live-queries/db-query.test.d.ts.map +1 -0
- package/dist/live-queries/db-query.test.js +133 -0
- package/dist/live-queries/db-query.test.js.map +1 -0
- package/dist/live-queries/mod.d.ts +5 -0
- package/dist/live-queries/mod.d.ts.map +1 -0
- package/dist/live-queries/mod.js +5 -0
- package/dist/live-queries/mod.js.map +1 -0
- package/dist/live-queries/signal.d.ts +25 -0
- package/dist/live-queries/signal.d.ts.map +1 -0
- package/dist/live-queries/signal.js +50 -0
- package/dist/live-queries/signal.js.map +1 -0
- package/dist/live-queries/signal.test.d.ts +2 -0
- package/dist/live-queries/signal.test.d.ts.map +1 -0
- package/dist/live-queries/signal.test.js +25 -0
- package/dist/live-queries/signal.test.js.map +1 -0
- package/dist/mod.d.ts +14 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +13 -0
- package/dist/mod.js.map +1 -0
- package/dist/reactive.d.ts +23 -17
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +23 -19
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +1 -1
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/create-store.d.ts +70 -12
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +68 -19
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +5 -4
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +92 -40
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-types.d.ts +54 -42
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +2 -5
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store.d.ts +141 -35
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +322 -154
- package/dist/store/store.js.map +1 -1
- package/dist/utils/data-structures.d.ts.map +1 -1
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js +6 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/function-string.d.ts +7 -0
- package/dist/utils/function-string.d.ts.map +1 -0
- package/dist/utils/function-string.js +9 -0
- package/dist/utils/function-string.js.map +1 -0
- package/dist/utils/stack-info.d.ts.map +1 -1
- package/dist/utils/stack-info.js +6 -1
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/stack-info.test.js +54 -1
- package/dist/utils/stack-info.test.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +59 -216
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +23 -18
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/mod.d.ts +1 -0
- package/dist/utils/tests/mod.d.ts.map +1 -1
- package/dist/utils/tests/mod.js +1 -0
- package/dist/utils/tests/mod.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +8 -3
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +29 -26
- package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +92 -42
- package/src/effect/LiveStore.ts +27 -64
- package/src/effect/{index.ts → mod.ts} +2 -3
- package/src/internal/mod.ts +2 -0
- package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +241 -45
- package/src/live-queries/base-class.ts +170 -53
- package/src/live-queries/client-document-get-query.ts +52 -0
- package/src/live-queries/computed.ts +51 -33
- package/src/live-queries/db-query.test.ts +192 -0
- package/src/live-queries/{db.ts → db-query.ts} +171 -82
- package/src/live-queries/mod.ts +4 -0
- package/src/live-queries/signal.test.ts +40 -0
- package/src/live-queries/signal.ts +81 -0
- package/src/mod.ts +51 -0
- package/src/reactive.test.ts +1 -1
- package/src/reactive.ts +66 -43
- package/src/store/create-store.ts +188 -62
- package/src/store/devtools.ts +124 -46
- package/src/store/store-types.ts +54 -43
- package/src/store/store.ts +457 -237
- package/src/utils/dev.ts +6 -1
- package/src/utils/function-string.ts +12 -0
- package/src/utils/stack-info.test.ts +58 -1
- package/src/utils/stack-info.ts +6 -1
- package/src/utils/tests/fixture.ts +22 -31
- package/src/utils/tests/mod.ts +1 -0
- package/src/utils/tests/otel.ts +10 -3
- package/dist/SynchronousDatabaseWrapper.d.ts +0 -41
- package/dist/SynchronousDatabaseWrapper.d.ts.map +0 -1
- package/dist/SynchronousDatabaseWrapper.js.map +0 -1
- package/dist/effect/index.d.ts +0 -2
- package/dist/effect/index.d.ts.map +0 -1
- package/dist/effect/index.js +0 -2
- package/dist/effect/index.js.map +0 -1
- package/dist/global-state.d.ts +0 -14
- package/dist/global-state.d.ts.map +0 -1
- package/dist/global-state.js +0 -16
- package/dist/global-state.js.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/live-queries/db.d.ts +0 -66
- package/dist/live-queries/db.d.ts.map +0 -1
- package/dist/live-queries/db.js.map +0 -1
- package/dist/live-queries/db.test.d.ts +0 -2
- package/dist/live-queries/db.test.d.ts.map +0 -1
- package/dist/live-queries/db.test.js +0 -118
- package/dist/live-queries/db.test.js.map +0 -1
- package/dist/live-queries/graphql.d.ts +0 -49
- package/dist/live-queries/graphql.d.ts.map +0 -1
- package/dist/live-queries/graphql.js +0 -122
- package/dist/live-queries/graphql.js.map +0 -1
- package/dist/row-query-utils.d.ts +0 -17
- package/dist/row-query-utils.d.ts.map +0 -1
- package/dist/row-query-utils.js +0 -30
- package/dist/row-query-utils.js.map +0 -1
- package/dist/utils/otel.d.ts +0 -4
- package/dist/utils/otel.d.ts.map +0 -1
- package/dist/utils/otel.js +0 -6
- package/dist/utils/otel.js.map +0 -1
- package/src/global-state.ts +0 -20
- package/src/index.ts +0 -66
- package/src/live-queries/db.test.ts +0 -154
- package/src/live-queries/graphql.ts +0 -219
- package/src/row-query-utils.ts +0 -65
- package/src/utils/otel.ts +0 -9
- package/tsconfig.json +0 -18
- package/vitest.config.js +0 -9
package/dist/store/store.js
CHANGED
@@ -1,72 +1,73 @@
|
|
1
|
-
import { Devtools,
|
2
|
-
import {
|
3
|
-
import { assertNever, isDevEnv } from '@livestore/utils';
|
4
|
-
import { Cause,
|
1
|
+
import { Devtools, getDurationMsFromSpan, getExecArgsFromEvent, getResultSchema, IntentionalShutdownCause, isQueryBuilder, liveStoreVersion, makeClientSessionSyncProcessor, prepareBindValues, QueryBuilderAstSymbol, replaceSessionIdSymbol, } from '@livestore/common';
|
2
|
+
import { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema';
|
3
|
+
import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils';
|
4
|
+
import { Cause, Effect, Fiber, Inspectable, OtelTracer, Runtime, Schema, Stream } from '@livestore/utils/effect';
|
5
5
|
import { nanoid } from '@livestore/utils/nanoid';
|
6
6
|
import * as otel from '@opentelemetry/api';
|
7
|
-
import {
|
8
|
-
import {
|
7
|
+
import { makeReactivityGraph } from '../live-queries/base-class.js';
|
8
|
+
import { makeExecBeforeFirstRun } from '../live-queries/client-document-get-query.js';
|
9
|
+
import { SqliteDbWrapper } from '../SqliteDbWrapper.js';
|
9
10
|
import { ReferenceCountedSet } from '../utils/data-structures.js';
|
10
11
|
import { downloadBlob, exposeDebugUtils } from '../utils/dev.js';
|
11
|
-
import { getDurationMsFromSpan } from '../utils/otel.js';
|
12
12
|
if (isDevEnv()) {
|
13
13
|
exposeDebugUtils();
|
14
14
|
}
|
15
15
|
export class Store extends Inspectable.Class {
|
16
16
|
storeId;
|
17
17
|
reactivityGraph;
|
18
|
-
|
18
|
+
sqliteDbWrapper;
|
19
19
|
clientSession;
|
20
20
|
schema;
|
21
|
-
|
22
|
-
graphQLContext;
|
21
|
+
context;
|
23
22
|
otel;
|
24
23
|
/**
|
25
24
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
26
25
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
27
26
|
*/
|
28
27
|
tableRefs;
|
29
|
-
|
28
|
+
effectContext;
|
30
29
|
/** RC-based set to see which queries are currently subscribed to */
|
31
30
|
activeQueries;
|
32
|
-
// NOTE this is currently exposed for the Devtools databrowser to
|
33
|
-
|
34
|
-
unsyncedMutationEvents;
|
31
|
+
// NOTE this is currently exposed for the Devtools databrowser to commit events
|
32
|
+
__eventSchema;
|
35
33
|
syncProcessor;
|
36
|
-
|
34
|
+
boot;
|
37
35
|
// #region constructor
|
38
|
-
constructor({ clientSession, schema,
|
36
|
+
constructor({ clientSession, schema, otelOptions, context, batchUpdates, storeId, effectContext, params, confirmUnsavedChanges, __runningInDevtools, }) {
|
39
37
|
super();
|
40
38
|
this.storeId = storeId;
|
41
|
-
this.
|
42
|
-
this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb });
|
39
|
+
this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb });
|
43
40
|
this.clientSession = clientSession;
|
44
41
|
this.schema = schema;
|
45
|
-
this.
|
46
|
-
this.
|
42
|
+
this.context = context;
|
43
|
+
this.effectContext = effectContext;
|
44
|
+
const reactivityGraph = makeReactivityGraph();
|
47
45
|
const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext);
|
48
46
|
this.syncProcessor = makeClientSessionSyncProcessor({
|
49
47
|
schema,
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
48
|
+
clientSession,
|
49
|
+
runtime: effectContext.runtime,
|
50
|
+
materializeEvent: (eventDecoded, { otelContext, withChangeset }) => {
|
51
|
+
const { eventDef, materializer } = getEventDef(schema, eventDecoded.name);
|
52
|
+
const execArgsArr = getExecArgsFromEvent({
|
53
|
+
eventDef,
|
54
|
+
materializer,
|
55
|
+
db: this.sqliteDbWrapper,
|
56
|
+
event: { decoded: eventDecoded, encoded: undefined },
|
57
|
+
});
|
59
58
|
const writeTablesForEvent = new Set();
|
60
59
|
const exec = () => {
|
61
|
-
for (const { statementSql, bindValues, writeTables = this.
|
62
|
-
this.
|
60
|
+
for (const { statementSql, bindValues, writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql), } of execArgsArr) {
|
61
|
+
this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables });
|
63
62
|
// durationMsTotal += durationMs
|
64
|
-
|
63
|
+
for (const table of writeTables) {
|
64
|
+
writeTablesForEvent.add(table);
|
65
|
+
}
|
65
66
|
}
|
66
67
|
};
|
67
|
-
let sessionChangeset;
|
68
|
+
let sessionChangeset = { _tag: 'unset' };
|
68
69
|
if (withChangeset === true) {
|
69
|
-
sessionChangeset = this.
|
70
|
+
sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset;
|
70
71
|
}
|
71
72
|
else {
|
72
73
|
exec();
|
@@ -74,7 +75,7 @@ export class Store extends Inspectable.Class {
|
|
74
75
|
return { writeTables: writeTablesForEvent, sessionChangeset };
|
75
76
|
},
|
76
77
|
rollback: (changeset) => {
|
77
|
-
this.
|
78
|
+
this.sqliteDbWrapper.rollback(changeset);
|
78
79
|
},
|
79
80
|
refreshTables: (tables) => {
|
80
81
|
const tablesToUpdate = [];
|
@@ -83,51 +84,57 @@ export class Store extends Inspectable.Class {
|
|
83
84
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`);
|
84
85
|
tablesToUpdate.push([tableRef, null]);
|
85
86
|
}
|
86
|
-
|
87
|
+
reactivityGraph.setRefs(tablesToUpdate);
|
87
88
|
},
|
88
89
|
span: syncSpan,
|
90
|
+
params: {
|
91
|
+
leaderPushBatchSize: params.leaderPushBatchSize,
|
92
|
+
},
|
93
|
+
confirmUnsavedChanges,
|
89
94
|
});
|
90
|
-
this.
|
95
|
+
this.__eventSchema = LiveStoreEvent.makeEventDefSchemaMemo(schema);
|
91
96
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
92
97
|
this.tableRefs = {};
|
93
98
|
this.activeQueries = new ReferenceCountedSet();
|
94
|
-
const
|
95
|
-
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(),
|
99
|
+
const commitsSpan = otelOptions.tracer.startSpan('LiveStore:commits', {}, otelOptions.rootSpanContext);
|
100
|
+
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), commitsSpan);
|
96
101
|
const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext);
|
97
102
|
const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan);
|
98
103
|
this.reactivityGraph = reactivityGraph;
|
99
104
|
this.reactivityGraph.context = {
|
100
105
|
store: this,
|
106
|
+
defRcMap: new Map(),
|
107
|
+
reactivityGraph: new WeakRef(reactivityGraph),
|
101
108
|
otelTracer: otelOptions.tracer,
|
102
109
|
rootOtelContext: otelQueriesSpanContext,
|
103
110
|
effectsWrapper: batchUpdates,
|
104
111
|
};
|
105
112
|
this.otel = {
|
106
113
|
tracer: otelOptions.tracer,
|
107
|
-
|
114
|
+
rootSpanContext: otelOptions.rootSpanContext,
|
115
|
+
commitsSpanContext: otelMuationsSpanContext,
|
108
116
|
queriesSpanContext: otelQueriesSpanContext,
|
109
117
|
};
|
110
|
-
// TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
|
111
|
-
// But for now this is a good enough approximation with little downsides
|
112
|
-
const isRunningInDevtools = disableDevtools === true;
|
113
118
|
// Need a set here since `schema.tables` might contain duplicates and some componentStateTables
|
114
119
|
const allTableNames = new Set(
|
115
|
-
// NOTE we're excluding the LiveStore schema and
|
120
|
+
// NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
|
116
121
|
// unless LiveStore is running in the devtools
|
117
|
-
|
118
|
-
? this.schema.tables.keys()
|
119
|
-
: Array.from(this.schema.tables.keys()).filter((_) => _
|
122
|
+
__runningInDevtools
|
123
|
+
? this.schema.state.sqlite.tables.keys()
|
124
|
+
: Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)));
|
120
125
|
const existingTableRefs = new Map(Array.from(this.reactivityGraph.atoms.values())
|
121
126
|
.filter((_) => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
|
122
127
|
.map((_) => [_.label.slice('tableRef:'.length), _]));
|
123
128
|
for (const tableName of allTableNames) {
|
124
|
-
this.tableRefs[tableName] =
|
129
|
+
this.tableRefs[tableName] =
|
130
|
+
existingTableRefs.get(tableName) ??
|
131
|
+
this.reactivityGraph.makeRef(null, {
|
132
|
+
equal: () => false,
|
133
|
+
label: `tableRef:${tableName}`,
|
134
|
+
meta: { liveStoreRefType: 'table' },
|
135
|
+
});
|
125
136
|
}
|
126
|
-
|
127
|
-
this.graphQLSchema = graphQLOptions.schema;
|
128
|
-
this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer, clientSession.sessionId);
|
129
|
-
}
|
130
|
-
Effect.gen(this, function* () {
|
137
|
+
this.boot = Effect.gen(this, function* () {
|
131
138
|
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
132
139
|
// Remove all table refs from the reactivity graph
|
133
140
|
for (const tableRef of Object.values(this.tableRefs)) {
|
@@ -137,47 +144,59 @@ export class Store extends Inspectable.Class {
|
|
137
144
|
}
|
138
145
|
// End the otel spans
|
139
146
|
syncSpan.end();
|
140
|
-
|
147
|
+
commitsSpan.end();
|
141
148
|
queriesSpan.end();
|
142
149
|
}));
|
143
150
|
yield* this.syncProcessor.boot;
|
144
|
-
})
|
151
|
+
});
|
145
152
|
}
|
146
153
|
// #endregion constructor
|
147
|
-
static createStore = (storeOptions, parentSpan) => {
|
148
|
-
const ctx = otel.trace.setSpan(otel.context.active(), parentSpan);
|
149
|
-
return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
|
150
|
-
try {
|
151
|
-
return new Store(storeOptions);
|
152
|
-
}
|
153
|
-
finally {
|
154
|
-
span.end();
|
155
|
-
}
|
156
|
-
});
|
157
|
-
};
|
158
154
|
get sessionId() {
|
159
155
|
return this.clientSession.sessionId;
|
160
156
|
}
|
157
|
+
get clientId() {
|
158
|
+
return this.clientSession.clientId;
|
159
|
+
}
|
161
160
|
/**
|
162
161
|
* Subscribe to the results of a query
|
163
162
|
* Returns a function to cancel the subscription.
|
163
|
+
*
|
164
|
+
* @example
|
165
|
+
* ```ts
|
166
|
+
* const unsubscribe = store.subscribe(query$, { onUpdate: (result) => console.log(result) })
|
167
|
+
* ```
|
164
168
|
*/
|
165
|
-
subscribe = (query
|
169
|
+
subscribe = (query, options) => this.otel.tracer.startActiveSpan(`LiveStore.subscribe`, { attributes: { label: options?.label, queryLabel: query.label } }, options?.otelContext ?? this.otel.queriesSpanContext, (span) => {
|
166
170
|
// console.debug('store sub', query$.id, query$.label)
|
167
171
|
const otelContext = otel.trace.setSpan(otel.context.active(), span);
|
172
|
+
const queryRcRef = query._tag === 'def' || query._tag === 'signal-def'
|
173
|
+
? query.make(this.reactivityGraph.context)
|
174
|
+
: {
|
175
|
+
value: query,
|
176
|
+
deref: () => { },
|
177
|
+
};
|
178
|
+
const query$ = queryRcRef.value;
|
168
179
|
const label = `subscribe:${options?.label}`;
|
169
|
-
const effect = this.reactivityGraph.makeEffect((get) =>
|
180
|
+
const effect = this.reactivityGraph.makeEffect((get, _otelContext, debugRefreshReason) => options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)), { label });
|
181
|
+
if (options?.stackInfo) {
|
182
|
+
query$.activeSubscriptions.add(options.stackInfo);
|
183
|
+
}
|
184
|
+
options?.onSubscribe?.(query$);
|
170
185
|
this.activeQueries.add(query$);
|
171
186
|
// Running effect right away to get initial value (unless `skipInitialRun` is set)
|
172
|
-
if (options?.skipInitialRun !== true) {
|
173
|
-
effect.doEffect(otelContext);
|
187
|
+
if (options?.skipInitialRun !== true && !query$.isDestroyed) {
|
188
|
+
effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` });
|
174
189
|
}
|
175
190
|
const unsubscribe = () => {
|
176
191
|
// console.debug('store unsub', query$.id, query$.label)
|
177
192
|
try {
|
178
193
|
this.reactivityGraph.destroyNode(effect);
|
179
194
|
this.activeQueries.remove(query$);
|
180
|
-
|
195
|
+
if (options?.stackInfo) {
|
196
|
+
query$.activeSubscriptions.delete(options.stackInfo);
|
197
|
+
}
|
198
|
+
queryRcRef.deref();
|
199
|
+
options?.onUnsubsubscribe?.();
|
181
200
|
}
|
182
201
|
finally {
|
183
202
|
span.end();
|
@@ -185,6 +204,15 @@ export class Store extends Inspectable.Class {
|
|
185
204
|
};
|
186
205
|
return unsubscribe;
|
187
206
|
});
|
207
|
+
subscribeStream = (query$, options) => Stream.asyncPush((emit) => Effect.gen(this, function* () {
|
208
|
+
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)));
|
209
|
+
const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active();
|
210
|
+
yield* Effect.acquireRelease(Effect.sync(() => this.subscribe(query$, {
|
211
|
+
onUpdate: (result) => emit.single(result),
|
212
|
+
otelContext,
|
213
|
+
label: options?.label,
|
214
|
+
})), (unsub) => Effect.sync(() => unsub()));
|
215
|
+
}));
|
188
216
|
/**
|
189
217
|
* Synchronously queries the database without creating a LiveQuery.
|
190
218
|
* This is useful for queries that don't need to be reactive.
|
@@ -201,8 +229,7 @@ export class Store extends Inspectable.Class {
|
|
201
229
|
*/
|
202
230
|
query = (query, options) => {
|
203
231
|
if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
|
204
|
-
return this.
|
205
|
-
bindValues: prepareBindValues(query.bindValues, query.query),
|
232
|
+
return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
|
206
233
|
otelContext: options?.otelContext,
|
207
234
|
});
|
208
235
|
}
|
@@ -212,53 +239,145 @@ export class Store extends Inspectable.Class {
|
|
212
239
|
makeExecBeforeFirstRun({
|
213
240
|
table: ast.tableDef,
|
214
241
|
id: ast.id,
|
215
|
-
|
242
|
+
explicitDefaultValues: ast.explicitDefaultValues,
|
216
243
|
otelContext: options?.otelContext,
|
217
244
|
})(this.reactivityGraph.context);
|
218
245
|
}
|
219
246
|
const sqlRes = query.asSql();
|
220
247
|
const schema = getResultSchema(query);
|
221
|
-
const rawRes = this.
|
222
|
-
bindValues: sqlRes.bindValues,
|
248
|
+
const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues, {
|
223
249
|
otelContext: options?.otelContext,
|
224
250
|
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
225
251
|
});
|
226
252
|
return Schema.decodeSync(schema)(rawRes);
|
227
253
|
}
|
254
|
+
else if (query._tag === 'def') {
|
255
|
+
const query$ = query.make(this.reactivityGraph.context);
|
256
|
+
const result = this.query(query$.value, options);
|
257
|
+
query$.deref();
|
258
|
+
return result;
|
259
|
+
}
|
260
|
+
else if (query._tag === 'signal-def') {
|
261
|
+
const signal$ = query.make(this.reactivityGraph.context);
|
262
|
+
return signal$.value.get();
|
263
|
+
}
|
228
264
|
else {
|
229
|
-
return query.run(options?.otelContext);
|
265
|
+
return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason });
|
230
266
|
}
|
231
267
|
};
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
268
|
+
/**
|
269
|
+
* Set the value of a signal
|
270
|
+
*
|
271
|
+
* @example
|
272
|
+
* ```ts
|
273
|
+
* const count$ = signal(0, { label: 'count$' })
|
274
|
+
* store.setSignal(count$, 2)
|
275
|
+
* ```
|
276
|
+
*
|
277
|
+
* @example
|
278
|
+
* ```ts
|
279
|
+
* const count$ = signal(0, { label: 'count$' })
|
280
|
+
* store.setSignal(count$, (prev) => prev + 1)
|
281
|
+
* ```
|
282
|
+
*/
|
283
|
+
setSignal = (signalDef, value) => {
|
284
|
+
const signalRef = signalDef.make(this.reactivityGraph.context);
|
285
|
+
const newValue = typeof value === 'function' ? value(signalRef.value.get()) : value;
|
286
|
+
signalRef.value.set(newValue);
|
287
|
+
// The current implementation of signals i.e. the separation into `signal-def` and `signal`
|
288
|
+
// can lead to a situation where a reffed signal is immediately de-reffed when calling `store.setSignal`,
|
289
|
+
// in case there is nothing else holding a reference to the signal which leads to the set value being "lost".
|
290
|
+
// To avoid this, we don't deref the signal here if this set call is the only reference to the signal.
|
291
|
+
// Hopefully this won't lead to any issues in the future. 🤞
|
292
|
+
if (signalRef.rc > 1) {
|
293
|
+
signalRef.deref();
|
237
294
|
}
|
238
|
-
|
295
|
+
};
|
296
|
+
// #region commit
|
297
|
+
/**
|
298
|
+
* Commit a list of events to the store which will immediately update the local database
|
299
|
+
* and sync the events across other clients (similar to a `git commit`).
|
300
|
+
*
|
301
|
+
* @example
|
302
|
+
* ```ts
|
303
|
+
* store.commit(events.todoCreated({ id: nanoid(), text: 'Make coffee' }))
|
304
|
+
* ```
|
305
|
+
*
|
306
|
+
* You can call `commit` with multiple events to apply them in a single database transaction.
|
307
|
+
*
|
308
|
+
* @example
|
309
|
+
* ```ts
|
310
|
+
* const todoId = nanoid()
|
311
|
+
* store.commit(
|
312
|
+
* events.todoCreated({ id: todoId, text: 'Make coffee' }),
|
313
|
+
* events.todoCompleted({ id: todoId }))
|
314
|
+
* ```
|
315
|
+
*
|
316
|
+
* For more advanced transaction scenarios, you can pass a synchronous function to `commit` which will receive a callback
|
317
|
+
* to which you can pass multiple events to be committed in the same database transaction.
|
318
|
+
* Under the hood this will simply collect all events and apply them in a single database transaction.
|
319
|
+
*
|
320
|
+
* @example
|
321
|
+
* ```ts
|
322
|
+
* store.commit((commit) => {
|
323
|
+
* const todoId = nanoid()
|
324
|
+
* if (Math.random() > 0.5) {
|
325
|
+
* commit(events.todoCreated({ id: todoId, text: 'Make coffee' }))
|
326
|
+
* } else {
|
327
|
+
* commit(events.todoCompleted({ id: todoId }))
|
328
|
+
* }
|
329
|
+
* })
|
330
|
+
* ```
|
331
|
+
*
|
332
|
+
* When committing a large batch of events, you can also skip the database refresh to improve performance
|
333
|
+
* and call `store.manualRefresh()` after all events have been committed.
|
334
|
+
*
|
335
|
+
* @example
|
336
|
+
* ```ts
|
337
|
+
* const todos = [
|
338
|
+
* { id: nanoid(), text: 'Make coffee' },
|
339
|
+
* { id: nanoid(), text: 'Buy groceries' },
|
340
|
+
* // ... 1000 more todos
|
341
|
+
* ]
|
342
|
+
* for (const todo of todos) {
|
343
|
+
* store.commit({ skipRefresh: true }, events.todoCreated({ id: todo.id, text: todo.text }))
|
344
|
+
* }
|
345
|
+
* store.manualRefresh()
|
346
|
+
* ```
|
347
|
+
*/
|
348
|
+
commit = (firstEventOrTxnFnOrOptions, ...restEvents) => {
|
349
|
+
const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents);
|
350
|
+
for (const event of events) {
|
351
|
+
replaceSessionIdSymbol(event.args, this.clientSession.sessionId);
|
352
|
+
}
|
353
|
+
if (events.length === 0)
|
239
354
|
return;
|
240
|
-
const label = options?.label ?? 'mutate';
|
241
355
|
const skipRefresh = options?.skipRefresh ?? false;
|
242
|
-
const
|
243
|
-
|
244
|
-
// console.group('LiveStore.
|
245
|
-
//
|
356
|
+
const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext);
|
357
|
+
commitsSpan.addEvent('commit');
|
358
|
+
// console.group('LiveStore.commit', { skipRefresh })
|
359
|
+
// events.forEach((_) => console.debug(_.name, _.args))
|
246
360
|
// console.groupEnd()
|
247
361
|
let durationMs;
|
248
|
-
return this.otel.tracer.startActiveSpan('LiveStore:
|
362
|
+
return this.otel.tracer.startActiveSpan('LiveStore:commit', {
|
363
|
+
attributes: {
|
364
|
+
'livestore.eventsCount': events.length,
|
365
|
+
'livestore.eventTags': events.map((_) => _.name),
|
366
|
+
'livestore.commitLabel': options?.label,
|
367
|
+
},
|
368
|
+
links: options?.spanLinks,
|
369
|
+
}, options?.otelContext ?? this.otel.commitsSpanContext, (span) => {
|
249
370
|
const otelContext = otel.trace.setSpan(otel.context.active(), span);
|
250
371
|
try {
|
251
|
-
const { writeTables } =
|
372
|
+
const { writeTables } = (() => {
|
252
373
|
try {
|
253
|
-
const
|
254
|
-
|
255
|
-
const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext });
|
256
|
-
if (mutationsEvents.length > 1) {
|
374
|
+
const materializeEvents = () => this.syncProcessor.push(events, { otelContext });
|
375
|
+
if (events.length > 1) {
|
257
376
|
// TODO: what to do about leader transaction here?
|
258
|
-
return this.
|
377
|
+
return this.sqliteDbWrapper.txn(materializeEvents);
|
259
378
|
}
|
260
379
|
else {
|
261
|
-
return
|
380
|
+
return materializeEvents();
|
262
381
|
}
|
263
382
|
}
|
264
383
|
catch (e) {
|
@@ -269,7 +388,7 @@ export class Store extends Inspectable.Class {
|
|
269
388
|
finally {
|
270
389
|
span.end();
|
271
390
|
}
|
272
|
-
});
|
391
|
+
})();
|
273
392
|
const tablesToUpdate = [];
|
274
393
|
for (const tableName of writeTables) {
|
275
394
|
const tableRef = this.tableRefs[tableName];
|
@@ -277,8 +396,8 @@ export class Store extends Inspectable.Class {
|
|
277
396
|
tablesToUpdate.push([tableRef, null]);
|
278
397
|
}
|
279
398
|
const debugRefreshReason = {
|
280
|
-
_tag: '
|
281
|
-
|
399
|
+
_tag: 'commit',
|
400
|
+
events,
|
282
401
|
writeTables: Array.from(writeTables),
|
283
402
|
};
|
284
403
|
// Update all table refs together in a batch, to only trigger one reactive update
|
@@ -296,85 +415,134 @@ export class Store extends Inspectable.Class {
|
|
296
415
|
return { durationMs };
|
297
416
|
});
|
298
417
|
};
|
299
|
-
// #endregion
|
418
|
+
// #endregion commit
|
300
419
|
/**
|
301
|
-
*
|
420
|
+
* Returns an async iterable of events.
|
421
|
+
*
|
422
|
+
* @example
|
423
|
+
* ```ts
|
424
|
+
* for await (const event of store.events()) {
|
425
|
+
* console.log(event)
|
426
|
+
* }
|
427
|
+
* ```
|
428
|
+
*
|
429
|
+
* @example
|
430
|
+
* ```ts
|
431
|
+
* // Get all events from the beginning of time
|
432
|
+
* for await (const event of store.events({ cursor: EventSequenceNumber.ROOT })) {
|
433
|
+
* console.log(event)
|
434
|
+
* }
|
435
|
+
* ```
|
436
|
+
*/
|
437
|
+
events = (_options) => {
|
438
|
+
return notYetImplemented(`store.events() is not yet implemented but planned soon`);
|
439
|
+
};
|
440
|
+
eventsStream = (_options) => {
|
441
|
+
return notYetImplemented(`store.eventsStream() is not yet implemented but planned soon`);
|
442
|
+
};
|
443
|
+
/**
|
444
|
+
* This can be used in combination with `skipRefresh` when committing events.
|
302
445
|
* We might need a better solution for this. Let's see.
|
303
446
|
*/
|
304
447
|
manualRefresh = (options) => {
|
305
448
|
const { label } = options ?? {};
|
306
|
-
this.otel.tracer.startActiveSpan('LiveStore:manualRefresh', { attributes: { 'livestore.manualRefreshLabel': label } }, this.otel.
|
449
|
+
this.otel.tracer.startActiveSpan('LiveStore:manualRefresh', { attributes: { 'livestore.manualRefreshLabel': label } }, this.otel.commitsSpanContext, (span) => {
|
307
450
|
const otelContext = otel.trace.setSpan(otel.context.active(), span);
|
308
451
|
this.reactivityGraph.runDeferredEffects({ otelContext });
|
309
452
|
span.end();
|
310
453
|
});
|
311
454
|
};
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
const data = source === 'local' ? this.syncDbWrapper.export() : yield* this.clientSession.leaderThread.export;
|
320
|
-
downloadBlob(data, `livestore-${Date.now()}.db`);
|
321
|
-
}).pipe(this.runEffectFork);
|
322
|
-
};
|
323
|
-
__devDownloadMutationLogDb = () => {
|
324
|
-
Effect.gen(this, function* () {
|
325
|
-
const data = yield* this.clientSession.leaderThread.getMutationLogData;
|
326
|
-
downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`);
|
327
|
-
}).pipe(this.runEffectFork);
|
328
|
-
};
|
329
|
-
__devHardReset = (mode = 'all-data') => {
|
330
|
-
Effect.gen(this, function* () {
|
331
|
-
yield* this.clientSession.leaderThread.sendDevtoolsMessage(Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }));
|
332
|
-
}).pipe(this.runEffectFork);
|
333
|
-
};
|
334
|
-
__devSyncStates = () => {
|
335
|
-
Effect.gen(this, function* () {
|
336
|
-
const session = this.syncProcessor.syncStateRef.current;
|
337
|
-
console.log('Session sync state:', session);
|
338
|
-
const leader = yield* this.clientSession.leaderThread.getSyncState;
|
339
|
-
console.log('Leader sync state:', leader);
|
340
|
-
}).pipe(this.runEffectFork);
|
341
|
-
};
|
342
|
-
__devShutdown = (cause) => {
|
343
|
-
this.clientSession
|
455
|
+
/**
|
456
|
+
* Shuts down the store and closes the client session.
|
457
|
+
*
|
458
|
+
* This is called automatically when the store was created using the React or Effect API.
|
459
|
+
*/
|
460
|
+
shutdown = async (cause) => {
|
461
|
+
await this.clientSession
|
344
462
|
.shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
|
345
|
-
.pipe(
|
463
|
+
.pipe(this.runEffectFork, Fiber.join, Effect.runPromise);
|
464
|
+
};
|
465
|
+
/**
|
466
|
+
* Helper methods useful during development
|
467
|
+
*
|
468
|
+
* @internal
|
469
|
+
*/
|
470
|
+
_dev = {
|
471
|
+
downloadDb: (source = 'local') => {
|
472
|
+
Effect.gen(this, function* () {
|
473
|
+
const data = source === 'local' ? this.sqliteDbWrapper.export() : yield* this.clientSession.leaderThread.export;
|
474
|
+
downloadBlob(data, `livestore-${Date.now()}.db`);
|
475
|
+
}).pipe(this.runEffectFork);
|
476
|
+
},
|
477
|
+
downloadEventlogDb: () => {
|
478
|
+
Effect.gen(this, function* () {
|
479
|
+
const data = yield* this.clientSession.leaderThread.getEventlogData;
|
480
|
+
downloadBlob(data, `livestore-eventlog-${Date.now()}.db`);
|
481
|
+
}).pipe(this.runEffectFork);
|
482
|
+
},
|
483
|
+
hardReset: (mode = 'all-data') => {
|
484
|
+
Effect.gen(this, function* () {
|
485
|
+
const clientId = this.clientSession.clientId;
|
486
|
+
yield* this.clientSession.leaderThread.sendDevtoolsMessage(Devtools.Leader.ResetAllData.Request.make({ liveStoreVersion, mode, requestId: nanoid(), clientId }));
|
487
|
+
}).pipe(this.runEffectFork);
|
488
|
+
},
|
489
|
+
overrideNetworkStatus: (status) => {
|
490
|
+
const clientId = this.clientSession.clientId;
|
491
|
+
this.clientSession.leaderThread
|
492
|
+
.sendDevtoolsMessage(Devtools.Leader.SetSyncLatch.Request.make({
|
493
|
+
clientId,
|
494
|
+
closeLatch: status === 'offline',
|
495
|
+
liveStoreVersion,
|
496
|
+
requestId: nanoid(),
|
497
|
+
}))
|
498
|
+
.pipe(this.runEffectFork);
|
499
|
+
},
|
500
|
+
syncStates: () => {
|
501
|
+
Effect.gen(this, function* () {
|
502
|
+
const session = yield* this.syncProcessor.syncState;
|
503
|
+
console.log('Session sync state:', session.toJSON());
|
504
|
+
const leader = yield* this.clientSession.leaderThread.getSyncState;
|
505
|
+
console.log('Leader sync state:', leader.toJSON());
|
506
|
+
}).pipe(this.runEffectFork);
|
507
|
+
},
|
508
|
+
version: liveStoreVersion,
|
509
|
+
otel: {
|
510
|
+
rootSpanContext: () => otel.trace.getSpan(this.otel.rootSpanContext)?.spanContext(),
|
511
|
+
},
|
346
512
|
};
|
347
513
|
// NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
|
348
514
|
toJSON = () => ({
|
349
515
|
_tag: 'livestore.Store',
|
350
516
|
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
|
351
517
|
});
|
352
|
-
runEffectFork = (effect) => effect.pipe(Effect.forkIn(this.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.runtime));
|
353
|
-
|
354
|
-
let
|
518
|
+
runEffectFork = (effect) => effect.pipe(Effect.forkIn(this.effectContext.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.effectContext.runtime));
|
519
|
+
getCommitArgs = (firstEventOrTxnFnOrOptions, restEvents) => {
|
520
|
+
let events;
|
355
521
|
let options;
|
356
|
-
if (typeof
|
522
|
+
if (typeof firstEventOrTxnFnOrOptions === 'function') {
|
357
523
|
// TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
|
358
|
-
|
524
|
+
events = firstEventOrTxnFnOrOptions((arg) => events.push(arg));
|
359
525
|
}
|
360
|
-
else if (
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
options =
|
365
|
-
|
526
|
+
else if (firstEventOrTxnFnOrOptions?.label !== undefined ||
|
527
|
+
firstEventOrTxnFnOrOptions?.skipRefresh !== undefined ||
|
528
|
+
firstEventOrTxnFnOrOptions?.otelContext !== undefined ||
|
529
|
+
firstEventOrTxnFnOrOptions?.spanLinks !== undefined) {
|
530
|
+
options = firstEventOrTxnFnOrOptions;
|
531
|
+
events = restEvents;
|
366
532
|
}
|
367
|
-
else if (
|
368
|
-
// When `
|
369
|
-
|
533
|
+
else if (firstEventOrTxnFnOrOptions === undefined) {
|
534
|
+
// When `commit` is called with no arguments (which sometimes happens when dynamically filtering events)
|
535
|
+
events = [];
|
370
536
|
}
|
371
537
|
else {
|
372
|
-
|
538
|
+
events = [firstEventOrTxnFnOrOptions, ...restEvents];
|
373
539
|
}
|
374
|
-
|
375
|
-
//
|
376
|
-
|
377
|
-
|
540
|
+
// for (const event of events) {
|
541
|
+
// if (event.args.id === SessionIdSymbol) {
|
542
|
+
// event.args.id = this.clientSession.sessionId
|
543
|
+
// }
|
544
|
+
// }
|
545
|
+
return { events, options };
|
378
546
|
};
|
379
547
|
}
|
380
548
|
//# sourceMappingURL=store.js.map
|