@livestore/common 0.0.0-snapshot-f6ec49b1a18859aad769f0a0d8edf8bae231ed07 → 0.0.0-snapshot-2ef046b02334f52613d31dbe06af53487685edc0
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/adapter-types.d.ts +7 -12
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +1 -7
- package/dist/adapter-types.js.map +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +13 -6
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +6 -0
- package/dist/devtools/devtools-messages-common.js.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +25 -25
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.js +1 -2
- package/dist/devtools/devtools-messages-leader.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +16 -6
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +227 -215
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/apply-mutation.d.ts +14 -9
- package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
- package/dist/leader-thread/apply-mutation.js +43 -36
- package/dist/leader-thread/apply-mutation.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +2 -5
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +22 -33
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +1 -1
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -1
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/mutationlog.d.ts +20 -3
- package/dist/leader-thread/mutationlog.d.ts.map +1 -1
- package/dist/leader-thread/mutationlog.js +106 -12
- package/dist/leader-thread/mutationlog.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +4 -3
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +35 -19
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts +5 -4
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +7 -9
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/EventId.d.ts +4 -0
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +7 -1
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/MutationEvent.d.ts +87 -18
- package/dist/schema/MutationEvent.d.ts.map +1 -1
- package/dist/schema/MutationEvent.js +35 -6
- package/dist/schema/MutationEvent.js.map +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/system-tables.d.ts +67 -0
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +12 -1
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +11 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +54 -47
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/sync.d.ts +16 -5
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +81 -83
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +159 -125
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +97 -138
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/adapter-types.ts +5 -12
- package/src/devtools/devtools-messages-common.ts +9 -0
- package/src/devtools/devtools-messages-leader.ts +1 -2
- package/src/leader-thread/LeaderSyncProcessor.ts +398 -370
- package/src/leader-thread/apply-mutation.ts +81 -71
- package/src/leader-thread/leader-worker-devtools.ts +3 -8
- package/src/leader-thread/make-leader-thread-layer.ts +27 -41
- package/src/leader-thread/mod.ts +1 -1
- package/src/leader-thread/mutationlog.ts +167 -13
- package/src/leader-thread/recreate-db.ts +4 -3
- package/src/leader-thread/types.ts +34 -23
- package/src/rehydrate-from-mutationlog.ts +12 -12
- package/src/schema/EventId.ts +8 -1
- package/src/schema/MutationEvent.ts +42 -10
- package/src/schema/schema.ts +1 -1
- package/src/schema/system-tables.ts +20 -1
- package/src/sync/ClientSessionSyncProcessor.ts +64 -50
- package/src/sync/sync.ts +16 -9
- package/src/sync/syncstate.test.ts +173 -217
- package/src/sync/syncstate.ts +184 -151
- package/src/version.ts +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts +0 -7
- package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
- package/dist/leader-thread/pull-queue-set.js +0 -48
- package/dist/leader-thread/pull-queue-set.js.map +0 -1
- package/src/leader-thread/pull-queue-set.ts +0 -67
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
|
|
2
|
-
import type { HttpClient, Scope, Tracer } from '@livestore/utils/effect'
|
|
2
|
+
import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
|
|
3
3
|
import {
|
|
4
4
|
BucketQueue,
|
|
5
5
|
Deferred,
|
|
6
6
|
Effect,
|
|
7
7
|
Exit,
|
|
8
8
|
FiberHandle,
|
|
9
|
-
Option,
|
|
10
9
|
OtelTracer,
|
|
10
|
+
Queue,
|
|
11
11
|
ReadonlyArray,
|
|
12
|
-
Schema,
|
|
13
12
|
Stream,
|
|
14
13
|
Subscribable,
|
|
15
14
|
SubscriptionRef,
|
|
@@ -18,30 +17,29 @@ import type * as otel from '@opentelemetry/api'
|
|
|
18
17
|
|
|
19
18
|
import type { SqliteDb } from '../adapter-types.js'
|
|
20
19
|
import { UnexpectedError } from '../adapter-types.js'
|
|
21
|
-
import type { LiveStoreSchema
|
|
20
|
+
import type { LiveStoreSchema } from '../schema/mod.js'
|
|
22
21
|
import {
|
|
23
22
|
EventId,
|
|
24
23
|
getMutationDef,
|
|
25
|
-
|
|
24
|
+
LEADER_MERGE_COUNTER_TABLE,
|
|
26
25
|
MutationEvent,
|
|
27
|
-
mutationLogMetaTable,
|
|
28
26
|
SESSION_CHANGESET_META_TABLE,
|
|
29
27
|
} from '../schema/mod.js'
|
|
30
|
-
import { updateRows } from '../sql-queries/index.js'
|
|
31
28
|
import { LeaderAheadError } from '../sync/sync.js'
|
|
32
29
|
import * as SyncState from '../sync/syncstate.js'
|
|
33
30
|
import { sql } from '../util.js'
|
|
34
|
-
import {
|
|
35
|
-
import
|
|
36
|
-
import {
|
|
37
|
-
import type { InitialBlockingSyncContext, InitialSyncInfo, LeaderSyncProcessor } from './types.js'
|
|
31
|
+
import { rollback } from './apply-mutation.js'
|
|
32
|
+
import * as Mutationlog from './mutationlog.js'
|
|
33
|
+
import type { InitialBlockingSyncContext, LeaderSyncProcessor } from './types.js'
|
|
38
34
|
import { LeaderThreadCtx } from './types.js'
|
|
39
35
|
|
|
40
36
|
export const BACKEND_PUSH_BATCH_SIZE = 50
|
|
37
|
+
export const LOCAL_PUSH_BATCH_SIZE = 10
|
|
41
38
|
|
|
42
39
|
type LocalPushQueueItem = [
|
|
43
40
|
mutationEvent: MutationEvent.EncodedWithMeta,
|
|
44
41
|
deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
|
|
42
|
+
/** Used to determine whether the batch has become invalid due to a rejected local push batch */
|
|
45
43
|
generation: number,
|
|
46
44
|
]
|
|
47
45
|
|
|
@@ -52,53 +50,69 @@ type LocalPushQueueItem = [
|
|
|
52
50
|
* In the LeaderSyncProcessor, pulling always has precedence over pushing.
|
|
53
51
|
*
|
|
54
52
|
* Responsibilities:
|
|
55
|
-
* - Queueing incoming local mutations in a
|
|
53
|
+
* - Queueing incoming local mutations in a localPushesQueue.
|
|
56
54
|
* - Broadcasting mutations to client sessions via pull queues.
|
|
57
55
|
* - Pushing mutations to the sync backend.
|
|
58
56
|
*
|
|
59
57
|
* Notes:
|
|
60
58
|
*
|
|
61
59
|
* local push processing:
|
|
62
|
-
* -
|
|
60
|
+
* - localPushesQueue:
|
|
63
61
|
* - Maintains events in ascending order.
|
|
64
62
|
* - Uses `Deferred` objects to resolve/reject events based on application success.
|
|
65
|
-
* - Processes events from the
|
|
63
|
+
* - Processes events from the queue, applying mutations in batches.
|
|
66
64
|
* - Controlled by a `Latch` to manage execution flow.
|
|
67
65
|
* - The latch closes on pull receipt and re-opens post-pull completion.
|
|
68
66
|
* - Processes up to `maxBatchSize` events per cycle.
|
|
69
67
|
*
|
|
68
|
+
* Currently we're advancing the db read model and mutation log in lockstep, but we could also decouple this in the future
|
|
69
|
+
*
|
|
70
|
+
* Tricky concurrency scenarios:
|
|
71
|
+
* - Queued local push batches becoming invalid due to a prior local push item being rejected.
|
|
72
|
+
* Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
|
|
73
|
+
*
|
|
70
74
|
*/
|
|
71
75
|
export const makeLeaderSyncProcessor = ({
|
|
72
76
|
schema,
|
|
73
|
-
|
|
77
|
+
dbMutationLogMissing,
|
|
74
78
|
dbMutationLog,
|
|
75
|
-
|
|
79
|
+
dbReadModel,
|
|
80
|
+
dbReadModelMissing,
|
|
76
81
|
initialBlockingSyncContext,
|
|
82
|
+
onError,
|
|
77
83
|
}: {
|
|
78
84
|
schema: LiveStoreSchema
|
|
79
85
|
/** Only used to know whether we can safely query dbMutationLog during setup execution */
|
|
80
|
-
|
|
86
|
+
dbMutationLogMissing: boolean
|
|
81
87
|
dbMutationLog: SqliteDb
|
|
82
|
-
|
|
88
|
+
dbReadModel: SqliteDb
|
|
89
|
+
/** Only used to know whether we can safely query dbReadModel during setup execution */
|
|
90
|
+
dbReadModelMissing: boolean
|
|
83
91
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
|
92
|
+
onError: 'shutdown' | 'ignore'
|
|
84
93
|
}): Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope> =>
|
|
85
94
|
Effect.gen(function* () {
|
|
86
|
-
const
|
|
95
|
+
const syncBackendPushQueue = yield* BucketQueue.make<MutationEvent.EncodedWithMeta>()
|
|
87
96
|
|
|
88
97
|
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
|
89
98
|
|
|
90
|
-
const
|
|
99
|
+
const isClientEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) => {
|
|
91
100
|
const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
|
|
92
101
|
return mutationDef.options.clientOnly
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
|
105
|
+
|
|
95
106
|
/**
|
|
96
107
|
* Tracks generations of queued local push events.
|
|
97
|
-
* If a batch is rejected, all subsequent push queue items with the same generation are also rejected,
|
|
108
|
+
* If a local-push batch is rejected, all subsequent push queue items with the same generation are also rejected,
|
|
98
109
|
* even if they would be valid on their own.
|
|
99
110
|
*/
|
|
100
111
|
const currentLocalPushGenerationRef = { current: 0 }
|
|
101
112
|
|
|
113
|
+
const mergeCounterRef = { current: dbReadModelMissing ? 0 : yield* getMergeCounterFromDb(dbReadModel) }
|
|
114
|
+
const mergePayloads = new Map<number, typeof SyncState.PayloadUpstream.Type>()
|
|
115
|
+
|
|
102
116
|
// This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
|
|
103
117
|
const ctxRef = {
|
|
104
118
|
current: undefined as
|
|
@@ -107,6 +121,7 @@ export const makeLeaderSyncProcessor = ({
|
|
|
107
121
|
otelSpan: otel.Span | undefined
|
|
108
122
|
span: Tracer.Span
|
|
109
123
|
devtoolsLatch: Effect.Latch | undefined
|
|
124
|
+
runtime: Runtime.Runtime<LeaderThreadCtx>
|
|
110
125
|
},
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -114,24 +129,12 @@ export const makeLeaderSyncProcessor = ({
|
|
|
114
129
|
const localPushesLatch = yield* Effect.makeLatch(true)
|
|
115
130
|
const pullLatch = yield* Effect.makeLatch(true)
|
|
116
131
|
|
|
132
|
+
// NOTE: New events are only pushed to sync backend after successful local push processing
|
|
117
133
|
const push: LeaderSyncProcessor['push'] = (newEvents, options) =>
|
|
118
134
|
Effect.gen(function* () {
|
|
119
135
|
// TODO validate batch
|
|
120
136
|
if (newEvents.length === 0) return
|
|
121
137
|
|
|
122
|
-
// if (options.generation < currentLocalPushGenerationRef.current) {
|
|
123
|
-
// debugger
|
|
124
|
-
// // We can safely drop this batch as it's from a previous push generation
|
|
125
|
-
// return
|
|
126
|
-
// }
|
|
127
|
-
|
|
128
|
-
if (clientId === 'client-b') {
|
|
129
|
-
// console.log(
|
|
130
|
-
// 'push from client session',
|
|
131
|
-
// newEvents.map((item) => item.toJSON()),
|
|
132
|
-
// )
|
|
133
|
-
}
|
|
134
|
-
|
|
135
138
|
const waitForProcessing = options?.waitForProcessing ?? false
|
|
136
139
|
const generation = currentLocalPushGenerationRef.current
|
|
137
140
|
|
|
@@ -152,7 +155,7 @@ export const makeLeaderSyncProcessor = ({
|
|
|
152
155
|
yield* BucketQueue.offerAll(localPushesQueue, items)
|
|
153
156
|
}
|
|
154
157
|
}).pipe(
|
|
155
|
-
Effect.withSpan('@livestore/common:
|
|
158
|
+
Effect.withSpan('@livestore/common:LeaderSyncProcessor:local-push', {
|
|
156
159
|
attributes: {
|
|
157
160
|
batchSize: newEvents.length,
|
|
158
161
|
batch: TRACE_VERBOSE ? newEvents : undefined,
|
|
@@ -162,7 +165,7 @@ export const makeLeaderSyncProcessor = ({
|
|
|
162
165
|
)
|
|
163
166
|
|
|
164
167
|
const pushPartial: LeaderSyncProcessor['pushPartial'] = ({
|
|
165
|
-
mutationEvent:
|
|
168
|
+
mutationEvent: { mutation, args },
|
|
166
169
|
clientId,
|
|
167
170
|
sessionId,
|
|
168
171
|
}) =>
|
|
@@ -170,10 +173,11 @@ export const makeLeaderSyncProcessor = ({
|
|
|
170
173
|
const syncState = yield* syncStateSref
|
|
171
174
|
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
172
175
|
|
|
173
|
-
const mutationDef = getMutationDef(schema,
|
|
176
|
+
const mutationDef = getMutationDef(schema, mutation)
|
|
174
177
|
|
|
175
178
|
const mutationEventEncoded = new MutationEvent.EncodedWithMeta({
|
|
176
|
-
|
|
179
|
+
mutation,
|
|
180
|
+
args,
|
|
177
181
|
clientId,
|
|
178
182
|
sessionId,
|
|
179
183
|
...EventId.nextPair(syncState.localHead, mutationDef.options.clientOnly),
|
|
@@ -183,132 +187,166 @@ export const makeLeaderSyncProcessor = ({
|
|
|
183
187
|
}).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie))
|
|
184
188
|
|
|
185
189
|
// Starts various background loops
|
|
186
|
-
const boot: LeaderSyncProcessor['boot'] = (
|
|
187
|
-
Effect.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
const boot: LeaderSyncProcessor['boot'] = Effect.gen(function* () {
|
|
191
|
+
const span = yield* Effect.currentSpan.pipe(Effect.orDie)
|
|
192
|
+
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
|
|
193
|
+
const { devtools, shutdownChannel } = yield* LeaderThreadCtx
|
|
194
|
+
const runtime = yield* Effect.runtime<LeaderThreadCtx>()
|
|
195
|
+
|
|
196
|
+
ctxRef.current = {
|
|
197
|
+
otelSpan,
|
|
198
|
+
span,
|
|
199
|
+
devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
|
|
200
|
+
runtime,
|
|
201
|
+
}
|
|
191
202
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
203
|
+
const initialBackendHead = dbMutationLogMissing
|
|
204
|
+
? EventId.ROOT.global
|
|
205
|
+
: Mutationlog.getBackendHeadFromDb(dbMutationLog)
|
|
206
|
+
const initialLocalHead = dbMutationLogMissing ? EventId.ROOT : Mutationlog.getClientHeadFromDb(dbMutationLog)
|
|
197
207
|
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
if (initialBackendHead > initialLocalHead.global) {
|
|
209
|
+
return shouldNeverHappen(
|
|
210
|
+
`During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
200
213
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
205
|
-
}
|
|
214
|
+
const pendingMutationEvents = dbMutationLogMissing
|
|
215
|
+
? []
|
|
216
|
+
: yield* Mutationlog.getMutationEventsSince({ global: initialBackendHead, client: EventId.clientDefault })
|
|
206
217
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const initialSyncState = new SyncState.SyncState({
|
|
213
|
-
pending: pendingMutationEvents,
|
|
214
|
-
// On the leader we don't need a rollback tail beyond `pending` items
|
|
215
|
-
rollbackTail: [],
|
|
216
|
-
upstreamHead: { global: initialBackendHead, client: EventId.clientDefault },
|
|
217
|
-
localHead: initialLocalHead,
|
|
218
|
-
})
|
|
218
|
+
const initialSyncState = new SyncState.SyncState({
|
|
219
|
+
pending: pendingMutationEvents,
|
|
220
|
+
upstreamHead: { global: initialBackendHead, client: EventId.clientDefault },
|
|
221
|
+
localHead: initialLocalHead,
|
|
222
|
+
})
|
|
219
223
|
|
|
220
|
-
|
|
221
|
-
|
|
224
|
+
/** State transitions need to happen atomically, so we use a Ref to track the state */
|
|
225
|
+
yield* SubscriptionRef.set(syncStateSref, initialSyncState)
|
|
222
226
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
// Rehydrate sync queue
|
|
228
|
+
if (pendingMutationEvents.length > 0) {
|
|
229
|
+
const globalPendingMutationEvents = pendingMutationEvents
|
|
230
|
+
// Don't sync clientOnly mutations
|
|
231
|
+
.filter((mutationEventEncoded) => {
|
|
232
|
+
const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
|
|
233
|
+
return mutationDef.options.clientOnly === false
|
|
234
|
+
})
|
|
231
235
|
|
|
232
|
-
|
|
236
|
+
if (globalPendingMutationEvents.length > 0) {
|
|
237
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, globalPendingMutationEvents)
|
|
233
238
|
}
|
|
239
|
+
}
|
|
234
240
|
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
const shutdownOnError = (cause: unknown) =>
|
|
242
|
+
Effect.gen(function* () {
|
|
243
|
+
if (onError === 'shutdown') {
|
|
237
244
|
yield* shutdownChannel.send(UnexpectedError.make({ cause }))
|
|
238
245
|
yield* Effect.die(cause)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
yield* backgroundApplyLocalPushes({
|
|
242
|
-
localPushesLatch,
|
|
243
|
-
localPushesQueue,
|
|
244
|
-
pullLatch,
|
|
245
|
-
syncStateSref,
|
|
246
|
-
syncBackendQueue,
|
|
247
|
-
schema,
|
|
248
|
-
isLocalEvent,
|
|
249
|
-
otelSpan,
|
|
250
|
-
currentLocalPushGenerationRef,
|
|
251
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
|
252
|
-
|
|
253
|
-
const backendPushingFiberHandle = yield* FiberHandle.make()
|
|
254
|
-
|
|
255
|
-
yield* FiberHandle.run(
|
|
256
|
-
backendPushingFiberHandle,
|
|
257
|
-
backgroundBackendPushing({
|
|
258
|
-
dbReady,
|
|
259
|
-
syncBackendQueue,
|
|
260
|
-
otelSpan,
|
|
261
|
-
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
262
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
|
|
263
|
-
)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
264
248
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
|
|
287
|
-
)
|
|
288
|
-
}),
|
|
289
|
-
syncStateSref,
|
|
290
|
-
localPushesLatch,
|
|
291
|
-
pullLatch,
|
|
249
|
+
yield* backgroundApplyLocalPushes({
|
|
250
|
+
localPushesLatch,
|
|
251
|
+
localPushesQueue,
|
|
252
|
+
pullLatch,
|
|
253
|
+
syncStateSref,
|
|
254
|
+
syncBackendPushQueue,
|
|
255
|
+
schema,
|
|
256
|
+
isClientEvent,
|
|
257
|
+
otelSpan,
|
|
258
|
+
currentLocalPushGenerationRef,
|
|
259
|
+
connectedClientSessionPullQueues,
|
|
260
|
+
mergeCounterRef,
|
|
261
|
+
mergePayloads,
|
|
262
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
|
263
|
+
|
|
264
|
+
const backendPushingFiberHandle = yield* FiberHandle.make()
|
|
265
|
+
|
|
266
|
+
yield* FiberHandle.run(
|
|
267
|
+
backendPushingFiberHandle,
|
|
268
|
+
backgroundBackendPushing({
|
|
269
|
+
syncBackendPushQueue,
|
|
292
270
|
otelSpan,
|
|
293
|
-
initialBlockingSyncContext,
|
|
294
271
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
295
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError),
|
|
272
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
yield* backgroundBackendPulling({
|
|
276
|
+
initialBackendHead,
|
|
277
|
+
isClientEvent,
|
|
278
|
+
restartBackendPushing: (filteredRebasedPending) =>
|
|
279
|
+
Effect.gen(function* () {
|
|
280
|
+
// Stop current pushing fiber
|
|
281
|
+
yield* FiberHandle.clear(backendPushingFiberHandle)
|
|
282
|
+
|
|
283
|
+
// Reset the sync backend push queue
|
|
284
|
+
yield* BucketQueue.clear(syncBackendPushQueue)
|
|
285
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredRebasedPending)
|
|
286
|
+
|
|
287
|
+
// Restart pushing fiber
|
|
288
|
+
yield* FiberHandle.run(
|
|
289
|
+
backendPushingFiberHandle,
|
|
290
|
+
backgroundBackendPushing({
|
|
291
|
+
syncBackendPushQueue,
|
|
292
|
+
otelSpan,
|
|
293
|
+
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
294
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
|
|
295
|
+
)
|
|
296
|
+
}),
|
|
297
|
+
syncStateSref,
|
|
298
|
+
localPushesLatch,
|
|
299
|
+
pullLatch,
|
|
300
|
+
otelSpan,
|
|
301
|
+
initialBlockingSyncContext,
|
|
302
|
+
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
303
|
+
connectedClientSessionPullQueues,
|
|
304
|
+
mergeCounterRef,
|
|
305
|
+
mergePayloads,
|
|
306
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
|
307
|
+
|
|
308
|
+
return { initialLeaderHead: initialLocalHead }
|
|
309
|
+
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
|
|
310
|
+
|
|
311
|
+
const pull: LeaderSyncProcessor['pull'] = ({ cursor }) => {
|
|
312
|
+
return Effect.gen(function* () {
|
|
313
|
+
const queue = yield* pullQueue({ cursor })
|
|
314
|
+
return Stream.fromQueue(queue)
|
|
315
|
+
}).pipe(Stream.unwrapScoped)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) => {
|
|
319
|
+
const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized')
|
|
320
|
+
return Effect.gen(function* () {
|
|
321
|
+
const queue = yield* connectedClientSessionPullQueues.makeQueue(cursor)
|
|
322
|
+
const payloadsSinceCursor = Array.from(mergePayloads.entries())
|
|
323
|
+
.map(([mergeCounter, payload]) => ({ payload, mergeCounter }))
|
|
324
|
+
.filter(({ mergeCounter }) => mergeCounter > cursor)
|
|
325
|
+
.toSorted((a, b) => a.mergeCounter - b.mergeCounter)
|
|
296
326
|
|
|
297
|
-
|
|
298
|
-
|
|
327
|
+
yield* queue.offerAll(payloadsSinceCursor)
|
|
328
|
+
|
|
329
|
+
return queue
|
|
330
|
+
}).pipe(Effect.provide(runtime))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const syncState = Subscribable.make({
|
|
334
|
+
get: Effect.gen(function* () {
|
|
335
|
+
const syncState = yield* syncStateSref
|
|
336
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
337
|
+
return syncState
|
|
338
|
+
}),
|
|
339
|
+
changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
|
|
340
|
+
})
|
|
299
341
|
|
|
300
342
|
return {
|
|
343
|
+
pull,
|
|
344
|
+
pullQueue,
|
|
301
345
|
push,
|
|
302
346
|
pushPartial,
|
|
303
347
|
boot,
|
|
304
|
-
syncState
|
|
305
|
-
|
|
306
|
-
const syncState = yield* syncStateSref
|
|
307
|
-
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
308
|
-
return syncState
|
|
309
|
-
}),
|
|
310
|
-
changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
|
|
311
|
-
}),
|
|
348
|
+
syncState,
|
|
349
|
+
getMergeCounter: () => mergeCounterRef.current,
|
|
312
350
|
} satisfies LeaderSyncProcessor
|
|
313
351
|
})
|
|
314
352
|
|
|
@@ -317,30 +355,32 @@ const backgroundApplyLocalPushes = ({
|
|
|
317
355
|
localPushesQueue,
|
|
318
356
|
pullLatch,
|
|
319
357
|
syncStateSref,
|
|
320
|
-
|
|
358
|
+
syncBackendPushQueue,
|
|
321
359
|
schema,
|
|
322
|
-
|
|
360
|
+
isClientEvent,
|
|
323
361
|
otelSpan,
|
|
324
362
|
currentLocalPushGenerationRef,
|
|
363
|
+
connectedClientSessionPullQueues,
|
|
364
|
+
mergeCounterRef,
|
|
365
|
+
mergePayloads,
|
|
325
366
|
}: {
|
|
326
367
|
pullLatch: Effect.Latch
|
|
327
368
|
localPushesLatch: Effect.Latch
|
|
328
369
|
localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
|
|
329
370
|
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
|
330
|
-
|
|
371
|
+
syncBackendPushQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
|
|
331
372
|
schema: LiveStoreSchema
|
|
332
|
-
|
|
373
|
+
isClientEvent: (mutationEventEncoded: MutationEvent.EncodedWithMeta) => boolean
|
|
333
374
|
otelSpan: otel.Span | undefined
|
|
334
375
|
currentLocalPushGenerationRef: { current: number }
|
|
376
|
+
connectedClientSessionPullQueues: PullQueueSet
|
|
377
|
+
mergeCounterRef: { current: number }
|
|
378
|
+
mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
|
|
335
379
|
}) =>
|
|
336
380
|
Effect.gen(function* () {
|
|
337
|
-
const { connectedClientSessionPullQueues, clientId } = yield* LeaderThreadCtx
|
|
338
|
-
|
|
339
|
-
const applyMutationItems = yield* makeApplyMutationItems
|
|
340
|
-
|
|
341
381
|
while (true) {
|
|
342
382
|
// TODO make batch size configurable
|
|
343
|
-
const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1,
|
|
383
|
+
const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, LOCAL_PUSH_BATCH_SIZE)
|
|
344
384
|
|
|
345
385
|
// Wait for the backend pulling to finish
|
|
346
386
|
yield* localPushesLatch.await
|
|
@@ -366,34 +406,33 @@ const backgroundApplyLocalPushes = ({
|
|
|
366
406
|
const syncState = yield* syncStateSref
|
|
367
407
|
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
368
408
|
|
|
369
|
-
const
|
|
409
|
+
const mergeResult = SyncState.merge({
|
|
370
410
|
syncState,
|
|
371
411
|
payload: { _tag: 'local-push', newEvents },
|
|
372
|
-
|
|
412
|
+
isClientEvent,
|
|
373
413
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
|
374
414
|
})
|
|
375
415
|
|
|
376
|
-
|
|
416
|
+
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
|
|
417
|
+
|
|
418
|
+
switch (mergeResult._tag) {
|
|
377
419
|
case 'unexpected-error': {
|
|
378
|
-
otelSpan?.addEvent(
|
|
420
|
+
otelSpan?.addEvent(`merge[${mergeCounter}]:local-push:unexpected-error`, {
|
|
379
421
|
batchSize: newEvents.length,
|
|
380
422
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
381
423
|
})
|
|
382
|
-
return yield* Effect.fail(
|
|
424
|
+
return yield* Effect.fail(mergeResult.cause)
|
|
383
425
|
}
|
|
384
426
|
case 'rebase': {
|
|
385
427
|
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
|
386
428
|
}
|
|
387
429
|
case 'reject': {
|
|
388
|
-
otelSpan?.addEvent(
|
|
430
|
+
otelSpan?.addEvent(`merge[${mergeCounter}]:local-push:reject`, {
|
|
389
431
|
batchSize: newEvents.length,
|
|
390
|
-
|
|
432
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
391
433
|
})
|
|
392
434
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
TODO: how to test this?
|
|
396
|
-
*/
|
|
435
|
+
// TODO: how to test this?
|
|
397
436
|
currentLocalPushGenerationRef.current++
|
|
398
437
|
|
|
399
438
|
const nextGeneration = currentLocalPushGenerationRef.current
|
|
@@ -407,7 +446,8 @@ const backgroundApplyLocalPushes = ({
|
|
|
407
446
|
(item) => item[2] >= nextGeneration,
|
|
408
447
|
)
|
|
409
448
|
|
|
410
|
-
|
|
449
|
+
// TODO we still need to better understand and handle this scenario
|
|
450
|
+
if (LS_DEV && (yield* BucketQueue.size(localPushesQueue)) > 0) {
|
|
411
451
|
console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
|
|
412
452
|
debugger
|
|
413
453
|
}
|
|
@@ -421,7 +461,7 @@ const backgroundApplyLocalPushes = ({
|
|
|
421
461
|
Deferred.fail(
|
|
422
462
|
deferred,
|
|
423
463
|
LeaderAheadError.make({
|
|
424
|
-
minimumExpectedId:
|
|
464
|
+
minimumExpectedId: mergeResult.expectedMinimumId,
|
|
425
465
|
providedId,
|
|
426
466
|
// nextGeneration,
|
|
427
467
|
}),
|
|
@@ -439,95 +479,90 @@ const backgroundApplyLocalPushes = ({
|
|
|
439
479
|
break
|
|
440
480
|
}
|
|
441
481
|
default: {
|
|
442
|
-
casesHandled(
|
|
482
|
+
casesHandled(mergeResult)
|
|
443
483
|
}
|
|
444
484
|
}
|
|
445
485
|
|
|
446
|
-
yield* SubscriptionRef.set(syncStateSref,
|
|
486
|
+
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
|
447
487
|
|
|
448
|
-
if (clientId === 'client-b') {
|
|
449
|
-
// yield* Effect.log('offer upstream-advance due to local-push')
|
|
450
|
-
// debugger
|
|
451
|
-
}
|
|
452
488
|
yield* connectedClientSessionPullQueues.offer({
|
|
453
|
-
payload: {
|
|
454
|
-
|
|
489
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
|
|
490
|
+
mergeCounter,
|
|
455
491
|
})
|
|
492
|
+
mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
|
|
456
493
|
|
|
457
|
-
otelSpan?.addEvent(
|
|
494
|
+
otelSpan?.addEvent(`merge[${mergeCounter}]:local-push:advance`, {
|
|
458
495
|
batchSize: newEvents.length,
|
|
459
|
-
|
|
496
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
460
497
|
})
|
|
461
498
|
|
|
462
499
|
// Don't sync clientOnly mutations
|
|
463
|
-
const filteredBatch =
|
|
500
|
+
const filteredBatch = mergeResult.newEvents.filter((mutationEventEncoded) => {
|
|
464
501
|
const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
|
|
465
502
|
return mutationDef.options.clientOnly === false
|
|
466
503
|
})
|
|
467
504
|
|
|
468
|
-
yield* BucketQueue.offerAll(
|
|
505
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
|
|
469
506
|
|
|
470
|
-
yield*
|
|
507
|
+
yield* applyMutationsBatch({ batchItems: newEvents, deferreds })
|
|
471
508
|
|
|
472
509
|
// Allow the backend pulling to start
|
|
473
510
|
yield* pullLatch.open
|
|
474
511
|
}
|
|
475
512
|
})
|
|
476
513
|
|
|
477
|
-
type
|
|
514
|
+
type ApplyMutationsBatch = (_: {
|
|
478
515
|
batchItems: ReadonlyArray<MutationEvent.EncodedWithMeta>
|
|
479
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* The deferreds are used by the caller to know when the mutation has been processed.
|
|
518
|
+
* Indexes are aligned with `batchItems`
|
|
519
|
+
*/
|
|
480
520
|
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
|
|
481
|
-
}) => Effect.Effect<void, UnexpectedError>
|
|
521
|
+
}) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
|
|
482
522
|
|
|
483
523
|
// TODO how to handle errors gracefully
|
|
484
|
-
const
|
|
524
|
+
const applyMutationsBatch: ApplyMutationsBatch = ({ batchItems, deferreds }) =>
|
|
485
525
|
Effect.gen(function* () {
|
|
486
|
-
const
|
|
487
|
-
const { dbReadModel: db, dbMutationLog } = leaderThreadCtx
|
|
526
|
+
const { dbReadModel: db, dbMutationLog, applyMutation } = yield* LeaderThreadCtx
|
|
488
527
|
|
|
489
|
-
|
|
528
|
+
// NOTE We always start a transaction to ensure consistency between db and mutation log (even for single-item batches)
|
|
529
|
+
db.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
|
530
|
+
dbMutationLog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
|
490
531
|
|
|
491
|
-
|
|
532
|
+
yield* Effect.addFinalizer((exit) =>
|
|
492
533
|
Effect.gen(function* () {
|
|
493
|
-
|
|
494
|
-
dbMutationLog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
|
534
|
+
if (Exit.isSuccess(exit)) return
|
|
495
535
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
db.execute('ROLLBACK', undefined)
|
|
502
|
-
dbMutationLog.execute('ROLLBACK', undefined)
|
|
503
|
-
}),
|
|
504
|
-
)
|
|
536
|
+
// Rollback in case of an error
|
|
537
|
+
db.execute('ROLLBACK', undefined)
|
|
538
|
+
dbMutationLog.execute('ROLLBACK', undefined)
|
|
539
|
+
}),
|
|
540
|
+
)
|
|
505
541
|
|
|
506
|
-
|
|
507
|
-
|
|
542
|
+
for (let i = 0; i < batchItems.length; i++) {
|
|
543
|
+
const { sessionChangeset } = yield* applyMutation(batchItems[i]!)
|
|
544
|
+
batchItems[i]!.meta.sessionChangeset = sessionChangeset
|
|
508
545
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
546
|
+
if (deferreds?.[i] !== undefined) {
|
|
547
|
+
yield* Deferred.succeed(deferreds[i]!, void 0)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
513
550
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
})
|
|
551
|
+
db.execute('COMMIT', undefined) // Commit the transaction
|
|
552
|
+
dbMutationLog.execute('COMMIT', undefined) // Commit the transaction
|
|
553
|
+
}).pipe(
|
|
554
|
+
Effect.uninterruptible,
|
|
555
|
+
Effect.scoped,
|
|
556
|
+
Effect.withSpan('@livestore/common:LeaderSyncProcessor:applyMutationItems', {
|
|
557
|
+
attributes: { batchSize: batchItems.length },
|
|
558
|
+
}),
|
|
559
|
+
Effect.tapCauseLogPretty,
|
|
560
|
+
UnexpectedError.mapToUnexpectedError,
|
|
561
|
+
)
|
|
526
562
|
|
|
527
563
|
const backgroundBackendPulling = ({
|
|
528
|
-
dbReady,
|
|
529
564
|
initialBackendHead,
|
|
530
|
-
|
|
565
|
+
isClientEvent,
|
|
531
566
|
restartBackendPushing,
|
|
532
567
|
otelSpan,
|
|
533
568
|
syncStateSref,
|
|
@@ -535,10 +570,12 @@ const backgroundBackendPulling = ({
|
|
|
535
570
|
pullLatch,
|
|
536
571
|
devtoolsLatch,
|
|
537
572
|
initialBlockingSyncContext,
|
|
573
|
+
connectedClientSessionPullQueues,
|
|
574
|
+
mergeCounterRef,
|
|
575
|
+
mergePayloads,
|
|
538
576
|
}: {
|
|
539
|
-
dbReady: Deferred.Deferred<void>
|
|
540
577
|
initialBackendHead: EventId.GlobalEventId
|
|
541
|
-
|
|
578
|
+
isClientEvent: (mutationEventEncoded: MutationEvent.EncodedWithMeta) => boolean
|
|
542
579
|
restartBackendPushing: (
|
|
543
580
|
filteredRebasedPending: ReadonlyArray<MutationEvent.EncodedWithMeta>,
|
|
544
581
|
) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | HttpClient.HttpClient>
|
|
@@ -548,23 +585,15 @@ const backgroundBackendPulling = ({
|
|
|
548
585
|
pullLatch: Effect.Latch
|
|
549
586
|
devtoolsLatch: Effect.Latch | undefined
|
|
550
587
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
|
588
|
+
connectedClientSessionPullQueues: PullQueueSet
|
|
589
|
+
mergeCounterRef: { current: number }
|
|
590
|
+
mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
|
|
551
591
|
}) =>
|
|
552
592
|
Effect.gen(function* () {
|
|
553
|
-
const {
|
|
554
|
-
syncBackend,
|
|
555
|
-
dbReadModel: db,
|
|
556
|
-
dbMutationLog,
|
|
557
|
-
connectedClientSessionPullQueues,
|
|
558
|
-
schema,
|
|
559
|
-
clientId,
|
|
560
|
-
} = yield* LeaderThreadCtx
|
|
593
|
+
const { syncBackend, dbReadModel: db, dbMutationLog, schema } = yield* LeaderThreadCtx
|
|
561
594
|
|
|
562
595
|
if (syncBackend === undefined) return
|
|
563
596
|
|
|
564
|
-
const cursorInfo = yield* getCursorInfo(initialBackendHead)
|
|
565
|
-
|
|
566
|
-
const applyMutationItems = yield* makeApplyMutationItems
|
|
567
|
-
|
|
568
597
|
const onNewPullChunk = (newEvents: MutationEvent.EncodedWithMeta[], remaining: number) =>
|
|
569
598
|
Effect.gen(function* () {
|
|
570
599
|
if (newEvents.length === 0) return
|
|
@@ -582,84 +611,101 @@ const backgroundBackendPulling = ({
|
|
|
582
611
|
const syncState = yield* syncStateSref
|
|
583
612
|
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
584
613
|
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
const updateResult = SyncState.updateSyncState({
|
|
614
|
+
const mergeResult = SyncState.merge({
|
|
588
615
|
syncState,
|
|
589
|
-
payload: {
|
|
590
|
-
|
|
616
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
|
|
617
|
+
isClientEvent,
|
|
591
618
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
|
592
|
-
|
|
619
|
+
ignoreClientEvents: true,
|
|
593
620
|
})
|
|
594
621
|
|
|
595
|
-
|
|
622
|
+
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
|
|
623
|
+
|
|
624
|
+
if (mergeResult._tag === 'reject') {
|
|
596
625
|
return shouldNeverHappen('The leader thread should never reject upstream advances')
|
|
597
|
-
} else if (
|
|
598
|
-
otelSpan?.addEvent(
|
|
626
|
+
} else if (mergeResult._tag === 'unexpected-error') {
|
|
627
|
+
otelSpan?.addEvent(`merge[${mergeCounter}]:backend-pull:unexpected-error`, {
|
|
599
628
|
newEventsCount: newEvents.length,
|
|
600
629
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
601
630
|
})
|
|
602
|
-
return yield* Effect.fail(
|
|
631
|
+
return yield* Effect.fail(mergeResult.cause)
|
|
603
632
|
}
|
|
604
633
|
|
|
605
634
|
const newBackendHead = newEvents.at(-1)!.id
|
|
606
635
|
|
|
607
|
-
updateBackendHead(dbMutationLog, newBackendHead)
|
|
636
|
+
Mutationlog.updateBackendHead(dbMutationLog, newBackendHead)
|
|
608
637
|
|
|
609
|
-
if (
|
|
610
|
-
otelSpan?.addEvent(
|
|
638
|
+
if (mergeResult._tag === 'rebase') {
|
|
639
|
+
otelSpan?.addEvent(`merge[${mergeCounter}]:backend-pull:rebase`, {
|
|
611
640
|
newEventsCount: newEvents.length,
|
|
612
641
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
613
|
-
rollbackCount:
|
|
614
|
-
|
|
642
|
+
rollbackCount: mergeResult.rollbackEvents.length,
|
|
643
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
615
644
|
})
|
|
616
645
|
|
|
617
|
-
const
|
|
646
|
+
const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((mutationEvent) => {
|
|
618
647
|
const mutationDef = getMutationDef(schema, mutationEvent.mutation)
|
|
619
648
|
return mutationDef.options.clientOnly === false
|
|
620
649
|
})
|
|
621
|
-
yield* restartBackendPushing(
|
|
650
|
+
yield* restartBackendPushing(globalRebasedPendingEvents)
|
|
622
651
|
|
|
623
|
-
if (
|
|
624
|
-
yield* rollback({ db, dbMutationLog, eventIdsToRollback:
|
|
652
|
+
if (mergeResult.rollbackEvents.length > 0) {
|
|
653
|
+
yield* rollback({ db, dbMutationLog, eventIdsToRollback: mergeResult.rollbackEvents.map((_) => _.id) })
|
|
625
654
|
}
|
|
626
655
|
|
|
627
656
|
yield* connectedClientSessionPullQueues.offer({
|
|
628
|
-
payload: {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
},
|
|
634
|
-
remaining,
|
|
657
|
+
payload: SyncState.PayloadUpstreamRebase.make({
|
|
658
|
+
newEvents: mergeResult.newEvents,
|
|
659
|
+
rollbackEvents: mergeResult.rollbackEvents,
|
|
660
|
+
}),
|
|
661
|
+
mergeCounter,
|
|
635
662
|
})
|
|
663
|
+
mergePayloads.set(
|
|
664
|
+
mergeCounter,
|
|
665
|
+
SyncState.PayloadUpstreamRebase.make({
|
|
666
|
+
newEvents: mergeResult.newEvents,
|
|
667
|
+
rollbackEvents: mergeResult.rollbackEvents,
|
|
668
|
+
}),
|
|
669
|
+
)
|
|
636
670
|
} else {
|
|
637
|
-
otelSpan?.addEvent(
|
|
671
|
+
otelSpan?.addEvent(`merge[${mergeCounter}]:backend-pull:advance`, {
|
|
638
672
|
newEventsCount: newEvents.length,
|
|
639
|
-
|
|
673
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
640
674
|
})
|
|
641
675
|
|
|
642
|
-
if (clientId === 'client-b') {
|
|
643
|
-
// yield* Effect.log('offer upstream-advance due to pull')
|
|
644
|
-
}
|
|
645
676
|
yield* connectedClientSessionPullQueues.offer({
|
|
646
|
-
payload: {
|
|
647
|
-
|
|
677
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
|
|
678
|
+
mergeCounter,
|
|
648
679
|
})
|
|
680
|
+
mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
|
|
681
|
+
|
|
682
|
+
if (mergeResult.confirmedEvents.length > 0) {
|
|
683
|
+
// `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
|
|
684
|
+
// `newEvents` instead which we filter via `mergeResult.confirmedEvents`
|
|
685
|
+
const confirmedNewEvents = newEvents.filter((mutationEvent) =>
|
|
686
|
+
mergeResult.confirmedEvents.some((confirmedEvent) =>
|
|
687
|
+
EventId.isEqual(mutationEvent.id, confirmedEvent.id),
|
|
688
|
+
),
|
|
689
|
+
)
|
|
690
|
+
yield* Mutationlog.updateSyncMetadata(confirmedNewEvents)
|
|
691
|
+
}
|
|
649
692
|
}
|
|
650
693
|
|
|
694
|
+
// Removes the changeset rows which are no longer needed as we'll never have to rollback beyond this point
|
|
651
695
|
trimChangesetRows(db, newBackendHead)
|
|
652
696
|
|
|
653
|
-
yield*
|
|
697
|
+
yield* applyMutationsBatch({ batchItems: mergeResult.newEvents, deferreds: undefined })
|
|
654
698
|
|
|
655
|
-
yield* SubscriptionRef.set(syncStateSref,
|
|
699
|
+
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
|
656
700
|
|
|
701
|
+
// Allow local pushes to be processed again
|
|
657
702
|
if (remaining === 0) {
|
|
658
|
-
// Allow local pushes to be processed again
|
|
659
703
|
yield* localPushesLatch.open
|
|
660
704
|
}
|
|
661
705
|
})
|
|
662
706
|
|
|
707
|
+
const cursorInfo = yield* Mutationlog.getSyncBackendCursorInfo(initialBackendHead)
|
|
708
|
+
|
|
663
709
|
yield* syncBackend.pull(cursorInfo).pipe(
|
|
664
710
|
// TODO only take from queue while connected
|
|
665
711
|
Stream.tap(({ batch, remaining }) =>
|
|
@@ -671,16 +717,13 @@ const backgroundBackendPulling = ({
|
|
|
671
717
|
// },
|
|
672
718
|
// })
|
|
673
719
|
|
|
674
|
-
// Wait for the db to be initially created
|
|
675
|
-
yield* dbReady
|
|
676
|
-
|
|
677
720
|
// NOTE we only want to take process mutations when the sync backend is connected
|
|
678
721
|
// (e.g. needed for simulating being offline)
|
|
679
722
|
// TODO remove when there's a better way to handle this in stream above
|
|
680
723
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
681
724
|
|
|
682
725
|
yield* onNewPullChunk(
|
|
683
|
-
batch.map((_) => MutationEvent.EncodedWithMeta.fromGlobal(_.mutationEventEncoded)),
|
|
726
|
+
batch.map((_) => MutationEvent.EncodedWithMeta.fromGlobal(_.mutationEventEncoded, _.metadata)),
|
|
684
727
|
remaining,
|
|
685
728
|
)
|
|
686
729
|
|
|
@@ -690,102 +733,26 @@ const backgroundBackendPulling = ({
|
|
|
690
733
|
Stream.runDrain,
|
|
691
734
|
Effect.interruptible,
|
|
692
735
|
)
|
|
693
|
-
}).pipe(Effect.withSpan('@livestore/common:
|
|
694
|
-
|
|
695
|
-
const rollback = ({
|
|
696
|
-
db,
|
|
697
|
-
dbMutationLog,
|
|
698
|
-
eventIdsToRollback,
|
|
699
|
-
}: {
|
|
700
|
-
db: SqliteDb
|
|
701
|
-
dbMutationLog: SqliteDb
|
|
702
|
-
eventIdsToRollback: EventId.EventId[]
|
|
703
|
-
}) =>
|
|
704
|
-
Effect.gen(function* () {
|
|
705
|
-
const rollbackEvents = db
|
|
706
|
-
.select<SessionChangesetMetaRow>(
|
|
707
|
-
sql`SELECT * FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
|
|
708
|
-
)
|
|
709
|
-
.map((_) => ({ id: { global: _.idGlobal, client: _.idClient }, changeset: _.changeset, debug: _.debug }))
|
|
710
|
-
.sort((a, b) => EventId.compare(a.id, b.id))
|
|
711
|
-
// TODO bring back `.toSorted` once Expo supports it
|
|
712
|
-
// .toSorted((a, b) => EventId.compare(a.id, b.id))
|
|
713
|
-
|
|
714
|
-
// Apply changesets in reverse order
|
|
715
|
-
for (let i = rollbackEvents.length - 1; i >= 0; i--) {
|
|
716
|
-
const { changeset } = rollbackEvents[i]!
|
|
717
|
-
if (changeset !== null) {
|
|
718
|
-
db.makeChangeset(changeset).invert().apply()
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const eventIdPairChunks = ReadonlyArray.chunksOf(100)(
|
|
723
|
-
eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`),
|
|
724
|
-
)
|
|
725
|
-
|
|
726
|
-
// Delete the changeset rows
|
|
727
|
-
for (const eventIdPairChunk of eventIdPairChunks) {
|
|
728
|
-
db.execute(
|
|
729
|
-
sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
|
|
730
|
-
)
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Delete the mutation log rows
|
|
734
|
-
for (const eventIdPairChunk of eventIdPairChunks) {
|
|
735
|
-
dbMutationLog.execute(
|
|
736
|
-
sql`DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
|
|
737
|
-
)
|
|
738
|
-
}
|
|
739
|
-
}).pipe(
|
|
740
|
-
Effect.withSpan('@livestore/common:leader-thread:syncing:rollback', {
|
|
741
|
-
attributes: { count: eventIdsToRollback.length },
|
|
742
|
-
}),
|
|
743
|
-
)
|
|
744
|
-
|
|
745
|
-
const getCursorInfo = (remoteHead: EventId.GlobalEventId) =>
|
|
746
|
-
Effect.gen(function* () {
|
|
747
|
-
const { dbMutationLog } = yield* LeaderThreadCtx
|
|
748
|
-
|
|
749
|
-
if (remoteHead === EventId.ROOT.global) return Option.none()
|
|
750
|
-
|
|
751
|
-
const MutationlogQuerySchema = Schema.Struct({
|
|
752
|
-
syncMetadataJson: Schema.parseJson(Schema.Option(Schema.JsonValue)),
|
|
753
|
-
}).pipe(Schema.pluck('syncMetadataJson'), Schema.Array, Schema.head)
|
|
754
|
-
|
|
755
|
-
const syncMetadataOption = yield* Effect.sync(() =>
|
|
756
|
-
dbMutationLog.select<{ syncMetadataJson: string }>(
|
|
757
|
-
sql`SELECT syncMetadataJson FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ${remoteHead} ORDER BY idClient ASC LIMIT 1`,
|
|
758
|
-
),
|
|
759
|
-
).pipe(Effect.andThen(Schema.decode(MutationlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
|
|
760
|
-
|
|
761
|
-
return Option.some({
|
|
762
|
-
cursor: { global: remoteHead, client: EventId.clientDefault },
|
|
763
|
-
metadata: syncMetadataOption,
|
|
764
|
-
}) satisfies InitialSyncInfo
|
|
765
|
-
}).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:getCursorInfo', { attributes: { remoteHead } }))
|
|
736
|
+
}).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'))
|
|
766
737
|
|
|
767
738
|
const backgroundBackendPushing = ({
|
|
768
|
-
|
|
769
|
-
syncBackendQueue,
|
|
739
|
+
syncBackendPushQueue,
|
|
770
740
|
otelSpan,
|
|
771
741
|
devtoolsLatch,
|
|
772
742
|
}: {
|
|
773
|
-
|
|
774
|
-
syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
|
|
743
|
+
syncBackendPushQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
|
|
775
744
|
otelSpan: otel.Span | undefined
|
|
776
745
|
devtoolsLatch: Effect.Latch | undefined
|
|
777
746
|
}) =>
|
|
778
747
|
Effect.gen(function* () {
|
|
779
|
-
const { syncBackend
|
|
748
|
+
const { syncBackend } = yield* LeaderThreadCtx
|
|
780
749
|
if (syncBackend === undefined) return
|
|
781
750
|
|
|
782
|
-
yield* dbReady
|
|
783
|
-
|
|
784
751
|
while (true) {
|
|
785
752
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
786
753
|
|
|
787
754
|
// TODO make batch size configurable
|
|
788
|
-
const queueItems = yield* BucketQueue.takeBetween(
|
|
755
|
+
const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, BACKEND_PUSH_BATCH_SIZE)
|
|
789
756
|
|
|
790
757
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
791
758
|
|
|
@@ -809,27 +776,88 @@ const backgroundBackendPushing = ({
|
|
|
809
776
|
// wait for interrupt caused by background pulling which will then restart pushing
|
|
810
777
|
return yield* Effect.never
|
|
811
778
|
}
|
|
812
|
-
|
|
813
|
-
const { metadata } = pushResult.right
|
|
814
|
-
|
|
815
|
-
// TODO try to do this in a single query
|
|
816
|
-
for (let i = 0; i < queueItems.length; i++) {
|
|
817
|
-
const mutationEventEncoded = queueItems[i]!
|
|
818
|
-
yield* execSql(
|
|
819
|
-
dbMutationLog,
|
|
820
|
-
...updateRows({
|
|
821
|
-
tableName: MUTATION_LOG_META_TABLE,
|
|
822
|
-
columns: mutationLogMetaTable.sqliteDef.columns,
|
|
823
|
-
where: { idGlobal: mutationEventEncoded.id.global, idClient: mutationEventEncoded.id.client },
|
|
824
|
-
updateValues: { syncMetadataJson: metadata[i]! },
|
|
825
|
-
}),
|
|
826
|
-
)
|
|
827
|
-
}
|
|
828
779
|
}
|
|
829
|
-
}).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:
|
|
780
|
+
}).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
|
|
830
781
|
|
|
831
782
|
const trimChangesetRows = (db: SqliteDb, newHead: EventId.EventId) => {
|
|
832
783
|
// Since we're using the session changeset rows to query for the current head,
|
|
833
784
|
// we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
|
|
834
785
|
db.execute(sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE idGlobal < ${newHead.global}`)
|
|
835
786
|
}
|
|
787
|
+
|
|
788
|
+
interface PullQueueSet {
|
|
789
|
+
makeQueue: (
|
|
790
|
+
cursor: number,
|
|
791
|
+
) => Effect.Effect<
|
|
792
|
+
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
|
|
793
|
+
UnexpectedError,
|
|
794
|
+
Scope.Scope | LeaderThreadCtx
|
|
795
|
+
>
|
|
796
|
+
offer: (item: {
|
|
797
|
+
payload: typeof SyncState.PayloadUpstream.Type
|
|
798
|
+
mergeCounter: number
|
|
799
|
+
}) => Effect.Effect<void, UnexpectedError>
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const makePullQueueSet = Effect.gen(function* () {
|
|
803
|
+
const set = new Set<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>>()
|
|
804
|
+
|
|
805
|
+
yield* Effect.addFinalizer(() =>
|
|
806
|
+
Effect.gen(function* () {
|
|
807
|
+
for (const queue of set) {
|
|
808
|
+
yield* Queue.shutdown(queue)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
set.clear()
|
|
812
|
+
}),
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
const makeQueue: PullQueueSet['makeQueue'] = () =>
|
|
816
|
+
Effect.gen(function* () {
|
|
817
|
+
const queue = yield* Queue.unbounded<{
|
|
818
|
+
payload: typeof SyncState.PayloadUpstream.Type
|
|
819
|
+
mergeCounter: number
|
|
820
|
+
}>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
821
|
+
|
|
822
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
|
|
823
|
+
|
|
824
|
+
set.add(queue)
|
|
825
|
+
|
|
826
|
+
return queue
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
const offer: PullQueueSet['offer'] = (item) =>
|
|
830
|
+
Effect.gen(function* () {
|
|
831
|
+
// Short-circuit if the payload is an empty upstream advance
|
|
832
|
+
if (item.payload._tag === 'upstream-advance' && item.payload.newEvents.length === 0) {
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
for (const queue of set) {
|
|
837
|
+
yield* Queue.offer(queue, item)
|
|
838
|
+
}
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
makeQueue,
|
|
843
|
+
offer,
|
|
844
|
+
}
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
const incrementMergeCounter = (mergeCounterRef: { current: number }) =>
|
|
848
|
+
Effect.gen(function* () {
|
|
849
|
+
const { dbReadModel } = yield* LeaderThreadCtx
|
|
850
|
+
mergeCounterRef.current++
|
|
851
|
+
dbReadModel.execute(
|
|
852
|
+
sql`INSERT OR REPLACE INTO ${LEADER_MERGE_COUNTER_TABLE} (id, mergeCounter) VALUES (0, ${mergeCounterRef.current})`,
|
|
853
|
+
)
|
|
854
|
+
return mergeCounterRef.current
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
const getMergeCounterFromDb = (dbReadModel: SqliteDb) =>
|
|
858
|
+
Effect.gen(function* () {
|
|
859
|
+
const result = dbReadModel.select<{ mergeCounter: number }>(
|
|
860
|
+
sql`SELECT mergeCounter FROM ${LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
|
|
861
|
+
)
|
|
862
|
+
return result[0]?.mergeCounter ?? 0
|
|
863
|
+
})
|