@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/src/store/store.ts
CHANGED
@@ -8,7 +8,8 @@ import type {
|
|
8
8
|
} from '@livestore/common'
|
9
9
|
import {
|
10
10
|
Devtools,
|
11
|
-
|
11
|
+
getDurationMsFromSpan,
|
12
|
+
getExecArgsFromEvent,
|
12
13
|
getResultSchema,
|
13
14
|
IntentionalShutdownCause,
|
14
15
|
isQueryBuilder,
|
@@ -19,103 +20,109 @@ import {
|
|
19
20
|
replaceSessionIdSymbol,
|
20
21
|
} from '@livestore/common'
|
21
22
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
22
|
-
import {
|
23
|
-
|
24
|
-
SCHEMA_META_TABLE,
|
25
|
-
SCHEMA_MUTATIONS_META_TABLE,
|
26
|
-
SESSION_CHANGESET_META_TABLE,
|
27
|
-
} from '@livestore/common/schema'
|
28
|
-
import { assertNever, isDevEnv } from '@livestore/utils'
|
23
|
+
import { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
|
24
|
+
import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils'
|
29
25
|
import type { Scope } from '@livestore/utils/effect'
|
30
|
-
import { Cause,
|
26
|
+
import { Cause, Effect, Fiber, Inspectable, OtelTracer, Runtime, Schema, Stream } from '@livestore/utils/effect'
|
31
27
|
import { nanoid } from '@livestore/utils/nanoid'
|
32
28
|
import * as otel from '@opentelemetry/api'
|
33
|
-
import { type GraphQLSchema } from 'graphql'
|
34
29
|
|
35
|
-
import type {
|
30
|
+
import type {
|
31
|
+
LiveQuery,
|
32
|
+
LiveQueryDef,
|
33
|
+
ReactivityGraph,
|
34
|
+
ReactivityGraphContext,
|
35
|
+
SignalDef,
|
36
|
+
} from '../live-queries/base-class.js'
|
37
|
+
import { makeReactivityGraph } from '../live-queries/base-class.js'
|
38
|
+
import { makeExecBeforeFirstRun } from '../live-queries/client-document-get-query.js'
|
36
39
|
import type { Ref } from '../reactive.js'
|
37
|
-
import {
|
38
|
-
import { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
|
40
|
+
import { SqliteDbWrapper } from '../SqliteDbWrapper.js'
|
39
41
|
import { ReferenceCountedSet } from '../utils/data-structures.js'
|
40
42
|
import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
|
41
|
-
import {
|
42
|
-
import type {
|
43
|
+
import type { StackInfo } from '../utils/stack-info.js'
|
44
|
+
import type {
|
45
|
+
RefreshReason,
|
46
|
+
StoreCommitOptions,
|
47
|
+
StoreEventsOptions,
|
48
|
+
StoreOptions,
|
49
|
+
StoreOtel,
|
50
|
+
Unsubscribe,
|
51
|
+
} from './store-types.js'
|
43
52
|
|
44
53
|
if (isDevEnv()) {
|
45
54
|
exposeDebugUtils()
|
46
55
|
}
|
47
56
|
|
48
|
-
export class Store<
|
49
|
-
TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
|
50
|
-
TSchema extends LiveStoreSchema = LiveStoreSchema,
|
51
|
-
> extends Inspectable.Class {
|
57
|
+
export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}> extends Inspectable.Class {
|
52
58
|
readonly storeId: string
|
53
59
|
reactivityGraph: ReactivityGraph
|
54
|
-
|
60
|
+
sqliteDbWrapper: SqliteDbWrapper
|
55
61
|
clientSession: ClientSession
|
56
62
|
schema: LiveStoreSchema
|
57
|
-
|
58
|
-
graphQLContext?: TGraphQLContext
|
63
|
+
context: TContext
|
59
64
|
otel: StoreOtel
|
60
65
|
/**
|
61
66
|
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
62
67
|
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
63
68
|
*/
|
64
|
-
tableRefs: { [key: string]: Ref<null,
|
69
|
+
tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> }
|
65
70
|
|
66
|
-
private
|
71
|
+
private effectContext: {
|
72
|
+
runtime: Runtime.Runtime<Scope.Scope>
|
73
|
+
lifetimeScope: Scope.Scope
|
74
|
+
}
|
67
75
|
|
68
76
|
/** RC-based set to see which queries are currently subscribed to */
|
69
77
|
activeQueries: ReferenceCountedSet<LiveQuery<any>>
|
70
78
|
|
71
|
-
// NOTE this is currently exposed for the Devtools databrowser to
|
72
|
-
readonly
|
73
|
-
|
74
|
-
|
75
|
-
readonly
|
79
|
+
// NOTE this is currently exposed for the Devtools databrowser to commit events
|
80
|
+
readonly __eventSchema
|
81
|
+
readonly syncProcessor: ClientSessionSyncProcessor
|
82
|
+
|
83
|
+
readonly boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
|
76
84
|
|
77
85
|
// #region constructor
|
78
|
-
|
86
|
+
constructor({
|
79
87
|
clientSession,
|
80
88
|
schema,
|
81
|
-
graphQLOptions,
|
82
|
-
reactivityGraph,
|
83
89
|
otelOptions,
|
84
|
-
|
90
|
+
context,
|
85
91
|
batchUpdates,
|
86
|
-
unsyncedMutationEvents,
|
87
92
|
storeId,
|
88
|
-
|
89
|
-
|
90
|
-
|
93
|
+
effectContext,
|
94
|
+
params,
|
95
|
+
confirmUnsavedChanges,
|
96
|
+
__runningInDevtools,
|
97
|
+
}: StoreOptions<TSchema, TContext>) {
|
91
98
|
super()
|
92
99
|
|
93
100
|
this.storeId = storeId
|
94
|
-
this.unsyncedMutationEvents = unsyncedMutationEvents
|
95
101
|
|
96
|
-
this.
|
102
|
+
this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb })
|
97
103
|
this.clientSession = clientSession
|
98
104
|
this.schema = schema
|
105
|
+
this.context = context
|
106
|
+
|
107
|
+
this.effectContext = effectContext
|
99
108
|
|
100
|
-
|
101
|
-
this.runtime = runtime
|
109
|
+
const reactivityGraph = makeReactivityGraph()
|
102
110
|
|
103
111
|
const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
|
104
112
|
|
105
113
|
this.syncProcessor = makeClientSessionSyncProcessor({
|
106
114
|
schema,
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
115
|
+
clientSession,
|
116
|
+
runtime: effectContext.runtime,
|
117
|
+
materializeEvent: (eventDecoded, { otelContext, withChangeset }) => {
|
118
|
+
const { eventDef, materializer } = getEventDef(schema, eventDecoded.name)
|
119
|
+
|
120
|
+
const execArgsArr = getExecArgsFromEvent({
|
121
|
+
eventDef,
|
122
|
+
materializer,
|
123
|
+
db: this.sqliteDbWrapper,
|
124
|
+
event: { decoded: eventDecoded, encoded: undefined },
|
125
|
+
})
|
119
126
|
|
120
127
|
const writeTablesForEvent = new Set<string>()
|
121
128
|
|
@@ -123,18 +130,21 @@ export class Store<
|
|
123
130
|
for (const {
|
124
131
|
statementSql,
|
125
132
|
bindValues,
|
126
|
-
writeTables = this.
|
133
|
+
writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
|
127
134
|
} of execArgsArr) {
|
128
|
-
this.
|
135
|
+
this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables })
|
129
136
|
|
130
137
|
// durationMsTotal += durationMs
|
131
138
|
writeTables.forEach((table) => writeTablesForEvent.add(table))
|
132
139
|
}
|
133
140
|
}
|
134
141
|
|
135
|
-
let sessionChangeset:
|
142
|
+
let sessionChangeset:
|
143
|
+
| { _tag: 'sessionChangeset'; data: Uint8Array; debug: any }
|
144
|
+
| { _tag: 'no-op' }
|
145
|
+
| { _tag: 'unset' } = { _tag: 'unset' }
|
136
146
|
if (withChangeset === true) {
|
137
|
-
sessionChangeset = this.
|
147
|
+
sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
|
138
148
|
} else {
|
139
149
|
exec()
|
140
150
|
}
|
@@ -142,35 +152,41 @@ export class Store<
|
|
142
152
|
return { writeTables: writeTablesForEvent, sessionChangeset }
|
143
153
|
},
|
144
154
|
rollback: (changeset) => {
|
145
|
-
this.
|
155
|
+
this.sqliteDbWrapper.rollback(changeset)
|
146
156
|
},
|
147
157
|
refreshTables: (tables) => {
|
148
|
-
const tablesToUpdate = [] as [Ref<null,
|
158
|
+
const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
|
149
159
|
for (const tableName of tables) {
|
150
160
|
const tableRef = this.tableRefs[tableName]
|
151
161
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
152
162
|
tablesToUpdate.push([tableRef!, null])
|
153
163
|
}
|
154
|
-
|
164
|
+
reactivityGraph.setRefs(tablesToUpdate)
|
155
165
|
},
|
156
166
|
span: syncSpan,
|
167
|
+
params: {
|
168
|
+
leaderPushBatchSize: params.leaderPushBatchSize,
|
169
|
+
},
|
170
|
+
confirmUnsavedChanges,
|
157
171
|
})
|
158
172
|
|
159
|
-
this.
|
173
|
+
this.__eventSchema = LiveStoreEvent.makeEventDefSchemaMemo(schema)
|
160
174
|
|
161
175
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
162
176
|
this.tableRefs = {}
|
163
177
|
this.activeQueries = new ReferenceCountedSet()
|
164
178
|
|
165
|
-
const
|
166
|
-
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(),
|
179
|
+
const commitsSpan = otelOptions.tracer.startSpan('LiveStore:commits', {}, otelOptions.rootSpanContext)
|
180
|
+
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), commitsSpan)
|
167
181
|
|
168
182
|
const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext)
|
169
183
|
const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
|
170
184
|
|
171
185
|
this.reactivityGraph = reactivityGraph
|
172
186
|
this.reactivityGraph.context = {
|
173
|
-
store: this as unknown as Store<
|
187
|
+
store: this as unknown as Store<LiveStoreSchema>,
|
188
|
+
defRcMap: new Map(),
|
189
|
+
reactivityGraph: new WeakRef(reactivityGraph),
|
174
190
|
otelTracer: otelOptions.tracer,
|
175
191
|
rootOtelContext: otelQueriesSpanContext,
|
176
192
|
effectsWrapper: batchUpdates,
|
@@ -178,23 +194,18 @@ export class Store<
|
|
178
194
|
|
179
195
|
this.otel = {
|
180
196
|
tracer: otelOptions.tracer,
|
181
|
-
|
197
|
+
rootSpanContext: otelOptions.rootSpanContext,
|
198
|
+
commitsSpanContext: otelMuationsSpanContext,
|
182
199
|
queriesSpanContext: otelQueriesSpanContext,
|
183
200
|
}
|
184
201
|
|
185
|
-
// TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
|
186
|
-
// But for now this is a good enough approximation with little downsides
|
187
|
-
const isRunningInDevtools = disableDevtools === true
|
188
|
-
|
189
202
|
// Need a set here since `schema.tables` might contain duplicates and some componentStateTables
|
190
203
|
const allTableNames = new Set(
|
191
|
-
// NOTE we're excluding the LiveStore schema and
|
204
|
+
// NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
|
192
205
|
// unless LiveStore is running in the devtools
|
193
|
-
|
194
|
-
? this.schema.tables.keys()
|
195
|
-
: Array.from(this.schema.tables.keys()).filter(
|
196
|
-
(_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE && _ !== SESSION_CHANGESET_META_TABLE,
|
197
|
-
),
|
206
|
+
__runningInDevtools
|
207
|
+
? this.schema.state.sqlite.tables.keys()
|
208
|
+
: Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)),
|
198
209
|
)
|
199
210
|
const existingTableRefs = new Map(
|
200
211
|
Array.from(this.reactivityGraph.atoms.values())
|
@@ -202,15 +213,16 @@ export class Store<
|
|
202
213
|
.map((_) => [_.label!.slice('tableRef:'.length), _] as const),
|
203
214
|
)
|
204
215
|
for (const tableName of allTableNames) {
|
205
|
-
this.tableRefs[tableName] =
|
216
|
+
this.tableRefs[tableName] =
|
217
|
+
existingTableRefs.get(tableName) ??
|
218
|
+
this.reactivityGraph.makeRef(null, {
|
219
|
+
equal: () => false,
|
220
|
+
label: `tableRef:${tableName}`,
|
221
|
+
meta: { liveStoreRefType: 'table' },
|
222
|
+
})
|
206
223
|
}
|
207
224
|
|
208
|
-
|
209
|
-
this.graphQLSchema = graphQLOptions.schema
|
210
|
-
this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer, clientSession.sessionId)
|
211
|
-
}
|
212
|
-
|
213
|
-
Effect.gen(this, function* () {
|
225
|
+
this.boot = Effect.gen(this, function* () {
|
214
226
|
yield* Effect.addFinalizer(() =>
|
215
227
|
Effect.sync(() => {
|
216
228
|
// Remove all table refs from the reactivity graph
|
@@ -222,60 +234,87 @@ export class Store<
|
|
222
234
|
|
223
235
|
// End the otel spans
|
224
236
|
syncSpan.end()
|
225
|
-
|
237
|
+
commitsSpan.end()
|
226
238
|
queriesSpan.end()
|
227
239
|
}),
|
228
240
|
)
|
229
241
|
|
230
242
|
yield* this.syncProcessor.boot
|
231
|
-
}).pipe(this.runEffectFork)
|
232
|
-
}
|
233
|
-
// #endregion constructor
|
234
|
-
|
235
|
-
static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
|
236
|
-
storeOptions: StoreOptions<TGraphQLContext, TSchema>,
|
237
|
-
parentSpan: otel.Span,
|
238
|
-
): Store<TGraphQLContext, TSchema> => {
|
239
|
-
const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
|
240
|
-
return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
|
241
|
-
try {
|
242
|
-
return new Store(storeOptions)
|
243
|
-
} finally {
|
244
|
-
span.end()
|
245
|
-
}
|
246
243
|
})
|
247
244
|
}
|
245
|
+
// #endregion constructor
|
248
246
|
|
249
247
|
get sessionId(): string {
|
250
248
|
return this.clientSession.sessionId
|
251
249
|
}
|
252
250
|
|
251
|
+
get clientId(): string {
|
252
|
+
return this.clientSession.clientId
|
253
|
+
}
|
254
|
+
|
253
255
|
/**
|
254
256
|
* Subscribe to the results of a query
|
255
257
|
* Returns a function to cancel the subscription.
|
258
|
+
*
|
259
|
+
* @example
|
260
|
+
* ```ts
|
261
|
+
* const unsubscribe = store.subscribe(query$, { onUpdate: (result) => console.log(result) })
|
262
|
+
* ```
|
256
263
|
*/
|
257
264
|
subscribe = <TResult>(
|
258
|
-
query
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
265
|
+
query: LiveQueryDef<TResult> | LiveQuery<TResult>,
|
266
|
+
options: {
|
267
|
+
/** Called when the query result has changed */
|
268
|
+
onUpdate: (value: TResult) => void
|
269
|
+
onSubscribe?: (query$: LiveQuery<TResult>) => void
|
270
|
+
/** Gets called after the query subscription has been removed */
|
271
|
+
onUnsubsubscribe?: () => void
|
272
|
+
label?: string
|
273
|
+
/**
|
274
|
+
* Skips the initial `onUpdate` callback
|
275
|
+
* @default false
|
276
|
+
*/
|
277
|
+
skipInitialRun?: boolean
|
278
|
+
otelContext?: otel.Context
|
279
|
+
/** If provided, the stack info will be added to the `activeSubscriptions` set of the query */
|
280
|
+
stackInfo?: StackInfo
|
281
|
+
},
|
282
|
+
): Unsubscribe =>
|
263
283
|
this.otel.tracer.startActiveSpan(
|
264
284
|
`LiveStore.subscribe`,
|
265
|
-
{ attributes: { label: options?.label, queryLabel: query
|
285
|
+
{ attributes: { label: options?.label, queryLabel: query.label } },
|
266
286
|
options?.otelContext ?? this.otel.queriesSpanContext,
|
267
287
|
(span) => {
|
268
288
|
// console.debug('store sub', query$.id, query$.label)
|
269
289
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
270
290
|
|
291
|
+
const queryRcRef =
|
292
|
+
query._tag === 'def'
|
293
|
+
? query.make(this.reactivityGraph.context!)
|
294
|
+
: {
|
295
|
+
value: query,
|
296
|
+
deref: () => {},
|
297
|
+
}
|
298
|
+
const query$ = queryRcRef.value
|
299
|
+
|
271
300
|
const label = `subscribe:${options?.label}`
|
272
|
-
const effect = this.reactivityGraph.makeEffect(
|
301
|
+
const effect = this.reactivityGraph.makeEffect(
|
302
|
+
(get, _otelContext, debugRefreshReason) =>
|
303
|
+
options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
|
304
|
+
{ label },
|
305
|
+
)
|
306
|
+
|
307
|
+
if (options?.stackInfo) {
|
308
|
+
query$.activeSubscriptions.add(options.stackInfo)
|
309
|
+
}
|
310
|
+
|
311
|
+
options?.onSubscribe?.(query$)
|
273
312
|
|
274
313
|
this.activeQueries.add(query$ as LiveQuery<TResult>)
|
275
314
|
|
276
315
|
// Running effect right away to get initial value (unless `skipInitialRun` is set)
|
277
|
-
if (options?.skipInitialRun !== true) {
|
278
|
-
effect.doEffect(otelContext)
|
316
|
+
if (options?.skipInitialRun !== true && !query$.isDestroyed) {
|
317
|
+
effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` })
|
279
318
|
}
|
280
319
|
|
281
320
|
const unsubscribe = () => {
|
@@ -283,7 +322,14 @@ export class Store<
|
|
283
322
|
try {
|
284
323
|
this.reactivityGraph.destroyNode(effect)
|
285
324
|
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
286
|
-
|
325
|
+
|
326
|
+
if (options?.stackInfo) {
|
327
|
+
query$.activeSubscriptions.delete(options.stackInfo)
|
328
|
+
}
|
329
|
+
|
330
|
+
queryRcRef.deref()
|
331
|
+
|
332
|
+
options?.onUnsubsubscribe?.()
|
287
333
|
} finally {
|
288
334
|
span.end()
|
289
335
|
}
|
@@ -293,6 +339,30 @@ export class Store<
|
|
293
339
|
},
|
294
340
|
)
|
295
341
|
|
342
|
+
subscribeStream = <TResult>(
|
343
|
+
query$: LiveQueryDef<TResult>,
|
344
|
+
options?: { label?: string; skipInitialRun?: boolean } | undefined,
|
345
|
+
): Stream.Stream<TResult> =>
|
346
|
+
Stream.asyncPush<TResult>((emit) =>
|
347
|
+
Effect.gen(this, function* () {
|
348
|
+
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
|
349
|
+
Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
|
350
|
+
)
|
351
|
+
const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
|
352
|
+
|
353
|
+
yield* Effect.acquireRelease(
|
354
|
+
Effect.sync(() =>
|
355
|
+
this.subscribe(query$, {
|
356
|
+
onUpdate: (result) => emit.single(result),
|
357
|
+
otelContext,
|
358
|
+
label: options?.label,
|
359
|
+
}),
|
360
|
+
),
|
361
|
+
(unsub) => Effect.sync(() => unsub()),
|
362
|
+
)
|
363
|
+
}),
|
364
|
+
)
|
365
|
+
|
296
366
|
/**
|
297
367
|
* Synchronously queries the database without creating a LiveQuery.
|
298
368
|
* This is useful for queries that don't need to be reactive.
|
@@ -308,12 +378,16 @@ export class Store<
|
|
308
378
|
* ```
|
309
379
|
*/
|
310
380
|
query = <TResult>(
|
311
|
-
query:
|
312
|
-
|
381
|
+
query:
|
382
|
+
| QueryBuilder<TResult, any, any>
|
383
|
+
| LiveQuery<TResult>
|
384
|
+
| LiveQueryDef<TResult>
|
385
|
+
| SignalDef<TResult>
|
386
|
+
| { query: string; bindValues: ParamsObject },
|
387
|
+
options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
|
313
388
|
): TResult => {
|
314
389
|
if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
|
315
|
-
return this.
|
316
|
-
bindValues: prepareBindValues(query.bindValues, query.query),
|
390
|
+
return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
|
317
391
|
otelContext: options?.otelContext,
|
318
392
|
}) as any
|
319
393
|
} else if (isQueryBuilder(query)) {
|
@@ -322,99 +396,186 @@ export class Store<
|
|
322
396
|
makeExecBeforeFirstRun({
|
323
397
|
table: ast.tableDef,
|
324
398
|
id: ast.id,
|
325
|
-
|
399
|
+
explicitDefaultValues: ast.explicitDefaultValues,
|
326
400
|
otelContext: options?.otelContext,
|
327
401
|
})(this.reactivityGraph.context!)
|
328
402
|
}
|
329
403
|
|
330
404
|
const sqlRes = query.asSql()
|
331
405
|
const schema = getResultSchema(query)
|
332
|
-
const rawRes = this.
|
333
|
-
bindValues: sqlRes.bindValues as any as PreparedBindValues,
|
406
|
+
const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
|
334
407
|
otelContext: options?.otelContext,
|
335
408
|
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
336
409
|
})
|
410
|
+
|
337
411
|
return Schema.decodeSync(schema)(rawRes)
|
412
|
+
} else if (query._tag === 'def') {
|
413
|
+
const query$ = query.make(this.reactivityGraph.context!)
|
414
|
+
const result = this.query(query$.value, options)
|
415
|
+
query$.deref()
|
416
|
+
return result
|
417
|
+
} else if (query._tag === 'signal-def') {
|
418
|
+
const signal$ = query.make(this.reactivityGraph.context!)
|
419
|
+
return signal$.value.get()
|
338
420
|
} else {
|
339
|
-
return query.run(options?.otelContext)
|
421
|
+
return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason })
|
422
|
+
}
|
423
|
+
}
|
424
|
+
|
425
|
+
/**
|
426
|
+
* Set the value of a signal
|
427
|
+
*
|
428
|
+
* @example
|
429
|
+
* ```ts
|
430
|
+
* const count$ = signal(0, { label: 'count$' })
|
431
|
+
* store.setSignal(count$, 2)
|
432
|
+
* ```
|
433
|
+
*
|
434
|
+
* @example
|
435
|
+
* ```ts
|
436
|
+
* const count$ = signal(0, { label: 'count$' })
|
437
|
+
* store.setSignal(count$, (prev) => prev + 1)
|
438
|
+
* ```
|
439
|
+
*/
|
440
|
+
setSignal = <T>(signalDef: SignalDef<T>, value: T | ((prev: T) => T)): void => {
|
441
|
+
const signalRef = signalDef.make(this.reactivityGraph.context!)
|
442
|
+
const newValue: T = typeof value === 'function' ? (value as any)(signalRef.value.get()) : value
|
443
|
+
signalRef.value.set(newValue)
|
444
|
+
|
445
|
+
// The current implementation of signals i.e. the separation into `signal-def` and `signal`
|
446
|
+
// can lead to a situation where a reffed signal is immediately de-reffed when calling `store.setSignal`,
|
447
|
+
// in case there is nothing else holding a reference to the signal which leads to the set value being "lost".
|
448
|
+
// To avoid this, we don't deref the signal here if this set call is the only reference to the signal.
|
449
|
+
// Hopefully this won't lead to any issues in the future. 🤞
|
450
|
+
if (signalRef.rc > 1) {
|
451
|
+
signalRef.deref()
|
340
452
|
}
|
341
453
|
}
|
342
454
|
|
343
|
-
// #region
|
344
|
-
|
345
|
-
|
455
|
+
// #region commit
|
456
|
+
/**
|
457
|
+
* Commit a list of events to the store which will immediately update the local database
|
458
|
+
* and sync the events across other clients (similar to a `git commit`).
|
459
|
+
*
|
460
|
+
* @example
|
461
|
+
* ```ts
|
462
|
+
* store.commit(events.todoCreated({ id: nanoid(), text: 'Make coffee' }))
|
463
|
+
* ```
|
464
|
+
*
|
465
|
+
* You can call `commit` with multiple events to apply them in a single database transaction.
|
466
|
+
*
|
467
|
+
* @example
|
468
|
+
* ```ts
|
469
|
+
* const todoId = nanoid()
|
470
|
+
* store.commit(
|
471
|
+
* events.todoCreated({ id: todoId, text: 'Make coffee' }),
|
472
|
+
* events.todoCompleted({ id: todoId }))
|
473
|
+
* ```
|
474
|
+
*
|
475
|
+
* For more advanced transaction scenarios, you can pass a synchronous function to `commit` which will receive a callback
|
476
|
+
* to which you can pass multiple events to be committed in the same database transaction.
|
477
|
+
* Under the hood this will simply collect all events and apply them in a single database transaction.
|
478
|
+
*
|
479
|
+
* @example
|
480
|
+
* ```ts
|
481
|
+
* store.commit((commit) => {
|
482
|
+
* const todoId = nanoid()
|
483
|
+
* if (Math.random() > 0.5) {
|
484
|
+
* commit(events.todoCreated({ id: todoId, text: 'Make coffee' }))
|
485
|
+
* } else {
|
486
|
+
* commit(events.todoCompleted({ id: todoId }))
|
487
|
+
* }
|
488
|
+
* })
|
489
|
+
* ```
|
490
|
+
*
|
491
|
+
* When committing a large batch of events, you can also skip the database refresh to improve performance
|
492
|
+
* and call `store.manualRefresh()` after all events have been committed.
|
493
|
+
*
|
494
|
+
* @example
|
495
|
+
* ```ts
|
496
|
+
* const todos = [
|
497
|
+
* { id: nanoid(), text: 'Make coffee' },
|
498
|
+
* { id: nanoid(), text: 'Buy groceries' },
|
499
|
+
* // ... 1000 more todos
|
500
|
+
* ]
|
501
|
+
* for (const todo of todos) {
|
502
|
+
* store.commit({ skipRefresh: true }, events.todoCreated({ id: todo.id, text: todo.text }))
|
503
|
+
* }
|
504
|
+
* store.manualRefresh()
|
505
|
+
* ```
|
506
|
+
*/
|
507
|
+
commit: {
|
508
|
+
<const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(...list: TCommitArg): void
|
346
509
|
(
|
347
|
-
txn: <const
|
348
|
-
...list:
|
510
|
+
txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(
|
511
|
+
...list: TCommitArg
|
349
512
|
) => void,
|
350
513
|
): void
|
351
|
-
<const
|
352
|
-
options:
|
353
|
-
...list:
|
514
|
+
<const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(
|
515
|
+
options: StoreCommitOptions,
|
516
|
+
...list: TCommitArg
|
354
517
|
): void
|
355
518
|
(
|
356
|
-
options:
|
357
|
-
txn: <const
|
358
|
-
...list:
|
519
|
+
options: StoreCommitOptions,
|
520
|
+
txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(
|
521
|
+
...list: TCommitArg
|
359
522
|
) => void,
|
360
523
|
): void
|
361
|
-
} = (
|
362
|
-
const {
|
524
|
+
} = (firstEventOrTxnFnOrOptions: any, ...restEvents: any[]) => {
|
525
|
+
const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
|
363
526
|
|
364
|
-
for (const
|
365
|
-
replaceSessionIdSymbol(
|
527
|
+
for (const event of events) {
|
528
|
+
replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
|
366
529
|
}
|
367
530
|
|
368
|
-
if (
|
531
|
+
if (events.length === 0) return
|
369
532
|
|
370
|
-
const label = options?.label ?? 'mutate'
|
371
533
|
const skipRefresh = options?.skipRefresh ?? false
|
372
534
|
|
373
|
-
const
|
374
|
-
|
535
|
+
const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)!
|
536
|
+
commitsSpan.addEvent('commit')
|
375
537
|
|
376
|
-
// console.group('LiveStore.
|
377
|
-
//
|
538
|
+
// console.group('LiveStore.commit', { skipRefresh })
|
539
|
+
// events.forEach((_) => console.debug(_.name, _.args))
|
378
540
|
// console.groupEnd()
|
379
541
|
|
380
542
|
let durationMs: number
|
381
543
|
|
382
544
|
return this.otel.tracer.startActiveSpan(
|
383
|
-
'LiveStore:
|
384
|
-
{
|
385
|
-
|
545
|
+
'LiveStore:commit',
|
546
|
+
{
|
547
|
+
attributes: {
|
548
|
+
'livestore.eventsCount': events.length,
|
549
|
+
'livestore.eventTags': events.map((_) => _.name),
|
550
|
+
'livestore.commitLabel': options?.label,
|
551
|
+
},
|
552
|
+
links: options?.spanLinks,
|
553
|
+
},
|
554
|
+
options?.otelContext ?? this.otel.commitsSpanContext,
|
386
555
|
(span) => {
|
387
556
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
388
557
|
|
389
558
|
try {
|
390
|
-
const { writeTables } =
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
|
400
|
-
|
401
|
-
if (mutationsEvents.length > 1) {
|
402
|
-
// TODO: what to do about leader transaction here?
|
403
|
-
return this.syncDbWrapper.txn(applyMutations)
|
404
|
-
} else {
|
405
|
-
return applyMutations()
|
406
|
-
}
|
407
|
-
} catch (e: any) {
|
408
|
-
console.error(e)
|
409
|
-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
410
|
-
throw e
|
411
|
-
} finally {
|
412
|
-
span.end()
|
559
|
+
const { writeTables } = (() => {
|
560
|
+
try {
|
561
|
+
const materializeEvents = () => this.syncProcessor.push(events, { otelContext })
|
562
|
+
|
563
|
+
if (events.length > 1) {
|
564
|
+
// TODO: what to do about leader transaction here?
|
565
|
+
return this.sqliteDbWrapper.txn(materializeEvents)
|
566
|
+
} else {
|
567
|
+
return materializeEvents()
|
413
568
|
}
|
414
|
-
}
|
415
|
-
|
569
|
+
} catch (e: any) {
|
570
|
+
console.error(e)
|
571
|
+
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
|
572
|
+
throw e
|
573
|
+
} finally {
|
574
|
+
span.end()
|
575
|
+
}
|
576
|
+
})()
|
416
577
|
|
417
|
-
const tablesToUpdate = [] as [Ref<null,
|
578
|
+
const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
|
418
579
|
for (const tableName of writeTables) {
|
419
580
|
const tableRef = this.tableRefs[tableName]
|
420
581
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
@@ -422,8 +583,8 @@ export class Store<
|
|
422
583
|
}
|
423
584
|
|
424
585
|
const debugRefreshReason = {
|
425
|
-
_tag: '
|
426
|
-
|
586
|
+
_tag: 'commit' as const,
|
587
|
+
events,
|
427
588
|
writeTables: Array.from(writeTables),
|
428
589
|
}
|
429
590
|
|
@@ -443,10 +604,36 @@ export class Store<
|
|
443
604
|
},
|
444
605
|
)
|
445
606
|
}
|
446
|
-
// #endregion
|
607
|
+
// #endregion commit
|
447
608
|
|
448
609
|
/**
|
449
|
-
*
|
610
|
+
* Returns an async iterable of events.
|
611
|
+
*
|
612
|
+
* @example
|
613
|
+
* ```ts
|
614
|
+
* for await (const event of store.events()) {
|
615
|
+
* console.log(event)
|
616
|
+
* }
|
617
|
+
* ```
|
618
|
+
*
|
619
|
+
* @example
|
620
|
+
* ```ts
|
621
|
+
* // Get all events from the beginning of time
|
622
|
+
* for await (const event of store.events({ cursor: EventSequenceNumber.ROOT })) {
|
623
|
+
* console.log(event)
|
624
|
+
* }
|
625
|
+
* ```
|
626
|
+
*/
|
627
|
+
events = (_options?: StoreEventsOptions<TSchema>): AsyncIterable<LiveStoreEvent.ForSchema<TSchema>> => {
|
628
|
+
return notYetImplemented(`store.events() is not yet implemented but planned soon`)
|
629
|
+
}
|
630
|
+
|
631
|
+
eventsStream = (_options?: StoreEventsOptions<TSchema>): Stream.Stream<LiveStoreEvent.ForSchema<TSchema>> => {
|
632
|
+
return notYetImplemented(`store.eventsStream() is not yet implemented but planned soon`)
|
633
|
+
}
|
634
|
+
|
635
|
+
/**
|
636
|
+
* This can be used in combination with `skipRefresh` when committing events.
|
450
637
|
* We might need a better solution for this. Let's see.
|
451
638
|
*/
|
452
639
|
manualRefresh = (options?: { label?: string }) => {
|
@@ -454,7 +641,7 @@ export class Store<
|
|
454
641
|
this.otel.tracer.startActiveSpan(
|
455
642
|
'LiveStore:manualRefresh',
|
456
643
|
{ attributes: { 'livestore.manualRefreshLabel': label } },
|
457
|
-
this.otel.
|
644
|
+
this.otel.commitsSpanContext,
|
458
645
|
(span) => {
|
459
646
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
460
647
|
this.reactivityGraph.runDeferredEffects({ otelContext })
|
@@ -463,48 +650,74 @@ export class Store<
|
|
463
650
|
)
|
464
651
|
}
|
465
652
|
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
const data = source === 'local' ? this.syncDbWrapper.export() : yield* this.clientSession.leaderThread.export
|
476
|
-
downloadBlob(data, `livestore-${Date.now()}.db`)
|
477
|
-
}).pipe(this.runEffectFork)
|
478
|
-
}
|
479
|
-
|
480
|
-
__devDownloadMutationLogDb = () => {
|
481
|
-
Effect.gen(this, function* () {
|
482
|
-
const data = yield* this.clientSession.leaderThread.getMutationLogData
|
483
|
-
downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
|
484
|
-
}).pipe(this.runEffectFork)
|
485
|
-
}
|
486
|
-
|
487
|
-
__devHardReset = (mode: 'all-data' | 'only-app-db' = 'all-data') => {
|
488
|
-
Effect.gen(this, function* () {
|
489
|
-
yield* this.clientSession.leaderThread.sendDevtoolsMessage(
|
490
|
-
Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }),
|
491
|
-
)
|
492
|
-
}).pipe(this.runEffectFork)
|
493
|
-
}
|
494
|
-
|
495
|
-
__devSyncStates = () => {
|
496
|
-
Effect.gen(this, function* () {
|
497
|
-
const session = this.syncProcessor.syncStateRef.current
|
498
|
-
console.log('Session sync state:', session)
|
499
|
-
const leader = yield* this.clientSession.leaderThread.getSyncState
|
500
|
-
console.log('Leader sync state:', leader)
|
501
|
-
}).pipe(this.runEffectFork)
|
653
|
+
/**
|
654
|
+
* Shuts down the store and closes the client session.
|
655
|
+
*
|
656
|
+
* This is called automatically when the store was created using the React or Effect API.
|
657
|
+
*/
|
658
|
+
shutdown = async (cause?: Cause.Cause<UnexpectedError>) => {
|
659
|
+
await this.clientSession
|
660
|
+
.shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
|
661
|
+
.pipe(this.runEffectFork, Fiber.join, Effect.runPromise)
|
502
662
|
}
|
503
663
|
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
664
|
+
/**
|
665
|
+
* Helper methods useful during development
|
666
|
+
*
|
667
|
+
* @internal
|
668
|
+
*/
|
669
|
+
_dev = {
|
670
|
+
downloadDb: (source: 'local' | 'leader' = 'local') => {
|
671
|
+
Effect.gen(this, function* () {
|
672
|
+
const data = source === 'local' ? this.sqliteDbWrapper.export() : yield* this.clientSession.leaderThread.export
|
673
|
+
downloadBlob(data, `livestore-${Date.now()}.db`)
|
674
|
+
}).pipe(this.runEffectFork)
|
675
|
+
},
|
676
|
+
|
677
|
+
downloadEventlogDb: () => {
|
678
|
+
Effect.gen(this, function* () {
|
679
|
+
const data = yield* this.clientSession.leaderThread.getEventlogData
|
680
|
+
downloadBlob(data, `livestore-eventlog-${Date.now()}.db`)
|
681
|
+
}).pipe(this.runEffectFork)
|
682
|
+
},
|
683
|
+
|
684
|
+
hardReset: (mode: 'all-data' | 'only-app-db' = 'all-data') => {
|
685
|
+
Effect.gen(this, function* () {
|
686
|
+
const clientId = this.clientSession.clientId
|
687
|
+
yield* this.clientSession.leaderThread.sendDevtoolsMessage(
|
688
|
+
Devtools.Leader.ResetAllData.Request.make({ liveStoreVersion, mode, requestId: nanoid(), clientId }),
|
689
|
+
)
|
690
|
+
}).pipe(this.runEffectFork)
|
691
|
+
},
|
692
|
+
|
693
|
+
overrideNetworkStatus: (status: 'online' | 'offline') => {
|
694
|
+
const clientId = this.clientSession.clientId
|
695
|
+
this.clientSession.leaderThread
|
696
|
+
.sendDevtoolsMessage(
|
697
|
+
Devtools.Leader.SetSyncLatch.Request.make({
|
698
|
+
clientId,
|
699
|
+
closeLatch: status === 'offline',
|
700
|
+
liveStoreVersion,
|
701
|
+
requestId: nanoid(),
|
702
|
+
}),
|
703
|
+
)
|
704
|
+
.pipe(this.runEffectFork)
|
705
|
+
},
|
706
|
+
|
707
|
+
syncStates: () => {
|
708
|
+
Effect.gen(this, function* () {
|
709
|
+
const session = yield* this.syncProcessor.syncState
|
710
|
+
console.log('Session sync state:', session.toJSON())
|
711
|
+
const leader = yield* this.clientSession.leaderThread.getSyncState
|
712
|
+
console.log('Leader sync state:', leader.toJSON())
|
713
|
+
}).pipe(this.runEffectFork)
|
714
|
+
},
|
715
|
+
|
716
|
+
version: liveStoreVersion,
|
717
|
+
|
718
|
+
otel: {
|
719
|
+
rootSpanContext: () => otel.trace.getSpan(this.otel.rootSpanContext)?.spanContext(),
|
720
|
+
},
|
508
721
|
}
|
509
722
|
|
510
723
|
// NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
|
@@ -514,41 +727,46 @@ export class Store<
|
|
514
727
|
})
|
515
728
|
|
516
729
|
private runEffectFork = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
517
|
-
effect.pipe(
|
730
|
+
effect.pipe(
|
731
|
+
Effect.forkIn(this.effectContext.lifetimeScope),
|
732
|
+
Effect.tapCauseLogPretty,
|
733
|
+
Runtime.runFork(this.effectContext.runtime),
|
734
|
+
)
|
518
735
|
|
519
|
-
private
|
520
|
-
|
521
|
-
|
736
|
+
private getCommitArgs = (
|
737
|
+
firstEventOrTxnFnOrOptions: any,
|
738
|
+
restEvents: any[],
|
522
739
|
): {
|
523
|
-
|
524
|
-
options:
|
740
|
+
events: LiveStoreEvent.PartialForSchema<TSchema>[]
|
741
|
+
options: StoreCommitOptions | undefined
|
525
742
|
} => {
|
526
|
-
let
|
527
|
-
let options:
|
743
|
+
let events: LiveStoreEvent.PartialForSchema<TSchema>[]
|
744
|
+
let options: StoreCommitOptions | undefined
|
528
745
|
|
529
|
-
if (typeof
|
746
|
+
if (typeof firstEventOrTxnFnOrOptions === 'function') {
|
530
747
|
// TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
|
531
|
-
|
748
|
+
events = firstEventOrTxnFnOrOptions((arg: any) => events.push(arg))
|
532
749
|
} else if (
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
750
|
+
firstEventOrTxnFnOrOptions?.label !== undefined ||
|
751
|
+
firstEventOrTxnFnOrOptions?.skipRefresh !== undefined ||
|
752
|
+
firstEventOrTxnFnOrOptions?.otelContext !== undefined ||
|
753
|
+
firstEventOrTxnFnOrOptions?.spanLinks !== undefined
|
537
754
|
) {
|
538
|
-
options =
|
539
|
-
|
540
|
-
} else if (
|
541
|
-
// When `
|
542
|
-
|
755
|
+
options = firstEventOrTxnFnOrOptions
|
756
|
+
events = restEvents
|
757
|
+
} else if (firstEventOrTxnFnOrOptions === undefined) {
|
758
|
+
// When `commit` is called with no arguments (which sometimes happens when dynamically filtering events)
|
759
|
+
events = []
|
543
760
|
} else {
|
544
|
-
|
761
|
+
events = [firstEventOrTxnFnOrOptions, ...restEvents]
|
545
762
|
}
|
546
763
|
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
764
|
+
// for (const event of events) {
|
765
|
+
// if (event.args.id === SessionIdSymbol) {
|
766
|
+
// event.args.id = this.clientSession.sessionId
|
767
|
+
// }
|
768
|
+
// }
|
551
769
|
|
552
|
-
return {
|
770
|
+
return { events, options }
|
553
771
|
}
|
554
772
|
}
|