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

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 (105) 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 +29 -7
  16. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  17. package/dist/leader-thread/LeaderSyncProcessor.js +259 -199
  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 +15 -3
  28. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  29. package/dist/leader-thread/make-leader-thread-layer.js +29 -34
  30. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  31. package/dist/leader-thread/mod.d.ts +1 -1
  32. package/dist/leader-thread/mod.d.ts.map +1 -1
  33. package/dist/leader-thread/mod.js +1 -1
  34. package/dist/leader-thread/mod.js.map +1 -1
  35. package/dist/leader-thread/mutationlog.d.ts +19 -3
  36. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  37. package/dist/leader-thread/mutationlog.js +105 -12
  38. package/dist/leader-thread/mutationlog.js.map +1 -1
  39. package/dist/leader-thread/pull-queue-set.d.ts +1 -1
  40. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  41. package/dist/leader-thread/pull-queue-set.js +6 -16
  42. package/dist/leader-thread/pull-queue-set.js.map +1 -1
  43. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  44. package/dist/leader-thread/recreate-db.js +4 -3
  45. package/dist/leader-thread/recreate-db.js.map +1 -1
  46. package/dist/leader-thread/types.d.ts +34 -19
  47. package/dist/leader-thread/types.d.ts.map +1 -1
  48. package/dist/leader-thread/types.js.map +1 -1
  49. package/dist/rehydrate-from-mutationlog.d.ts +5 -4
  50. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  51. package/dist/rehydrate-from-mutationlog.js +7 -9
  52. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  53. package/dist/schema/EventId.d.ts +9 -0
  54. package/dist/schema/EventId.d.ts.map +1 -1
  55. package/dist/schema/EventId.js +22 -2
  56. package/dist/schema/EventId.js.map +1 -1
  57. package/dist/schema/MutationEvent.d.ts +78 -25
  58. package/dist/schema/MutationEvent.d.ts.map +1 -1
  59. package/dist/schema/MutationEvent.js +25 -12
  60. package/dist/schema/MutationEvent.js.map +1 -1
  61. package/dist/schema/schema.js +1 -1
  62. package/dist/schema/schema.js.map +1 -1
  63. package/dist/schema/system-tables.d.ts +67 -0
  64. package/dist/schema/system-tables.d.ts.map +1 -1
  65. package/dist/schema/system-tables.js +12 -1
  66. package/dist/schema/system-tables.js.map +1 -1
  67. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -1
  68. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  69. package/dist/sync/ClientSessionSyncProcessor.js +25 -19
  70. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  71. package/dist/sync/sync.d.ts +6 -5
  72. package/dist/sync/sync.d.ts.map +1 -1
  73. package/dist/sync/sync.js.map +1 -1
  74. package/dist/sync/syncstate.d.ts +47 -71
  75. package/dist/sync/syncstate.d.ts.map +1 -1
  76. package/dist/sync/syncstate.js +136 -139
  77. package/dist/sync/syncstate.js.map +1 -1
  78. package/dist/sync/syncstate.test.js +203 -284
  79. package/dist/sync/syncstate.test.js.map +1 -1
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +2 -2
  83. package/src/adapter-types.ts +11 -13
  84. package/src/devtools/devtools-messages-common.ts +9 -0
  85. package/src/devtools/devtools-messages-leader.ts +1 -2
  86. package/src/leader-thread/LeaderSyncProcessor.ts +457 -351
  87. package/src/leader-thread/apply-mutation.ts +81 -71
  88. package/src/leader-thread/leader-worker-devtools.ts +5 -7
  89. package/src/leader-thread/make-leader-thread-layer.ts +60 -53
  90. package/src/leader-thread/mod.ts +1 -1
  91. package/src/leader-thread/mutationlog.ts +166 -13
  92. package/src/leader-thread/recreate-db.ts +4 -3
  93. package/src/leader-thread/types.ts +33 -23
  94. package/src/rehydrate-from-mutationlog.ts +12 -12
  95. package/src/schema/EventId.ts +26 -2
  96. package/src/schema/MutationEvent.ts +32 -16
  97. package/src/schema/schema.ts +1 -1
  98. package/src/schema/system-tables.ts +20 -1
  99. package/src/sync/ClientSessionSyncProcessor.ts +35 -23
  100. package/src/sync/sync.ts +6 -9
  101. package/src/sync/syncstate.test.ts +228 -315
  102. package/src/sync/syncstate.ts +202 -187
  103. package/src/version.ts +1 -1
  104. package/tmp/pack.tgz +0 -0
  105. package/src/leader-thread/pull-queue-set.ts +0 -67
@@ -12,7 +12,7 @@ export const recreateDb: Effect.Effect<
12
12
  UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
13
13
  LeaderThreadCtx | HttpClient.HttpClient
14
14
  > = Effect.gen(function* () {
15
- const { dbReadModel, dbMutationLog, schema, bootStatusQueue } = yield* LeaderThreadCtx
15
+ const { dbReadModel, dbMutationLog, schema, bootStatusQueue, applyMutation } = yield* LeaderThreadCtx
16
16
 
17
17
  const migrationOptions = schema.migrationOptions
18
18
  let migrationsReport: MigrationsReport
@@ -56,10 +56,11 @@ export const recreateDb: Effect.Effect<
56
56
  migrationsReport = initResult.migrationsReport
57
57
 
58
58
  yield* rehydrateFromMutationLog({
59
- db: initResult.tmpDb,
60
- logDb: dbMutationLog,
59
+ // db: initResult.tmpDb,
60
+ dbMutationLog,
61
61
  schema,
62
62
  migrationOptions,
63
+ applyMutation,
63
64
  onProgress: ({ done, total }) =>
64
65
  Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
65
66
  })
@@ -5,12 +5,14 @@ import type {
5
5
  Option,
6
6
  Queue,
7
7
  Scope,
8
+ Stream,
8
9
  Subscribable,
9
10
  SubscriptionRef,
10
11
  WebChannel,
11
12
  } from '@livestore/utils/effect'
12
13
  import { Context, Schema } from '@livestore/utils/effect'
13
14
 
15
+ import type { LeaderPullCursor, SqliteError } from '../adapter-types.js'
14
16
  import type {
15
17
  BootStatus,
16
18
  Devtools,
@@ -97,7 +99,7 @@ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
97
99
  devtools: DevtoolsContext
98
100
  syncBackend: SyncBackend | undefined
99
101
  syncProcessor: LeaderSyncProcessor
100
- connectedClientSessionPullQueues: PullQueueSet
102
+ applyMutation: ApplyMutation
101
103
  initialState: {
102
104
  leaderHead: EventId.EventId
103
105
  migrationsReport: MigrationsReport
@@ -111,27 +113,41 @@ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
111
113
  }
112
114
  >() {}
113
115
 
116
+ export type ApplyMutation = (
117
+ mutationEventEncoded: MutationEvent.EncodedWithMeta,
118
+ options?: {
119
+ /** Needed for rehydrateFromMutationLog */
120
+ skipMutationLog?: boolean
121
+ },
122
+ ) => Effect.Effect<
123
+ { sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' } },
124
+ SqliteError | UnexpectedError
125
+ >
126
+
114
127
  export type InitialBlockingSyncContext = {
115
128
  blockingDeferred: Deferred.Deferred<void> | undefined
116
129
  update: (_: { remaining: number; processed: number }) => Effect.Effect<void>
117
130
  }
118
131
 
119
- export type PullQueueItem = {
120
- payload: SyncState.PayloadUpstream
121
- remaining: number
122
- }
123
-
124
132
  export interface LeaderSyncProcessor {
133
+ /** Used by client sessions to subscribe to upstream sync state changes */
134
+ pull: (args: {
135
+ cursor: LeaderPullCursor
136
+ }) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }, UnexpectedError>
137
+ /** The `pullQueue` API can be used instead of `pull` when more convenient */
138
+ pullQueue: (args: {
139
+ cursor: LeaderPullCursor
140
+ }) => Effect.Effect<
141
+ Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
142
+ UnexpectedError,
143
+ Scope.Scope
144
+ >
145
+
146
+ /** Used by client sessions to push mutations to the leader thread */
125
147
  push: (
126
148
  /** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
127
149
  batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
128
150
  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
135
151
  /**
136
152
  * If true, the effect will only finish when the local push has been processed (i.e. succeeded or was rejected).
137
153
  * @default false
@@ -140,24 +156,18 @@ export interface LeaderSyncProcessor {
140
156
  },
141
157
  ) => Effect.Effect<void, LeaderAheadError>
142
158
 
159
+ /** Currently only used by devtools which don't provide their own event numbers */
143
160
  pushPartial: (args: {
144
161
  mutationEvent: MutationEvent.PartialAnyEncoded
145
162
  clientId: string
146
163
  sessionId: string
147
- }) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
148
- boot: (args: {
149
- dbReady: Deferred.Deferred<void>
150
- }) => Effect.Effect<
164
+ }) => Effect.Effect<void, UnexpectedError>
165
+
166
+ boot: Effect.Effect<
151
167
  { initialLeaderHead: EventId.EventId },
152
168
  UnexpectedError,
153
169
  LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
154
170
  >
155
171
  syncState: Subscribable.Subscribable<SyncState.SyncState>
156
- }
157
-
158
- export interface PullQueueSet {
159
- makeQueue: (
160
- since: EventId.EventId,
161
- ) => Effect.Effect<Queue.Queue<PullQueueItem>, UnexpectedError, Scope.Scope | LeaderThreadCtx>
162
- offer: (item: PullQueueItem) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
172
+ getMergeCounter: () => number
163
173
  }
@@ -2,35 +2,35 @@ import { memoizeByRef } from '@livestore/utils'
2
2
  import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
3
3
 
4
4
  import { type MigrationOptionsFromMutationLog, type SqliteDb, UnexpectedError } from './adapter-types.js'
5
- import { makeApplyMutation } from './leader-thread/apply-mutation.js'
6
- import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/mod.js'
7
- import { EventId, getMutationDef, MUTATION_LOG_META_TABLE } from './schema/mod.js'
5
+ import type { ApplyMutation } from './leader-thread/mod.js'
6
+ import type { LiveStoreSchema, MutationDef, MutationLogMetaRow } from './schema/mod.js'
7
+ import { EventId, getMutationDef, MUTATION_LOG_META_TABLE, MutationEvent } from './schema/mod.js'
8
8
  import type { PreparedBindValues } from './util.js'
9
9
  import { sql } from './util.js'
10
10
 
11
11
  export const rehydrateFromMutationLog = ({
12
- logDb,
12
+ dbMutationLog,
13
13
  // TODO re-use this db when bringing back the boot in-memory db implementation
14
14
  // db,
15
15
  schema,
16
16
  migrationOptions,
17
17
  onProgress,
18
+ applyMutation,
18
19
  }: {
19
- logDb: SqliteDb
20
- db: SqliteDb
20
+ dbMutationLog: SqliteDb
21
+ // db: SqliteDb
21
22
  schema: LiveStoreSchema
22
23
  migrationOptions: MigrationOptionsFromMutationLog
23
24
  onProgress: (_: { done: number; total: number }) => Effect.Effect<void>
25
+ applyMutation: ApplyMutation
24
26
  }) =>
25
27
  Effect.gen(function* () {
26
- const mutationsCount = logDb.select<{ count: number }>(
28
+ const mutationsCount = dbMutationLog.select<{ count: number }>(
27
29
  `SELECT COUNT(*) AS count FROM ${MUTATION_LOG_META_TABLE}`,
28
30
  )[0]!.count
29
31
 
30
32
  const hashMutation = memoizeByRef((mutation: MutationDef.Any) => Schema.hash(mutation.schema))
31
33
 
32
- const applyMutation = yield* makeApplyMutation
33
-
34
34
  const processMutation = (row: MutationLogMetaRow) =>
35
35
  Effect.gen(function* () {
36
36
  const mutationDef = getMutationDef(schema, row.mutation)
@@ -59,21 +59,21 @@ This likely means the schema has changed in an incompatible way.
59
59
  ),
60
60
  )
61
61
 
62
- const mutationEventEncoded = {
62
+ const mutationEventEncoded = MutationEvent.EncodedWithMeta.make({
63
63
  id: { global: row.idGlobal, client: row.idClient },
64
64
  parentId: { global: row.parentIdGlobal, client: row.parentIdClient },
65
65
  mutation: row.mutation,
66
66
  args,
67
67
  clientId: row.clientId,
68
68
  sessionId: row.sessionId,
69
- } satisfies MutationEvent.AnyEncoded
69
+ })
70
70
 
71
71
  yield* applyMutation(mutationEventEncoded, { skipMutationLog: true })
72
72
  }).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
73
73
 
74
74
  const CHUNK_SIZE = 100
75
75
 
76
- const stmt = logDb.prepare(sql`\
76
+ const stmt = dbMutationLog.prepare(sql`\
77
77
  SELECT * FROM ${MUTATION_LOG_META_TABLE}
78
78
  WHERE idGlobal > $idGlobal OR (idGlobal = $idGlobal AND idClient > $idClient)
79
79
  ORDER BY idGlobal ASC, idClient ASC
@@ -18,9 +18,22 @@ export const clientDefault = 0 as any as ClientEventId
18
18
  */
19
19
  export type EventId = { global: GlobalEventId; client: ClientEventId }
20
20
 
21
+ // export const EventSequenceNumber = Schema.Struct({})
22
+ // export const EventNumber = Schema.Struct({})
23
+ // export const ClientEventNumber = Schema.Struct({})
24
+ // export const GlobalEventNumber = Schema.Struct({})
25
+
26
+ /**
27
+ * NOTE: Client mutation events with a non-0 client id, won't be synced to the sync backend.
28
+ */
21
29
  export const EventId = Schema.Struct({
22
30
  global: GlobalEventId,
31
+ /** Only increments for clientOnly mutations */
23
32
  client: ClientEventId,
33
+
34
+ // TODO: actually add this field
35
+ // Client only
36
+ // generation: Schema.Number.pipe(Schema.optional),
24
37
  }).annotations({ title: 'LiveStore.EventId' })
25
38
 
26
39
  /**
@@ -36,7 +49,7 @@ export const compare = (a: EventId, b: EventId) => {
36
49
  /**
37
50
  * Convert an event id to a string representation.
38
51
  */
39
- export const toString = (id: EventId) => `(${id.global},${id.client})`
52
+ export const toString = (id: EventId) => (id.client === 0 ? `e${id.global}` : `e${id.global}+${id.client}`)
40
53
 
41
54
  /**
42
55
  * Convert a string representation of an event id to an event id.
@@ -53,7 +66,7 @@ export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.cl
53
66
 
54
67
  export type EventIdPair = { id: EventId; parentId: EventId }
55
68
 
56
- export const ROOT = { global: -1 as any as GlobalEventId, client: clientDefault } satisfies EventId
69
+ export const ROOT = { global: 0 as any as GlobalEventId, client: clientDefault } satisfies EventId
57
70
 
58
71
  export const isGreaterThan = (a: EventId, b: EventId) => {
59
72
  return a.global > b.global || (a.global === b.global && a.client > b.client)
@@ -63,6 +76,17 @@ export const isGreaterThanOrEqual = (a: EventId, b: EventId) => {
63
76
  return a.global > b.global || (a.global === b.global && a.client >= b.client)
64
77
  }
65
78
 
79
+ export const max = (a: EventId, b: EventId) => {
80
+ return a.global > b.global || (a.global === b.global && a.client > b.client) ? a : b
81
+ }
82
+
83
+ export const diff = (a: EventId, b: EventId) => {
84
+ return {
85
+ global: a.global - b.global,
86
+ client: a.client - b.client,
87
+ }
88
+ }
89
+
66
90
  export const make = (id: EventId | typeof EventId.Encoded): EventId => {
67
91
  return Schema.is(EventId)(id) ? id : Schema.decodeSync(EventId)(id)
68
92
  }
@@ -1,5 +1,5 @@
1
1
  import { memoizeByRef } from '@livestore/utils'
2
- import { Schema } from '@livestore/utils/effect'
2
+ import { Option, Schema } from '@livestore/utils/effect'
3
3
 
4
4
  import * as EventId from './EventId.js'
5
5
  import type { MutationDef, MutationDefRecord } from './mutations.js'
@@ -155,16 +155,29 @@ export const makeMutationEventSchemaMemo = memoizeByRef(makeMutationEventSchema)
155
155
  export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEvent.EncodedWithMeta')({
156
156
  mutation: Schema.String,
157
157
  args: Schema.Any,
158
+ // TODO rename to `.num` / `.parentNum`
158
159
  id: EventId.EventId,
159
160
  parentId: EventId.EventId,
160
161
  clientId: Schema.String,
161
162
  sessionId: Schema.String,
162
163
  // TODO get rid of `meta` again by cleaning up the usage implementations
163
- meta: Schema.optionalWith(
164
- Schema.Any as Schema.Schema<{
165
- sessionChangeset?: Uint8Array
166
- }>,
167
- { default: () => ({}) },
164
+ meta: Schema.Struct({
165
+ sessionChangeset: Schema.Union(
166
+ Schema.TaggedStruct('sessionChangeset', {
167
+ data: Schema.Uint8Array,
168
+ debug: Schema.Any.pipe(Schema.optional),
169
+ }),
170
+ Schema.TaggedStruct('no-op', {}),
171
+ Schema.TaggedStruct('unset', {}),
172
+ ),
173
+ syncMetadata: Schema.Option(Schema.JsonValue),
174
+ }).pipe(
175
+ Schema.mutable,
176
+ Schema.optional,
177
+ Schema.withDefaults({
178
+ constructor: () => ({ sessionChangeset: { _tag: 'unset' as const }, syncMetadata: Option.none() }),
179
+ decoding: () => ({ sessionChangeset: { _tag: 'unset' as const }, syncMetadata: Option.none() }),
180
+ }),
168
181
  ),
169
182
  }) {
170
183
  toJSON = (): any => {
@@ -172,7 +185,7 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
172
185
  // - More readable way to print the id + parentId
173
186
  // - not including `meta`, `clientId`, `sessionId`
174
187
  return {
175
- id: `(${this.id.global},${this.id.client}) (${this.parentId.global},${this.parentId.client})`,
188
+ id: `${EventId.toString(this.id)} → ${EventId.toString(this.parentId)} (${this.clientId}, ${this.sessionId})`,
176
189
  mutation: this.mutation,
177
190
  args: this.args,
178
191
  }
@@ -180,19 +193,20 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
180
193
 
181
194
  /**
182
195
  * 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)
196
+ * For event id e2e1 which should be rebased on event id e3e2
197
+ * the resulting event id will be e4e3
185
198
  *
186
199
  * 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)
200
+ * For event id e2+1 → e2 which should be rebased on event id e3e2
201
+ * the resulting event id will be e3+1 → e3
189
202
  *
190
- * Syntax: (2,1)(2,0)
191
- * ^ ^ ^ ^
192
- * | | | +- client parent id
193
- * | | +--- global parent id
203
+ * Syntax: e2+2 → e2+1
204
+ * ^ ^ ^ ^
205
+ * | | | +- client parent id
206
+ * | | +--- global parent id
194
207
  * | +-- client id
195
208
  * +---- global id
209
+ * Client id is ommitted for global events
196
210
  */
197
211
  rebase = (parentId: EventId.EventId, isClient: boolean) =>
198
212
  new EncodedWithMeta({
@@ -200,11 +214,12 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
200
214
  ...EventId.nextPair(parentId, isClient),
201
215
  })
202
216
 
203
- static fromGlobal = (mutationEvent: AnyEncodedGlobal) =>
217
+ static fromGlobal = (mutationEvent: AnyEncodedGlobal, syncMetadata: Option.Option<Schema.JsonValue>) =>
204
218
  new EncodedWithMeta({
205
219
  ...mutationEvent,
206
220
  id: { global: mutationEvent.id, client: EventId.clientDefault },
207
221
  parentId: { global: mutationEvent.parentId, client: EventId.clientDefault },
222
+ meta: { sessionChangeset: { _tag: 'unset' as const }, syncMetadata },
208
223
  })
209
224
 
210
225
  toGlobal = (): AnyEncodedGlobal => ({
@@ -214,6 +229,7 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
214
229
  })
215
230
  }
216
231
 
232
+ /** NOTE `meta` is not considered for equality */
217
233
  export const isEqualEncoded = (a: AnyEncoded, b: AnyEncoded) =>
218
234
  a.id.global === b.id.global &&
219
235
  a.id.client === b.id.client &&
@@ -112,7 +112,7 @@ export const makeSchema = <TInputSchema extends InputSchema>(
112
112
  _MutationDefMapType: Symbol.for('livestore.MutationDefMapType') as any,
113
113
  tables,
114
114
  mutations,
115
- migrationOptions: inputSchema.migrations ?? { strategy: 'hard-reset' },
115
+ migrationOptions: inputSchema.migrations ?? { strategy: 'from-mutation-log' },
116
116
  hash,
117
117
  } satisfies LiveStoreSchema
118
118
  }
@@ -60,7 +60,25 @@ export const sessionChangesetMetaTable = table(
60
60
 
61
61
  export type SessionChangesetMetaRow = FromTable.RowDecoded<typeof sessionChangesetMetaTable>
62
62
 
63
- export const systemTables = [schemaMetaTable, schemaMutationsMetaTable, sessionChangesetMetaTable]
63
+ export const LEADER_MERGE_COUNTER_TABLE = '__livestore_leader_merge_counter'
64
+
65
+ export const leaderMergeCounterTable = table(
66
+ LEADER_MERGE_COUNTER_TABLE,
67
+ {
68
+ id: SqliteDsl.integer({ primaryKey: true, schema: Schema.Literal(0) }),
69
+ mergeCounter: SqliteDsl.integer({ primaryKey: true }),
70
+ },
71
+ { disableAutomaticIdColumn: true },
72
+ )
73
+
74
+ export type LeaderMergeCounterRow = FromTable.RowDecoded<typeof leaderMergeCounterTable>
75
+
76
+ export const systemTables = [
77
+ schemaMetaTable,
78
+ schemaMutationsMetaTable,
79
+ sessionChangesetMetaTable,
80
+ leaderMergeCounterTable,
81
+ ]
64
82
 
65
83
  /// Mutation log DB
66
84
 
@@ -72,6 +90,7 @@ export const MUTATION_LOG_META_TABLE = 'mutation_log'
72
90
  export const mutationLogMetaTable = table(
73
91
  MUTATION_LOG_META_TABLE,
74
92
  {
93
+ // TODO Adjust modeling so a global event never needs a client id component
75
94
  idGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventId.GlobalEventId }),
76
95
  idClient: SqliteDsl.integer({ primaryKey: true, schema: EventId.ClientEventId }),
77
96
  parentIdGlobal: SqliteDsl.integer({ schema: EventId.GlobalEventId }),
@@ -5,8 +5,9 @@ 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 { getMutationDef, type LiveStoreSchema } from '../schema/mod.js'
8
+ import { getMutationDef, LEADER_MERGE_COUNTER_TABLE, type LiveStoreSchema } from '../schema/mod.js'
9
9
  import * as MutationEvent from '../schema/MutationEvent.js'
10
+ import { sql } from '../util.js'
10
11
  import * as SyncState from './syncstate.js'
11
12
 
12
13
  /**
@@ -39,7 +40,7 @@ export const makeClientSessionSyncProcessor = ({
39
40
  options: { otelContext: otel.Context; withChangeset: boolean },
40
41
  ) => {
41
42
  writeTables: Set<string>
42
- sessionChangeset: Uint8Array | undefined
43
+ sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' } | { _tag: 'unset' }
43
44
  }
44
45
  rollback: (changeset: Uint8Array) => void
45
46
  refreshTables: (tables: Set<string>) => void
@@ -56,12 +57,12 @@ export const makeClientSessionSyncProcessor = ({
56
57
  const mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
57
58
 
58
59
  const syncStateRef = {
60
+ // The initial state is identical to the leader's initial state
59
61
  current: new SyncState.SyncState({
60
62
  localHead: clientSession.leaderThread.initialState.leaderHead,
61
63
  upstreamHead: clientSession.leaderThread.initialState.leaderHead,
64
+ // Given we're starting with the leader's snapshot, we don't have any pending mutations intially
62
65
  pending: [],
63
- // TODO init rollbackTail from leader to be ready for backend rebasing
64
- rollbackTail: [],
65
66
  }),
66
67
  }
67
68
 
@@ -76,13 +77,14 @@ export const makeClientSessionSyncProcessor = ({
76
77
  // TODO validate batch
77
78
 
78
79
  let baseEventId = syncStateRef.current.localHead
79
- const encodedMutationEvents = batch.map((mutationEvent) => {
80
- const mutationDef = getMutationDef(schema, mutationEvent.mutation)
80
+ const encodedMutationEvents = batch.map(({ mutation, args }) => {
81
+ const mutationDef = getMutationDef(schema, mutation)
81
82
  const nextIdPair = EventId.nextPair(baseEventId, mutationDef.options.clientOnly)
82
83
  baseEventId = nextIdPair.id
83
84
  return new MutationEvent.EncodedWithMeta(
84
85
  Schema.encodeUnknownSync(mutationEventSchema)({
85
- ...mutationEvent,
86
+ mutation,
87
+ args,
86
88
  ...nextIdPair,
87
89
  clientId: clientSession.clientId,
88
90
  sessionId: clientSession.sessionId,
@@ -168,13 +170,21 @@ export const makeClientSessionSyncProcessor = ({
168
170
 
169
171
  yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
170
172
 
173
+ const getMergeCounter = () =>
174
+ clientSession.sqliteDb.select<{ mergeCounter: number }>(
175
+ sql`SELECT mergeCounter FROM ${LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
176
+ )[0]?.mergeCounter ?? 0
177
+
171
178
  // NOTE We need to lazily call `.pull` as we want the cursor to be updated
172
179
  yield* Stream.suspend(() =>
173
- clientSession.leaderThread.mutations.pull({ cursor: syncStateRef.current.localHead }),
180
+ clientSession.leaderThread.mutations.pull({
181
+ cursor: { mergeCounter: getMergeCounter(), eventId: syncStateRef.current.localHead },
182
+ }),
174
183
  ).pipe(
175
- Stream.tap(({ payload, remaining }) =>
184
+ Stream.tap(({ payload, mergeCounter: leaderMergeCounter }) =>
176
185
  Effect.gen(function* () {
177
- // console.log('pulled payload from leader', { payload, remaining })
186
+ // yield* Effect.logDebug('ClientSessionSyncProcessor:pull', payload)
187
+
178
188
  if (clientSession.devtools.enabled) {
179
189
  yield* clientSession.devtools.pullLatch.await
180
190
  }
@@ -196,13 +206,13 @@ export const makeClientSessionSyncProcessor = ({
196
206
  syncStateUpdateQueue.offer(mergeResult.newSyncState).pipe(Effect.runSync)
197
207
 
198
208
  if (mergeResult._tag === 'rebase') {
199
- span.addEvent('pull:rebase', {
209
+ span.addEvent('merge:pull:rebase', {
200
210
  payloadTag: payload._tag,
201
211
  payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
202
212
  newEventsCount: mergeResult.newEvents.length,
203
- rollbackCount: mergeResult.eventsToRollback.length,
213
+ rollbackCount: mergeResult.rollbackEvents.length,
204
214
  res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
205
- remaining,
215
+ leaderMergeCounter,
206
216
  })
207
217
 
208
218
  debugInfo.rebaseCount++
@@ -216,28 +226,29 @@ export const makeClientSessionSyncProcessor = ({
216
226
 
217
227
  if (LS_DEV) {
218
228
  Effect.logDebug(
219
- 'pull:rebase: rollback',
220
- mergeResult.eventsToRollback.length,
221
- ...mergeResult.eventsToRollback.slice(0, 10).map((_) => _.toJSON()),
229
+ 'merge:pull:rebase: rollback',
230
+ mergeResult.rollbackEvents.length,
231
+ ...mergeResult.rollbackEvents.slice(0, 10).map((_) => _.toJSON()),
232
+ { leaderMergeCounter },
222
233
  ).pipe(Effect.provide(runtime), Effect.runSync)
223
234
  }
224
235
 
225
- for (let i = mergeResult.eventsToRollback.length - 1; i >= 0; i--) {
226
- const event = mergeResult.eventsToRollback[i]!
227
- if (event.meta.sessionChangeset) {
228
- rollback(event.meta.sessionChangeset)
229
- event.meta.sessionChangeset = undefined
236
+ for (let i = mergeResult.rollbackEvents.length - 1; i >= 0; i--) {
237
+ const event = mergeResult.rollbackEvents[i]!
238
+ if (event.meta.sessionChangeset._tag !== 'no-op' && event.meta.sessionChangeset._tag !== 'unset') {
239
+ rollback(event.meta.sessionChangeset.data)
240
+ event.meta.sessionChangeset = { _tag: 'unset' }
230
241
  }
231
242
  }
232
243
 
233
244
  yield* BucketQueue.offerAll(leaderPushQueue, mergeResult.newSyncState.pending)
234
245
  } else {
235
- span.addEvent('pull:advance', {
246
+ span.addEvent('merge:pull:advance', {
236
247
  payloadTag: payload._tag,
237
248
  payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
238
249
  newEventsCount: mergeResult.newEvents.length,
239
250
  res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
240
- remaining,
251
+ leaderMergeCounter,
241
252
  })
242
253
 
243
254
  debugInfo.advanceCount++
@@ -247,6 +258,7 @@ export const makeClientSessionSyncProcessor = ({
247
258
 
248
259
  const writeTables = new Set<string>()
249
260
  for (const mutationEvent of mergeResult.newEvents) {
261
+ // TODO apply changeset if available (will require tracking of write tables as well)
250
262
  const decodedMutationEvent = Schema.decodeSync(mutationEventSchema)(mutationEvent)
251
263
  const res = applyMutation(decodedMutationEvent, { otelContext, withChangeset: true })
252
264
  for (const table of res.writeTables) {
package/src/sync/sync.ts CHANGED
@@ -36,6 +36,10 @@ export type SyncBackendConstructor<TSyncMetadata = Schema.JsonValue> = (
36
36
  ) => Effect.Effect<SyncBackend<TSyncMetadata>, UnexpectedError, Scope.Scope | HttpClient.HttpClient>
37
37
 
38
38
  export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
39
+ /**
40
+ * Can be implemented to prepare a connection to the sync backend to speed up the first pull/push.
41
+ */
42
+ connect: Effect.Effect<void, IsOfflineError | UnexpectedError, HttpClient.HttpClient | Scope.Scope>
39
43
  pull: (
40
44
  args: Option.Option<{
41
45
  cursor: EventId.EventId
@@ -60,17 +64,10 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
60
64
  * - event ids must be in ascending order
61
65
  * */
62
66
  batch: ReadonlyArray<MutationEvent.AnyEncodedGlobal>,
63
- ) => Effect.Effect<
64
- {
65
- /** Indexes are relative to `batch` */
66
- metadata: ReadonlyArray<Option.Option<TSyncMetadata>>
67
- },
68
- IsOfflineError | InvalidPushError,
69
- HttpClient.HttpClient
70
- >
67
+ ) => Effect.Effect<void, IsOfflineError | InvalidPushError, HttpClient.HttpClient>
71
68
  isConnected: SubscriptionRef.SubscriptionRef<boolean>
72
69
  /**
73
- * Metadata describing the sync backend.
70
+ * Metadata describing the sync backend. (Currently only used by devtools.)
74
71
  */
75
72
  metadata: { name: string; description: string } & Record<string, Schema.JsonValue>
76
73
  }