@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
@@ -1,46 +1,43 @@
1
1
  import { LS_DEV, memoizeByRef, shouldNeverHappen } from '@livestore/utils'
2
- import type { Scope } from '@livestore/utils/effect'
3
- import { Effect, Option, Schema } from '@livestore/utils/effect'
2
+ import { Effect, ReadonlyArray, Schema } from '@livestore/utils/effect'
4
3
 
5
- import type { PreparedBindValues, SqliteDb, SqliteError, UnexpectedError } from '../index.js'
4
+ import type { SqliteDb } from '../adapter-types.js'
6
5
  import { getExecArgsFromMutation } from '../mutation.js'
6
+ import type { LiveStoreSchema, MutationEvent, SessionChangesetMetaRow } from '../schema/mod.js'
7
7
  import {
8
8
  EventId,
9
9
  getMutationDef,
10
- type LiveStoreSchema,
11
10
  MUTATION_LOG_META_TABLE,
12
- type MutationEvent,
13
- mutationLogMetaTable,
14
11
  SESSION_CHANGESET_META_TABLE,
15
12
  sessionChangesetMetaTable,
16
13
  } from '../schema/mod.js'
17
14
  import { insertRow } from '../sql-queries/index.js'
15
+ import { sql } from '../util.js'
18
16
  import { execSql, execSqlPrepared } from './connection.js'
19
- import { LeaderThreadCtx } from './types.js'
20
-
21
- export type ApplyMutation = (
22
- mutationEventEncoded: MutationEvent.AnyEncoded,
23
- options?: {
24
- /** Needed for rehydrateFromMutationLog */
25
- skipMutationLog?: boolean
26
- },
27
- ) => Effect.Effect<void, SqliteError | UnexpectedError>
28
-
29
- export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope | LeaderThreadCtx> = Effect.gen(
30
- function* () {
31
- const leaderThreadCtx = yield* LeaderThreadCtx
32
- const shouldExcludeMutationFromLog = makeShouldExcludeMutationFromLog(leaderThreadCtx.schema)
17
+ import * as Mutationlog from './mutationlog.js'
18
+ import type { ApplyMutation } from './types.js'
19
+
20
+ export const makeApplyMutation = ({
21
+ schema,
22
+ dbReadModel: db,
23
+ dbMutationLog,
24
+ }: {
25
+ schema: LiveStoreSchema
26
+ dbReadModel: SqliteDb
27
+ dbMutationLog: SqliteDb
28
+ }): Effect.Effect<ApplyMutation, never> =>
29
+ Effect.gen(function* () {
30
+ const shouldExcludeMutationFromLog = makeShouldExcludeMutationFromLog(schema)
33
31
 
34
32
  const mutationDefSchemaHashMap = new Map(
35
33
  // TODO Running `Schema.hash` can be a bottleneck for larger schemas. There is an opportunity to run this
36
34
  // at build time and lookup the pre-computed hash at runtime.
37
35
  // Also see https://github.com/Effect-TS/effect/issues/2719
38
- [...leaderThreadCtx.schema.mutations.map.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
36
+ [...schema.mutations.map.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
39
37
  )
40
38
 
41
39
  return (mutationEventEncoded, options) =>
42
40
  Effect.gen(function* () {
43
- const { schema, dbReadModel: db, dbMutationLog } = leaderThreadCtx
44
41
  const skipMutationLog = options?.skipMutationLog ?? false
45
42
 
46
43
  const mutationName = mutationEventEncoded.mutation
@@ -84,7 +81,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
84
81
  idClient: mutationEventEncoded.id.client,
85
82
  // NOTE the changeset will be empty (i.e. null) for no-op mutations
86
83
  changeset: changeset ?? null,
87
- debug: execArgsArr,
84
+ debug: LS_DEV ? execArgsArr : null,
88
85
  },
89
86
  }),
90
87
  )
@@ -94,76 +91,89 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
94
91
  // write to mutation_log
95
92
  const excludeFromMutationLog = shouldExcludeMutationFromLog(mutationName, mutationEventEncoded)
96
93
  if (skipMutationLog === false && excludeFromMutationLog === false) {
97
- yield* insertIntoMutationLog(
94
+ const mutationName = mutationEventEncoded.mutation
95
+ const mutationDefSchemaHash =
96
+ mutationDefSchemaHashMap.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
97
+
98
+ yield* Mutationlog.insertIntoMutationLog(
98
99
  mutationEventEncoded,
99
100
  dbMutationLog,
100
- mutationDefSchemaHashMap,
101
+ mutationDefSchemaHash,
101
102
  mutationEventEncoded.clientId,
102
103
  mutationEventEncoded.sessionId,
103
104
  )
104
105
  } else {
105
106
  // console.debug('[@livestore/common:leader-thread] skipping mutation log write', mutation, statementSql, bindValues)
106
107
  }
108
+
109
+ return {
110
+ sessionChangeset: changeset
111
+ ? {
112
+ _tag: 'sessionChangeset' as const,
113
+ data: changeset,
114
+ debug: LS_DEV ? execArgsArr : null,
115
+ }
116
+ : { _tag: 'no-op' as const },
117
+ }
107
118
  }).pipe(
108
119
  Effect.withSpan(`@livestore/common:leader-thread:applyMutation`, {
109
120
  attributes: {
110
121
  mutationName: mutationEventEncoded.mutation,
111
122
  mutationId: mutationEventEncoded.id,
112
- 'span.label': `(${mutationEventEncoded.id.global},${mutationEventEncoded.id.client}) ${mutationEventEncoded.mutation}`,
123
+ 'span.label': `${EventId.toString(mutationEventEncoded.id)} ${mutationEventEncoded.mutation}`,
113
124
  },
114
125
  }),
115
126
  // Effect.logDuration('@livestore/common:leader-thread:applyMutation'),
116
127
  )
117
- },
118
- )
119
-
120
- const insertIntoMutationLog = (
121
- mutationEventEncoded: MutationEvent.AnyEncoded,
122
- dbMutationLog: SqliteDb,
123
- mutationDefSchemaHashMap: Map<string, number>,
124
- clientId: string,
125
- sessionId: string,
126
- ) =>
128
+ })
129
+
130
+ export const rollback = ({
131
+ db,
132
+ dbMutationLog,
133
+ eventIdsToRollback,
134
+ }: {
135
+ db: SqliteDb
136
+ dbMutationLog: SqliteDb
137
+ eventIdsToRollback: EventId.EventId[]
138
+ }) =>
127
139
  Effect.gen(function* () {
128
- const mutationName = mutationEventEncoded.mutation
129
- const mutationDefSchemaHash =
130
- mutationDefSchemaHashMap.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
131
-
132
- if (LS_DEV && mutationEventEncoded.parentId.global !== EventId.ROOT.global) {
133
- const parentMutationExists =
134
- dbMutationLog.select<{ count: number }>(
135
- `SELECT COUNT(*) as count FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ? AND idClient = ?`,
136
- [mutationEventEncoded.parentId.global, mutationEventEncoded.parentId.client] as any as PreparedBindValues,
137
- )[0]!.count === 1
138
-
139
- if (parentMutationExists === false) {
140
- shouldNeverHappen(
141
- `Parent mutation ${mutationEventEncoded.parentId.global},${mutationEventEncoded.parentId.client} does not exist`,
142
- )
140
+ const rollbackEvents = db
141
+ .select<SessionChangesetMetaRow>(
142
+ sql`SELECT * FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
143
+ )
144
+ .map((_) => ({ id: { global: _.idGlobal, client: _.idClient }, changeset: _.changeset, debug: _.debug }))
145
+ .toSorted((a, b) => EventId.compare(a.id, b.id))
146
+
147
+ // Apply changesets in reverse order
148
+ for (let i = rollbackEvents.length - 1; i >= 0; i--) {
149
+ const { changeset } = rollbackEvents[i]!
150
+ if (changeset !== null) {
151
+ db.makeChangeset(changeset).invert().apply()
143
152
  }
144
153
  }
145
154
 
146
- // TODO use prepared statements
147
- yield* execSql(
148
- dbMutationLog,
149
- ...insertRow({
150
- tableName: MUTATION_LOG_META_TABLE,
151
- columns: mutationLogMetaTable.sqliteDef.columns,
152
- values: {
153
- idGlobal: mutationEventEncoded.id.global,
154
- idClient: mutationEventEncoded.id.client,
155
- parentIdGlobal: mutationEventEncoded.parentId.global,
156
- parentIdClient: mutationEventEncoded.parentId.client,
157
- mutation: mutationEventEncoded.mutation,
158
- argsJson: mutationEventEncoded.args ?? {},
159
- clientId,
160
- sessionId,
161
- schemaHash: mutationDefSchemaHash,
162
- syncMetadataJson: Option.none(),
163
- },
164
- }),
155
+ const eventIdPairChunks = ReadonlyArray.chunksOf(100)(
156
+ eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`),
165
157
  )
166
- })
158
+
159
+ // Delete the changeset rows
160
+ for (const eventIdPairChunk of eventIdPairChunks) {
161
+ db.execute(
162
+ sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
163
+ )
164
+ }
165
+
166
+ // Delete the mutation log rows
167
+ for (const eventIdPairChunk of eventIdPairChunks) {
168
+ dbMutationLog.execute(
169
+ sql`DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
170
+ )
171
+ }
172
+ }).pipe(
173
+ Effect.withSpan('@livestore/common:LeaderSyncProcessor:rollback', {
174
+ attributes: { count: eventIdsToRollback.length },
175
+ }),
176
+ )
167
177
 
168
178
  // TODO let's consider removing this "should exclude" mechanism in favour of log compaction etc
169
179
  const makeShouldExcludeMutationFromLog = memoizeByRef((schema: LiveStoreSchema) => {
@@ -15,7 +15,7 @@ export const bootDevtools = (options: DevtoolsOptions) =>
15
15
  return
16
16
  }
17
17
 
18
- const { connectedClientSessionPullQueues, syncProcessor, extraIncomingMessagesQueue } = yield* LeaderThreadCtx
18
+ const { syncProcessor, extraIncomingMessagesQueue } = yield* LeaderThreadCtx
19
19
 
20
20
  yield* listenToDevtools({
21
21
  incomingMessages: Stream.fromQueue(extraIncomingMessagesQueue),
@@ -33,13 +33,11 @@ export const bootDevtools = (options: DevtoolsOptions) =>
33
33
  Effect.ignoreLogged,
34
34
  )
35
35
 
36
- const { localHead } = yield* syncProcessor.syncState
36
+ const syncState = yield* syncProcessor.syncState
37
+ const mergeCounter = syncProcessor.getMergeCounter()
37
38
 
38
- // TODO close queue when devtools disconnects
39
- const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(localHead)
40
-
41
- yield* Stream.fromQueue(pullQueue).pipe(
42
- Stream.tap((msg) => sendMessage(Devtools.Leader.SyncPull.make({ payload: msg.payload, liveStoreVersion }))),
39
+ yield* syncProcessor.pull({ cursor: { mergeCounter, eventId: syncState.localHead } }).pipe(
40
+ Stream.tap(({ payload }) => sendMessage(Devtools.Leader.SyncPull.make({ payload, liveStoreVersion }))),
43
41
  Stream.runDrain,
44
42
  Effect.forkScoped,
45
43
  )
@@ -5,14 +5,13 @@ import type { BootStatus, MakeSqliteDb, MigrationsReport, SqliteError } from '..
5
5
  import { UnexpectedError } from '../adapter-types.js'
6
6
  import type * as Devtools from '../devtools/mod.js'
7
7
  import type { LiveStoreSchema } from '../schema/mod.js'
8
- import { EventId, MutationEvent, mutationLogMetaTable, SYNC_STATUS_TABLE, syncStatusTable } from '../schema/mod.js'
9
- import { migrateTable } from '../schema-management/migrations.js'
8
+ import { MutationEvent } from '../schema/mod.js'
10
9
  import type { InvalidPullError, IsOfflineError, SyncOptions } from '../sync/sync.js'
11
10
  import { sql } from '../util.js'
12
- import { execSql } from './connection.js'
11
+ import { makeApplyMutation } from './apply-mutation.js'
13
12
  import { bootDevtools } from './leader-worker-devtools.js'
14
13
  import { makeLeaderSyncProcessor } from './LeaderSyncProcessor.js'
15
- import { makePullQueueSet } from './pull-queue-set.js'
14
+ import * as Mutationlog from './mutationlog.js'
16
15
  import { recreateDb } from './recreate-db.js'
17
16
  import type { ShutdownChannel } from './shutdown-channel.js'
18
17
  import type {
@@ -52,7 +51,10 @@ export const makeLeaderThreadLayer = ({
52
51
 
53
52
  // TODO do more validation here than just checking the count of tables
54
53
  // Either happens on initial boot or if schema changes
55
- const dbMissing =
54
+ const dbMutationLogMissing =
55
+ dbMutationLog.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
56
+
57
+ const dbReadModelMissing =
56
58
  dbReadModel.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
57
59
 
58
60
  const syncBackend =
@@ -60,6 +62,11 @@ export const makeLeaderThreadLayer = ({
60
62
  ? undefined
61
63
  : yield* syncOptions.backend({ storeId, clientId, payload: syncPayload })
62
64
 
65
+ if (syncBackend !== undefined) {
66
+ // We're already connecting to the sync backend concurrently
67
+ yield* syncBackend.connect.pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
68
+ }
69
+
63
70
  const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({
64
71
  initialSyncOptions: syncOptions?.initialSyncOptions ?? { _tag: 'Skip' },
65
72
  bootStatusQueue,
@@ -67,10 +74,11 @@ export const makeLeaderThreadLayer = ({
67
74
 
68
75
  const syncProcessor = yield* makeLeaderSyncProcessor({
69
76
  schema,
70
- dbMissing,
77
+ dbMutationLogMissing,
71
78
  dbMutationLog,
79
+ dbReadModel,
80
+ dbReadModelMissing,
72
81
  initialBlockingSyncContext,
73
- clientId,
74
82
  onError: syncOptions?.onSyncError ?? 'ignore',
75
83
  })
76
84
 
@@ -86,6 +94,8 @@ export const makeLeaderThreadLayer = ({
86
94
  }
87
95
  : { enabled: false as const }
88
96
 
97
+ const applyMutation = yield* makeApplyMutation({ schema, dbReadModel, dbMutationLog })
98
+
89
99
  const ctx = {
90
100
  schema,
91
101
  bootStatusQueue,
@@ -99,7 +109,7 @@ export const makeLeaderThreadLayer = ({
99
109
  shutdownChannel,
100
110
  syncBackend,
101
111
  syncProcessor,
102
- connectedClientSessionPullQueues: yield* makePullQueueSet,
112
+ applyMutation,
103
113
  extraIncomingMessagesQueue,
104
114
  devtools: devtoolsContext,
105
115
  // State will be set during `bootLeaderThread`
@@ -112,7 +122,7 @@ export const makeLeaderThreadLayer = ({
112
122
  const layer = Layer.succeed(LeaderThreadCtx, ctx)
113
123
 
114
124
  ctx.initialState = yield* bootLeaderThread({
115
- dbMissing,
125
+ dbReadModelMissing,
116
126
  initialBlockingSyncContext,
117
127
  devtoolsOptions,
118
128
  }).pipe(Effect.provide(layer))
@@ -122,6 +132,7 @@ export const makeLeaderThreadLayer = ({
122
132
  Effect.withSpan('@livestore/common:leader-thread:boot'),
123
133
  Effect.withSpanScoped('@livestore/common:leader-thread'),
124
134
  UnexpectedError.mapToUnexpectedError,
135
+ Effect.tapCauseLogPretty,
125
136
  Layer.unwrapScoped,
126
137
  )
127
138
 
@@ -177,11 +188,11 @@ const makeInitialBlockingSyncContext = ({
177
188
  * It also starts various background processes (e.g. syncing)
178
189
  */
179
190
  const bootLeaderThread = ({
180
- dbMissing,
191
+ dbReadModelMissing,
181
192
  initialBlockingSyncContext,
182
193
  devtoolsOptions,
183
194
  }: {
184
- dbMissing: boolean
195
+ dbReadModelMissing: boolean
185
196
  initialBlockingSyncContext: InitialBlockingSyncContext
186
197
  devtoolsOptions: DevtoolsOptions
187
198
  }): Effect.Effect<
@@ -192,44 +203,18 @@ const bootLeaderThread = ({
192
203
  Effect.gen(function* () {
193
204
  const { dbMutationLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
194
205
 
195
- yield* migrateTable({
196
- db: dbMutationLog,
197
- behaviour: 'create-if-not-exists',
198
- tableAst: mutationLogMetaTable.sqliteDef.ast,
199
- skipMetaTable: true,
200
- })
201
-
202
- yield* migrateTable({
203
- db: dbMutationLog,
204
- behaviour: 'create-if-not-exists',
205
- tableAst: syncStatusTable.sqliteDef.ast,
206
- skipMetaTable: true,
207
- })
208
-
209
- // Create sync status row if it doesn't exist
210
- yield* execSql(
211
- dbMutationLog,
212
- sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
213
- SELECT ${EventId.ROOT.global}
214
- WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
215
- {},
216
- )
217
-
218
- const dbReady = yield* Deferred.make<void>()
219
-
220
- // We're already starting pulling from the sync backend concurrently but wait until the db is ready before
221
- // processing any incoming mutations
222
- const { initialLeaderHead } = yield* syncProcessor.boot({ dbReady })
206
+ yield* Mutationlog.initMutationLogDb(dbMutationLog)
223
207
 
224
208
  let migrationsReport: MigrationsReport
225
- if (dbMissing) {
209
+ if (dbReadModelMissing) {
226
210
  const recreateResult = yield* recreateDb
227
211
  migrationsReport = recreateResult.migrationsReport
228
212
  } else {
229
213
  migrationsReport = { migrations: [] }
230
214
  }
231
215
 
232
- yield* Deferred.succeed(dbReady, void 0)
216
+ // NOTE the sync processor depends on the dbs being initialized properly
217
+ const { initialLeaderHead } = yield* syncProcessor.boot
233
218
 
234
219
  if (initialBlockingSyncContext.blockingDeferred !== undefined) {
235
220
  // Provides a syncing status right away before the first pull response comes in
@@ -3,4 +3,4 @@ export * from './types.js'
3
3
  export * as ShutdownChannel from './shutdown-channel.js'
4
4
  export * from './leader-worker-devtools.js'
5
5
  export * from './make-leader-thread-layer.js'
6
- export * from './mutationlog.js'
6
+ export * as Mutationlog from './mutationlog.js'
@@ -1,18 +1,56 @@
1
- import { Effect, Schema } from '@livestore/utils/effect'
1
+ import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
2
+ import { Effect, Option, Schema } from '@livestore/utils/effect'
2
3
 
3
4
  import type { SqliteDb } from '../adapter-types.js'
4
5
  import * as EventId from '../schema/EventId.js'
5
- import type * as MutationEvent from '../schema/MutationEvent.js'
6
- import { MUTATION_LOG_META_TABLE, mutationLogMetaTable, SYNC_STATUS_TABLE } from '../schema/system-tables.js'
6
+ import * as MutationEvent from '../schema/MutationEvent.js'
7
+ import {
8
+ MUTATION_LOG_META_TABLE,
9
+ mutationLogMetaTable,
10
+ sessionChangesetMetaTable,
11
+ SYNC_STATUS_TABLE,
12
+ syncStatusTable,
13
+ } from '../schema/system-tables.js'
14
+ import { migrateTable } from '../schema-management/migrations.js'
15
+ import { insertRow, updateRows } from '../sql-queries/sql-queries.js'
16
+ import type { PreparedBindValues } from '../util.js'
7
17
  import { prepareBindValues, sql } from '../util.js'
18
+ import { execSql } from './connection.js'
19
+ import type { InitialSyncInfo } from './types.js'
8
20
  import { LeaderThreadCtx } from './types.js'
9
21
 
22
+ export const initMutationLogDb = (dbMutationLog: SqliteDb) =>
23
+ Effect.gen(function* () {
24
+ yield* migrateTable({
25
+ db: dbMutationLog,
26
+ behaviour: 'create-if-not-exists',
27
+ tableAst: mutationLogMetaTable.sqliteDef.ast,
28
+ skipMetaTable: true,
29
+ })
30
+
31
+ yield* migrateTable({
32
+ db: dbMutationLog,
33
+ behaviour: 'create-if-not-exists',
34
+ tableAst: syncStatusTable.sqliteDef.ast,
35
+ skipMetaTable: true,
36
+ })
37
+
38
+ // Create sync status row if it doesn't exist
39
+ yield* execSql(
40
+ dbMutationLog,
41
+ sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
42
+ SELECT ${EventId.ROOT.global}
43
+ WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
44
+ {},
45
+ )
46
+ })
47
+
10
48
  /** Exclusive of the "since event" */
11
49
  export const getMutationEventsSince = (
12
50
  since: EventId.EventId,
13
- ): Effect.Effect<ReadonlyArray<MutationEvent.AnyEncoded>, never, LeaderThreadCtx> =>
51
+ ): Effect.Effect<ReadonlyArray<MutationEvent.EncodedWithMeta>, never, LeaderThreadCtx> =>
14
52
  Effect.gen(function* () {
15
- const { dbMutationLog } = yield* LeaderThreadCtx
53
+ const { dbMutationLog, dbReadModel } = yield* LeaderThreadCtx
16
54
 
17
55
  const query = mutationLogMetaTable.query.where('idGlobal', '>=', since.global).asSql()
18
56
  const pendingMutationEventsRaw = dbMutationLog.select(query.query, prepareBindValues(query.bindValues, query.query))
@@ -20,16 +58,44 @@ export const getMutationEventsSince = (
20
58
  pendingMutationEventsRaw,
21
59
  )
22
60
 
61
+ const sessionChangesetRows = sessionChangesetMetaTable.query.where('idGlobal', '>=', since.global).asSql()
62
+ const sessionChangesetRowsRaw = dbReadModel.select(
63
+ sessionChangesetRows.query,
64
+ prepareBindValues(sessionChangesetRows.bindValues, sessionChangesetRows.query),
65
+ )
66
+ const sessionChangesetRowsDecoded = Schema.decodeUnknownSync(sessionChangesetMetaTable.schema.pipe(Schema.Array))(
67
+ sessionChangesetRowsRaw,
68
+ )
69
+
23
70
  return pendingMutationEvents
24
- .map((_) => ({
25
- mutation: _.mutation,
26
- args: _.argsJson,
27
- id: { global: _.idGlobal, client: _.idClient },
28
- parentId: { global: _.parentIdGlobal, client: _.parentIdClient },
29
- clientId: _.clientId,
30
- sessionId: _.sessionId,
31
- }))
71
+ .map((mutationLogEvent) => {
72
+ const sessionChangeset = sessionChangesetRowsDecoded.find(
73
+ (readModelEvent) =>
74
+ readModelEvent.idGlobal === mutationLogEvent.idGlobal &&
75
+ readModelEvent.idClient === mutationLogEvent.idClient,
76
+ )
77
+ return MutationEvent.EncodedWithMeta.make({
78
+ mutation: mutationLogEvent.mutation,
79
+ args: mutationLogEvent.argsJson,
80
+ id: { global: mutationLogEvent.idGlobal, client: mutationLogEvent.idClient },
81
+ parentId: { global: mutationLogEvent.parentIdGlobal, client: mutationLogEvent.parentIdClient },
82
+ clientId: mutationLogEvent.clientId,
83
+ sessionId: mutationLogEvent.sessionId,
84
+ meta: {
85
+ sessionChangeset:
86
+ sessionChangeset && sessionChangeset.changeset !== null
87
+ ? {
88
+ _tag: 'sessionChangeset' as const,
89
+ data: sessionChangeset.changeset,
90
+ debug: sessionChangeset.debug,
91
+ }
92
+ : { _tag: 'unset' as const },
93
+ syncMetadata: mutationLogEvent.syncMetadataJson,
94
+ },
95
+ })
96
+ })
32
97
  .filter((_) => EventId.compare(_.id, since) > 0)
98
+ .sort((a, b) => EventId.compare(a.id, b.id))
33
99
  })
34
100
 
35
101
  export const getClientHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
@@ -47,3 +113,90 @@ export const getBackendHeadFromDb = (dbMutationLog: SqliteDb): EventId.GlobalEve
47
113
  // TODO use prepared statements
48
114
  export const updateBackendHead = (dbMutationLog: SqliteDb, head: EventId.EventId) =>
49
115
  dbMutationLog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
116
+
117
+ export const insertIntoMutationLog = (
118
+ mutationEventEncoded: MutationEvent.EncodedWithMeta,
119
+ dbMutationLog: SqliteDb,
120
+ mutationDefSchemaHash: number,
121
+ clientId: string,
122
+ sessionId: string,
123
+ ) =>
124
+ Effect.gen(function* () {
125
+ // Check history consistency during LS_DEV
126
+ if (LS_DEV && mutationEventEncoded.parentId.global !== EventId.ROOT.global) {
127
+ const parentMutationExists =
128
+ dbMutationLog.select<{ count: number }>(
129
+ `SELECT COUNT(*) as count FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ? AND idClient = ?`,
130
+ [mutationEventEncoded.parentId.global, mutationEventEncoded.parentId.client] as any as PreparedBindValues,
131
+ )[0]!.count === 1
132
+
133
+ if (parentMutationExists === false) {
134
+ shouldNeverHappen(
135
+ `Parent mutation ${mutationEventEncoded.parentId.global},${mutationEventEncoded.parentId.client} does not exist`,
136
+ )
137
+ }
138
+ }
139
+
140
+ // TODO use prepared statements
141
+ yield* execSql(
142
+ dbMutationLog,
143
+ ...insertRow({
144
+ tableName: MUTATION_LOG_META_TABLE,
145
+ columns: mutationLogMetaTable.sqliteDef.columns,
146
+ values: {
147
+ idGlobal: mutationEventEncoded.id.global,
148
+ idClient: mutationEventEncoded.id.client,
149
+ parentIdGlobal: mutationEventEncoded.parentId.global,
150
+ parentIdClient: mutationEventEncoded.parentId.client,
151
+ mutation: mutationEventEncoded.mutation,
152
+ argsJson: mutationEventEncoded.args ?? {},
153
+ clientId,
154
+ sessionId,
155
+ schemaHash: mutationDefSchemaHash,
156
+ syncMetadataJson: mutationEventEncoded.meta.syncMetadata,
157
+ },
158
+ }),
159
+ )
160
+ })
161
+
162
+ export const updateSyncMetadata = (items: ReadonlyArray<MutationEvent.EncodedWithMeta>) =>
163
+ Effect.gen(function* () {
164
+ const { dbMutationLog } = yield* LeaderThreadCtx
165
+
166
+ // TODO try to do this in a single query
167
+ for (let i = 0; i < items.length; i++) {
168
+ const mutationEvent = items[i]!
169
+
170
+ yield* execSql(
171
+ dbMutationLog,
172
+ ...updateRows({
173
+ tableName: MUTATION_LOG_META_TABLE,
174
+ columns: mutationLogMetaTable.sqliteDef.columns,
175
+ where: { idGlobal: mutationEvent.id.global, idClient: mutationEvent.id.client },
176
+ updateValues: { syncMetadataJson: mutationEvent.meta.syncMetadata },
177
+ }),
178
+ )
179
+ }
180
+ })
181
+
182
+ export const getSyncBackendCursorInfo = (remoteHead: EventId.GlobalEventId) =>
183
+ Effect.gen(function* () {
184
+ const { dbMutationLog } = yield* LeaderThreadCtx
185
+
186
+ if (remoteHead === EventId.ROOT.global) return Option.none()
187
+
188
+ const MutationlogQuerySchema = Schema.Struct({
189
+ syncMetadataJson: Schema.parseJson(Schema.Option(Schema.JsonValue)),
190
+ }).pipe(Schema.pluck('syncMetadataJson'), Schema.Array, Schema.head)
191
+
192
+ const syncMetadataOption = yield* Effect.sync(() =>
193
+ dbMutationLog.select<{ syncMetadataJson: string }>(
194
+ sql`SELECT syncMetadataJson FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ${remoteHead} ORDER BY idClient ASC LIMIT 1`,
195
+ ),
196
+ ).pipe(Effect.andThen(Schema.decode(MutationlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
197
+
198
+ return Option.some({
199
+ cursor: { global: remoteHead, client: EventId.clientDefault },
200
+ metadata: syncMetadataOption,
201
+ }) satisfies InitialSyncInfo
202
+ }).pipe(Effect.withSpan('@livestore/common:mutationlog:getSyncBackendCursorInfo', { attributes: { remoteHead } }))
@@ -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
  })