@livestore/common 0.0.0-snapshot-f6ec49b1a18859aad769f0a0d8edf8bae231ed07 → 0.0.0-snapshot-2ef046b02334f52613d31dbe06af53487685edc0

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 (102) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +7 -12
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js +1 -7
  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 +16 -6
  16. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  17. package/dist/leader-thread/LeaderSyncProcessor.js +227 -215
  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.map +1 -1
  24. package/dist/leader-thread/leader-worker-devtools.js +2 -5
  25. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  26. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  27. package/dist/leader-thread/make-leader-thread-layer.js +22 -33
  28. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  29. package/dist/leader-thread/mod.d.ts +1 -1
  30. package/dist/leader-thread/mod.d.ts.map +1 -1
  31. package/dist/leader-thread/mod.js +1 -1
  32. package/dist/leader-thread/mod.js.map +1 -1
  33. package/dist/leader-thread/mutationlog.d.ts +20 -3
  34. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  35. package/dist/leader-thread/mutationlog.js +106 -12
  36. package/dist/leader-thread/mutationlog.js.map +1 -1
  37. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  38. package/dist/leader-thread/recreate-db.js +4 -3
  39. package/dist/leader-thread/recreate-db.js.map +1 -1
  40. package/dist/leader-thread/types.d.ts +35 -19
  41. package/dist/leader-thread/types.d.ts.map +1 -1
  42. package/dist/leader-thread/types.js.map +1 -1
  43. package/dist/rehydrate-from-mutationlog.d.ts +5 -4
  44. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  45. package/dist/rehydrate-from-mutationlog.js +7 -9
  46. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  47. package/dist/schema/EventId.d.ts +4 -0
  48. package/dist/schema/EventId.d.ts.map +1 -1
  49. package/dist/schema/EventId.js +7 -1
  50. package/dist/schema/EventId.js.map +1 -1
  51. package/dist/schema/MutationEvent.d.ts +87 -18
  52. package/dist/schema/MutationEvent.d.ts.map +1 -1
  53. package/dist/schema/MutationEvent.js +35 -6
  54. package/dist/schema/MutationEvent.js.map +1 -1
  55. package/dist/schema/schema.js +1 -1
  56. package/dist/schema/schema.js.map +1 -1
  57. package/dist/schema/system-tables.d.ts +67 -0
  58. package/dist/schema/system-tables.d.ts.map +1 -1
  59. package/dist/schema/system-tables.js +12 -1
  60. package/dist/schema/system-tables.js.map +1 -1
  61. package/dist/sync/ClientSessionSyncProcessor.d.ts +11 -1
  62. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  63. package/dist/sync/ClientSessionSyncProcessor.js +54 -47
  64. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  65. package/dist/sync/sync.d.ts +16 -5
  66. package/dist/sync/sync.d.ts.map +1 -1
  67. package/dist/sync/sync.js.map +1 -1
  68. package/dist/sync/syncstate.d.ts +81 -83
  69. package/dist/sync/syncstate.d.ts.map +1 -1
  70. package/dist/sync/syncstate.js +159 -125
  71. package/dist/sync/syncstate.js.map +1 -1
  72. package/dist/sync/syncstate.test.js +97 -138
  73. package/dist/sync/syncstate.test.js.map +1 -1
  74. package/dist/version.d.ts +1 -1
  75. package/dist/version.js +1 -1
  76. package/package.json +2 -2
  77. package/src/adapter-types.ts +5 -12
  78. package/src/devtools/devtools-messages-common.ts +9 -0
  79. package/src/devtools/devtools-messages-leader.ts +1 -2
  80. package/src/leader-thread/LeaderSyncProcessor.ts +398 -370
  81. package/src/leader-thread/apply-mutation.ts +81 -71
  82. package/src/leader-thread/leader-worker-devtools.ts +3 -8
  83. package/src/leader-thread/make-leader-thread-layer.ts +27 -41
  84. package/src/leader-thread/mod.ts +1 -1
  85. package/src/leader-thread/mutationlog.ts +167 -13
  86. package/src/leader-thread/recreate-db.ts +4 -3
  87. package/src/leader-thread/types.ts +34 -23
  88. package/src/rehydrate-from-mutationlog.ts +12 -12
  89. package/src/schema/EventId.ts +8 -1
  90. package/src/schema/MutationEvent.ts +42 -10
  91. package/src/schema/schema.ts +1 -1
  92. package/src/schema/system-tables.ts +20 -1
  93. package/src/sync/ClientSessionSyncProcessor.ts +64 -50
  94. package/src/sync/sync.ts +16 -9
  95. package/src/sync/syncstate.test.ts +173 -217
  96. package/src/sync/syncstate.ts +184 -151
  97. package/src/version.ts +1 -1
  98. package/dist/leader-thread/pull-queue-set.d.ts +0 -7
  99. package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
  100. package/dist/leader-thread/pull-queue-set.js +0 -48
  101. package/dist/leader-thread/pull-queue-set.js.map +0 -1
  102. 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,8 @@ export const bootDevtools = (options: DevtoolsOptions) =>
33
33
  Effect.ignoreLogged,
34
34
  )
35
35
 
36
- const { localHead } = yield* syncProcessor.syncState
37
-
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 }))),
36
+ yield* syncProcessor.pull({ cursor: syncProcessor.getMergeCounter() }).pipe(
37
+ Stream.tap(({ payload }) => sendMessage(Devtools.Leader.SyncPull.make({ payload, liveStoreVersion }))),
43
38
  Stream.runDrain,
44
39
  Effect.forkScoped,
45
40
  )
@@ -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,12 @@ 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,
82
+ onError: syncOptions?.onSyncError ?? 'ignore',
74
83
  })
75
84
 
76
85
  const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.Leader.MessageToApp>().pipe(
@@ -85,6 +94,8 @@ export const makeLeaderThreadLayer = ({
85
94
  }
86
95
  : { enabled: false as const }
87
96
 
97
+ const applyMutation = yield* makeApplyMutation({ schema, dbReadModel, dbMutationLog })
98
+
88
99
  const ctx = {
89
100
  schema,
90
101
  bootStatusQueue,
@@ -98,7 +109,7 @@ export const makeLeaderThreadLayer = ({
98
109
  shutdownChannel,
99
110
  syncBackend,
100
111
  syncProcessor,
101
- connectedClientSessionPullQueues: yield* makePullQueueSet,
112
+ applyMutation,
102
113
  extraIncomingMessagesQueue,
103
114
  devtools: devtoolsContext,
104
115
  // State will be set during `bootLeaderThread`
@@ -111,7 +122,7 @@ export const makeLeaderThreadLayer = ({
111
122
  const layer = Layer.succeed(LeaderThreadCtx, ctx)
112
123
 
113
124
  ctx.initialState = yield* bootLeaderThread({
114
- dbMissing,
125
+ dbReadModelMissing,
115
126
  initialBlockingSyncContext,
116
127
  devtoolsOptions,
117
128
  }).pipe(Effect.provide(layer))
@@ -121,6 +132,7 @@ export const makeLeaderThreadLayer = ({
121
132
  Effect.withSpan('@livestore/common:leader-thread:boot'),
122
133
  Effect.withSpanScoped('@livestore/common:leader-thread'),
123
134
  UnexpectedError.mapToUnexpectedError,
135
+ Effect.tapCauseLogPretty,
124
136
  Layer.unwrapScoped,
125
137
  )
126
138
 
@@ -176,11 +188,11 @@ const makeInitialBlockingSyncContext = ({
176
188
  * It also starts various background processes (e.g. syncing)
177
189
  */
178
190
  const bootLeaderThread = ({
179
- dbMissing,
191
+ dbReadModelMissing,
180
192
  initialBlockingSyncContext,
181
193
  devtoolsOptions,
182
194
  }: {
183
- dbMissing: boolean
195
+ dbReadModelMissing: boolean
184
196
  initialBlockingSyncContext: InitialBlockingSyncContext
185
197
  devtoolsOptions: DevtoolsOptions
186
198
  }): Effect.Effect<
@@ -191,44 +203,18 @@ const bootLeaderThread = ({
191
203
  Effect.gen(function* () {
192
204
  const { dbMutationLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
193
205
 
194
- yield* migrateTable({
195
- db: dbMutationLog,
196
- behaviour: 'create-if-not-exists',
197
- tableAst: mutationLogMetaTable.sqliteDef.ast,
198
- skipMetaTable: true,
199
- })
200
-
201
- yield* migrateTable({
202
- db: dbMutationLog,
203
- behaviour: 'create-if-not-exists',
204
- tableAst: syncStatusTable.sqliteDef.ast,
205
- skipMetaTable: true,
206
- })
207
-
208
- // Create sync status row if it doesn't exist
209
- yield* execSql(
210
- dbMutationLog,
211
- sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
212
- SELECT ${EventId.ROOT.global}
213
- WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
214
- {},
215
- )
216
-
217
- const dbReady = yield* Deferred.make<void>()
218
-
219
- // We're already starting pulling from the sync backend concurrently but wait until the db is ready before
220
- // processing any incoming mutations
221
- const { initialLeaderHead } = yield* syncProcessor.boot({ dbReady })
206
+ yield* Mutationlog.initMutationLogDb(dbMutationLog)
222
207
 
223
208
  let migrationsReport: MigrationsReport
224
- if (dbMissing) {
209
+ if (dbReadModelMissing) {
225
210
  const recreateResult = yield* recreateDb
226
211
  migrationsReport = recreateResult.migrationsReport
227
212
  } else {
228
213
  migrationsReport = { migrations: [] }
229
214
  }
230
215
 
231
- yield* Deferred.succeed(dbReady, void 0)
216
+ // NOTE the sync processor depends on the dbs being initialized properly
217
+ const { initialLeaderHead } = yield* syncProcessor.boot
232
218
 
233
219
  if (initialBlockingSyncContext.blockingDeferred !== undefined) {
234
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,17 +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
+
48
+ /** Exclusive of the "since event" */
10
49
  export const getMutationEventsSince = (
11
50
  since: EventId.EventId,
12
- ): Effect.Effect<ReadonlyArray<MutationEvent.AnyEncoded>, never, LeaderThreadCtx> =>
51
+ ): Effect.Effect<ReadonlyArray<MutationEvent.EncodedWithMeta>, never, LeaderThreadCtx> =>
13
52
  Effect.gen(function* () {
14
- const { dbMutationLog } = yield* LeaderThreadCtx
53
+ const { dbMutationLog, dbReadModel } = yield* LeaderThreadCtx
15
54
 
16
55
  const query = mutationLogMetaTable.query.where('idGlobal', '>=', since.global).asSql()
17
56
  const pendingMutationEventsRaw = dbMutationLog.select(query.query, prepareBindValues(query.bindValues, query.query))
@@ -19,16 +58,44 @@ export const getMutationEventsSince = (
19
58
  pendingMutationEventsRaw,
20
59
  )
21
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
+
22
70
  return pendingMutationEvents
23
- .map((_) => ({
24
- mutation: _.mutation,
25
- args: _.argsJson,
26
- id: { global: _.idGlobal, client: _.idClient },
27
- parentId: { global: _.parentIdGlobal, client: _.parentIdClient },
28
- clientId: _.clientId,
29
- sessionId: _.sessionId,
30
- }))
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
+ })
31
97
  .filter((_) => EventId.compare(_.id, since) > 0)
98
+ .sort((a, b) => EventId.compare(a.id, b.id))
32
99
  })
33
100
 
34
101
  export const getClientHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
@@ -46,3 +113,90 @@ export const getBackendHeadFromDb = (dbMutationLog: SqliteDb): EventId.GlobalEve
46
113
  // TODO use prepared statements
47
114
  export const updateBackendHead = (dbMutationLog: SqliteDb, head: EventId.EventId) =>
48
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
  })