@livestore/common 0.3.0-dev.16 → 0.3.0-dev.18

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 (124) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +12 -4
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js +4 -0
  5. package/dist/adapter-types.js.map +1 -1
  6. package/dist/bounded-collections.d.ts +1 -1
  7. package/dist/bounded-collections.d.ts.map +1 -1
  8. package/dist/debug-info.d.ts.map +1 -1
  9. package/dist/derived-mutations.d.ts.map +1 -1
  10. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  11. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  12. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  13. package/dist/devtools/devtools-messages-leader.d.ts +28 -28
  14. package/dist/devtools/index.d.ts.map +1 -1
  15. package/dist/init-singleton-tables.d.ts.map +1 -1
  16. package/dist/leader-thread/LeaderSyncProcessor.d.ts +3 -1
  17. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  18. package/dist/leader-thread/LeaderSyncProcessor.js +130 -50
  19. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  20. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  21. package/dist/leader-thread/apply-mutation.js +11 -5
  22. package/dist/leader-thread/apply-mutation.js.map +1 -1
  23. package/dist/leader-thread/connection.d.ts.map +1 -1
  24. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  25. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  26. package/dist/leader-thread/make-leader-thread-layer.js +1 -0
  27. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  28. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  29. package/dist/leader-thread/pull-queue-set.d.ts +3 -3
  30. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  31. package/dist/leader-thread/pull-queue-set.js +9 -0
  32. package/dist/leader-thread/pull-queue-set.js.map +1 -1
  33. package/dist/leader-thread/shutdown-channel.d.ts +2 -5
  34. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  35. package/dist/leader-thread/shutdown-channel.js +2 -4
  36. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  37. package/dist/leader-thread/types.d.ts +7 -2
  38. package/dist/leader-thread/types.d.ts.map +1 -1
  39. package/dist/mutation.d.ts.map +1 -1
  40. package/dist/otel.d.ts.map +1 -1
  41. package/dist/query-builder/api.d.ts.map +1 -1
  42. package/dist/query-builder/impl.d.ts.map +1 -1
  43. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  44. package/dist/rehydrate-from-mutationlog.js +3 -3
  45. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  46. package/dist/schema/EventId.d.ts +8 -0
  47. package/dist/schema/EventId.d.ts.map +1 -1
  48. package/dist/schema/EventId.js +14 -0
  49. package/dist/schema/EventId.js.map +1 -1
  50. package/dist/schema/MutationEvent.d.ts.map +1 -1
  51. package/dist/schema/MutationEvent.js +3 -3
  52. package/dist/schema/MutationEvent.js.map +1 -1
  53. package/dist/schema/db-schema/ast/sqlite.d.ts.map +1 -1
  54. package/dist/schema/db-schema/ast/validate.d.ts.map +1 -1
  55. package/dist/schema/db-schema/dsl/field-defs.d.ts.map +1 -1
  56. package/dist/schema/db-schema/dsl/field-defs.js.map +1 -1
  57. package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
  58. package/dist/schema/db-schema/dsl/mod.js.map +1 -1
  59. package/dist/schema/db-schema/hash.d.ts.map +1 -1
  60. package/dist/schema/mutations.d.ts +5 -2
  61. package/dist/schema/mutations.d.ts.map +1 -1
  62. package/dist/schema/mutations.js.map +1 -1
  63. package/dist/schema/schema-helpers.d.ts.map +1 -1
  64. package/dist/schema/schema.d.ts +4 -1
  65. package/dist/schema/schema.d.ts.map +1 -1
  66. package/dist/schema/schema.js +19 -8
  67. package/dist/schema/schema.js.map +1 -1
  68. package/dist/schema/table-def.d.ts +1 -8
  69. package/dist/schema/table-def.d.ts.map +1 -1
  70. package/dist/schema-management/common.d.ts.map +1 -1
  71. package/dist/schema-management/migrations.d.ts.map +1 -1
  72. package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
  73. package/dist/schema-management/validate-mutation-defs.js +2 -2
  74. package/dist/schema-management/validate-mutation-defs.js.map +1 -1
  75. package/dist/sql-queries/misc.d.ts.map +1 -1
  76. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  77. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  78. package/dist/sql-queries/types.d.ts.map +1 -1
  79. package/dist/sync/ClientSessionSyncProcessor.d.ts +11 -1
  80. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  81. package/dist/sync/ClientSessionSyncProcessor.js +51 -19
  82. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  83. package/dist/sync/next/compact-events.d.ts.map +1 -1
  84. package/dist/sync/next/facts.d.ts.map +1 -1
  85. package/dist/sync/next/history-dag.d.ts.map +1 -1
  86. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  87. package/dist/sync/next/test/mutation-fixtures.d.ts +7 -7
  88. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  89. package/dist/sync/sync.d.ts +14 -9
  90. package/dist/sync/sync.d.ts.map +1 -1
  91. package/dist/sync/sync.js +7 -3
  92. package/dist/sync/sync.js.map +1 -1
  93. package/dist/sync/syncstate.d.ts +132 -21
  94. package/dist/sync/syncstate.d.ts.map +1 -1
  95. package/dist/sync/syncstate.js +129 -41
  96. package/dist/sync/syncstate.js.map +1 -1
  97. package/dist/sync/syncstate.test.js +19 -7
  98. package/dist/sync/syncstate.test.js.map +1 -1
  99. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  100. package/dist/util.d.ts.map +1 -1
  101. package/dist/version.d.ts +1 -1
  102. package/dist/version.js +1 -1
  103. package/package.json +2 -2
  104. package/src/adapter-types.ts +9 -4
  105. package/src/leader-thread/LeaderSyncProcessor.ts +169 -61
  106. package/src/leader-thread/apply-mutation.ts +21 -5
  107. package/src/leader-thread/make-leader-thread-layer.ts +1 -0
  108. package/src/leader-thread/pull-queue-set.ts +10 -1
  109. package/src/leader-thread/shutdown-channel.ts +2 -4
  110. package/src/leader-thread/types.ts +8 -2
  111. package/src/rehydrate-from-mutationlog.ts +2 -2
  112. package/src/schema/EventId.ts +16 -0
  113. package/src/schema/MutationEvent.ts +3 -3
  114. package/src/schema/db-schema/dsl/field-defs.ts +1 -2
  115. package/src/schema/db-schema/dsl/mod.ts +1 -1
  116. package/src/schema/mutations.ts +4 -1
  117. package/src/schema/schema.ts +20 -8
  118. package/src/schema-management/validate-mutation-defs.ts +2 -2
  119. package/src/sync/ClientSessionSyncProcessor.ts +82 -19
  120. package/src/sync/sync.ts +7 -4
  121. package/src/sync/syncstate.test.ts +32 -14
  122. package/src/sync/syncstate.ts +145 -60
  123. package/src/version.ts +1 -1
  124. package/tmp/pack.tgz +0 -0
@@ -124,7 +124,7 @@ export const makeMutationEventSchema = <TSchema extends LiveStoreSchema>(
124
124
  schema: TSchema,
125
125
  ): ForMutationDefRecord<TSchema['_MutationDefMapType']> =>
126
126
  Schema.Union(
127
- ...[...schema.mutations.values()].map((def) =>
127
+ ...[...schema.mutations.map.values()].map((def) =>
128
128
  Schema.Struct({
129
129
  mutation: Schema.Literal(def.name),
130
130
  args: def.schema,
@@ -140,7 +140,7 @@ export const makeMutationEventPartialSchema = <TSchema extends LiveStoreSchema>(
140
140
  schema: TSchema,
141
141
  ): MutationEventPartialSchema<TSchema['_MutationDefMapType']> =>
142
142
  Schema.Union(
143
- ...[...schema.mutations.values()].map((def) =>
143
+ ...[...schema.mutations.map.values()].map((def) =>
144
144
  Schema.Struct({
145
145
  mutation: Schema.Literal(def.name),
146
146
  args: def.schema,
@@ -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
@@ -30,7 +30,7 @@ export type DbSchemaFromInputSchema<TSchemaInput extends DbSchemaInput> =
30
30
  export const makeDbSchema = <TDbSchemaInput extends DbSchemaInput>(
31
31
  schema: TDbSchemaInput,
32
32
  ): DbSchemaFromInputSchema<TDbSchemaInput> => {
33
- return Array.isArray(schema) ? Object.fromEntries(schema.map((_) => [_.name, _])) : schema
33
+ return Array.isArray(schema) ? Object.fromEntries(schema.map((_) => [_.name, _])) : schema as any
34
34
  }
35
35
 
36
36
  export const table = <TTableName extends string, TColumns extends Columns, TIndexes extends Index[]>(
@@ -2,7 +2,10 @@ import { Schema } from '@livestore/utils/effect'
2
2
 
3
3
  import type { BindValues } from '../sql-queries/sql-queries.js'
4
4
 
5
- export type MutationDefMap = Map<string | 'livestore.RawSql', MutationDef.Any>
5
+ export type MutationDefMap = {
6
+ map: Map<string | 'livestore.RawSql', MutationDef.Any>
7
+ wasProvided: boolean
8
+ }
6
9
  export type MutationDefRecord = {
7
10
  'livestore.RawSql': RawSqlMutation
8
11
  [name: string]: MutationDef.Any
@@ -72,29 +72,32 @@ export const makeSchema = <TInputSchema extends InputSchema>(
72
72
  tables.set(tableDef.sqliteDef.name, tableDef)
73
73
  }
74
74
 
75
- const mutations: MutationDefMap = new Map()
75
+ const mutations: MutationDefMap = {
76
+ map: new Map(),
77
+ wasProvided: inputSchema.mutations !== undefined,
78
+ }
76
79
 
77
80
  if (isReadonlyArray(inputSchema.mutations)) {
78
81
  for (const mutation of inputSchema.mutations) {
79
- mutations.set(mutation.name, mutation)
82
+ mutations.map.set(mutation.name, mutation)
80
83
  }
81
84
  } else {
82
85
  for (const mutation of Object.values(inputSchema.mutations ?? {})) {
83
- if (mutations.has(mutation.name)) {
86
+ if (mutations.map.has(mutation.name)) {
84
87
  shouldNeverHappen(`Duplicate mutation name: ${mutation.name}. Please use unique names for mutations.`)
85
88
  }
86
- mutations.set(mutation.name, mutation)
89
+ mutations.map.set(mutation.name, mutation)
87
90
  }
88
91
  }
89
92
 
90
- mutations.set(rawSqlMutation.name, rawSqlMutation)
93
+ mutations.map.set(rawSqlMutation.name, rawSqlMutation)
91
94
 
92
95
  for (const tableDef of tables.values()) {
93
96
  if (tableHasDerivedMutations(tableDef)) {
94
97
  const derivedMutationDefs = makeDerivedMutationDefsForTable(tableDef)
95
- mutations.set(derivedMutationDefs.insert.name, derivedMutationDefs.insert)
96
- mutations.set(derivedMutationDefs.update.name, derivedMutationDefs.update)
97
- mutations.set(derivedMutationDefs.delete.name, derivedMutationDefs.delete)
98
+ mutations.map.set(derivedMutationDefs.insert.name, derivedMutationDefs.insert)
99
+ mutations.map.set(derivedMutationDefs.update.name, derivedMutationDefs.update)
100
+ mutations.map.set(derivedMutationDefs.delete.name, derivedMutationDefs.delete)
98
101
  }
99
102
  }
100
103
 
@@ -114,6 +117,15 @@ export const makeSchema = <TInputSchema extends InputSchema>(
114
117
  } satisfies LiveStoreSchema
115
118
  }
116
119
 
120
+ export const getMutationDef = <TSchema extends LiveStoreSchema>(schema: TSchema, mutationName: string) => {
121
+ const mutationDef = schema.mutations.map.get(mutationName)
122
+ if (mutationDef === undefined) {
123
+ const extraInfo = schema.mutations.wasProvided ? '' : ' Please provide \`mutations\` in the schema options.'
124
+ return shouldNeverHappen(`No mutation definition found for \`${mutationName}\`.${extraInfo}`)
125
+ }
126
+ return mutationDef
127
+ }
128
+
117
129
  export namespace FromInputSchema {
118
130
  export type DeriveSchema<TInputSchema extends InputSchema> = LiveStoreSchema<
119
131
  DbSchemaFromInputSchemaTables<TInputSchema['tables']>,
@@ -11,7 +11,7 @@ export const validateSchema = (schema: LiveStoreSchema, schemaManager: SchemaMan
11
11
  const registeredMutationDefInfos = schemaManager.getMutationDefInfos()
12
12
 
13
13
  const missingMutationDefs = registeredMutationDefInfos.filter(
14
- (registeredMutationDefInfo) => !schema.mutations.has(registeredMutationDefInfo.mutationName),
14
+ (registeredMutationDefInfo) => !schema.mutations.map.has(registeredMutationDefInfo.mutationName),
15
15
  )
16
16
 
17
17
  if (missingMutationDefs.length > 0) {
@@ -20,7 +20,7 @@ export const validateSchema = (schema: LiveStoreSchema, schemaManager: SchemaMan
20
20
  })
21
21
  }
22
22
 
23
- for (const [, mutationDef] of schema.mutations) {
23
+ for (const [, mutationDef] of schema.mutations.map) {
24
24
  const registeredMutationDefInfo = registeredMutationDefInfos.find(
25
25
  (info) => info.mutationName === mutationDef.name,
26
26
  )
@@ -1,11 +1,11 @@
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'
7
7
  import * as EventId from '../schema/EventId.js'
8
- import { type LiveStoreSchema } from '../schema/mod.js'
8
+ import { getMutationDef, type LiveStoreSchema } from '../schema/mod.js'
9
9
  import * as MutationEvent from '../schema/MutationEvent.js'
10
10
  import * as SyncState from './syncstate.js'
11
11
 
@@ -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
 
@@ -54,17 +58,18 @@ export const makeClientSessionSyncProcessor = ({
54
58
  }
55
59
 
56
60
  const syncStateUpdateQueue = Queue.unbounded<SyncState.SyncState>().pipe(Effect.runSync)
57
- const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) => {
58
- const mutationDef = schema.mutations.get(mutationEventEncoded.mutation)!
59
- return mutationDef.options.clientOnly
60
- }
61
+ const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) =>
62
+ getMutationDef(schema, mutationEventEncoded.mutation).options.clientOnly
63
+
64
+ /** We're queuing push requests to reduce the number of messages sent to the leader by batching them */
65
+ const leaderPushQueue = BucketQueue.make<MutationEvent.EncodedWithMeta>().pipe(Effect.runSync)
61
66
 
62
67
  const push: ClientSessionSyncProcessor['push'] = (batch, { otelContext }) => {
63
68
  // TODO validate batch
64
69
 
65
70
  let baseEventId = syncStateRef.current.localHead
66
71
  const encodedMutationEvents = batch.map((mutationEvent) => {
67
- const mutationDef = schema.mutations.get(mutationEvent.mutation)!
72
+ const mutationDef = getMutationDef(schema, mutationEvent.mutation)
68
73
  const nextIdPair = EventId.nextPair(baseEventId, mutationDef.options.clientOnly)
69
74
  baseEventId = nextIdPair.id
70
75
  return new MutationEvent.EncodedWithMeta(
@@ -84,6 +89,10 @@ export const makeClientSessionSyncProcessor = ({
84
89
  isEqualEvent: MutationEvent.isEqualEncoded,
85
90
  })
86
91
 
92
+ if (updateResult._tag === 'unexpected-error') {
93
+ return shouldNeverHappen('Unexpected error in client-session-sync-processor', updateResult.cause)
94
+ }
95
+
87
96
  span.addEvent('local-push', {
88
97
  batchSize: encodedMutationEvents.length,
89
98
  updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
@@ -108,13 +117,17 @@ export const makeClientSessionSyncProcessor = ({
108
117
  }
109
118
 
110
119
  // console.debug('pushToLeader', encodedMutationEvents.length, ...encodedMutationEvents.map((_) => _.toJSON()))
111
- clientSession.leaderThread.mutations
112
- .push(encodedMutationEvents)
113
- .pipe(Effect.tapCauseLogPretty, Effect.provide(runtime), Effect.runFork)
120
+ BucketQueue.offerAll(leaderPushQueue, encodedMutationEvents).pipe(Effect.runSync)
114
121
 
115
122
  return { writeTables }
116
123
  }
117
124
 
125
+ const debugInfo = {
126
+ rebaseCount: 0,
127
+ advanceCount: 0,
128
+ rejectCount: 0,
129
+ }
130
+
118
131
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
119
132
 
120
133
  const boot: ClientSessionSyncProcessor['boot'] = Effect.gen(function* () {
@@ -133,6 +146,20 @@ export const makeClientSessionSyncProcessor = ({
133
146
  )
134
147
  }
135
148
 
149
+ const leaderPushingFiberHandle = yield* FiberHandle.make()
150
+
151
+ const backgroundLeaderPushing = Effect.gen(function* () {
152
+ const batch = yield* BucketQueue.takeBetween(leaderPushQueue, 1, params.leaderPushBatchSize)
153
+ yield* clientSession.leaderThread.mutations.push(batch).pipe(
154
+ Effect.catchTag('LeaderAheadError', () => {
155
+ debugInfo.rejectCount++
156
+ return BucketQueue.clear(leaderPushQueue)
157
+ }),
158
+ )
159
+ }).pipe(Effect.forever, Effect.interruptible, Effect.tapCauseLogPretty)
160
+
161
+ yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
162
+
136
163
  yield* clientSession.leaderThread.mutations.pull.pipe(
137
164
  Stream.tap(({ payload, remaining }) =>
138
165
  Effect.gen(function* () {
@@ -148,9 +175,10 @@ export const makeClientSessionSyncProcessor = ({
148
175
  isEqualEvent: MutationEvent.isEqualEncoded,
149
176
  })
150
177
 
151
- if (updateResult._tag === 'reject') {
152
- debugger
153
- throw new Error('TODO: implement reject in client-session-sync-queue for pull')
178
+ if (updateResult._tag === 'unexpected-error') {
179
+ return yield* Effect.fail(updateResult.cause)
180
+ } else if (updateResult._tag === 'reject') {
181
+ return shouldNeverHappen('Unexpected reject in client-session-sync-processor', updateResult)
154
182
  }
155
183
 
156
184
  syncStateRef.current = updateResult.newSyncState
@@ -165,11 +193,21 @@ export const makeClientSessionSyncProcessor = ({
165
193
  res: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
166
194
  remaining,
167
195
  })
196
+
197
+ debugInfo.rebaseCount++
198
+
199
+ yield* FiberHandle.clear(leaderPushingFiberHandle)
200
+
201
+ // Reset the leader push queue since we're rebasing and will push again
202
+ yield* BucketQueue.clear(leaderPushQueue)
203
+
204
+ yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
205
+
168
206
  if (LS_DEV) {
169
207
  Effect.logDebug(
170
208
  'pull:rebase: rollback',
171
209
  updateResult.eventsToRollback.length,
172
- ...updateResult.eventsToRollback.map((_) => _.toJSON()),
210
+ ...updateResult.eventsToRollback.slice(0, 10).map((_) => _.toJSON()),
173
211
  ).pipe(Effect.provide(runtime), Effect.runSync)
174
212
  }
175
213
 
@@ -181,9 +219,7 @@ export const makeClientSessionSyncProcessor = ({
181
219
  }
182
220
  }
183
221
 
184
- clientSession.leaderThread.mutations
185
- .push(updateResult.newSyncState.pending)
186
- .pipe(Effect.tapCauseLogPretty, Effect.provide(runtime), Effect.runFork)
222
+ yield* BucketQueue.offerAll(leaderPushQueue, updateResult.newSyncState.pending)
187
223
  } else {
188
224
  span.addEvent('pull:advance', {
189
225
  payloadTag: payload._tag,
@@ -192,6 +228,8 @@ export const makeClientSessionSyncProcessor = ({
192
228
  res: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
193
229
  remaining,
194
230
  })
231
+
232
+ debugInfo.advanceCount++
195
233
  }
196
234
 
197
235
  if (updateResult.newEvents.length === 0) return
@@ -208,10 +246,14 @@ export const makeClientSessionSyncProcessor = ({
208
246
  }
209
247
 
210
248
  refreshTables(writeTables)
211
- }),
249
+ }).pipe(
250
+ Effect.tapCauseLogPretty,
251
+ Effect.catchAllCause((cause) => Effect.sync(() => clientSession.shutdown(cause))),
252
+ ),
212
253
  ),
213
254
  Stream.runDrain,
214
255
  Effect.forever, // NOTE Whenever the leader changes, we need to re-start the stream
256
+ Effect.withSpan('client-session-sync-processor:pull'),
215
257
  Effect.tapCauseLogPretty,
216
258
  Effect.forkScoped,
217
259
  )
@@ -228,6 +270,21 @@ export const makeClientSessionSyncProcessor = ({
228
270
  }),
229
271
  changes: Stream.fromQueue(syncStateUpdateQueue),
230
272
  }),
273
+ debug: {
274
+ print: () =>
275
+ Effect.gen(function* () {
276
+ console.log('debugInfo', debugInfo)
277
+ console.log('syncState', syncStateRef.current)
278
+ const pushQueueSize = yield* BucketQueue.size(leaderPushQueue)
279
+ console.log('pushQueueSize', pushQueueSize)
280
+ const pushQueueItems = yield* BucketQueue.peekAll(leaderPushQueue)
281
+ console.log(
282
+ 'pushQueueItems',
283
+ pushQueueItems.map((_) => _.toJSON()),
284
+ )
285
+ }).pipe(Effect.provide(runtime), Effect.runSync),
286
+ debugInfo: () => debugInfo,
287
+ },
231
288
  } satisfies ClientSessionSyncProcessor
232
289
  }
233
290
 
@@ -239,6 +296,12 @@ export interface ClientSessionSyncProcessor {
239
296
  writeTables: Set<string>
240
297
  }
241
298
  boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
242
-
243
299
  syncState: Subscribable.Subscribable<SyncState.SyncState>
300
+ debug: {
301
+ print: () => void
302
+ debugInfo: () => {
303
+ rebaseCount: number
304
+ advanceCount: number
305
+ }
306
+ }
244
307
  }
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
- meta: {},
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
- expect(() =>
152
- run({
153
- syncState,
154
- payload: { _tag: 'upstream-rebase', rollbackUntil: e_0_0.id, newEvents: [e_1_0] },
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
- expect(() => run({ syncState, payload: { _tag: 'upstream-advance', newEvents: [e_0_1, e_0_0] } })).toThrow()
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
- expect(() => run({ syncState, payload: { _tag: 'upstream-advance', newEvents: [e_1_0, e_0_0] } })).toThrow()
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(result: SyncState.UpdateResult): asserts result is SyncState.UpdateResultAdvance {
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(result: SyncState.UpdateResult): asserts result is SyncState.UpdateResultRebase {
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(result: SyncState.UpdateResult): asserts result is SyncState.UpdateResultReject {
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
  }