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