@livestore/livestore 0.4.0-dev.1 → 0.4.0-dev.11
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/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +2 -4
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/db-query.d.ts.map +1 -1
- package/dist/live-queries/db-query.js +7 -4
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +53 -24
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +10 -10
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +36 -27
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +115 -0
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +3 -3
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/store-types.d.ts +13 -2
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store.d.ts +45 -29
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +165 -100
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.d.ts +3 -0
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +2 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts +15 -14
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +20 -15
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +7 -7
- package/src/ambient.d.ts +3 -3
- package/src/effect/LiveStore.ts +2 -4
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +354 -130
- package/src/live-queries/base-class.ts +6 -3
- package/src/live-queries/db-query.test.ts +70 -24
- package/src/live-queries/db-query.ts +7 -4
- package/src/mod.ts +10 -1
- package/src/reactive.test.ts +150 -1
- package/src/reactive.ts +47 -39
- package/src/store/create-store.ts +12 -4
- package/src/store/store-types.ts +23 -2
- package/src/store/store.ts +262 -193
- package/src/utils/dev.ts +5 -0
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +31 -20
- package/dist/store/store-shutdown.test.d.ts +0 -2
- package/dist/store/store-shutdown.test.d.ts.map +0 -1
- package/dist/store/store-shutdown.test.js +0 -103
- package/dist/store/store-shutdown.test.js.map +0 -1
package/dist/store/store.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { type Bindable, type ClientSession, type ClientSessionSyncProcessor,
|
|
1
|
+
import { type Bindable, type ClientSession, type ClientSessionSyncProcessor, MaterializeError, UnexpectedError } from '@livestore/common';
|
|
2
2
|
import type { LiveStoreSchema } from '@livestore/common/schema';
|
|
3
3
|
import { LiveStoreEvent } from '@livestore/common/schema';
|
|
4
4
|
import type { Scope } from '@livestore/utils/effect';
|
|
5
5
|
import { Cause, Effect, Inspectable, Schema, Stream } from '@livestore/utils/effect';
|
|
6
6
|
import * as otel from '@opentelemetry/api';
|
|
7
|
-
import type { LiveQuery,
|
|
7
|
+
import type { LiveQuery, ReactivityGraph, ReactivityGraphContext, SignalDef } from '../live-queries/base-class.ts';
|
|
8
8
|
import type { Ref } from '../reactive.ts';
|
|
9
9
|
import { SqliteDbWrapper } from '../SqliteDbWrapper.ts';
|
|
10
10
|
import { ReferenceCountedSet } from '../utils/data-structures.ts';
|
|
11
|
-
import type {
|
|
12
|
-
|
|
11
|
+
import type { Queryable, RefreshReason, StoreCommitOptions, StoreEventsOptions, StoreOptions, StoreOtel, SubscribeOptions, Unsubscribe } from './store-types.ts';
|
|
12
|
+
type SubscribeFn = {
|
|
13
|
+
<TResult>(query: Queryable<TResult>, onUpdate: (value: TResult) => void, options?: SubscribeOptions<TResult>): Unsubscribe;
|
|
14
|
+
<TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): AsyncIterable<TResult>;
|
|
15
|
+
};
|
|
13
16
|
export declare class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> extends Inspectable.Class {
|
|
14
17
|
readonly storeId: string;
|
|
15
18
|
reactivityGraph: ReactivityGraph;
|
|
@@ -18,6 +21,24 @@ export declare class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any
|
|
|
18
21
|
schema: LiveStoreSchema;
|
|
19
22
|
context: TContext;
|
|
20
23
|
otel: StoreOtel;
|
|
24
|
+
/**
|
|
25
|
+
* Reactive connectivity updates emitted by the backing sync backend.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { Effect, Stream } from 'effect'
|
|
30
|
+
*
|
|
31
|
+
* const status = await store.networkStatus.pipe(Effect.runPromise)
|
|
32
|
+
*
|
|
33
|
+
* await store.networkStatus.changes.pipe(
|
|
34
|
+
* Stream.tap((next) => console.log('network status update', next)),
|
|
35
|
+
* Stream.runDrain,
|
|
36
|
+
* Effect.scoped,
|
|
37
|
+
* Effect.runPromise,
|
|
38
|
+
* )
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
readonly networkStatus: ClientSession['leaderThread']['networkStatus'];
|
|
21
42
|
/**
|
|
22
43
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
|
23
44
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
|
@@ -38,34 +59,27 @@ export declare class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any
|
|
|
38
59
|
get clientId(): string;
|
|
39
60
|
private checkShutdown;
|
|
40
61
|
/**
|
|
41
|
-
* Subscribe to the results of a query
|
|
42
|
-
*
|
|
62
|
+
* Subscribe to the results of a query.
|
|
63
|
+
*
|
|
64
|
+
* - When providing an `onUpdate` callback it returns an {@link Unsubscribe} function.
|
|
65
|
+
* - Without a callback it returns an {@link AsyncIterable} that yields query results.
|
|
43
66
|
*
|
|
44
67
|
* @example
|
|
45
68
|
* ```ts
|
|
46
|
-
* const unsubscribe = store.subscribe(query$,
|
|
69
|
+
* const unsubscribe = store.subscribe(query$, (result) => console.log(result))
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* for await (const result of store.subscribe(query$)) {
|
|
75
|
+
* console.log(result)
|
|
76
|
+
* }
|
|
47
77
|
* ```
|
|
48
78
|
*/
|
|
49
|
-
subscribe:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/** Gets called after the query subscription has been removed */
|
|
54
|
-
onUnsubsubscribe?: () => void;
|
|
55
|
-
label?: string;
|
|
56
|
-
/**
|
|
57
|
-
* Skips the initial `onUpdate` callback
|
|
58
|
-
* @default false
|
|
59
|
-
*/
|
|
60
|
-
skipInitialRun?: boolean;
|
|
61
|
-
otelContext?: otel.Context;
|
|
62
|
-
/** If provided, the stack info will be added to the `activeSubscriptions` set of the query */
|
|
63
|
-
stackInfo?: StackInfo;
|
|
64
|
-
}) => Unsubscribe;
|
|
65
|
-
subscribeStream: <TResult>(query$: LiveQueryDef<TResult>, options?: {
|
|
66
|
-
label?: string;
|
|
67
|
-
skipInitialRun?: boolean;
|
|
68
|
-
} | undefined) => Stream.Stream<TResult>;
|
|
79
|
+
subscribe: SubscribeFn;
|
|
80
|
+
private subscribeWithCallback;
|
|
81
|
+
private subscribeAsAsyncIterable;
|
|
82
|
+
subscribeStream: <TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>) => Stream.Stream<TResult>;
|
|
69
83
|
/**
|
|
70
84
|
* Synchronously queries the database without creating a LiveQuery.
|
|
71
85
|
* This is useful for queries that don't need to be reactive.
|
|
@@ -80,7 +94,7 @@ export declare class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any
|
|
|
80
94
|
* const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })
|
|
81
95
|
* ```
|
|
82
96
|
*/
|
|
83
|
-
query: <TResult>(query:
|
|
97
|
+
query: <TResult>(query: Queryable<TResult> | {
|
|
84
98
|
query: string;
|
|
85
99
|
bindValues: Bindable;
|
|
86
100
|
schema?: Schema.Schema<TResult>;
|
|
@@ -199,12 +213,14 @@ export declare class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any
|
|
|
199
213
|
*
|
|
200
214
|
* This is called automatically when the store was created using the React or Effect API.
|
|
201
215
|
*/
|
|
202
|
-
shutdown: (cause?: Cause.Cause<UnexpectedError>) => Effect.Effect<void>;
|
|
216
|
+
shutdown: (cause?: Cause.Cause<UnexpectedError | MaterializeError>) => Effect.Effect<void>;
|
|
203
217
|
toJSON: () => {
|
|
204
218
|
_tag: string;
|
|
205
219
|
reactivityGraph: import("../reactive.ts").ReactiveGraphSnapshot;
|
|
206
220
|
};
|
|
207
221
|
private runEffectFork;
|
|
222
|
+
private runEffectPromise;
|
|
208
223
|
private getCommitArgs;
|
|
209
224
|
}
|
|
225
|
+
export {};
|
|
210
226
|
//# sourceMappingURL=store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/store/store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,0BAA0B,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/store/store.ts"],"names":[],"mappings":"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"}
|
package/dist/store/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Devtools,
|
|
2
|
-
import {
|
|
3
|
-
import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils';
|
|
1
|
+
import { Devtools, getExecStatementsFromMaterializer, getResultSchema, hashMaterializerResults, IntentionalShutdownCause, isQueryBuilder, liveStoreVersion, MaterializeError, MaterializerHashMismatchError, makeClientSessionSyncProcessor, prepareBindValues, QueryBuilderAstSymbol, replaceSessionIdSymbol, UnexpectedError, } from '@livestore/common';
|
|
2
|
+
import { LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema';
|
|
3
|
+
import { assertNever, isDevEnv, notYetImplemented, omitUndefineds, shouldNeverHappen } from '@livestore/utils';
|
|
4
4
|
import { Cause, Effect, Exit, Fiber, Inspectable, Option, OtelTracer, Runtime, Schema, Stream, } from '@livestore/utils/effect';
|
|
5
5
|
import { nanoid } from '@livestore/utils/nanoid';
|
|
6
6
|
import * as otel from '@opentelemetry/api';
|
|
@@ -21,6 +21,24 @@ export class Store extends Inspectable.Class {
|
|
|
21
21
|
schema;
|
|
22
22
|
context;
|
|
23
23
|
otel;
|
|
24
|
+
/**
|
|
25
|
+
* Reactive connectivity updates emitted by the backing sync backend.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { Effect, Stream } from 'effect'
|
|
30
|
+
*
|
|
31
|
+
* const status = await store.networkStatus.pipe(Effect.runPromise)
|
|
32
|
+
*
|
|
33
|
+
* await store.networkStatus.changes.pipe(
|
|
34
|
+
* Stream.tap((next) => console.log('network status update', next)),
|
|
35
|
+
* Stream.runDrain,
|
|
36
|
+
* Effect.scoped,
|
|
37
|
+
* Effect.runPromise,
|
|
38
|
+
* )
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
networkStatus;
|
|
24
42
|
/**
|
|
25
43
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
|
26
44
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
|
@@ -43,6 +61,7 @@ export class Store extends Inspectable.Class {
|
|
|
43
61
|
this.clientSession = clientSession;
|
|
44
62
|
this.schema = schema;
|
|
45
63
|
this.context = context;
|
|
64
|
+
this.networkStatus = clientSession.leaderThread.networkStatus;
|
|
46
65
|
this.effectContext = effectContext;
|
|
47
66
|
const reactivityGraph = makeReactivityGraph();
|
|
48
67
|
const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext);
|
|
@@ -50,23 +69,41 @@ export class Store extends Inspectable.Class {
|
|
|
50
69
|
schema,
|
|
51
70
|
clientSession,
|
|
52
71
|
runtime: effectContext.runtime,
|
|
53
|
-
materializeEvent: (
|
|
54
|
-
|
|
72
|
+
materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')((eventEncoded, { withChangeset, materializerHashLeader }) =>
|
|
73
|
+
// We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
|
|
74
|
+
Effect.gen(this, function* () {
|
|
75
|
+
const resolution = yield* resolveEventDef(schema, {
|
|
76
|
+
operation: '@livestore/livestore:store:materializeEvent',
|
|
77
|
+
event: eventEncoded,
|
|
78
|
+
});
|
|
79
|
+
if (resolution._tag === 'unknown') {
|
|
80
|
+
// Runtime schema doesn't know this event yet; skip materialization but
|
|
81
|
+
// keep the log entry so upgraded clients can replay it later.
|
|
82
|
+
return {
|
|
83
|
+
writeTables: new Set(),
|
|
84
|
+
sessionChangeset: { _tag: 'no-op' },
|
|
85
|
+
materializerHash: Option.none(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const { eventDef, materializer } = resolution;
|
|
55
89
|
const execArgsArr = getExecStatementsFromMaterializer({
|
|
56
90
|
eventDef,
|
|
57
91
|
materializer,
|
|
58
92
|
dbState: this.sqliteDbWrapper,
|
|
59
|
-
event: { decoded:
|
|
93
|
+
event: { decoded: undefined, encoded: eventEncoded },
|
|
60
94
|
});
|
|
61
95
|
const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none();
|
|
96
|
+
// Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
|
|
97
|
+
// During push path (local commits), materializerHashLeader is always Option.none(), so this condition
|
|
98
|
+
// will never be met. The check happens when the same event comes back from the leader during sync,
|
|
99
|
+
// allowing us to compare the leader's computed hash with our local re-materialization hash.
|
|
62
100
|
if (materializerHashLeader._tag === 'Some' &&
|
|
63
101
|
materializerHash._tag === 'Some' &&
|
|
64
102
|
materializerHashLeader.value !== materializerHash.value) {
|
|
65
|
-
|
|
66
|
-
cause: `Materializer hash mismatch detected for event "${eventDecoded.name}".`,
|
|
67
|
-
note: `Please make sure your event materializer is a pure function without side effects.`,
|
|
68
|
-
})));
|
|
103
|
+
return yield* MaterializerHashMismatchError.make({ eventName: eventEncoded.name });
|
|
69
104
|
}
|
|
105
|
+
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie);
|
|
106
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span);
|
|
70
107
|
const writeTablesForEvent = new Set();
|
|
71
108
|
const exec = () => {
|
|
72
109
|
for (const { statementSql, bindValues, writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql), } of execArgsArr) {
|
|
@@ -74,16 +111,17 @@ export class Store extends Inspectable.Class {
|
|
|
74
111
|
this.sqliteDbWrapper.cachedExecute(statementSql, bindValues, { otelContext, writeTables });
|
|
75
112
|
}
|
|
76
113
|
catch (cause) {
|
|
114
|
+
// TOOD refactor with `SqliteError`
|
|
77
115
|
throw UnexpectedError.make({
|
|
78
116
|
cause,
|
|
79
|
-
note: `Error executing materializer for event "${
|
|
117
|
+
note: `Error executing materializer for event "${eventEncoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
|
|
80
118
|
});
|
|
81
119
|
}
|
|
82
120
|
// durationMsTotal += durationMs
|
|
83
121
|
for (const table of writeTables) {
|
|
84
122
|
writeTablesForEvent.add(table);
|
|
85
123
|
}
|
|
86
|
-
this.sqliteDbWrapper.debug.head =
|
|
124
|
+
this.sqliteDbWrapper.debug.head = eventEncoded.seqNum;
|
|
87
125
|
}
|
|
88
126
|
};
|
|
89
127
|
let sessionChangeset = { _tag: 'unset' };
|
|
@@ -94,7 +132,7 @@ export class Store extends Inspectable.Class {
|
|
|
94
132
|
exec();
|
|
95
133
|
}
|
|
96
134
|
return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash };
|
|
97
|
-
},
|
|
135
|
+
}).pipe(Effect.mapError((cause) => MaterializeError.make({ cause })))),
|
|
98
136
|
rollback: (changeset) => {
|
|
99
137
|
this.sqliteDbWrapper.rollback(changeset);
|
|
100
138
|
},
|
|
@@ -109,8 +147,12 @@ export class Store extends Inspectable.Class {
|
|
|
109
147
|
},
|
|
110
148
|
span: syncSpan,
|
|
111
149
|
params: {
|
|
112
|
-
|
|
113
|
-
|
|
150
|
+
...omitUndefineds({
|
|
151
|
+
leaderPushBatchSize: params.leaderPushBatchSize,
|
|
152
|
+
}),
|
|
153
|
+
...(params.simulation?.clientSessionSyncProcessor !== undefined
|
|
154
|
+
? { simulation: params.simulation.clientSessionSyncProcessor }
|
|
155
|
+
: {}),
|
|
114
156
|
},
|
|
115
157
|
confirmUnsavedChanges,
|
|
116
158
|
});
|
|
@@ -188,18 +230,32 @@ export class Store extends Inspectable.Class {
|
|
|
188
230
|
}
|
|
189
231
|
};
|
|
190
232
|
/**
|
|
191
|
-
* Subscribe to the results of a query
|
|
192
|
-
*
|
|
233
|
+
* Subscribe to the results of a query.
|
|
234
|
+
*
|
|
235
|
+
* - When providing an `onUpdate` callback it returns an {@link Unsubscribe} function.
|
|
236
|
+
* - Without a callback it returns an {@link AsyncIterable} that yields query results.
|
|
193
237
|
*
|
|
194
238
|
* @example
|
|
195
239
|
* ```ts
|
|
196
|
-
* const unsubscribe = store.subscribe(query$,
|
|
240
|
+
* const unsubscribe = store.subscribe(query$, (result) => console.log(result))
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* for await (const result of store.subscribe(query$)) {
|
|
246
|
+
* console.log(result)
|
|
247
|
+
* }
|
|
197
248
|
* ```
|
|
198
249
|
*/
|
|
199
|
-
subscribe = (query,
|
|
250
|
+
subscribe = ((query, onUpdateOrOptions, maybeOptions) => {
|
|
251
|
+
if (typeof onUpdateOrOptions === 'function') {
|
|
252
|
+
return this.subscribeWithCallback(query, onUpdateOrOptions, maybeOptions);
|
|
253
|
+
}
|
|
254
|
+
return this.subscribeAsAsyncIterable(query, onUpdateOrOptions);
|
|
255
|
+
});
|
|
256
|
+
subscribeWithCallback = (query, onUpdate, options) => {
|
|
200
257
|
this.checkShutdown('subscribe');
|
|
201
258
|
return this.otel.tracer.startActiveSpan(`LiveStore.subscribe`, { attributes: { label: options?.label, queryLabel: isQueryBuilder(query) ? query.toString() : query.label } }, options?.otelContext ?? this.otel.queriesSpanContext, (span) => {
|
|
202
|
-
// console.debug('store sub', query$.id, query$.label)
|
|
203
259
|
const otelContext = otel.trace.setSpan(otel.context.active(), span);
|
|
204
260
|
const queryRcRef = isQueryBuilder(query)
|
|
205
261
|
? queryDb(query).make(this.reactivityGraph.context)
|
|
@@ -211,18 +267,19 @@ export class Store extends Inspectable.Class {
|
|
|
211
267
|
};
|
|
212
268
|
const query$ = queryRcRef.value;
|
|
213
269
|
const label = `subscribe:${options?.label}`;
|
|
214
|
-
const effect = this.reactivityGraph.makeEffect((get, _otelContext, debugRefreshReason) =>
|
|
270
|
+
const effect = this.reactivityGraph.makeEffect((get, _otelContext, debugRefreshReason) => onUpdate(get(query$.results$, otelContext, debugRefreshReason)), { label });
|
|
215
271
|
if (options?.stackInfo) {
|
|
216
272
|
query$.activeSubscriptions.add(options.stackInfo);
|
|
217
273
|
}
|
|
218
274
|
options?.onSubscribe?.(query$);
|
|
219
275
|
this.activeQueries.add(query$);
|
|
220
|
-
// Running effect right away to get initial value (unless `skipInitialRun` is set)
|
|
221
276
|
if (options?.skipInitialRun !== true && !query$.isDestroyed) {
|
|
222
|
-
effect.doEffect(otelContext, {
|
|
277
|
+
effect.doEffect(otelContext, {
|
|
278
|
+
_tag: 'subscribe.initial',
|
|
279
|
+
label: `subscribe-initial-run:${options?.label}`,
|
|
280
|
+
});
|
|
223
281
|
}
|
|
224
282
|
const unsubscribe = () => {
|
|
225
|
-
// console.debug('store unsub', query$.id, query$.label)
|
|
226
283
|
try {
|
|
227
284
|
this.reactivityGraph.destroyNode(effect);
|
|
228
285
|
this.activeQueries.remove(query$);
|
|
@@ -239,13 +296,16 @@ export class Store extends Inspectable.Class {
|
|
|
239
296
|
return unsubscribe;
|
|
240
297
|
});
|
|
241
298
|
};
|
|
242
|
-
|
|
299
|
+
subscribeAsAsyncIterable = (query, options) => {
|
|
300
|
+
this.checkShutdown('subscribe');
|
|
301
|
+
return Stream.toAsyncIterable(this.subscribeStream(query, options));
|
|
302
|
+
};
|
|
303
|
+
subscribeStream = (query, options) => Stream.asyncPush((emit) => Effect.gen(this, function* () {
|
|
243
304
|
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)));
|
|
244
305
|
const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active();
|
|
245
|
-
yield* Effect.acquireRelease(Effect.sync(() => this.subscribe(query
|
|
246
|
-
|
|
306
|
+
yield* Effect.acquireRelease(Effect.sync(() => this.subscribe(query, (result) => emit.single(result), {
|
|
307
|
+
...(options ?? {}),
|
|
247
308
|
otelContext,
|
|
248
|
-
label: options?.label,
|
|
249
309
|
})), (unsub) => Effect.sync(() => unsub()));
|
|
250
310
|
}));
|
|
251
311
|
/**
|
|
@@ -266,7 +326,7 @@ export class Store extends Inspectable.Class {
|
|
|
266
326
|
this.checkShutdown('query');
|
|
267
327
|
if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
|
|
268
328
|
const res = this.sqliteDbWrapper.cachedSelect(query.query, prepareBindValues(query.bindValues, query.query), {
|
|
269
|
-
otelContext: options?.otelContext,
|
|
329
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
270
330
|
});
|
|
271
331
|
if (query.schema) {
|
|
272
332
|
return Schema.decodeSync(query.schema)(res);
|
|
@@ -290,10 +350,16 @@ export class Store extends Inspectable.Class {
|
|
|
290
350
|
replaceSessionIdSymbol(sqlRes.bindValues, this.clientSession.sessionId);
|
|
291
351
|
}
|
|
292
352
|
const rawRes = this.sqliteDbWrapper.cachedSelect(sqlRes.query, sqlRes.bindValues, {
|
|
293
|
-
otelContext: options?.otelContext,
|
|
353
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
294
354
|
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
|
295
355
|
});
|
|
296
|
-
|
|
356
|
+
const decodeResult = Schema.decodeEither(schema)(rawRes);
|
|
357
|
+
if (decodeResult._tag === 'Right') {
|
|
358
|
+
return decodeResult.right;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
return shouldNeverHappen(`Failed to decode query result with for schema:`, schema.toString(), 'raw result:', rawRes, 'decode error:', decodeResult.left);
|
|
362
|
+
}
|
|
297
363
|
}
|
|
298
364
|
else if (query._tag === 'def') {
|
|
299
365
|
const query$ = query.make(this.reactivityGraph.context);
|
|
@@ -306,7 +372,9 @@ export class Store extends Inspectable.Class {
|
|
|
306
372
|
return signal$.value.get();
|
|
307
373
|
}
|
|
308
374
|
else {
|
|
309
|
-
return query.run({
|
|
375
|
+
return query.run({
|
|
376
|
+
...omitUndefineds({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason }),
|
|
377
|
+
});
|
|
310
378
|
}
|
|
311
379
|
};
|
|
312
380
|
/**
|
|
@@ -393,73 +461,65 @@ export class Store extends Inspectable.Class {
|
|
|
393
461
|
commit = (firstEventOrTxnFnOrOptions, ...restEvents) => {
|
|
394
462
|
this.checkShutdown('commit');
|
|
395
463
|
const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
464
|
+
Effect.gen(this, function* () {
|
|
465
|
+
const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext);
|
|
466
|
+
commitsSpan?.addEvent('commit');
|
|
467
|
+
const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie);
|
|
468
|
+
commitsSpan?.addLink({ context: currentSpan.spanContext() });
|
|
469
|
+
for (const event of events) {
|
|
470
|
+
replaceSessionIdSymbol(event.args, this.clientSession.sessionId);
|
|
471
|
+
}
|
|
472
|
+
if (events.length === 0)
|
|
473
|
+
return;
|
|
474
|
+
const localRuntime = yield* Effect.runtime();
|
|
475
|
+
const materializeEventsTx = Effect.try({
|
|
476
|
+
try: () => {
|
|
477
|
+
const runMaterializeEvents = () => {
|
|
478
|
+
return this.syncProcessor.push(events).pipe(Runtime.runSync(localRuntime));
|
|
479
|
+
};
|
|
480
|
+
if (events.length > 1) {
|
|
481
|
+
return this.sqliteDbWrapper.txn(runMaterializeEvents);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
return runMaterializeEvents();
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
catch: (cause) => UnexpectedError.make({ cause }),
|
|
488
|
+
});
|
|
489
|
+
// Materialize events to state
|
|
490
|
+
const { writeTables } = yield* materializeEventsTx;
|
|
491
|
+
const tablesToUpdate = [];
|
|
492
|
+
for (const tableName of writeTables) {
|
|
493
|
+
const tableRef = this.tableRefs[tableName];
|
|
494
|
+
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`);
|
|
495
|
+
tablesToUpdate.push([tableRef, null]);
|
|
496
|
+
}
|
|
497
|
+
const debugRefreshReason = {
|
|
498
|
+
_tag: 'commit',
|
|
499
|
+
events,
|
|
500
|
+
writeTables: Array.from(writeTables),
|
|
501
|
+
};
|
|
502
|
+
const skipRefresh = options?.skipRefresh ?? false;
|
|
503
|
+
// Update all table refs together in a batch, to only trigger one reactive update
|
|
504
|
+
this.reactivityGraph.setRefs(tablesToUpdate, {
|
|
505
|
+
debugRefreshReason,
|
|
506
|
+
skipRefresh,
|
|
507
|
+
otelContext: otel.trace.setSpan(otel.context.active(), currentSpan),
|
|
508
|
+
});
|
|
509
|
+
}).pipe(Effect.withSpan('LiveStore:commit', {
|
|
510
|
+
root: true,
|
|
409
511
|
attributes: {
|
|
410
512
|
'livestore.eventsCount': events.length,
|
|
411
513
|
'livestore.eventTags': events.map((_) => _.name),
|
|
412
|
-
'livestore.commitLabel': options
|
|
514
|
+
...(options?.label && { 'livestore.commitLabel': options.label }),
|
|
413
515
|
},
|
|
414
|
-
links:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const materializeEvents = () => this.syncProcessor.push(events, { otelContext });
|
|
422
|
-
if (events.length > 1) {
|
|
423
|
-
return this.sqliteDbWrapper.txn(materializeEvents);
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
return materializeEvents();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
catch (e) {
|
|
430
|
-
console.error(e);
|
|
431
|
-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() });
|
|
432
|
-
throw e;
|
|
433
|
-
}
|
|
434
|
-
finally {
|
|
435
|
-
span.end();
|
|
436
|
-
}
|
|
437
|
-
})();
|
|
438
|
-
const tablesToUpdate = [];
|
|
439
|
-
for (const tableName of writeTables) {
|
|
440
|
-
const tableRef = this.tableRefs[tableName];
|
|
441
|
-
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`);
|
|
442
|
-
tablesToUpdate.push([tableRef, null]);
|
|
443
|
-
}
|
|
444
|
-
const debugRefreshReason = {
|
|
445
|
-
_tag: 'commit',
|
|
446
|
-
events,
|
|
447
|
-
writeTables: Array.from(writeTables),
|
|
448
|
-
};
|
|
449
|
-
// Update all table refs together in a batch, to only trigger one reactive update
|
|
450
|
-
this.reactivityGraph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext, skipRefresh });
|
|
451
|
-
}
|
|
452
|
-
catch (e) {
|
|
453
|
-
console.error(e);
|
|
454
|
-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() });
|
|
455
|
-
throw e;
|
|
456
|
-
}
|
|
457
|
-
finally {
|
|
458
|
-
span.end();
|
|
459
|
-
durationMs = getDurationMsFromSpan(span);
|
|
460
|
-
}
|
|
461
|
-
return { durationMs };
|
|
462
|
-
});
|
|
516
|
+
links: [
|
|
517
|
+
// Span link to LiveStore:commits
|
|
518
|
+
OtelTracer.makeSpanLink({ context: otel.trace.getSpanContext(this.otel.commitsSpanContext) }),
|
|
519
|
+
// User-provided span links
|
|
520
|
+
...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
|
|
521
|
+
],
|
|
522
|
+
}), Effect.tapErrorCause(Effect.logError), Effect.catchAllCause((cause) => Effect.fork(this.shutdown(cause))), Runtime.runSync(this.effectContext.runtime));
|
|
463
523
|
};
|
|
464
524
|
// #endregion commit
|
|
465
525
|
/**
|
|
@@ -517,7 +577,6 @@ export class Store extends Inspectable.Class {
|
|
|
517
577
|
* This is called automatically when the store was created using the React or Effect API.
|
|
518
578
|
*/
|
|
519
579
|
shutdown = (cause) => {
|
|
520
|
-
this.checkShutdown('shutdown');
|
|
521
580
|
this.isShutdown = true;
|
|
522
581
|
return this.clientSession.shutdown(cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })));
|
|
523
582
|
};
|
|
@@ -556,12 +615,17 @@ export class Store extends Inspectable.Class {
|
|
|
556
615
|
}))
|
|
557
616
|
.pipe(this.runEffectFork);
|
|
558
617
|
},
|
|
559
|
-
syncStates: () => {
|
|
618
|
+
syncStates: () => Effect.gen(this, function* () {
|
|
619
|
+
const session = yield* this.syncProcessor.syncState;
|
|
620
|
+
const leader = yield* this.clientSession.leaderThread.syncState;
|
|
621
|
+
return { session, leader };
|
|
622
|
+
}).pipe(this.runEffectPromise),
|
|
623
|
+
printSyncStates: () => {
|
|
560
624
|
Effect.gen(this, function* () {
|
|
561
625
|
const session = yield* this.syncProcessor.syncState;
|
|
562
|
-
|
|
563
|
-
const leader = yield* this.clientSession.leaderThread.
|
|
564
|
-
|
|
626
|
+
yield* Effect.log(`Session sync state: ${session.localHead} (upstream: ${session.upstreamHead})`, session.toJSON());
|
|
627
|
+
const leader = yield* this.clientSession.leaderThread.syncState;
|
|
628
|
+
yield* Effect.log(`Leader sync state: ${leader.localHead} (upstream: ${leader.upstreamHead})`, leader.toJSON());
|
|
565
629
|
}).pipe(this.runEffectFork);
|
|
566
630
|
},
|
|
567
631
|
version: liveStoreVersion,
|
|
@@ -575,6 +639,7 @@ export class Store extends Inspectable.Class {
|
|
|
575
639
|
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
|
|
576
640
|
});
|
|
577
641
|
runEffectFork = (effect) => effect.pipe(Effect.forkIn(this.effectContext.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.effectContext.runtime));
|
|
642
|
+
runEffectPromise = (effect) => effect.pipe(Effect.tapCauseLogPretty, Runtime.runPromise(this.effectContext.runtime));
|
|
578
643
|
getCommitArgs = (firstEventOrTxnFnOrOptions, restEvents) => {
|
|
579
644
|
let events;
|
|
580
645
|
let options;
|