@livestore/common 0.0.0-snapshot-4872718a28e4357bb1acdfb14eb69083ed0a480c → 0.0.0-snapshot-484c9684bac8056d764aa460fd025c45f5856aa5
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 +5 -4
- package/dist/adapter-types.d.ts.map +1 -1
- 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 +6 -6
- package/dist/devtools/devtools-messages-leader.d.ts +28 -28
- package/dist/devtools/index.d.ts +17 -0
- package/dist/devtools/index.d.ts.map +1 -1
- package/dist/devtools/index.js +20 -0
- package/dist/devtools/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +3 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +124 -43
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
- package/dist/leader-thread/apply-mutation.js +8 -2
- package/dist/leader-thread/apply-mutation.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 +1 -0
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts +3 -3
- package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
- package/dist/leader-thread/pull-queue-set.js +9 -0
- package/dist/leader-thread/pull-queue-set.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -5
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -4
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/types.d.ts +7 -2
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/schema/EventId.d.ts +8 -0
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +14 -0
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/MutationEvent.js +1 -1
- package/dist/schema/MutationEvent.js.map +1 -1
- package/dist/schema/db-schema/dsl/field-defs.d.ts.map +1 -1
- package/dist/schema/db-schema/dsl/field-defs.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 +48 -14
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/sync.d.ts +14 -9
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +7 -3
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +132 -21
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +129 -41
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +19 -7
- 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 -4
- package/src/devtools/index.ts +27 -0
- package/src/leader-thread/LeaderSyncProcessor.ts +164 -54
- package/src/leader-thread/apply-mutation.ts +17 -2
- package/src/leader-thread/make-leader-thread-layer.ts +1 -0
- package/src/leader-thread/pull-queue-set.ts +10 -1
- package/src/leader-thread/shutdown-channel.ts +2 -4
- package/src/leader-thread/types.ts +8 -2
- package/src/schema/EventId.ts +16 -0
- package/src/schema/MutationEvent.ts +1 -1
- package/src/schema/db-schema/dsl/field-defs.ts +1 -2
- package/src/sync/ClientSessionSyncProcessor.ts +78 -13
- package/src/sync/sync.ts +7 -4
- package/src/sync/syncstate.test.ts +32 -14
- package/src/sync/syncstate.ts +145 -60
- package/src/version.ts +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
|
1
|
+
import { LS_DEV, memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
|
2
2
|
import type { Scope } from '@livestore/utils/effect'
|
|
3
3
|
import { Effect, Option, Schema } from '@livestore/utils/effect'
|
|
4
4
|
|
|
5
|
-
import type { SqliteDb, SqliteError, UnexpectedError } from '../index.js'
|
|
5
|
+
import type { PreparedBindValues, SqliteDb, SqliteError, UnexpectedError } from '../index.js'
|
|
6
6
|
import { getExecArgsFromMutation } from '../mutation.js'
|
|
7
7
|
import {
|
|
8
|
+
EventId,
|
|
8
9
|
type LiveStoreSchema,
|
|
9
10
|
MUTATION_LOG_META_TABLE,
|
|
10
11
|
type MutationEvent,
|
|
@@ -127,6 +128,20 @@ const insertIntoMutationLog = (
|
|
|
127
128
|
const mutationDefSchemaHash =
|
|
128
129
|
mutationDefSchemaHashMap.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
|
|
129
130
|
|
|
131
|
+
if (LS_DEV && mutationEventEncoded.parentId.global !== EventId.ROOT.global) {
|
|
132
|
+
const parentMutationExists =
|
|
133
|
+
dbMutationLog.select<{ count: number }>(
|
|
134
|
+
`SELECT COUNT(*) as count FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ? AND idClient = ?`,
|
|
135
|
+
[mutationEventEncoded.parentId.global, mutationEventEncoded.parentId.client] as any as PreparedBindValues,
|
|
136
|
+
)[0]!.count === 1
|
|
137
|
+
|
|
138
|
+
if (parentMutationExists === false) {
|
|
139
|
+
shouldNeverHappen(
|
|
140
|
+
`Parent mutation ${mutationEventEncoded.parentId.global},${mutationEventEncoded.parentId.client} does not exist`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
// TODO use prepared statements
|
|
131
146
|
yield* execSql(
|
|
132
147
|
dbMutationLog,
|
|
@@ -2,7 +2,7 @@ import { Effect, Queue } from '@livestore/utils/effect'
|
|
|
2
2
|
|
|
3
3
|
import * as MutationEvent from '../schema/MutationEvent.js'
|
|
4
4
|
import { getMutationEventsSince } from './mutationlog.js'
|
|
5
|
-
import { type PullQueueItem, type PullQueueSet } from './types.js'
|
|
5
|
+
import { LeaderThreadCtx, type PullQueueItem, type PullQueueSet } from './types.js'
|
|
6
6
|
|
|
7
7
|
export const makePullQueueSet = Effect.gen(function* () {
|
|
8
8
|
const set = new Set<Queue.Queue<PullQueueItem>>()
|
|
@@ -46,6 +46,15 @@ export const makePullQueueSet = Effect.gen(function* () {
|
|
|
46
46
|
return
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const { clientId } = yield* LeaderThreadCtx
|
|
50
|
+
if (clientId === 'client-b') {
|
|
51
|
+
// console.log(
|
|
52
|
+
// 'offer',
|
|
53
|
+
// item.payload._tag,
|
|
54
|
+
// item.payload.newEvents.map((_) => _.toJSON()),
|
|
55
|
+
// )
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
for (const queue of set) {
|
|
50
59
|
yield* Queue.offer(queue, item)
|
|
51
60
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { WebChannel } from '@livestore/utils/effect'
|
|
2
2
|
import { Schema } from '@livestore/utils/effect'
|
|
3
3
|
|
|
4
|
-
import { IntentionalShutdownCause } from '../index.js'
|
|
4
|
+
import { IntentionalShutdownCause, UnexpectedError } from '../index.js'
|
|
5
5
|
|
|
6
|
-
export class
|
|
7
|
-
|
|
8
|
-
export class All extends Schema.Union(IntentionalShutdownCause, DedicatedWorkerDisconnectBroadcast) {}
|
|
6
|
+
export class All extends Schema.Union(IntentionalShutdownCause, UnexpectedError) {}
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* Used internally by an adapter to shutdown gracefully.
|
|
@@ -14,7 +14,7 @@ import { Context, Schema } from '@livestore/utils/effect'
|
|
|
14
14
|
import type {
|
|
15
15
|
BootStatus,
|
|
16
16
|
Devtools,
|
|
17
|
-
|
|
17
|
+
LeaderAheadError,
|
|
18
18
|
MakeSqliteDb,
|
|
19
19
|
MigrationsReport,
|
|
20
20
|
PersistenceInfo,
|
|
@@ -126,13 +126,19 @@ export interface LeaderSyncProcessor {
|
|
|
126
126
|
/** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
|
|
127
127
|
batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
|
|
128
128
|
options?: {
|
|
129
|
+
/**
|
|
130
|
+
* This generation number is used to automatically reject subsequent pushes
|
|
131
|
+
* of a previously rejected push from a client session. This might occur in
|
|
132
|
+
* certain concurrent scenarios.
|
|
133
|
+
*/
|
|
134
|
+
// generation: number
|
|
129
135
|
/**
|
|
130
136
|
* If true, the effect will only finish when the local push has been processed (i.e. succeeded or was rejected).
|
|
131
137
|
* @default false
|
|
132
138
|
*/
|
|
133
139
|
waitForProcessing?: boolean
|
|
134
140
|
},
|
|
135
|
-
) => Effect.Effect<void,
|
|
141
|
+
) => Effect.Effect<void, LeaderAheadError>
|
|
136
142
|
|
|
137
143
|
pushPartial: (args: {
|
|
138
144
|
mutationEvent: MutationEvent.PartialAnyEncoded
|
package/src/schema/EventId.ts
CHANGED
|
@@ -33,6 +33,22 @@ export const compare = (a: EventId, b: EventId) => {
|
|
|
33
33
|
return a.client - b.client
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Convert an event id to a string representation.
|
|
38
|
+
*/
|
|
39
|
+
export const toString = (id: EventId) => `(${id.global},${id.client})`
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a string representation of an event id to an event id.
|
|
43
|
+
*/
|
|
44
|
+
export const fromString = (str: string): EventId => {
|
|
45
|
+
const [global, client] = str.slice(1, -1).split(',').map(Number)
|
|
46
|
+
if (global === undefined || client === undefined) {
|
|
47
|
+
throw new Error('Invalid event id string')
|
|
48
|
+
}
|
|
49
|
+
return { global, client } as EventId
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.client === b.client
|
|
37
53
|
|
|
38
54
|
export type EventIdPair = { id: EventId; parentId: EventId }
|
|
@@ -169,7 +169,7 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
|
|
|
169
169
|
toJSON = (): any => {
|
|
170
170
|
// Only used for logging/debugging
|
|
171
171
|
// - More readable way to print the id + parentId
|
|
172
|
-
// - not including `meta`
|
|
172
|
+
// - not including `meta`, `clientId`, `sessionId`
|
|
173
173
|
return {
|
|
174
174
|
id: `(${this.id.global},${this.id.client}) → (${this.parentId.global},${this.parentId.client})`,
|
|
175
175
|
mutation: this.mutation,
|
|
@@ -53,8 +53,7 @@ export type ColDefFn<TColumnType extends FieldColumnType> = {
|
|
|
53
53
|
const TNullable extends boolean = false,
|
|
54
54
|
const TDefault extends TDecoded | SqlDefaultValue | NoDefault | (TNullable extends true ? null : never) = NoDefault,
|
|
55
55
|
const TPrimaryKey extends boolean = false,
|
|
56
|
-
>(args: {
|
|
57
|
-
schema?: Schema.Schema<TDecoded, TEncoded>
|
|
56
|
+
>(args: { schema?: Schema.Schema<TDecoded, TEncoded>
|
|
58
57
|
default?: TDefault
|
|
59
58
|
nullable?: TNullable
|
|
60
59
|
primaryKey?: TPrimaryKey
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
|
|
2
2
|
import type { Runtime, Scope } from '@livestore/utils/effect'
|
|
3
|
-
import { Effect, Queue, Schema, Stream, Subscribable } from '@livestore/utils/effect'
|
|
3
|
+
import { BucketQueue, Effect, FiberHandle, Queue, Schema, Stream, Subscribable } from '@livestore/utils/effect'
|
|
4
4
|
import * as otel from '@opentelemetry/api'
|
|
5
5
|
|
|
6
6
|
import type { ClientSession, UnexpectedError } from '../adapter-types.js'
|
|
@@ -26,6 +26,7 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
26
26
|
rollback,
|
|
27
27
|
refreshTables,
|
|
28
28
|
span,
|
|
29
|
+
params,
|
|
29
30
|
}: {
|
|
30
31
|
schema: LiveStoreSchema
|
|
31
32
|
clientSession: ClientSession
|
|
@@ -40,6 +41,9 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
40
41
|
rollback: (changeset: Uint8Array) => void
|
|
41
42
|
refreshTables: (tables: Set<string>) => void
|
|
42
43
|
span: otel.Span
|
|
44
|
+
params: {
|
|
45
|
+
leaderPushBatchSize: number
|
|
46
|
+
}
|
|
43
47
|
}): ClientSessionSyncProcessor => {
|
|
44
48
|
const mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
|
|
45
49
|
|
|
@@ -59,6 +63,9 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
59
63
|
return mutationDef.options.clientOnly
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/** We're queuing push requests to reduce the number of messages sent to the leader by batching them */
|
|
67
|
+
const leaderPushQueue = BucketQueue.make<MutationEvent.EncodedWithMeta>().pipe(Effect.runSync)
|
|
68
|
+
|
|
62
69
|
const push: ClientSessionSyncProcessor['push'] = (batch, { otelContext }) => {
|
|
63
70
|
// TODO validate batch
|
|
64
71
|
|
|
@@ -84,6 +91,10 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
84
91
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
|
85
92
|
})
|
|
86
93
|
|
|
94
|
+
if (updateResult._tag === 'unexpected-error') {
|
|
95
|
+
return shouldNeverHappen('Unexpected error in client-session-sync-processor', updateResult.cause)
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
span.addEvent('local-push', {
|
|
88
99
|
batchSize: encodedMutationEvents.length,
|
|
89
100
|
updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
|
@@ -108,13 +119,17 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
// console.debug('pushToLeader', encodedMutationEvents.length, ...encodedMutationEvents.map((_) => _.toJSON()))
|
|
111
|
-
|
|
112
|
-
.push(encodedMutationEvents)
|
|
113
|
-
.pipe(Effect.tapCauseLogPretty, Effect.provide(runtime), Effect.runFork)
|
|
122
|
+
BucketQueue.offerAll(leaderPushQueue, encodedMutationEvents).pipe(Effect.runSync)
|
|
114
123
|
|
|
115
124
|
return { writeTables }
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
const debugInfo = {
|
|
128
|
+
rebaseCount: 0,
|
|
129
|
+
advanceCount: 0,
|
|
130
|
+
rejectCount: 0,
|
|
131
|
+
}
|
|
132
|
+
|
|
118
133
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
119
134
|
|
|
120
135
|
const boot: ClientSessionSyncProcessor['boot'] = Effect.gen(function* () {
|
|
@@ -133,6 +148,20 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
133
148
|
)
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
const leaderPushingFiberHandle = yield* FiberHandle.make()
|
|
152
|
+
|
|
153
|
+
const backgroundLeaderPushing = Effect.gen(function* () {
|
|
154
|
+
const batch = yield* BucketQueue.takeBetween(leaderPushQueue, 1, params.leaderPushBatchSize)
|
|
155
|
+
yield* clientSession.leaderThread.mutations.push(batch).pipe(
|
|
156
|
+
Effect.catchTag('LeaderAheadError', () => {
|
|
157
|
+
debugInfo.rejectCount++
|
|
158
|
+
return BucketQueue.clear(leaderPushQueue)
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
}).pipe(Effect.forever, Effect.interruptible, Effect.tapCauseLogPretty)
|
|
162
|
+
|
|
163
|
+
yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
|
|
164
|
+
|
|
136
165
|
yield* clientSession.leaderThread.mutations.pull.pipe(
|
|
137
166
|
Stream.tap(({ payload, remaining }) =>
|
|
138
167
|
Effect.gen(function* () {
|
|
@@ -148,9 +177,10 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
148
177
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
|
149
178
|
})
|
|
150
179
|
|
|
151
|
-
if (updateResult._tag === '
|
|
152
|
-
|
|
153
|
-
|
|
180
|
+
if (updateResult._tag === 'unexpected-error') {
|
|
181
|
+
return yield* Effect.fail(updateResult.cause)
|
|
182
|
+
} else if (updateResult._tag === 'reject') {
|
|
183
|
+
return shouldNeverHappen('Unexpected reject in client-session-sync-processor', updateResult)
|
|
154
184
|
}
|
|
155
185
|
|
|
156
186
|
syncStateRef.current = updateResult.newSyncState
|
|
@@ -165,11 +195,21 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
165
195
|
res: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
|
166
196
|
remaining,
|
|
167
197
|
})
|
|
198
|
+
|
|
199
|
+
debugInfo.rebaseCount++
|
|
200
|
+
|
|
201
|
+
yield* FiberHandle.clear(leaderPushingFiberHandle)
|
|
202
|
+
|
|
203
|
+
// Reset the leader push queue since we're rebasing and will push again
|
|
204
|
+
yield* BucketQueue.clear(leaderPushQueue)
|
|
205
|
+
|
|
206
|
+
yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
|
|
207
|
+
|
|
168
208
|
if (LS_DEV) {
|
|
169
209
|
Effect.logDebug(
|
|
170
210
|
'pull:rebase: rollback',
|
|
171
211
|
updateResult.eventsToRollback.length,
|
|
172
|
-
...updateResult.eventsToRollback.map((_) => _.toJSON()),
|
|
212
|
+
...updateResult.eventsToRollback.slice(0, 10).map((_) => _.toJSON()),
|
|
173
213
|
).pipe(Effect.provide(runtime), Effect.runSync)
|
|
174
214
|
}
|
|
175
215
|
|
|
@@ -181,9 +221,7 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
181
221
|
}
|
|
182
222
|
}
|
|
183
223
|
|
|
184
|
-
|
|
185
|
-
.push(updateResult.newSyncState.pending)
|
|
186
|
-
.pipe(Effect.tapCauseLogPretty, Effect.provide(runtime), Effect.runFork)
|
|
224
|
+
yield* BucketQueue.offerAll(leaderPushQueue, updateResult.newSyncState.pending)
|
|
187
225
|
} else {
|
|
188
226
|
span.addEvent('pull:advance', {
|
|
189
227
|
payloadTag: payload._tag,
|
|
@@ -192,6 +230,8 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
192
230
|
res: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
|
|
193
231
|
remaining,
|
|
194
232
|
})
|
|
233
|
+
|
|
234
|
+
debugInfo.advanceCount++
|
|
195
235
|
}
|
|
196
236
|
|
|
197
237
|
if (updateResult.newEvents.length === 0) return
|
|
@@ -208,10 +248,14 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
208
248
|
}
|
|
209
249
|
|
|
210
250
|
refreshTables(writeTables)
|
|
211
|
-
})
|
|
251
|
+
}).pipe(
|
|
252
|
+
Effect.tapCauseLogPretty,
|
|
253
|
+
Effect.catchAllCause((cause) => Effect.sync(() => clientSession.shutdown(cause))),
|
|
254
|
+
),
|
|
212
255
|
),
|
|
213
256
|
Stream.runDrain,
|
|
214
257
|
Effect.forever, // NOTE Whenever the leader changes, we need to re-start the stream
|
|
258
|
+
Effect.withSpan('client-session-sync-processor:pull'),
|
|
215
259
|
Effect.tapCauseLogPretty,
|
|
216
260
|
Effect.forkScoped,
|
|
217
261
|
)
|
|
@@ -228,6 +272,21 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
228
272
|
}),
|
|
229
273
|
changes: Stream.fromQueue(syncStateUpdateQueue),
|
|
230
274
|
}),
|
|
275
|
+
debug: {
|
|
276
|
+
print: () =>
|
|
277
|
+
Effect.gen(function* () {
|
|
278
|
+
console.log('debugInfo', debugInfo)
|
|
279
|
+
console.log('syncState', syncStateRef.current)
|
|
280
|
+
const pushQueueSize = yield* BucketQueue.size(leaderPushQueue)
|
|
281
|
+
console.log('pushQueueSize', pushQueueSize)
|
|
282
|
+
const pushQueueItems = yield* BucketQueue.peekAll(leaderPushQueue)
|
|
283
|
+
console.log(
|
|
284
|
+
'pushQueueItems',
|
|
285
|
+
pushQueueItems.map((_) => _.toJSON()),
|
|
286
|
+
)
|
|
287
|
+
}).pipe(Effect.provide(runtime), Effect.runSync),
|
|
288
|
+
debugInfo: () => debugInfo,
|
|
289
|
+
},
|
|
231
290
|
} satisfies ClientSessionSyncProcessor
|
|
232
291
|
}
|
|
233
292
|
|
|
@@ -239,6 +298,12 @@ export interface ClientSessionSyncProcessor {
|
|
|
239
298
|
writeTables: Set<string>
|
|
240
299
|
}
|
|
241
300
|
boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
|
|
242
|
-
|
|
243
301
|
syncState: Subscribable.Subscribable<SyncState.SyncState>
|
|
302
|
+
debug: {
|
|
303
|
+
print: () => void
|
|
304
|
+
debugInfo: () => {
|
|
305
|
+
rebaseCount: number
|
|
306
|
+
advanceCount: number
|
|
307
|
+
}
|
|
308
|
+
}
|
|
244
309
|
}
|
package/src/sync/sync.ts
CHANGED
|
@@ -67,13 +67,16 @@ export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('In
|
|
|
67
67
|
minimumExpectedId: Schema.Number,
|
|
68
68
|
providedId: Schema.Number,
|
|
69
69
|
}),
|
|
70
|
-
Schema.TaggedStruct('LeaderAhead', {
|
|
71
|
-
minimumExpectedId: EventId.EventId,
|
|
72
|
-
providedId: EventId.EventId,
|
|
73
|
-
}),
|
|
74
70
|
),
|
|
75
71
|
}) {}
|
|
76
72
|
|
|
77
73
|
export class InvalidPullError extends Schema.TaggedError<InvalidPullError>()('InvalidPullError', {
|
|
78
74
|
message: Schema.String,
|
|
79
75
|
}) {}
|
|
76
|
+
|
|
77
|
+
export class LeaderAheadError extends Schema.TaggedError<LeaderAheadError>()('LeaderAheadError', {
|
|
78
|
+
minimumExpectedId: EventId.EventId,
|
|
79
|
+
providedId: EventId.EventId,
|
|
80
|
+
/** Generation number the client session should use for subsequent pushes */
|
|
81
|
+
// nextGeneration: Schema.Number,
|
|
82
|
+
}) {}
|
|
@@ -17,7 +17,7 @@ class TestEvent extends MutationEvent.EncodedWithMeta {
|
|
|
17
17
|
parentId: EventId.make(parentId),
|
|
18
18
|
mutation: 'a',
|
|
19
19
|
args: payload,
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
clientId: 'static-local-id',
|
|
22
22
|
sessionId: undefined,
|
|
23
23
|
})
|
|
@@ -87,7 +87,7 @@ describe('syncstate', () => {
|
|
|
87
87
|
}
|
|
88
88
|
expect(result.newSyncState.upstreamHead).toMatchObject(e_0_1_e_1_1.id)
|
|
89
89
|
expect(result.newSyncState.localHead).toMatchObject(e_1_0_e_2_0.id)
|
|
90
|
-
expectEventArraysEqual(result.newEvents, [e_0_0_e_1_0, e_0_1_e_1_1])
|
|
90
|
+
expectEventArraysEqual(result.newEvents, [e_0_0_e_1_0, e_0_1_e_1_1, e_1_0_e_2_0])
|
|
91
91
|
expectEventArraysEqual(result.eventsToRollback, [e_0_0, e_0_1, e_1_0])
|
|
92
92
|
})
|
|
93
93
|
|
|
@@ -118,7 +118,7 @@ describe('syncstate', () => {
|
|
|
118
118
|
}
|
|
119
119
|
expect(result.newSyncState.upstreamHead).toMatchObject(e_0_1_e_1_0.id)
|
|
120
120
|
expect(result.newSyncState.localHead).toMatchObject(e_1_0_e_2_0.id)
|
|
121
|
-
expectEventArraysEqual(result.newEvents, [e_0_1_e_1_0])
|
|
121
|
+
expectEventArraysEqual(result.newEvents, [e_0_1_e_1_0, e_1_0_e_2_0])
|
|
122
122
|
expectEventArraysEqual(result.eventsToRollback, [e_0_1, e_1_0])
|
|
123
123
|
})
|
|
124
124
|
|
|
@@ -148,12 +148,11 @@ describe('syncstate', () => {
|
|
|
148
148
|
upstreamHead: EventId.ROOT,
|
|
149
149
|
localHead: e_0_0.id,
|
|
150
150
|
})
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
).toThrow()
|
|
151
|
+
const result = run({
|
|
152
|
+
syncState,
|
|
153
|
+
payload: { _tag: 'upstream-rebase', rollbackUntil: e_0_0.id, newEvents: [e_1_0] },
|
|
154
|
+
})
|
|
155
|
+
expect(result).toMatchObject({ _tag: 'unexpected-error' })
|
|
157
156
|
})
|
|
158
157
|
|
|
159
158
|
it('should work for empty incoming', () => {
|
|
@@ -185,7 +184,8 @@ describe('syncstate', () => {
|
|
|
185
184
|
upstreamHead: EventId.ROOT,
|
|
186
185
|
localHead: e_0_0.id,
|
|
187
186
|
})
|
|
188
|
-
|
|
187
|
+
const result = run({ syncState, payload: { _tag: 'upstream-advance', newEvents: [e_0_1, e_0_0] } })
|
|
188
|
+
expect(result).toMatchObject({ _tag: 'unexpected-error' })
|
|
189
189
|
})
|
|
190
190
|
|
|
191
191
|
it('should throw error if newEvents are not sorted in ascending order by eventId (global)', () => {
|
|
@@ -195,7 +195,8 @@ describe('syncstate', () => {
|
|
|
195
195
|
upstreamHead: EventId.ROOT,
|
|
196
196
|
localHead: e_0_0.id,
|
|
197
197
|
})
|
|
198
|
-
|
|
198
|
+
const result = run({ syncState, payload: { _tag: 'upstream-advance', newEvents: [e_1_0, e_0_0] } })
|
|
199
|
+
expect(result).toMatchObject({ _tag: 'unexpected-error' })
|
|
199
200
|
})
|
|
200
201
|
|
|
201
202
|
it('should acknowledge pending event when receiving matching event', () => {
|
|
@@ -329,6 +330,17 @@ describe('syncstate', () => {
|
|
|
329
330
|
expect(result.newSyncState.localHead).toMatchObject(e_1_0.id)
|
|
330
331
|
expect(result.newEvents).toStrictEqual([e_1_0])
|
|
331
332
|
})
|
|
333
|
+
|
|
334
|
+
it('should fail if incoming event is ≤ local head', () => {
|
|
335
|
+
const syncState = new SyncState.SyncState({
|
|
336
|
+
pending: [],
|
|
337
|
+
rollbackTail: [],
|
|
338
|
+
upstreamHead: e_1_0.id,
|
|
339
|
+
localHead: e_1_0.id,
|
|
340
|
+
})
|
|
341
|
+
const result = run({ syncState, payload: { _tag: 'upstream-advance', newEvents: [e_0_0] } })
|
|
342
|
+
expect(result).toMatchObject({ _tag: 'unexpected-error' })
|
|
343
|
+
})
|
|
332
344
|
})
|
|
333
345
|
|
|
334
346
|
describe('upstream-advance: rebase', () => {
|
|
@@ -513,14 +525,20 @@ const expectEventArraysEqual = (
|
|
|
513
525
|
})
|
|
514
526
|
}
|
|
515
527
|
|
|
516
|
-
function expectAdvance(
|
|
528
|
+
function expectAdvance(
|
|
529
|
+
result: typeof SyncState.UpdateResult.Type,
|
|
530
|
+
): asserts result is typeof SyncState.UpdateResultAdvance.Type {
|
|
517
531
|
expect(result._tag).toBe('advance')
|
|
518
532
|
}
|
|
519
533
|
|
|
520
|
-
function expectRebase(
|
|
534
|
+
function expectRebase(
|
|
535
|
+
result: typeof SyncState.UpdateResult.Type,
|
|
536
|
+
): asserts result is typeof SyncState.UpdateResultRebase.Type {
|
|
521
537
|
expect(result._tag).toBe('rebase')
|
|
522
538
|
}
|
|
523
539
|
|
|
524
|
-
function expectReject(
|
|
540
|
+
function expectReject(
|
|
541
|
+
result: typeof SyncState.UpdateResult.Type,
|
|
542
|
+
): asserts result is typeof SyncState.UpdateResultReject.Type {
|
|
525
543
|
expect(result._tag).toBe('reject')
|
|
526
544
|
}
|