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