@livestore/common 0.0.0-snapshot-83c6b3d6e39244b59235009057fd5c48f6c3103f → 0.0.0-snapshot-2b8a9de3ec1a701aca891ebc2c98eb328274ae9e

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 (96) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +4 -2
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js +1 -1
  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 +6 -6
  8. package/dist/devtools/devtools-messages-leader.d.ts +24 -24
  9. package/dist/leader-thread/LeaderSyncProcessor.d.ts +2 -1
  10. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  11. package/dist/leader-thread/LeaderSyncProcessor.js +42 -38
  12. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  13. package/dist/leader-thread/leader-worker-devtools.js +1 -1
  14. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  15. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  16. package/dist/leader-thread/make-leader-thread-layer.js +1 -0
  17. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  18. package/dist/leader-thread/mutationlog.d.ts +1 -0
  19. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  20. package/dist/leader-thread/mutationlog.js +1 -0
  21. package/dist/leader-thread/mutationlog.js.map +1 -1
  22. package/dist/mutation.d.ts.map +1 -1
  23. package/dist/mutation.js +13 -2
  24. package/dist/mutation.js.map +1 -1
  25. package/dist/query-builder/api.d.ts +118 -20
  26. package/dist/query-builder/api.d.ts.map +1 -1
  27. package/dist/query-builder/api.js.map +1 -1
  28. package/dist/query-builder/astToSql.d.ts +7 -0
  29. package/dist/query-builder/astToSql.d.ts.map +1 -0
  30. package/dist/query-builder/astToSql.js +168 -0
  31. package/dist/query-builder/astToSql.js.map +1 -0
  32. package/dist/query-builder/impl.d.ts +1 -5
  33. package/dist/query-builder/impl.d.ts.map +1 -1
  34. package/dist/query-builder/impl.js +130 -96
  35. package/dist/query-builder/impl.js.map +1 -1
  36. package/dist/query-builder/impl.test.js +94 -0
  37. package/dist/query-builder/impl.test.js.map +1 -1
  38. package/dist/query-builder/mod.d.ts +7 -0
  39. package/dist/query-builder/mod.d.ts.map +1 -1
  40. package/dist/query-builder/mod.js +7 -0
  41. package/dist/query-builder/mod.js.map +1 -1
  42. package/dist/query-info.d.ts +4 -1
  43. package/dist/query-info.d.ts.map +1 -1
  44. package/dist/query-info.js.map +1 -1
  45. package/dist/schema/MutationEvent.d.ts +17 -1
  46. package/dist/schema/MutationEvent.d.ts.map +1 -1
  47. package/dist/schema/MutationEvent.js +18 -2
  48. package/dist/schema/MutationEvent.js.map +1 -1
  49. package/dist/schema/db-schema/dsl/mod.d.ts +7 -5
  50. package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
  51. package/dist/schema/db-schema/dsl/mod.js +6 -0
  52. package/dist/schema/db-schema/dsl/mod.js.map +1 -1
  53. package/dist/schema/mutations.d.ts +11 -2
  54. package/dist/schema/mutations.d.ts.map +1 -1
  55. package/dist/schema/mutations.js.map +1 -1
  56. package/dist/schema/table-def.d.ts +7 -3
  57. package/dist/schema/table-def.d.ts.map +1 -1
  58. package/dist/schema/table-def.js +7 -1
  59. package/dist/schema/table-def.js.map +1 -1
  60. package/dist/sync/ClientSessionSyncProcessor.d.ts +2 -0
  61. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  62. package/dist/sync/ClientSessionSyncProcessor.js +36 -33
  63. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  64. package/dist/sync/sync.d.ts +17 -0
  65. package/dist/sync/sync.d.ts.map +1 -1
  66. package/dist/sync/sync.js.map +1 -1
  67. package/dist/sync/syncstate.d.ts +38 -16
  68. package/dist/sync/syncstate.d.ts.map +1 -1
  69. package/dist/sync/syncstate.js +110 -40
  70. package/dist/sync/syncstate.js.map +1 -1
  71. package/dist/sync/syncstate.test.js +60 -29
  72. package/dist/sync/syncstate.test.js.map +1 -1
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/package.json +2 -2
  76. package/src/adapter-types.ts +4 -2
  77. package/src/leader-thread/LeaderSyncProcessor.ts +45 -39
  78. package/src/leader-thread/leader-worker-devtools.ts +1 -1
  79. package/src/leader-thread/make-leader-thread-layer.ts +1 -0
  80. package/src/leader-thread/mutationlog.ts +1 -0
  81. package/src/mutation.ts +20 -3
  82. package/src/query-builder/api.ts +192 -15
  83. package/src/query-builder/astToSql.ts +203 -0
  84. package/src/query-builder/impl.test.ts +104 -0
  85. package/src/query-builder/impl.ts +157 -113
  86. package/src/query-builder/mod.ts +7 -0
  87. package/src/query-info.ts +6 -1
  88. package/src/schema/MutationEvent.ts +18 -2
  89. package/src/schema/db-schema/dsl/mod.ts +30 -2
  90. package/src/schema/mutations.ts +12 -1
  91. package/src/schema/table-def.ts +14 -4
  92. package/src/sync/ClientSessionSyncProcessor.ts +39 -33
  93. package/src/sync/sync.ts +14 -0
  94. package/src/sync/syncstate.test.ts +72 -38
  95. package/src/sync/syncstate.ts +138 -58
  96. package/src/version.ts +1 -1
package/src/query-info.ts CHANGED
@@ -9,7 +9,7 @@ import type { DbSchema } from './schema/mod.js'
9
9
  *
10
10
  * This information is currently only used for derived mutations.
11
11
  */
12
- export type QueryInfo = QueryInfo.None | QueryInfo.Row | QueryInfo.Col | QueryInfo.ColJsonValue
12
+ export type QueryInfo = QueryInfo.None | QueryInfo.Row | QueryInfo.Col | QueryInfo.ColJsonValue | QueryInfo.Write
13
13
  // export type QueryInfo<TTableDef extends DbSchema.TableDefBase = DbSchema.TableDefBase> =
14
14
  // | QueryInfo.None
15
15
  // | QueryInfo.Row<TTableDef>
@@ -45,6 +45,11 @@ export namespace QueryInfo {
45
45
  jsonPath: string
46
46
  }
47
47
 
48
+ // NOTE Not yet used but we might want to use this in order to avoid write queries in read-only situations
49
+ export type Write = {
50
+ _tag: 'Write'
51
+ }
52
+
48
53
  // NOTE maybe we want to bring back type-params back like below
49
54
  // export type Row<TTableDef extends DbSchema.TableDefBase> = {
50
55
  // _tag: 'Row'
@@ -178,10 +178,26 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
178
178
  }
179
179
  }
180
180
 
181
- rebase = (parentId: EventId.EventId, isLocal: boolean) =>
181
+ /**
182
+ * Example: (global event)
183
+ * For event id (2,0) → (1,0) which should be rebased on event id (3,1) → (3,0)
184
+ * the resulting event id will be (4,0) → (3,0)
185
+ *
186
+ * Example: (client event)
187
+ * For event id (2,1) → (2,0) which should be rebased on event id (3,0) → (2,0)
188
+ * the resulting event id will be (3,1) → (3,0)
189
+ *
190
+ * Syntax: (2,1) → (2,0)
191
+ * ^ ^ ^ ^
192
+ * | | | +- client parent id
193
+ * | | +--- global parent id
194
+ * | +-- client id
195
+ * +---- global id
196
+ */
197
+ rebase = (parentId: EventId.EventId, isClient: boolean) =>
182
198
  new EncodedWithMeta({
183
199
  ...this,
184
- ...EventId.nextPair(parentId, isLocal),
200
+ ...EventId.nextPair(parentId, isClient),
185
201
  })
186
202
 
187
203
  static fromGlobal = (mutationEvent: AnyEncodedGlobal) =>
@@ -52,8 +52,13 @@ export type AnyIfConstained<In, Out> = '__constrained' extends keyof In ? any :
52
52
  export type EmptyObjIfConstained<In> = '__constrained' extends keyof In ? {} : In
53
53
 
54
54
  export type StructSchemaForColumns<TCols extends ConstraintColumns> = Schema.Schema<
55
- AnyIfConstained<TCols, { readonly [K in keyof TCols]: TCols[K]['schema']['Type'] }>,
56
- AnyIfConstained<TCols, { readonly [K in keyof TCols]: TCols[K]['schema']['Encoded'] }>
55
+ AnyIfConstained<TCols, FromColumns.RowDecoded<TCols>>,
56
+ AnyIfConstained<TCols, FromColumns.RowEncoded<TCols>>
57
+ >
58
+
59
+ export type InsertStructSchemaForColumns<TCols extends ConstraintColumns> = Schema.Schema<
60
+ AnyIfConstained<TCols, FromColumns.InsertRowDecoded<TCols>>,
61
+ AnyIfConstained<TCols, FromColumns.InsertRowEncoded<TCols>>
57
62
  >
58
63
 
59
64
  export const structSchemaForTable = <TTableDefinition extends TableDefinition<any, any>>(
@@ -63,6 +68,20 @@ export const structSchemaForTable = <TTableDefinition extends TableDefinition<an
63
68
  title: tableDef.name,
64
69
  }) as any
65
70
 
71
+ export const insertStructSchemaForTable = <TTableDefinition extends TableDefinition<any, any>>(
72
+ tableDef: TTableDefinition,
73
+ ): InsertStructSchemaForColumns<TTableDefinition['columns']> =>
74
+ Schema.Struct(
75
+ Object.fromEntries(
76
+ tableDef.ast.columns.map((column) => [
77
+ column.name,
78
+ column.nullable === true || column.default._tag === 'Some' ? Schema.optional(column.schema) : column.schema,
79
+ ]),
80
+ ),
81
+ ).annotations({
82
+ title: tableDef.name,
83
+ }) as any
84
+
66
85
  const columsToAst = (columns: Columns): ReadonlyArray<SqliteAst.Column> => {
67
86
  return Object.entries(columns).map(([name, column]) => {
68
87
  return {
@@ -161,6 +180,10 @@ export namespace FromColumns {
161
180
  readonly [K in keyof TColumns]: Schema.Schema.Type<TColumns[K]['schema']>
162
181
  }
163
182
 
183
+ export type RowEncodedAll<TColumns extends Columns> = {
184
+ readonly [K in keyof TColumns]: Schema.Schema.Encoded<TColumns[K]['schema']>
185
+ }
186
+
164
187
  export type RowEncoded<TColumns extends Columns> = Types.Simplify<
165
188
  Nullable<Pick<RowEncodeNonNullable<TColumns>, NullableColumnNames<TColumns>>> &
166
189
  Omit<RowEncodeNonNullable<TColumns>, NullableColumnNames<TColumns>>
@@ -192,4 +215,9 @@ export namespace FromColumns {
192
215
  Pick<RowDecodedAll<TColumns>, RequiredInsertColumnNames<TColumns>> &
193
216
  Partial<Omit<RowDecodedAll<TColumns>, RequiredInsertColumnNames<TColumns>>>
194
217
  >
218
+
219
+ export type InsertRowEncoded<TColumns extends Columns> = Types.Simplify<
220
+ Pick<RowEncodedAll<TColumns>, RequiredInsertColumnNames<TColumns>> &
221
+ Partial<Omit<RowEncodedAll<TColumns>, RequiredInsertColumnNames<TColumns>>>
222
+ >
195
223
  }
@@ -1,5 +1,6 @@
1
1
  import { Schema } from '@livestore/utils/effect'
2
2
 
3
+ import type { QueryBuilder } from '../query-builder/mod.js'
3
4
  import type { BindValues } from '../sql-queries/sql-queries.js'
4
5
 
5
6
  export type MutationDefMap = {
@@ -20,7 +21,10 @@ export type InternalMutationSchema<TRecord extends MutationDefRecord = MutationD
20
21
 
21
22
  export type MutationDefSqlResult<TTo> =
22
23
  | SingleOrReadonlyArray<string>
23
- | ((args: TTo) => SingleOrReadonlyArray<
24
+ | ((
25
+ args: TTo,
26
+ context: { currentFacts: MutationEventFacts; clientOnly: boolean },
27
+ ) => SingleOrReadonlyArray<
24
28
  | string
25
29
  | {
26
30
  sql: string
@@ -28,8 +32,15 @@ export type MutationDefSqlResult<TTo> =
28
32
  bindValues: BindValues
29
33
  writeTables?: ReadonlySet<string>
30
34
  }
35
+ | QueryBuilder.Any
31
36
  >)
32
37
 
38
+ export type MutationHandlerResult = {
39
+ sql: string
40
+ bindValues: BindValues
41
+ writeTables?: ReadonlySet<string>
42
+ }
43
+
33
44
  export type SingleOrReadonlyArray<T> = T | ReadonlyArray<T>
34
45
 
35
46
  export type MutationDef<TName extends string, TFrom, TTo> = {
@@ -19,12 +19,12 @@ export type DefaultSqliteTableDefConstrained = SqliteDsl.TableDefinition<string,
19
19
  export type TableDefBase<
20
20
  TSqliteDef extends DefaultSqliteTableDef = DefaultSqliteTableDefConstrained,
21
21
  TOptions extends TableOptions = TableOptions,
22
- TSchema = SqliteDsl.StructSchemaForColumns<TSqliteDef['columns']>,
23
22
  > = {
24
23
  sqliteDef: TSqliteDef
25
24
  options: TOptions
26
25
  // Derived from `sqliteDef`, so only exposed for convenience
27
- schema: TSchema
26
+ schema: SqliteDsl.StructSchemaForColumns<TSqliteDef['columns']>
27
+ insertSchema: SqliteDsl.InsertStructSchemaForColumns<TSqliteDef['columns']>
28
28
  }
29
29
 
30
30
  export type TableDef<
@@ -47,7 +47,10 @@ export type TableDef<
47
47
  options: TOptions
48
48
  // Derived from `sqliteDef`, so only exposed for convenience
49
49
  schema: TSchema
50
- query: QueryBuilder<ReadonlyArray<Schema.Schema.Type<TSchema>>, TableDef<TSqliteDef & {}, TOptions>>
50
+ insertSchema: SqliteDsl.InsertStructSchemaForColumns<TSqliteDef['columns']>
51
+ query: QueryBuilder<ReadonlyArray<Schema.Schema.Type<TSchema>>, TableDefBase<TSqliteDef & {}, TOptions>>
52
+ readonly Type: Schema.Schema.Type<TSchema>
53
+ readonly Encoded: Schema.Schema.Encoded<TSchema>
51
54
  } & (TOptions['deriveMutations']['enabled'] extends true
52
55
  ? DerivedMutationHelperFns<TSqliteDef['columns'], TOptions>
53
56
  : {})
@@ -195,7 +198,14 @@ export const table = <
195
198
  const isSingleColumn = SqliteDsl.isColumnDefinition(columnOrColumns) === true
196
199
 
197
200
  const schema = SqliteDsl.structSchemaForTable(sqliteDef)
198
- const tableDef = { sqliteDef, options: options_, schema } satisfies TableDefBase
201
+ const insertSchema = SqliteDsl.insertStructSchemaForTable(sqliteDef)
202
+ const tableDef = {
203
+ sqliteDef,
204
+ options: options_,
205
+ schema,
206
+ insertSchema,
207
+ } satisfies TableDefBase
208
+
199
209
  const query = makeQueryBuilder(tableDef)
200
210
  // const tableDef = { ...tableDefBase, query } satisfies TableDef
201
211
 
@@ -17,6 +17,8 @@ import * as SyncState from './syncstate.js'
17
17
  * - The goal is to never block the UI, so we'll interrupt rebasing if a new mutations is pushed by the client session.
18
18
  * - We also want to avoid "backwards-jumping" in the UI, so we'll transactionally apply a read model changes during a rebase.
19
19
  * - We might need to make the rebase behaviour configurable e.g. to let users manually trigger a rebase
20
+ *
21
+ * Longer term we should evalutate whether we can unify the ClientSessionSyncProcessor with the LeaderSyncProcessor.
20
22
  */
21
23
  export const makeClientSessionSyncProcessor = ({
22
24
  schema,
@@ -64,7 +66,7 @@ export const makeClientSessionSyncProcessor = ({
64
66
  }
65
67
 
66
68
  const syncStateUpdateQueue = Queue.unbounded<SyncState.SyncState>().pipe(Effect.runSync)
67
- const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) =>
69
+ const isClientEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) =>
68
70
  getMutationDef(schema, mutationEventEncoded.mutation).options.clientOnly
69
71
 
70
72
  /** We're queuing push requests to reduce the number of messages sent to the leader by batching them */
@@ -88,31 +90,31 @@ export const makeClientSessionSyncProcessor = ({
88
90
  )
89
91
  })
90
92
 
91
- const updateResult = SyncState.updateSyncState({
93
+ const mergeResult = SyncState.merge({
92
94
  syncState: syncStateRef.current,
93
95
  payload: { _tag: 'local-push', newEvents: encodedMutationEvents },
94
- isLocalEvent,
96
+ isClientEvent,
95
97
  isEqualEvent: MutationEvent.isEqualEncoded,
96
98
  })
97
99
 
98
- if (updateResult._tag === 'unexpected-error') {
99
- return shouldNeverHappen('Unexpected error in client-session-sync-processor', updateResult.cause)
100
+ if (mergeResult._tag === 'unexpected-error') {
101
+ return shouldNeverHappen('Unexpected error in client-session-sync-processor', mergeResult.cause)
100
102
  }
101
103
 
102
104
  span.addEvent('local-push', {
103
105
  batchSize: encodedMutationEvents.length,
104
- updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
106
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
105
107
  })
106
108
 
107
- if (updateResult._tag !== 'advance') {
108
- return shouldNeverHappen(`Expected advance, got ${updateResult._tag}`)
109
+ if (mergeResult._tag !== 'advance') {
110
+ return shouldNeverHappen(`Expected advance, got ${mergeResult._tag}`)
109
111
  }
110
112
 
111
- syncStateRef.current = updateResult.newSyncState
112
- syncStateUpdateQueue.offer(updateResult.newSyncState).pipe(Effect.runSync)
113
+ syncStateRef.current = mergeResult.newSyncState
114
+ syncStateUpdateQueue.offer(mergeResult.newSyncState).pipe(Effect.runSync)
113
115
 
114
116
  const writeTables = new Set<string>()
115
- for (const mutationEvent of updateResult.newEvents) {
117
+ for (const mutationEvent of mergeResult.newEvents) {
116
118
  // TODO avoid encoding and decoding here again
117
119
  const decodedMutationEvent = Schema.decodeSync(mutationEventSchema)(mutationEvent)
118
120
  const res = applyMutation(decodedMutationEvent, { otelContext, withChangeset: true })
@@ -166,7 +168,10 @@ export const makeClientSessionSyncProcessor = ({
166
168
 
167
169
  yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
168
170
 
169
- yield* clientSession.leaderThread.mutations.pull.pipe(
171
+ // NOTE We need to lazily call `.pull` as we want the cursor to be updated
172
+ yield* Stream.suspend(() =>
173
+ clientSession.leaderThread.mutations.pull({ cursor: syncStateRef.current.localHead }),
174
+ ).pipe(
170
175
  Stream.tap(({ payload, remaining }) =>
171
176
  Effect.gen(function* () {
172
177
  // console.log('pulled payload from leader', { payload, remaining })
@@ -174,29 +179,29 @@ export const makeClientSessionSyncProcessor = ({
174
179
  yield* clientSession.devtools.pullLatch.await
175
180
  }
176
181
 
177
- const updateResult = SyncState.updateSyncState({
182
+ const mergeResult = SyncState.merge({
178
183
  syncState: syncStateRef.current,
179
184
  payload,
180
- isLocalEvent,
185
+ isClientEvent,
181
186
  isEqualEvent: MutationEvent.isEqualEncoded,
182
187
  })
183
188
 
184
- if (updateResult._tag === 'unexpected-error') {
185
- return yield* Effect.fail(updateResult.cause)
186
- } else if (updateResult._tag === 'reject') {
187
- return shouldNeverHappen('Unexpected reject in client-session-sync-processor', updateResult)
189
+ if (mergeResult._tag === 'unexpected-error') {
190
+ return yield* Effect.fail(mergeResult.cause)
191
+ } else if (mergeResult._tag === 'reject') {
192
+ return shouldNeverHappen('Unexpected reject in client-session-sync-processor', mergeResult)
188
193
  }
189
194
 
190
- syncStateRef.current = updateResult.newSyncState
191
- syncStateUpdateQueue.offer(updateResult.newSyncState).pipe(Effect.runSync)
195
+ syncStateRef.current = mergeResult.newSyncState
196
+ syncStateUpdateQueue.offer(mergeResult.newSyncState).pipe(Effect.runSync)
192
197
 
193
- if (updateResult._tag === 'rebase') {
198
+ if (mergeResult._tag === 'rebase') {
194
199
  span.addEvent('pull:rebase', {
195
200
  payloadTag: payload._tag,
196
201
  payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
197
- newEventsCount: updateResult.newEvents.length,
198
- rollbackCount: updateResult.eventsToRollback.length,
199
- res: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
202
+ newEventsCount: mergeResult.newEvents.length,
203
+ rollbackCount: mergeResult.eventsToRollback.length,
204
+ res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
200
205
  remaining,
201
206
  })
202
207
 
@@ -212,36 +217,36 @@ export const makeClientSessionSyncProcessor = ({
212
217
  if (LS_DEV) {
213
218
  Effect.logDebug(
214
219
  'pull:rebase: rollback',
215
- updateResult.eventsToRollback.length,
216
- ...updateResult.eventsToRollback.slice(0, 10).map((_) => _.toJSON()),
220
+ mergeResult.eventsToRollback.length,
221
+ ...mergeResult.eventsToRollback.slice(0, 10).map((_) => _.toJSON()),
217
222
  ).pipe(Effect.provide(runtime), Effect.runSync)
218
223
  }
219
224
 
220
- for (let i = updateResult.eventsToRollback.length - 1; i >= 0; i--) {
221
- const event = updateResult.eventsToRollback[i]!
225
+ for (let i = mergeResult.eventsToRollback.length - 1; i >= 0; i--) {
226
+ const event = mergeResult.eventsToRollback[i]!
222
227
  if (event.meta.sessionChangeset) {
223
228
  rollback(event.meta.sessionChangeset)
224
229
  event.meta.sessionChangeset = undefined
225
230
  }
226
231
  }
227
232
 
228
- yield* BucketQueue.offerAll(leaderPushQueue, updateResult.newSyncState.pending)
233
+ yield* BucketQueue.offerAll(leaderPushQueue, mergeResult.newSyncState.pending)
229
234
  } else {
230
235
  span.addEvent('pull:advance', {
231
236
  payloadTag: payload._tag,
232
237
  payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
233
- newEventsCount: updateResult.newEvents.length,
234
- res: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
238
+ newEventsCount: mergeResult.newEvents.length,
239
+ res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
235
240
  remaining,
236
241
  })
237
242
 
238
243
  debugInfo.advanceCount++
239
244
  }
240
245
 
241
- if (updateResult.newEvents.length === 0) return
246
+ if (mergeResult.newEvents.length === 0) return
242
247
 
243
248
  const writeTables = new Set<string>()
244
- for (const mutationEvent of updateResult.newEvents) {
249
+ for (const mutationEvent of mergeResult.newEvents) {
245
250
  const decodedMutationEvent = Schema.decodeSync(mutationEventSchema)(mutationEvent)
246
251
  const res = applyMutation(decodedMutationEvent, { otelContext, withChangeset: true })
247
252
  for (const table of res.writeTables) {
@@ -259,6 +264,7 @@ export const makeClientSessionSyncProcessor = ({
259
264
  ),
260
265
  Stream.runDrain,
261
266
  Effect.forever, // NOTE Whenever the leader changes, we need to re-start the stream
267
+ Effect.interruptible,
262
268
  Effect.withSpan('client-session-sync-processor:pull'),
263
269
  Effect.tapCauseLogPretty,
264
270
  Effect.forkScoped,
package/src/sync/sync.ts CHANGED
@@ -19,6 +19,16 @@ export type SyncOptions = {
19
19
  backend?: SyncBackendConstructor<any>
20
20
  /** @default { _tag: 'Skip' } */
21
21
  initialSyncOptions?: InitialSyncOptions
22
+ /**
23
+ * What to do if there is an error during sync.
24
+ *
25
+ * Options:
26
+ * `shutdown` will stop the sync processor and cause the app to crash.
27
+ * `ignore` will log the error and let the app continue running acting as if it was offline.
28
+ *
29
+ * @default 'ignore'
30
+ * */
31
+ onSyncError?: 'shutdown' | 'ignore'
22
32
  }
23
33
 
24
34
  export type SyncBackendConstructor<TSyncMetadata = Schema.JsonValue> = (
@@ -59,6 +69,10 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
59
69
  HttpClient.HttpClient
60
70
  >
61
71
  isConnected: SubscriptionRef.SubscriptionRef<boolean>
72
+ /**
73
+ * Metadata describing the sync backend.
74
+ */
75
+ metadata: { name: string; description: string } & Record<string, Schema.JsonValue>
62
76
  }
63
77
 
64
78
  export class IsOfflineError extends Schema.TaggedError<IsOfflineError>()('IsOfflineError', {}) {}