@livestore/common 0.3.0-dev.26 → 0.3.0-dev.27

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.
Files changed (103) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +13 -12
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js +5 -6
  5. package/dist/adapter-types.js.map +1 -1
  6. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  7. package/dist/devtools/devtools-messages-common.d.ts +13 -6
  8. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  9. package/dist/devtools/devtools-messages-common.js +6 -0
  10. package/dist/devtools/devtools-messages-common.js.map +1 -1
  11. package/dist/devtools/devtools-messages-leader.d.ts +25 -25
  12. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  13. package/dist/devtools/devtools-messages-leader.js +1 -2
  14. package/dist/devtools/devtools-messages-leader.js.map +1 -1
  15. package/dist/leader-thread/LeaderSyncProcessor.d.ts +15 -6
  16. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  17. package/dist/leader-thread/LeaderSyncProcessor.js +211 -189
  18. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  19. package/dist/leader-thread/apply-mutation.d.ts +14 -9
  20. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  21. package/dist/leader-thread/apply-mutation.js +43 -36
  22. package/dist/leader-thread/apply-mutation.js.map +1 -1
  23. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  24. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  25. package/dist/leader-thread/leader-worker-devtools.js +4 -5
  26. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  27. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  28. package/dist/leader-thread/make-leader-thread-layer.js +21 -33
  29. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  30. package/dist/leader-thread/mod.d.ts +1 -1
  31. package/dist/leader-thread/mod.d.ts.map +1 -1
  32. package/dist/leader-thread/mod.js +1 -1
  33. package/dist/leader-thread/mod.js.map +1 -1
  34. package/dist/leader-thread/mutationlog.d.ts +19 -3
  35. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  36. package/dist/leader-thread/mutationlog.js +105 -12
  37. package/dist/leader-thread/mutationlog.js.map +1 -1
  38. package/dist/leader-thread/pull-queue-set.d.ts +1 -1
  39. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  40. package/dist/leader-thread/pull-queue-set.js +6 -16
  41. package/dist/leader-thread/pull-queue-set.js.map +1 -1
  42. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  43. package/dist/leader-thread/recreate-db.js +4 -3
  44. package/dist/leader-thread/recreate-db.js.map +1 -1
  45. package/dist/leader-thread/types.d.ts +34 -19
  46. package/dist/leader-thread/types.d.ts.map +1 -1
  47. package/dist/leader-thread/types.js.map +1 -1
  48. package/dist/rehydrate-from-mutationlog.d.ts +5 -4
  49. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  50. package/dist/rehydrate-from-mutationlog.js +7 -9
  51. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  52. package/dist/schema/EventId.d.ts +9 -0
  53. package/dist/schema/EventId.d.ts.map +1 -1
  54. package/dist/schema/EventId.js +17 -2
  55. package/dist/schema/EventId.js.map +1 -1
  56. package/dist/schema/MutationEvent.d.ts +78 -25
  57. package/dist/schema/MutationEvent.d.ts.map +1 -1
  58. package/dist/schema/MutationEvent.js +25 -12
  59. package/dist/schema/MutationEvent.js.map +1 -1
  60. package/dist/schema/schema.js +1 -1
  61. package/dist/schema/schema.js.map +1 -1
  62. package/dist/schema/system-tables.d.ts +67 -0
  63. package/dist/schema/system-tables.d.ts.map +1 -1
  64. package/dist/schema/system-tables.js +12 -1
  65. package/dist/schema/system-tables.js.map +1 -1
  66. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -1
  67. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  68. package/dist/sync/ClientSessionSyncProcessor.js +25 -19
  69. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  70. package/dist/sync/sync.d.ts +6 -5
  71. package/dist/sync/sync.d.ts.map +1 -1
  72. package/dist/sync/sync.js.map +1 -1
  73. package/dist/sync/syncstate.d.ts +47 -71
  74. package/dist/sync/syncstate.d.ts.map +1 -1
  75. package/dist/sync/syncstate.js +118 -127
  76. package/dist/sync/syncstate.js.map +1 -1
  77. package/dist/sync/syncstate.test.js +204 -275
  78. package/dist/sync/syncstate.test.js.map +1 -1
  79. package/dist/version.d.ts +1 -1
  80. package/dist/version.js +1 -1
  81. package/package.json +2 -2
  82. package/src/adapter-types.ts +11 -13
  83. package/src/devtools/devtools-messages-common.ts +9 -0
  84. package/src/devtools/devtools-messages-leader.ts +1 -2
  85. package/src/leader-thread/LeaderSyncProcessor.ts +381 -346
  86. package/src/leader-thread/apply-mutation.ts +81 -71
  87. package/src/leader-thread/leader-worker-devtools.ts +5 -7
  88. package/src/leader-thread/make-leader-thread-layer.ts +26 -41
  89. package/src/leader-thread/mod.ts +1 -1
  90. package/src/leader-thread/mutationlog.ts +166 -13
  91. package/src/leader-thread/recreate-db.ts +4 -3
  92. package/src/leader-thread/types.ts +33 -23
  93. package/src/rehydrate-from-mutationlog.ts +12 -12
  94. package/src/schema/EventId.ts +20 -2
  95. package/src/schema/MutationEvent.ts +32 -16
  96. package/src/schema/schema.ts +1 -1
  97. package/src/schema/system-tables.ts +20 -1
  98. package/src/sync/ClientSessionSyncProcessor.ts +35 -23
  99. package/src/sync/sync.ts +6 -9
  100. package/src/sync/syncstate.test.ts +230 -306
  101. package/src/sync/syncstate.ts +176 -171
  102. package/src/version.ts +1 -1
  103. package/src/leader-thread/pull-queue-set.ts +0 -67
@@ -9,75 +9,65 @@ import * as MutationEvent from '../schema/MutationEvent.js'
9
9
  * SyncState represents the current sync state of a sync node relative to an upstream node.
10
10
  * Events flow from local to upstream, with each state maintaining its own event head.
11
11
  *
12
- * Event Chain Structure:
12
+ * Example:
13
13
  * ```
14
- * +-------------------------+------------------------+
15
- * | ROLLBACK TAIL | PENDING EVENTS |
16
- * +-------------------------+------------------------+
17
- * ▼ ▼
18
- * Upstream Head Local Head
19
- * Example: (0,0), (0,1), (1,0) (1,1), (1,2), (2,0)
14
+ * +------------------------+
15
+ * | PENDING EVENTS |
16
+ * +------------------------+
17
+ * ▼ ▼
18
+ * Upstream Head Local Head
19
+ * (1,0) (1,1), (1,2), (2,0)
20
20
  * ```
21
21
  *
22
- * State:
23
- * - **Pending Events**: Events awaiting acknowledgment from the upstream.
24
- * - Can be confirmed or rejected by the upstream.
25
- * - Subject to rebase if rejected.
26
- * - **Rollback Tail**: Events that are kept around temporarily for potential rollback until confirmed by upstream.
27
- * - Currently only needed for ClientSessionSyncProcessor.
28
- * - Note: Confirmation of an event is stronger than acknowledgment of an event and can only be done by the
29
- * absolute authority in the sync hierarchy (i.e. the sync backend in our case).
22
+ * **Pending Events**: Events awaiting acknowledgment from the upstream.
23
+ * - Can be confirmed or rejected by the upstream.
24
+ * - Subject to rebase if rejected.
30
25
  *
31
26
  * Payloads:
32
27
  * - `PayloadUpstreamRebase`: Upstream has performed a rebase, so downstream must roll back to the specified event
33
28
  * and rebase the pending events on top of the new events.
34
29
  * - `PayloadUpstreamAdvance`: Upstream has advanced, so downstream must rebase the pending events on top of the new events.
35
- * - `PayloadUpstreamTrimRollbackTail`: Upstream has advanced, so downstream can trim the rollback tail.
36
30
  * - `PayloadLocalPush`: Local push payload
37
31
  *
38
32
  * Invariants:
39
33
  * 1. **Chain Continuity**: Each event must reference its immediate parent.
40
34
  * 2. **Head Ordering**: Upstream Head ≤ Local Head.
41
- * 3. **ID Sequence**: Must follow the pattern (1,0)→(1,1)→(1,2)→(2,0).
35
+ * 3. **Event number sequence**: Must follow the pattern (1,0)→(1,1)→(1,2)→(2,0).
42
36
  *
43
37
  * A few further notes to help form an intuition:
44
38
  * - The goal is to keep the pending events as small as possible (i.e. to have synced with the next upstream node)
45
39
  * - There are 2 cases for rebasing:
46
40
  * - The conflicting event only conflicts with the pending events -> only (some of) the pending events need to be rolled back
47
- * - The conflicting event conflicts even with the rollback tail (additionally to the pending events) -> events from both need to be rolled back
48
41
  *
49
42
  * The `merge` function processes updates to the sync state based on incoming payloads,
50
- * handling cases such as upstream rebase, advance, local push, and rollback tail trimming.
43
+ * handling cases such as upstream rebase, advance and local push.
51
44
  */
52
45
  export class SyncState extends Schema.Class<SyncState>('SyncState')({
53
46
  pending: Schema.Array(MutationEvent.EncodedWithMeta),
54
- rollbackTail: Schema.Array(MutationEvent.EncodedWithMeta),
55
47
  /** What this node expects the next upstream node to have as its own local head */
56
48
  upstreamHead: EventId.EventId,
49
+ /** Equivalent to `pending.at(-1)?.id` if there are pending events */
57
50
  localHead: EventId.EventId,
58
51
  }) {
59
- toJSON = (): any => {
60
- return {
61
- pending: this.pending.map((e) => e.toJSON()),
62
- rollbackTail: this.rollbackTail.map((e) => e.toJSON()),
63
- upstreamHead: `(${this.upstreamHead.global},${this.upstreamHead.client})`,
64
- localHead: `(${this.localHead.global},${this.localHead.client})`,
65
- }
66
- }
52
+ toJSON = (): any => ({
53
+ pending: this.pending.map((e) => e.toJSON()),
54
+ upstreamHead: EventId.toString(this.upstreamHead),
55
+ localHead: EventId.toString(this.localHead),
56
+ })
67
57
  }
68
58
 
59
+ /**
60
+ * This payload propagates a rebase from the upstream node
61
+ */
69
62
  export class PayloadUpstreamRebase extends Schema.TaggedStruct('upstream-rebase', {
70
- /** Rollback until this event in the rollback tail (inclusive). Starting from the end of the rollback tail. */
71
- rollbackUntil: EventId.EventId,
63
+ /** Events which need to be rolled back */
64
+ rollbackEvents: Schema.Array(MutationEvent.EncodedWithMeta),
65
+ /** Events which need to be applied after the rollback (already rebased by the upstream node) */
72
66
  newEvents: Schema.Array(MutationEvent.EncodedWithMeta),
73
- /** Trim rollback tail up to this event (inclusive). */
74
- trimRollbackUntil: Schema.optional(EventId.EventId),
75
67
  }) {}
76
68
 
77
69
  export class PayloadUpstreamAdvance extends Schema.TaggedStruct('upstream-advance', {
78
70
  newEvents: Schema.Array(MutationEvent.EncodedWithMeta),
79
- /** Trim rollback tail up to this event (inclusive). */
80
- trimRollbackUntil: Schema.optional(EventId.EventId),
81
71
  }) {}
82
72
 
83
73
  export class PayloadLocalPush extends Schema.TaggedStruct('local-push', {
@@ -86,12 +76,10 @@ export class PayloadLocalPush extends Schema.TaggedStruct('local-push', {
86
76
 
87
77
  export class Payload extends Schema.Union(PayloadUpstreamRebase, PayloadUpstreamAdvance, PayloadLocalPush) {}
88
78
 
89
- export const PayloadUpstream = Schema.Union(PayloadUpstreamRebase, PayloadUpstreamAdvance)
90
-
91
- export type PayloadUpstream = typeof PayloadUpstream.Type
79
+ export class PayloadUpstream extends Schema.Union(PayloadUpstreamRebase, PayloadUpstreamAdvance) {}
92
80
 
93
81
  /** Only used for debugging purposes */
94
- export class UpdateContext extends Schema.Class<UpdateContext>('UpdateContext')({
82
+ export class MergeContext extends Schema.Class<MergeContext>('MergeContext')({
95
83
  payload: Payload,
96
84
  syncState: SyncState,
97
85
  }) {
@@ -105,9 +93,10 @@ export class UpdateContext extends Schema.Class<UpdateContext>('UpdateContext')(
105
93
  _tag: 'upstream-advance',
106
94
  newEvents: this.payload.newEvents.map((e) => e.toJSON()),
107
95
  })),
108
- Match.tag('upstream-rebase', () => ({
96
+ Match.tag('upstream-rebase', (payload) => ({
109
97
  _tag: 'upstream-rebase',
110
- newEvents: this.payload.newEvents.map((e) => e.toJSON()),
98
+ newEvents: payload.newEvents.map((e) => e.toJSON()),
99
+ rollbackEvents: payload.rollbackEvents.map((e) => e.toJSON()),
111
100
  })),
112
101
  Match.exhaustive,
113
102
  )
@@ -121,16 +110,18 @@ export class UpdateContext extends Schema.Class<UpdateContext>('UpdateContext')(
121
110
  export class MergeResultAdvance extends Schema.Class<MergeResultAdvance>('MergeResultAdvance')({
122
111
  _tag: Schema.Literal('advance'),
123
112
  newSyncState: SyncState,
124
- /** Events which weren't pending before the update */
125
113
  newEvents: Schema.Array(MutationEvent.EncodedWithMeta),
126
- updateContext: UpdateContext,
114
+ /** Events which were previously pending but are now confirmed */
115
+ confirmedEvents: Schema.Array(MutationEvent.EncodedWithMeta),
116
+ mergeContext: MergeContext,
127
117
  }) {
128
118
  toJSON = (): any => {
129
119
  return {
130
120
  _tag: this._tag,
131
121
  newSyncState: this.newSyncState.toJSON(),
132
122
  newEvents: this.newEvents.map((e) => e.toJSON()),
133
- updateContext: this.updateContext.toJSON(),
123
+ confirmedEvents: this.confirmedEvents.map((e) => e.toJSON()),
124
+ mergeContext: this.mergeContext.toJSON(),
134
125
  }
135
126
  }
136
127
  }
@@ -138,18 +129,18 @@ export class MergeResultAdvance extends Schema.Class<MergeResultAdvance>('MergeR
138
129
  export class MergeResultRebase extends Schema.Class<MergeResultRebase>('MergeResultRebase')({
139
130
  _tag: Schema.Literal('rebase'),
140
131
  newSyncState: SyncState,
141
- /** Events which weren't pending before the update */
142
132
  newEvents: Schema.Array(MutationEvent.EncodedWithMeta),
143
- eventsToRollback: Schema.Array(MutationEvent.EncodedWithMeta),
144
- updateContext: UpdateContext,
133
+ /** Events which need to be rolled back */
134
+ rollbackEvents: Schema.Array(MutationEvent.EncodedWithMeta),
135
+ mergeContext: MergeContext,
145
136
  }) {
146
137
  toJSON = (): any => {
147
138
  return {
148
139
  _tag: this._tag,
149
140
  newSyncState: this.newSyncState.toJSON(),
150
141
  newEvents: this.newEvents.map((e) => e.toJSON()),
151
- eventsToRollback: this.eventsToRollback.map((e) => e.toJSON()),
152
- updateContext: this.updateContext.toJSON(),
142
+ rollbackEvents: this.rollbackEvents.map((e) => e.toJSON()),
143
+ mergeContext: this.mergeContext.toJSON(),
153
144
  }
154
145
  }
155
146
  }
@@ -158,13 +149,13 @@ export class MergeResultReject extends Schema.Class<MergeResultReject>('MergeRes
158
149
  _tag: Schema.Literal('reject'),
159
150
  /** The minimum id that the new events must have */
160
151
  expectedMinimumId: EventId.EventId,
161
- updateContext: UpdateContext,
152
+ mergeContext: MergeContext,
162
153
  }) {
163
154
  toJSON = (): any => {
164
155
  return {
165
156
  _tag: this._tag,
166
- expectedMinimumId: `(${this.expectedMinimumId.global},${this.expectedMinimumId.client})`,
167
- updateContext: this.updateContext.toJSON(),
157
+ expectedMinimumId: EventId.toString(this.expectedMinimumId),
158
+ mergeContext: this.mergeContext.toJSON(),
168
159
  }
169
160
  }
170
161
  }
@@ -187,6 +178,13 @@ const unexpectedError = (cause: unknown): MergeResultUnexpectedError =>
187
178
  cause: new UnexpectedError({ cause }),
188
179
  })
189
180
 
181
+ // TODO Idea: call merge recursively through hierarchy levels
182
+ /*
183
+ Idea: have a map that maps from `globalEventId` to Array<ClientEvents>
184
+ The same applies to even further hierarchy levels
185
+
186
+ TODO: possibly even keep the client events in a separate table in the client leader
187
+ */
190
188
  export const merge = ({
191
189
  syncState,
192
190
  payload,
@@ -203,31 +201,11 @@ export const merge = ({
203
201
  }): typeof MergeResult.Type => {
204
202
  validateSyncState(syncState)
205
203
 
206
- const trimRollbackTail = (
207
- rollbackTail: ReadonlyArray<MutationEvent.EncodedWithMeta>,
208
- ): ReadonlyArray<MutationEvent.EncodedWithMeta> => {
209
- const trimRollbackUntil = payload._tag === 'local-push' ? undefined : payload.trimRollbackUntil
210
- if (trimRollbackUntil === undefined) return rollbackTail
211
- const index = rollbackTail.findIndex((event) => EventId.isEqual(event.id, trimRollbackUntil))
212
- if (index === -1) return []
213
- return rollbackTail.slice(index + 1)
214
- }
215
-
216
- const updateContext = UpdateContext.make({ payload, syncState })
204
+ const mergeContext = MergeContext.make({ payload, syncState })
217
205
 
218
206
  switch (payload._tag) {
219
207
  case 'upstream-rebase': {
220
- // Find the index of the rollback event in the rollback tail
221
- const rollbackIndex = syncState.rollbackTail.findIndex((event) =>
222
- EventId.isEqual(event.id, payload.rollbackUntil),
223
- )
224
- if (rollbackIndex === -1) {
225
- return unexpectedError(
226
- `Rollback event not found in rollback tail. Rollback until: [${payload.rollbackUntil.global},${payload.rollbackUntil.client}]. Rollback tail: [${syncState.rollbackTail.map((e) => e.toString()).join(', ')}]`,
227
- )
228
- }
229
-
230
- const eventsToRollback = [...syncState.rollbackTail.slice(rollbackIndex), ...syncState.pending]
208
+ const rollbackEvents = [...payload.rollbackEvents, ...syncState.pending]
231
209
 
232
210
  // Get the last new event's ID as the new upstream head
233
211
  const newUpstreamHead = payload.newEvents.at(-1)?.id ?? syncState.upstreamHead
@@ -239,41 +217,44 @@ export const merge = ({
239
217
  isClientEvent,
240
218
  })
241
219
 
242
- return MergeResultRebase.make({
243
- _tag: 'rebase',
244
- newSyncState: new SyncState({
245
- pending: rebasedPending,
246
- rollbackTail: trimRollbackTail([...syncState.rollbackTail.slice(0, rollbackIndex), ...payload.newEvents]),
247
- upstreamHead: newUpstreamHead,
248
- localHead: rebasedPending.at(-1)?.id ?? newUpstreamHead,
220
+ return validateMergeResult(
221
+ MergeResultRebase.make({
222
+ _tag: 'rebase',
223
+ newSyncState: new SyncState({
224
+ pending: rebasedPending,
225
+ upstreamHead: newUpstreamHead,
226
+ localHead: rebasedPending.at(-1)?.id ?? newUpstreamHead,
227
+ }),
228
+ newEvents: [...payload.newEvents, ...rebasedPending],
229
+ rollbackEvents,
230
+ mergeContext,
249
231
  }),
250
- newEvents: [...payload.newEvents, ...rebasedPending],
251
- eventsToRollback,
252
- updateContext,
253
- })
232
+ )
254
233
  }
255
234
 
256
235
  // #region upstream-advance
257
236
  case 'upstream-advance': {
258
237
  if (payload.newEvents.length === 0) {
259
- return MergeResultAdvance.make({
260
- _tag: 'advance',
261
- newSyncState: new SyncState({
262
- pending: syncState.pending,
263
- rollbackTail: trimRollbackTail(syncState.rollbackTail),
264
- upstreamHead: syncState.upstreamHead,
265
- localHead: syncState.localHead,
238
+ return validateMergeResult(
239
+ MergeResultAdvance.make({
240
+ _tag: 'advance',
241
+ newSyncState: new SyncState({
242
+ pending: syncState.pending,
243
+ upstreamHead: syncState.upstreamHead,
244
+ localHead: syncState.localHead,
245
+ }),
246
+ newEvents: [],
247
+ confirmedEvents: [],
248
+ mergeContext: mergeContext,
266
249
  }),
267
- newEvents: [],
268
- updateContext,
269
- })
250
+ )
270
251
  }
271
252
 
272
253
  // Validate that newEvents are sorted in ascending order by eventId
273
254
  for (let i = 1; i < payload.newEvents.length; i++) {
274
255
  if (EventId.isGreaterThan(payload.newEvents[i - 1]!.id, payload.newEvents[i]!.id)) {
275
256
  return unexpectedError(
276
- `Events must be sorted in ascending order by eventId. Received: [${payload.newEvents.map((e) => `(${e.id.global},${e.id.client})`).join(', ')}]`,
257
+ `Events must be sorted in ascending order by eventId. Received: [${payload.newEvents.map((e) => EventId.toString(e.id)).join(', ')}]`,
277
258
  )
278
259
  }
279
260
  }
@@ -284,18 +265,18 @@ export const merge = ({
284
265
  EventId.isEqual(syncState.upstreamHead, payload.newEvents[0]!.id)
285
266
  ) {
286
267
  return unexpectedError(
287
- `Incoming events must be greater than upstream head. Expected greater than: (${syncState.upstreamHead.global},${syncState.upstreamHead.client}). Received: [${payload.newEvents.map((e) => `(${e.id.global},${e.id.client})`).join(', ')}]`,
268
+ `Incoming events must be greater than upstream head. Expected greater than: ${EventId.toString(syncState.upstreamHead)}. Received: [${payload.newEvents.map((e) => EventId.toString(e.id)).join(', ')}]`,
288
269
  )
289
270
  }
290
271
 
291
272
  // Validate that the parent id of the first incoming event is known
292
- const knownEventGlobalIds = [...syncState.rollbackTail, ...syncState.pending].map((e) => e.id.global)
273
+ const knownEventGlobalIds = [...syncState.pending].flatMap((e) => [e.id.global, e.parentId.global])
293
274
  knownEventGlobalIds.push(syncState.upstreamHead.global)
294
275
  const firstNewEvent = payload.newEvents[0]!
295
276
  const hasUnknownParentId = knownEventGlobalIds.includes(firstNewEvent.parentId.global) === false
296
277
  if (hasUnknownParentId) {
297
278
  return unexpectedError(
298
- `Incoming events must have a known parent id. Received: [${payload.newEvents.map((e) => `(${e.id.global},${e.id.client})`).join(', ')}]`,
279
+ `Incoming events must have a known parent id. Received: [${payload.newEvents.map((e) => EventId.toString(e.id)).join(', ')}]`,
299
280
  )
300
281
  }
301
282
 
@@ -336,27 +317,19 @@ export const merge = ({
336
317
  },
337
318
  )
338
319
 
339
- const seenEventIds = new Set<string>()
340
- const pendingAndNewEvents = [...pendingMatching, ...payload.newEvents].filter((event) => {
341
- const eventIdStr = `${event.id.global},${event.id.client}`
342
- if (seenEventIds.has(eventIdStr)) {
343
- return false
344
- }
345
- seenEventIds.add(eventIdStr)
346
- return true
347
- })
348
-
349
- return MergeResultAdvance.make({
350
- _tag: 'advance',
351
- newSyncState: new SyncState({
352
- pending: pendingRemaining,
353
- rollbackTail: trimRollbackTail([...syncState.rollbackTail, ...pendingAndNewEvents]),
354
- upstreamHead: newUpstreamHead,
355
- localHead: pendingRemaining.at(-1)?.id ?? newUpstreamHead,
320
+ return validateMergeResult(
321
+ MergeResultAdvance.make({
322
+ _tag: 'advance',
323
+ newSyncState: new SyncState({
324
+ pending: pendingRemaining,
325
+ upstreamHead: newUpstreamHead,
326
+ localHead: pendingRemaining.at(-1)?.id ?? EventId.max(syncState.localHead, newUpstreamHead),
327
+ }),
328
+ newEvents,
329
+ confirmedEvents: pendingMatching,
330
+ mergeContext: mergeContext,
356
331
  }),
357
- newEvents,
358
- updateContext,
359
- })
332
+ )
360
333
  } else {
361
334
  const divergentPending = syncState.pending.slice(divergentPendingIndex)
362
335
  const rebasedPending = rebaseEvents({
@@ -373,30 +346,35 @@ export const merge = ({
373
346
  ignoreClientEvents,
374
347
  })
375
348
 
376
- return MergeResultRebase.make({
377
- _tag: 'rebase',
378
- newSyncState: new SyncState({
379
- pending: rebasedPending,
380
- rollbackTail: trimRollbackTail([...syncState.rollbackTail, ...payload.newEvents]),
381
- upstreamHead: newUpstreamHead,
382
- localHead: rebasedPending.at(-1)!.id,
349
+ return validateMergeResult(
350
+ MergeResultRebase.make({
351
+ _tag: 'rebase',
352
+ newSyncState: new SyncState({
353
+ pending: rebasedPending,
354
+ upstreamHead: newUpstreamHead,
355
+ localHead: rebasedPending.at(-1)!.id,
356
+ }),
357
+ newEvents: [...payload.newEvents.slice(divergentNewEventsIndex), ...rebasedPending],
358
+ rollbackEvents: divergentPending,
359
+ mergeContext,
383
360
  }),
384
- newEvents: [...payload.newEvents.slice(divergentNewEventsIndex), ...rebasedPending],
385
- eventsToRollback: [...syncState.rollbackTail, ...divergentPending],
386
- updateContext,
387
- })
361
+ )
388
362
  }
389
363
  }
390
364
  // #endregion
391
365
 
366
+ // This is the same as what's running in the sync backend
392
367
  case 'local-push': {
393
368
  if (payload.newEvents.length === 0) {
394
- return MergeResultAdvance.make({
395
- _tag: 'advance',
396
- newSyncState: syncState,
397
- newEvents: [],
398
- updateContext,
399
- })
369
+ return validateMergeResult(
370
+ MergeResultAdvance.make({
371
+ _tag: 'advance',
372
+ newSyncState: syncState,
373
+ newEvents: [],
374
+ confirmedEvents: [],
375
+ mergeContext: mergeContext,
376
+ }),
377
+ )
400
378
  }
401
379
 
402
380
  const newEventsFirst = payload.newEvents.at(0)!
@@ -404,23 +382,27 @@ export const merge = ({
404
382
 
405
383
  if (invalidEventId) {
406
384
  const expectedMinimumId = EventId.nextPair(syncState.localHead, true).id
407
- return MergeResultReject.make({
408
- _tag: 'reject',
409
- expectedMinimumId,
410
- updateContext,
411
- })
385
+ return validateMergeResult(
386
+ MergeResultReject.make({
387
+ _tag: 'reject',
388
+ expectedMinimumId,
389
+ mergeContext,
390
+ }),
391
+ )
412
392
  } else {
413
- return MergeResultAdvance.make({
414
- _tag: 'advance',
415
- newSyncState: new SyncState({
416
- pending: [...syncState.pending, ...payload.newEvents],
417
- rollbackTail: syncState.rollbackTail,
418
- upstreamHead: syncState.upstreamHead,
419
- localHead: payload.newEvents.at(-1)!.id,
393
+ return validateMergeResult(
394
+ MergeResultAdvance.make({
395
+ _tag: 'advance',
396
+ newSyncState: new SyncState({
397
+ pending: [...syncState.pending, ...payload.newEvents],
398
+ upstreamHead: syncState.upstreamHead,
399
+ localHead: payload.newEvents.at(-1)!.id,
400
+ }),
401
+ newEvents: payload.newEvents,
402
+ confirmedEvents: [],
403
+ mergeContext: mergeContext,
420
404
  }),
421
- newEvents: payload.newEvents,
422
- updateContext,
423
- })
405
+ )
424
406
  }
425
407
  }
426
408
 
@@ -499,15 +481,13 @@ const rebaseEvents = ({
499
481
  const _flattenMergeResults = (_updateResults: ReadonlyArray<MergeResult>) => {}
500
482
 
501
483
  const validateSyncState = (syncState: SyncState) => {
502
- // Validate that the rollback tail and pending events together form a continuous chain of events / linked list via the parentId
503
- const chain = [...syncState.rollbackTail, ...syncState.pending]
504
- for (let i = 0; i < chain.length; i++) {
505
- const event = chain[i]!
506
- const nextEvent = chain[i + 1]
484
+ for (let i = 0; i < syncState.pending.length; i++) {
485
+ const event = syncState.pending[i]!
486
+ const nextEvent = syncState.pending[i + 1]
507
487
  if (nextEvent === undefined) break // Reached end of chain
508
488
 
509
489
  if (EventId.isGreaterThan(event.id, nextEvent.id)) {
510
- shouldNeverHappen('Events must be sorted in ascending order by eventId', chain, {
490
+ shouldNeverHappen('Events must be sorted in ascending order by eventId', {
511
491
  event,
512
492
  nextEvent,
513
493
  })
@@ -518,8 +498,8 @@ const validateSyncState = (syncState: SyncState) => {
518
498
  if (globalIdHasIncreased) {
519
499
  if (nextEvent.id.client !== 0) {
520
500
  shouldNeverHappen(
521
- `New global events must point to clientId 0 in the parentId. Received: (${nextEvent.id.global},${nextEvent.id.client})`,
522
- chain,
501
+ `New global events must point to clientId 0 in the parentId. Received: (${EventId.toString(nextEvent.id)})`,
502
+ syncState.pending,
523
503
  {
524
504
  event,
525
505
  nextEvent,
@@ -529,24 +509,49 @@ const validateSyncState = (syncState: SyncState) => {
529
509
  } else {
530
510
  // Otherwise, the parentId must be the same as the previous event's id
531
511
  if (EventId.isEqual(nextEvent.parentId, event.id) === false) {
532
- shouldNeverHappen('Events must be linked in a continuous chain via the parentId', chain, {
512
+ shouldNeverHappen('Events must be linked in a continuous chain via the parentId', syncState.pending, {
533
513
  event,
534
514
  nextEvent,
535
515
  })
536
516
  }
537
517
  }
538
518
  }
519
+ }
520
+
521
+ const validateMergeResult = (mergeResult: typeof MergeResult.Type) => {
522
+ if (mergeResult._tag === 'unexpected-error' || mergeResult._tag === 'reject') return mergeResult
523
+
524
+ // Ensure local head is always greater than or equal to upstream head
525
+ if (EventId.isGreaterThan(mergeResult.newSyncState.upstreamHead, mergeResult.newSyncState.localHead)) {
526
+ shouldNeverHappen('Local head must be greater than or equal to upstream head', {
527
+ localHead: mergeResult.newSyncState.localHead,
528
+ upstreamHead: mergeResult.newSyncState.upstreamHead,
529
+ })
530
+ }
531
+
532
+ // Ensure new local head is greater than or equal to the previous local head
533
+ if (
534
+ EventId.isGreaterThanOrEqual(mergeResult.newSyncState.localHead, mergeResult.mergeContext.syncState.localHead) ===
535
+ false
536
+ ) {
537
+ shouldNeverHappen('New local head must be greater than or equal to the previous local head', {
538
+ localHead: mergeResult.newSyncState.localHead,
539
+ previousLocalHead: mergeResult.mergeContext.syncState.localHead,
540
+ })
541
+ }
542
+
543
+ // Ensure new upstream head is greater than or equal to the previous upstream head
544
+ if (
545
+ EventId.isGreaterThanOrEqual(
546
+ mergeResult.newSyncState.upstreamHead,
547
+ mergeResult.mergeContext.syncState.upstreamHead,
548
+ ) === false
549
+ ) {
550
+ shouldNeverHappen('New upstream head must be greater than or equal to the previous upstream head', {
551
+ upstreamHead: mergeResult.newSyncState.upstreamHead,
552
+ previousUpstreamHead: mergeResult.mergeContext.syncState.upstreamHead,
553
+ })
554
+ }
539
555
 
540
- // TODO double check this
541
- // const globalRollbackTail = syncState.rollbackTail.filter((event) => event.id.client === 0)
542
- // // The parent of the first global rollback tail event ("oldest event") must be the upstream head (if there is a rollback tail)
543
- // if (globalRollbackTail.length > 0) {
544
- // const firstRollbackTailEvent = globalRollbackTail[0]!
545
- // if (EventId.isEqual(firstRollbackTailEvent.parentId, syncState.upstreamHead) === false) {
546
- // shouldNeverHappen('The parent of the first rollback tail event must be the upstream head', chain, {
547
- // event: firstRollbackTailEvent,
548
- // upstreamHead: syncState.upstreamHead,
549
- // })
550
- // }
551
- // }
556
+ return mergeResult
552
557
  }
package/src/version.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // import packageJson from '../package.json' with { type: 'json' }
3
3
  // export const liveStoreVersion = packageJson.version
4
4
 
5
- export const liveStoreVersion = '0.3.0-dev.26' as const
5
+ export const liveStoreVersion = '0.3.0-dev.27' as const
6
6
 
7
7
  /**
8
8
  * This version number is incremented whenever the internal storage format changes in a breaking way.
@@ -1,67 +0,0 @@
1
- import { Effect, Queue } from '@livestore/utils/effect'
2
-
3
- import * as MutationEvent from '../schema/MutationEvent.js'
4
- import { getMutationEventsSince } from './mutationlog.js'
5
- import { LeaderThreadCtx, type PullQueueItem, type PullQueueSet } from './types.js'
6
-
7
- export const makePullQueueSet = Effect.gen(function* () {
8
- const set = new Set<Queue.Queue<PullQueueItem>>()
9
-
10
- yield* Effect.addFinalizer(() =>
11
- Effect.gen(function* () {
12
- for (const queue of set) {
13
- yield* Queue.shutdown(queue)
14
- }
15
-
16
- set.clear()
17
- }),
18
- )
19
-
20
- const makeQueue: PullQueueSet['makeQueue'] = (since) =>
21
- Effect.gen(function* () {
22
- const queue = yield* Queue.unbounded<PullQueueItem>().pipe(Effect.acquireRelease(Queue.shutdown))
23
-
24
- yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
25
-
26
- const mutationEvents = yield* getMutationEventsSince(since)
27
-
28
- if (mutationEvents.length > 0) {
29
- const newEvents = mutationEvents.map((mutationEvent) => new MutationEvent.EncodedWithMeta(mutationEvent))
30
- yield* queue.offer({ payload: { _tag: 'upstream-advance', newEvents }, remaining: 0 })
31
- }
32
-
33
- set.add(queue)
34
-
35
- return queue
36
- })
37
-
38
- const offer: PullQueueSet['offer'] = (item) =>
39
- Effect.gen(function* () {
40
- // Short-circuit if the payload is an empty upstream advance
41
- if (
42
- item.payload._tag === 'upstream-advance' &&
43
- item.payload.newEvents.length === 0 &&
44
- item.payload.trimRollbackUntil === undefined
45
- ) {
46
- return
47
- }
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
-
58
- for (const queue of set) {
59
- yield* Queue.offer(queue, item)
60
- }
61
- })
62
-
63
- return {
64
- makeQueue,
65
- offer,
66
- }
67
- })