@livestore/common 0.3.0-dev.7 → 0.3.0-dev.9
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/devtools/devtools-messages.d.ts +47 -47
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +37 -0
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -0
- package/dist/leader-thread/LeaderSyncProcessor.js +421 -0
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -0
- package/dist/leader-thread/apply-mutation.js +1 -1
- package/dist/leader-thread/apply-mutation.js.map +1 -1
- package/dist/leader-thread/leader-sync-processor.d.ts +2 -2
- package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -1
- package/dist/leader-thread/leader-sync-processor.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +5 -6
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +7 -4
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
- package/dist/leader-thread/types.d.ts +11 -5
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/schema/EventId.test.d.ts +2 -0
- package/dist/schema/EventId.test.d.ts.map +1 -0
- package/dist/schema/EventId.test.js +11 -0
- package/dist/schema/EventId.test.js.map +1 -0
- package/dist/schema/MutationEvent.d.ts +6 -5
- package/dist/schema/MutationEvent.d.ts.map +1 -1
- package/dist/schema/MutationEvent.js +2 -2
- package/dist/schema/MutationEvent.js.map +1 -1
- package/dist/schema/MutationEvent.test.d.ts +2 -0
- package/dist/schema/MutationEvent.test.d.ts.map +1 -0
- package/dist/schema/MutationEvent.test.js +2 -0
- package/dist/schema/MutationEvent.test.js.map +1 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts +45 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -0
- package/dist/sync/ClientSessionSyncProcessor.js +133 -0
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -0
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/sync.d.ts +15 -5
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +39 -17
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +56 -12
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +123 -63
- 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 +3 -3
- package/src/index.ts +0 -6
- package/src/leader-thread/{leader-sync-processor.ts → LeaderSyncProcessor.ts} +205 -212
- package/src/leader-thread/apply-mutation.ts +1 -1
- package/src/leader-thread/make-leader-thread-layer.ts +9 -8
- package/src/leader-thread/types.ts +12 -4
- package/src/schema/EventId.test.ts +12 -0
- package/src/schema/MutationEvent.ts +7 -3
- package/src/sync/{client-session-sync-processor.ts → ClientSessionSyncProcessor.ts} +6 -5
- package/src/sync/index.ts +1 -1
- package/src/sync/sync.ts +15 -4
- package/src/sync/syncstate.test.ts +123 -63
- package/src/sync/syncstate.ts +21 -19
- package/src/version.ts +1 -1
@@ -1,18 +1,17 @@
|
|
1
|
-
import { shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
|
1
|
+
import { isNotUndefined, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
|
2
2
|
import type { HttpClient, Scope } from '@livestore/utils/effect'
|
3
3
|
import {
|
4
4
|
BucketQueue,
|
5
5
|
Deferred,
|
6
6
|
Effect,
|
7
7
|
Exit,
|
8
|
-
Fiber,
|
9
8
|
FiberHandle,
|
10
9
|
Option,
|
11
10
|
OtelTracer,
|
12
11
|
ReadonlyArray,
|
13
|
-
Ref,
|
14
12
|
Schema,
|
15
13
|
Stream,
|
14
|
+
Subscribable,
|
16
15
|
SubscriptionRef,
|
17
16
|
} from '@livestore/utils/effect'
|
18
17
|
import type * as otel from '@opentelemetry/api'
|
@@ -34,61 +33,31 @@ import { sql } from '../util.js'
|
|
34
33
|
import { makeApplyMutation } from './apply-mutation.js'
|
35
34
|
import { execSql } from './connection.js'
|
36
35
|
import { getBackendHeadFromDb, getLocalHeadFromDb, getMutationEventsSince, updateBackendHead } from './mutationlog.js'
|
37
|
-
import type { InitialBlockingSyncContext, InitialSyncInfo,
|
36
|
+
import type { InitialBlockingSyncContext, InitialSyncInfo, LeaderSyncProcessor } from './types.js'
|
38
37
|
import { LeaderThreadCtx } from './types.js'
|
39
38
|
|
40
|
-
type ProcessorStateInit = {
|
41
|
-
_tag: 'init'
|
42
|
-
}
|
43
|
-
|
44
|
-
type ProcessorStateInSync = {
|
45
|
-
_tag: 'in-sync'
|
46
|
-
syncState: SyncState.SyncState
|
47
|
-
}
|
48
|
-
|
49
|
-
type ProcessorStateApplyingSyncStateAdvance = {
|
50
|
-
_tag: 'applying-syncstate-advance'
|
51
|
-
origin: 'pull' | 'push'
|
52
|
-
syncState: SyncState.SyncState
|
53
|
-
// TODO re-introduce this
|
54
|
-
// proccesHead: EventId
|
55
|
-
fiber: Fiber.RuntimeFiber<void, UnexpectedError>
|
56
|
-
}
|
57
|
-
|
58
|
-
type ProcessorState = ProcessorStateInit | ProcessorStateInSync | ProcessorStateApplyingSyncStateAdvance
|
59
|
-
|
60
39
|
/**
|
61
|
-
* The
|
62
|
-
*
|
63
|
-
* The leader sync processor is also responsible for
|
64
|
-
* - broadcasting mutations to client sessions via the pull queues.
|
65
|
-
* - pushing mutations to the sync backend
|
40
|
+
* The LeaderSyncProcessor manages synchronization of mutations between
|
41
|
+
* the local state and the sync backend, ensuring efficient and orderly processing.
|
66
42
|
*
|
67
|
-
* In the
|
43
|
+
* In the LeaderSyncProcessor, pulling always has precedence over pushing.
|
68
44
|
*
|
69
|
-
*
|
70
|
-
* -
|
71
|
-
* -
|
72
|
-
* -
|
45
|
+
* Responsibilities:
|
46
|
+
* - Queueing incoming local mutations in a localPushMailbox.
|
47
|
+
* - Broadcasting mutations to client sessions via pull queues.
|
48
|
+
* - Pushing mutations to the sync backend.
|
73
49
|
*
|
74
|
-
*
|
75
|
-
* - in-sync: fully synced with remote, now idling
|
76
|
-
* - applying-syncstate-advance (with pointer to current progress in case of rebase interrupt)
|
50
|
+
* Notes:
|
77
51
|
*
|
78
|
-
*
|
79
|
-
* -
|
80
|
-
*
|
81
|
-
*
|
52
|
+
* local push processing:
|
53
|
+
* - localPushMailbox:
|
54
|
+
* - Maintains events in ascending order.
|
55
|
+
* - Uses `Deferred` objects to resolve/reject events based on application success.
|
56
|
+
* - Processes events from the mailbox, applying mutations in batches.
|
57
|
+
* - Controlled by a `Latch` to manage execution flow.
|
58
|
+
* - The latch closes on pull receipt and re-opens post-pull completion.
|
59
|
+
* - Processes up to `maxBatchSize` events per cycle.
|
82
60
|
*
|
83
|
-
* Queuing vs interrupting behaviour:
|
84
|
-
* - Operations caused by pull can never be interrupted
|
85
|
-
* - Incoming pull can interrupt current push
|
86
|
-
* - Incoming pull needs to wait to previous pull to finish
|
87
|
-
* - Incoming push needs to wait to previous push to finish
|
88
|
-
*
|
89
|
-
* Backend pushing:
|
90
|
-
* - continously push to backend
|
91
|
-
* - only interrupted and restarted on rebase
|
92
61
|
*/
|
93
62
|
export const makeLeaderSyncProcessor = ({
|
94
63
|
schema,
|
@@ -101,13 +70,11 @@ export const makeLeaderSyncProcessor = ({
|
|
101
70
|
dbMissing: boolean
|
102
71
|
dbLog: SynchronousDatabase
|
103
72
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
104
|
-
}): Effect.Effect<
|
73
|
+
}): Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope> =>
|
105
74
|
Effect.gen(function* () {
|
106
75
|
const syncBackendQueue = yield* BucketQueue.make<MutationEvent.EncodedWithMeta>()
|
107
76
|
|
108
|
-
const
|
109
|
-
|
110
|
-
const semaphore = yield* Effect.makeSemaphore(1)
|
77
|
+
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
111
78
|
|
112
79
|
const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) => {
|
113
80
|
const mutationDef = schema.mutations.get(mutationEventEncoded.mutation)!
|
@@ -115,105 +82,35 @@ export const makeLeaderSyncProcessor = ({
|
|
115
82
|
}
|
116
83
|
|
117
84
|
const spanRef = { current: undefined as otel.Span | undefined }
|
118
|
-
const applyMutationItemsRef = { current: undefined as ApplyMutationItems | undefined }
|
119
|
-
|
120
|
-
// TODO get rid of counters once Effect semaphore ordering is fixed
|
121
|
-
let counterRef = 0
|
122
|
-
let expectedCounter = 0
|
123
85
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
- Have a worker fiber that takes from the mailbox (wouldn't need a semaphore)
|
128
|
-
*/
|
86
|
+
const localPushesQueue = yield* BucketQueue.make<MutationEvent.EncodedWithMeta>()
|
87
|
+
const localPushesLatch = yield* Effect.makeLatch(true)
|
88
|
+
const pullLatch = yield* Effect.makeLatch(true)
|
129
89
|
|
130
|
-
const
|
90
|
+
const push: LeaderSyncProcessor['push'] = (newEvents, options) =>
|
131
91
|
Effect.gen(function* () {
|
132
|
-
// console.log('waitForSyncState: waiting for semaphore', counter)
|
133
|
-
yield* semaphore.take(1)
|
134
|
-
// NOTE this is a workaround to ensure the semaphore take-order is respected
|
135
|
-
// TODO this needs to be fixed upstream in Effect
|
136
|
-
if (counter !== expectedCounter) {
|
137
|
-
console.log(
|
138
|
-
`waitForSyncState: counter mismatch (expected: ${expectedCounter}, got: ${counter}), releasing semaphore`,
|
139
|
-
)
|
140
|
-
yield* semaphore.release(1)
|
141
|
-
yield* Effect.yieldNow()
|
142
|
-
// Retrying...
|
143
|
-
return yield* waitForSyncState(counter)
|
144
|
-
}
|
145
|
-
// console.log('waitForSyncState: took semaphore', counter)
|
146
|
-
const state = yield* Ref.get(stateRef)
|
147
|
-
if (state._tag !== 'in-sync') {
|
148
|
-
return shouldNeverHappen('Expected to be in-sync but got ' + state._tag)
|
149
|
-
}
|
150
|
-
expectedCounter = counter + 1
|
151
|
-
return state
|
152
|
-
}).pipe(Effect.withSpan(`@livestore/common:leader-thread:syncing:waitForSyncState(${counter})`))
|
153
|
-
|
154
|
-
const push = (newEvents: ReadonlyArray<MutationEvent.EncodedWithMeta>) =>
|
155
|
-
Effect.gen(function* () {
|
156
|
-
const counter = counterRef
|
157
|
-
counterRef++
|
158
92
|
// TODO validate batch
|
159
93
|
if (newEvents.length === 0) return
|
160
94
|
|
161
|
-
const
|
162
|
-
|
163
|
-
// TODO if there are multiple pending pushes, we should batch them together
|
164
|
-
const state = yield* waitForSyncState(counter)
|
95
|
+
const waitForProcessing = options?.waitForProcessing ?? false
|
165
96
|
|
166
|
-
const
|
167
|
-
|
168
|
-
|
169
|
-
isLocalEvent,
|
170
|
-
isEqualEvent: MutationEvent.isEqualEncoded,
|
171
|
-
})
|
97
|
+
const deferreds = waitForProcessing
|
98
|
+
? yield* Effect.forEach(newEvents, () => Deferred.make<void, InvalidPushError>())
|
99
|
+
: newEvents.map((_) => undefined)
|
172
100
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
_tag: 'LeaderAhead',
|
180
|
-
minimumExpectedId: updateResult.expectedMinimumId,
|
181
|
-
providedId: newEvents.at(0)!.id,
|
182
|
-
},
|
101
|
+
// TODO validate batch ordering
|
102
|
+
const mappedEvents = newEvents.map(
|
103
|
+
(mutationEventEncoded, i) =>
|
104
|
+
new MutationEvent.EncodedWithMeta({
|
105
|
+
...mutationEventEncoded,
|
106
|
+
meta: { deferred: deferreds[i] },
|
183
107
|
}),
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
const fiber = yield* applyMutationItemsRef.current!({ batchItems: updateResult.newEvents }).pipe(Effect.fork)
|
188
|
-
|
189
|
-
yield* Ref.set(stateRef, {
|
190
|
-
_tag: 'applying-syncstate-advance',
|
191
|
-
origin: 'push',
|
192
|
-
syncState: updateResult.newSyncState,
|
193
|
-
fiber,
|
194
|
-
})
|
195
|
-
|
196
|
-
// console.log('setRef:applying-syncstate-advance after push', counter)
|
197
|
-
|
198
|
-
yield* connectedClientSessionPullQueues.offer({
|
199
|
-
payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents },
|
200
|
-
remaining: 0,
|
201
|
-
})
|
202
|
-
|
203
|
-
spanRef.current?.addEvent('local-push', {
|
204
|
-
batchSize: newEvents.length,
|
205
|
-
updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
206
|
-
})
|
207
|
-
|
208
|
-
// Don't sync localOnly mutations
|
209
|
-
const filteredBatch = updateResult.newEvents.filter((mutationEventEncoded) => {
|
210
|
-
const mutationDef = schema.mutations.get(mutationEventEncoded.mutation)!
|
211
|
-
return mutationDef.options.localOnly === false
|
212
|
-
})
|
213
|
-
|
214
|
-
yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
|
108
|
+
)
|
109
|
+
yield* BucketQueue.offerAll(localPushesQueue, mappedEvents)
|
215
110
|
|
216
|
-
|
111
|
+
if (waitForProcessing) {
|
112
|
+
yield* Effect.all(deferreds as ReadonlyArray<Deferred.Deferred<void, InvalidPushError>>)
|
113
|
+
}
|
217
114
|
}).pipe(
|
218
115
|
Effect.withSpan('@livestore/common:leader-thread:syncing:local-push', {
|
219
116
|
attributes: {
|
@@ -226,10 +123,10 @@ export const makeLeaderSyncProcessor = ({
|
|
226
123
|
}),
|
227
124
|
)
|
228
125
|
|
229
|
-
const pushPartial:
|
126
|
+
const pushPartial: LeaderSyncProcessor['pushPartial'] = (mutationEventEncoded_) =>
|
230
127
|
Effect.gen(function* () {
|
231
|
-
const
|
232
|
-
if (
|
128
|
+
const syncState = yield* syncStateSref
|
129
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
233
130
|
|
234
131
|
const mutationDef =
|
235
132
|
schema.mutations.get(mutationEventEncoded_.mutation) ??
|
@@ -237,14 +134,14 @@ export const makeLeaderSyncProcessor = ({
|
|
237
134
|
|
238
135
|
const mutationEventEncoded = new MutationEvent.EncodedWithMeta({
|
239
136
|
...mutationEventEncoded_,
|
240
|
-
...EventId.nextPair(
|
137
|
+
...EventId.nextPair(syncState.localHead, mutationDef.options.localOnly),
|
241
138
|
})
|
242
139
|
|
243
140
|
yield* push([mutationEventEncoded])
|
244
141
|
}).pipe(Effect.catchTag('InvalidPushError', Effect.orDie))
|
245
142
|
|
246
143
|
// Starts various background loops
|
247
|
-
const boot:
|
144
|
+
const boot: LeaderSyncProcessor['boot'] = ({ dbReady }) =>
|
248
145
|
Effect.gen(function* () {
|
249
146
|
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
|
250
147
|
spanRef.current = span
|
@@ -263,18 +160,16 @@ export const makeLeaderSyncProcessor = ({
|
|
263
160
|
local: EventId.localDefault,
|
264
161
|
}).pipe(Effect.map(ReadonlyArray.map((_) => new MutationEvent.EncodedWithMeta(_))))
|
265
162
|
|
266
|
-
const initialSyncState = {
|
163
|
+
const initialSyncState = new SyncState.SyncState({
|
267
164
|
pending: pendingMutationEvents,
|
268
165
|
// On the leader we don't need a rollback tail beyond `pending` items
|
269
166
|
rollbackTail: [],
|
270
167
|
upstreamHead: { global: initialBackendHead, local: EventId.localDefault },
|
271
168
|
localHead: initialLocalHead,
|
272
|
-
}
|
169
|
+
})
|
273
170
|
|
274
171
|
/** State transitions need to happen atomically, so we use a Ref to track the state */
|
275
|
-
yield*
|
276
|
-
|
277
|
-
applyMutationItemsRef.current = yield* makeApplyMutationItems({ stateRef, semaphore })
|
172
|
+
yield* SubscriptionRef.set(syncStateSref, initialSyncState)
|
278
173
|
|
279
174
|
// Rehydrate sync queue
|
280
175
|
if (pendingMutationEvents.length > 0) {
|
@@ -288,6 +183,17 @@ export const makeLeaderSyncProcessor = ({
|
|
288
183
|
yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
|
289
184
|
}
|
290
185
|
|
186
|
+
yield* backgroundApplyLocalPushes({
|
187
|
+
localPushesLatch,
|
188
|
+
localPushesQueue,
|
189
|
+
pullLatch,
|
190
|
+
syncStateSref,
|
191
|
+
syncBackendQueue,
|
192
|
+
schema,
|
193
|
+
isLocalEvent,
|
194
|
+
span,
|
195
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
196
|
+
|
291
197
|
const backendPushingFiberHandle = yield* FiberHandle.make()
|
292
198
|
|
293
199
|
yield* FiberHandle.run(
|
@@ -314,9 +220,9 @@ export const makeLeaderSyncProcessor = ({
|
|
314
220
|
backgroundBackendPushing({ dbReady, syncBackendQueue, span }).pipe(Effect.tapCauseLogPretty),
|
315
221
|
)
|
316
222
|
}),
|
317
|
-
|
318
|
-
|
319
|
-
|
223
|
+
syncStateSref,
|
224
|
+
localPushesLatch,
|
225
|
+
pullLatch,
|
320
226
|
span,
|
321
227
|
initialBlockingSyncContext,
|
322
228
|
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
@@ -326,12 +232,122 @@ export const makeLeaderSyncProcessor = ({
|
|
326
232
|
push,
|
327
233
|
pushPartial,
|
328
234
|
boot,
|
329
|
-
syncState:
|
330
|
-
|
331
|
-
|
332
|
-
|
235
|
+
syncState: Subscribable.make({
|
236
|
+
get: Effect.gen(function* () {
|
237
|
+
const syncState = yield* syncStateSref
|
238
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
239
|
+
return syncState
|
240
|
+
}),
|
241
|
+
changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
|
333
242
|
}),
|
334
|
-
} satisfies
|
243
|
+
} satisfies LeaderSyncProcessor
|
244
|
+
})
|
245
|
+
|
246
|
+
const backgroundApplyLocalPushes = ({
|
247
|
+
localPushesLatch,
|
248
|
+
localPushesQueue,
|
249
|
+
pullLatch,
|
250
|
+
syncStateSref,
|
251
|
+
syncBackendQueue,
|
252
|
+
schema,
|
253
|
+
isLocalEvent,
|
254
|
+
span,
|
255
|
+
}: {
|
256
|
+
pullLatch: Effect.Latch
|
257
|
+
localPushesLatch: Effect.Latch
|
258
|
+
localPushesQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
|
259
|
+
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
260
|
+
syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
|
261
|
+
schema: LiveStoreSchema
|
262
|
+
isLocalEvent: (mutationEventEncoded: MutationEvent.EncodedWithMeta) => boolean
|
263
|
+
span: otel.Span | undefined
|
264
|
+
}) =>
|
265
|
+
Effect.gen(function* () {
|
266
|
+
const { connectedClientSessionPullQueues } = yield* LeaderThreadCtx
|
267
|
+
|
268
|
+
const applyMutationItems = yield* makeApplyMutationItems
|
269
|
+
|
270
|
+
while (true) {
|
271
|
+
// TODO make this configurable
|
272
|
+
const newEvents = yield* BucketQueue.takeBetween(localPushesQueue, 1, 10)
|
273
|
+
|
274
|
+
// Wait for the backend pulling to finish
|
275
|
+
yield* localPushesLatch.await
|
276
|
+
|
277
|
+
// Prevent the backend pulling from starting until this local push is finished
|
278
|
+
yield* pullLatch.close
|
279
|
+
|
280
|
+
const syncState = yield* syncStateSref
|
281
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
282
|
+
|
283
|
+
const updateResult = SyncState.updateSyncState({
|
284
|
+
syncState,
|
285
|
+
payload: { _tag: 'local-push', newEvents },
|
286
|
+
isLocalEvent,
|
287
|
+
isEqualEvent: MutationEvent.isEqualEncoded,
|
288
|
+
})
|
289
|
+
|
290
|
+
if (updateResult._tag === 'rebase') {
|
291
|
+
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
292
|
+
} else if (updateResult._tag === 'reject') {
|
293
|
+
span?.addEvent('local-push:reject', {
|
294
|
+
batchSize: newEvents.length,
|
295
|
+
updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
296
|
+
})
|
297
|
+
|
298
|
+
const providedId = newEvents.at(0)!.id
|
299
|
+
const remainingEvents = yield* BucketQueue.takeAll(localPushesQueue)
|
300
|
+
const allEvents = [...newEvents, ...remainingEvents]
|
301
|
+
yield* Effect.forEach(allEvents, (mutationEventEncoded) =>
|
302
|
+
mutationEventEncoded.meta.deferred
|
303
|
+
? Deferred.fail(
|
304
|
+
mutationEventEncoded.meta.deferred,
|
305
|
+
InvalidPushError.make({
|
306
|
+
// TODO improve error handling so it differentiates between a push being rejected
|
307
|
+
// because of itself or because of another push
|
308
|
+
reason: {
|
309
|
+
_tag: 'LeaderAhead',
|
310
|
+
minimumExpectedId: updateResult.expectedMinimumId,
|
311
|
+
providedId,
|
312
|
+
},
|
313
|
+
}),
|
314
|
+
)
|
315
|
+
: Effect.void,
|
316
|
+
)
|
317
|
+
|
318
|
+
// Allow the backend pulling to start
|
319
|
+
yield* pullLatch.open
|
320
|
+
|
321
|
+
// In this case we're skipping state update and down/upstream processing
|
322
|
+
// We've cleared the local push queue and are now waiting for new local pushes / backend pulls
|
323
|
+
continue
|
324
|
+
}
|
325
|
+
|
326
|
+
yield* SubscriptionRef.set(syncStateSref, updateResult.newSyncState)
|
327
|
+
|
328
|
+
yield* connectedClientSessionPullQueues.offer({
|
329
|
+
payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents },
|
330
|
+
remaining: 0,
|
331
|
+
})
|
332
|
+
|
333
|
+
span?.addEvent('local-push', {
|
334
|
+
batchSize: newEvents.length,
|
335
|
+
updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
336
|
+
})
|
337
|
+
|
338
|
+
// Don't sync localOnly mutations
|
339
|
+
const filteredBatch = updateResult.newEvents.filter((mutationEventEncoded) => {
|
340
|
+
const mutationDef = schema.mutations.get(mutationEventEncoded.mutation)!
|
341
|
+
return mutationDef.options.localOnly === false
|
342
|
+
})
|
343
|
+
|
344
|
+
yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
|
345
|
+
|
346
|
+
yield* applyMutationItems({ batchItems: newEvents })
|
347
|
+
|
348
|
+
// Allow the backend pulling to start
|
349
|
+
yield* pullLatch.open
|
350
|
+
}
|
335
351
|
})
|
336
352
|
|
337
353
|
type ApplyMutationItems = (_: {
|
@@ -339,13 +355,7 @@ type ApplyMutationItems = (_: {
|
|
339
355
|
}) => Effect.Effect<void, UnexpectedError>
|
340
356
|
|
341
357
|
// TODO how to handle errors gracefully
|
342
|
-
const makeApplyMutationItems =
|
343
|
-
stateRef,
|
344
|
-
semaphore,
|
345
|
-
}: {
|
346
|
-
stateRef: Ref.Ref<ProcessorState>
|
347
|
-
semaphore: Effect.Semaphore
|
348
|
-
}): Effect.Effect<ApplyMutationItems, UnexpectedError, LeaderThreadCtx | Scope.Scope> =>
|
358
|
+
const makeApplyMutationItems: Effect.Effect<ApplyMutationItems, UnexpectedError, LeaderThreadCtx | Scope.Scope> =
|
349
359
|
Effect.gen(function* () {
|
350
360
|
const leaderThreadCtx = yield* LeaderThreadCtx
|
351
361
|
const { db, dbLog } = leaderThreadCtx
|
@@ -354,12 +364,6 @@ const makeApplyMutationItems = ({
|
|
354
364
|
|
355
365
|
return ({ batchItems }) =>
|
356
366
|
Effect.gen(function* () {
|
357
|
-
const state = yield* Ref.get(stateRef)
|
358
|
-
if (state._tag !== 'applying-syncstate-advance') {
|
359
|
-
// console.log('applyMutationItems: counter', counter)
|
360
|
-
return shouldNeverHappen(`Expected to be applying-syncstate-advance but got ${state._tag}`)
|
361
|
-
}
|
362
|
-
|
363
367
|
db.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
364
368
|
dbLog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
365
369
|
|
@@ -381,22 +385,16 @@ const makeApplyMutationItems = ({
|
|
381
385
|
if (meta?.deferred) {
|
382
386
|
yield* Deferred.succeed(meta.deferred, void 0)
|
383
387
|
}
|
384
|
-
|
385
|
-
// TODO re-introduce this
|
386
|
-
// if (i < batchItems.length - 1) {
|
387
|
-
// yield* Ref.set(stateRef, { ...state, proccesHead: batchItems[i + 1]!.id })
|
388
|
-
// }
|
389
388
|
}
|
390
389
|
|
391
390
|
db.execute('COMMIT', undefined) // Commit the transaction
|
392
391
|
dbLog.execute('COMMIT', undefined) // Commit the transaction
|
393
|
-
|
394
|
-
yield* Ref.set(stateRef, { _tag: 'in-sync', syncState: state.syncState })
|
395
|
-
// console.log('setRef:sync after applyMutationItems', counter)
|
396
|
-
yield* semaphore.release(1)
|
397
392
|
}).pipe(
|
393
|
+
Effect.uninterruptible,
|
398
394
|
Effect.scoped,
|
399
|
-
Effect.withSpan('@livestore/common:leader-thread:syncing:applyMutationItems'
|
395
|
+
Effect.withSpan('@livestore/common:leader-thread:syncing:applyMutationItems', {
|
396
|
+
attributes: { count: batchItems.length },
|
397
|
+
}),
|
400
398
|
Effect.tapCauseLogPretty,
|
401
399
|
UnexpectedError.mapToUnexpectedError,
|
402
400
|
)
|
@@ -408,9 +406,9 @@ const backgroundBackendPulling = ({
|
|
408
406
|
isLocalEvent,
|
409
407
|
restartBackendPushing,
|
410
408
|
span,
|
411
|
-
|
412
|
-
|
413
|
-
|
409
|
+
syncStateSref,
|
410
|
+
localPushesLatch,
|
411
|
+
pullLatch,
|
414
412
|
initialBlockingSyncContext,
|
415
413
|
}: {
|
416
414
|
dbReady: Deferred.Deferred<void>
|
@@ -420,9 +418,9 @@ const backgroundBackendPulling = ({
|
|
420
418
|
filteredRebasedPending: ReadonlyArray<MutationEvent.EncodedWithMeta>,
|
421
419
|
) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | HttpClient.HttpClient>
|
422
420
|
span: otel.Span | undefined
|
423
|
-
|
424
|
-
|
425
|
-
|
421
|
+
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
422
|
+
localPushesLatch: Effect.Latch
|
423
|
+
pullLatch: Effect.Latch
|
426
424
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
427
425
|
}) =>
|
428
426
|
Effect.gen(function* () {
|
@@ -432,30 +430,25 @@ const backgroundBackendPulling = ({
|
|
432
430
|
|
433
431
|
const cursorInfo = yield* getCursorInfo(initialBackendHead)
|
434
432
|
|
433
|
+
const applyMutationItems = yield* makeApplyMutationItems
|
434
|
+
|
435
435
|
const onNewPullChunk = (newEvents: MutationEvent.EncodedWithMeta[], remaining: number) =>
|
436
436
|
Effect.gen(function* () {
|
437
437
|
if (newEvents.length === 0) return
|
438
438
|
|
439
|
-
|
440
|
-
|
439
|
+
// Prevent more local pushes from being processed until this pull is finished
|
440
|
+
yield* localPushesLatch.close
|
441
441
|
|
442
|
-
//
|
442
|
+
// Wait for pending local pushes to finish
|
443
|
+
yield* pullLatch.await
|
443
444
|
|
444
|
-
|
445
|
-
|
446
|
-
yield* Fiber.interrupt(state.fiber)
|
447
|
-
// In theory we should force-take the semaphore here, but as it's still taken,
|
448
|
-
// it's already in the right state we want it to be in
|
449
|
-
} else {
|
450
|
-
// Wait for previous advance to finish
|
451
|
-
yield* semaphore.take(1)
|
452
|
-
}
|
453
|
-
}
|
445
|
+
const syncState = yield* syncStateSref
|
446
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
454
447
|
|
455
448
|
const trimRollbackUntil = newEvents.at(-1)!.id
|
456
449
|
|
457
450
|
const updateResult = SyncState.updateSyncState({
|
458
|
-
syncState
|
451
|
+
syncState,
|
459
452
|
payload: { _tag: 'upstream-advance', newEvents, trimRollbackUntil },
|
460
453
|
isLocalEvent,
|
461
454
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
@@ -511,14 +504,14 @@ const backgroundBackendPulling = ({
|
|
511
504
|
|
512
505
|
trimChangesetRows(db, newBackendHead)
|
513
506
|
|
514
|
-
|
507
|
+
yield* applyMutationItems({ batchItems: updateResult.newEvents })
|
515
508
|
|
516
|
-
yield*
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
}
|
509
|
+
yield* SubscriptionRef.set(syncStateSref, updateResult.newSyncState)
|
510
|
+
|
511
|
+
if (remaining === 0) {
|
512
|
+
// Allow local pushes to be processed again
|
513
|
+
yield* localPushesLatch.open
|
514
|
+
}
|
522
515
|
})
|
523
516
|
|
524
517
|
yield* syncBackend.pull(cursorInfo).pipe(
|
@@ -648,7 +641,7 @@ const backgroundBackendPushing = ({
|
|
648
641
|
|
649
642
|
if (pushResult._tag === 'Left') {
|
650
643
|
span?.addEvent('backend-push-error', { error: pushResult.left.toString() })
|
651
|
-
// wait for interrupt
|
644
|
+
// wait for interrupt caused by background pulling which will then restart pushing
|
652
645
|
return yield* Effect.never
|
653
646
|
}
|
654
647
|
|
@@ -101,7 +101,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
|
|
101
101
|
attributes: {
|
102
102
|
mutationName: mutationEventEncoded.mutation,
|
103
103
|
mutationId: mutationEventEncoded.id,
|
104
|
-
'span.label': mutationEventEncoded.mutation
|
104
|
+
'span.label': `(${mutationEventEncoded.id.global},${mutationEventEncoded.id.local}) ${mutationEventEncoded.mutation}`,
|
105
105
|
},
|
106
106
|
}),
|
107
107
|
// Effect.logDuration('@livestore/common:leader-thread:applyMutation'),
|
@@ -7,11 +7,11 @@ import type * as Devtools from '../devtools/index.js'
|
|
7
7
|
import type { LiveStoreSchema } from '../schema/mod.js'
|
8
8
|
import { EventId, MutationEvent, mutationLogMetaTable, SYNC_STATUS_TABLE, syncStatusTable } from '../schema/mod.js'
|
9
9
|
import { migrateTable } from '../schema-management/migrations.js'
|
10
|
-
import type { InvalidPullError, IsOfflineError,
|
10
|
+
import type { InvalidPullError, IsOfflineError, SyncOptions } from '../sync/sync.js'
|
11
11
|
import { sql } from '../util.js'
|
12
12
|
import { execSql } from './connection.js'
|
13
|
-
import { makeLeaderSyncProcessor } from './leader-sync-processor.js'
|
14
13
|
import { bootDevtools } from './leader-worker-devtools.js'
|
14
|
+
import { makeLeaderSyncProcessor } from './LeaderSyncProcessor.js'
|
15
15
|
import { makePullQueueSet } from './pull-queue-set.js'
|
16
16
|
import { recreateDb } from './recreate-db.js'
|
17
17
|
import type { ShutdownChannel } from './shutdown-channel.js'
|
@@ -23,22 +23,20 @@ export const makeLeaderThreadLayer = ({
|
|
23
23
|
storeId,
|
24
24
|
clientId,
|
25
25
|
makeSyncDb,
|
26
|
-
|
26
|
+
syncOptions,
|
27
27
|
db,
|
28
28
|
dbLog,
|
29
29
|
devtoolsOptions,
|
30
|
-
initialSyncOptions = { _tag: 'Skip' },
|
31
30
|
shutdownChannel,
|
32
31
|
}: {
|
33
32
|
storeId: string
|
34
33
|
clientId: string
|
35
34
|
schema: LiveStoreSchema
|
36
35
|
makeSyncDb: MakeSynchronousDatabase
|
37
|
-
|
36
|
+
syncOptions: SyncOptions | undefined
|
38
37
|
db: SynchronousDatabase
|
39
38
|
dbLog: SynchronousDatabase
|
40
39
|
devtoolsOptions: DevtoolsOptions
|
41
|
-
initialSyncOptions: InitialSyncOptions | undefined
|
42
40
|
shutdownChannel: ShutdownChannel
|
43
41
|
}): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
|
44
42
|
Effect.gen(function* () {
|
@@ -48,9 +46,12 @@ export const makeLeaderThreadLayer = ({
|
|
48
46
|
// Either happens on initial boot or if schema changes
|
49
47
|
const dbMissing = db.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
|
50
48
|
|
51
|
-
const syncBackend =
|
49
|
+
const syncBackend = syncOptions === undefined ? undefined : yield* syncOptions.makeBackend({ storeId, clientId })
|
52
50
|
|
53
|
-
const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({
|
51
|
+
const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({
|
52
|
+
initialSyncOptions: syncOptions?.initialSyncOptions ?? { _tag: 'Skip' },
|
53
|
+
bootStatusQueue,
|
54
|
+
})
|
54
55
|
|
55
56
|
const syncProcessor = yield* makeLeaderSyncProcessor({ schema, dbMissing, dbLog, initialBlockingSyncContext })
|
56
57
|
|