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