@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
@@ -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 {
@@ -24,6 +23,30 @@ import type {
24
23
  } from './types.js'
25
24
  import { LeaderThreadCtx } from './types.js'
26
25
 
26
+ export interface MakeLeaderThreadLayerParams {
27
+ storeId: string
28
+ syncPayload: Schema.JsonValue | undefined
29
+ clientId: string
30
+ schema: LiveStoreSchema
31
+ makeSqliteDb: MakeSqliteDb
32
+ syncOptions: SyncOptions | undefined
33
+ dbReadModel: LeaderSqliteDb
34
+ dbMutationLog: LeaderSqliteDb
35
+ devtoolsOptions: DevtoolsOptions
36
+ shutdownChannel: ShutdownChannel
37
+ params?: {
38
+ localPushBatchSize?: number
39
+ backendPushBatchSize?: number
40
+ }
41
+ testing?: {
42
+ syncProcessor?: {
43
+ delays?: {
44
+ localPushProcessing?: Effect.Effect<void>
45
+ }
46
+ }
47
+ }
48
+ }
49
+
27
50
  export const makeLeaderThreadLayer = ({
28
51
  schema,
29
52
  storeId,
@@ -35,24 +58,18 @@ export const makeLeaderThreadLayer = ({
35
58
  dbMutationLog,
36
59
  devtoolsOptions,
37
60
  shutdownChannel,
38
- }: {
39
- storeId: string
40
- syncPayload: Schema.JsonValue | undefined
41
- clientId: string
42
- schema: LiveStoreSchema
43
- makeSqliteDb: MakeSqliteDb
44
- syncOptions: SyncOptions | undefined
45
- dbReadModel: LeaderSqliteDb
46
- dbMutationLog: LeaderSqliteDb
47
- devtoolsOptions: DevtoolsOptions
48
- shutdownChannel: ShutdownChannel
49
- }): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
61
+ params,
62
+ testing,
63
+ }: MakeLeaderThreadLayerParams): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
50
64
  Effect.gen(function* () {
51
65
  const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
52
66
 
53
67
  // TODO do more validation here than just checking the count of tables
54
68
  // Either happens on initial boot or if schema changes
55
- const dbMissing =
69
+ const dbMutationLogMissing =
70
+ dbMutationLog.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
71
+
72
+ const dbReadModelMissing =
56
73
  dbReadModel.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
57
74
 
58
75
  const syncBackend =
@@ -60,6 +77,11 @@ export const makeLeaderThreadLayer = ({
60
77
  ? undefined
61
78
  : yield* syncOptions.backend({ storeId, clientId, payload: syncPayload })
62
79
 
80
+ if (syncBackend !== undefined) {
81
+ // We're already connecting to the sync backend concurrently
82
+ yield* syncBackend.connect.pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
83
+ }
84
+
63
85
  const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({
64
86
  initialSyncOptions: syncOptions?.initialSyncOptions ?? { _tag: 'Skip' },
65
87
  bootStatusQueue,
@@ -67,11 +89,19 @@ export const makeLeaderThreadLayer = ({
67
89
 
68
90
  const syncProcessor = yield* makeLeaderSyncProcessor({
69
91
  schema,
70
- dbMissing,
92
+ dbMutationLogMissing,
71
93
  dbMutationLog,
94
+ dbReadModel,
95
+ dbReadModelMissing,
72
96
  initialBlockingSyncContext,
73
- clientId,
74
97
  onError: syncOptions?.onSyncError ?? 'ignore',
98
+ params: {
99
+ localPushBatchSize: params?.localPushBatchSize,
100
+ backendPushBatchSize: params?.backendPushBatchSize,
101
+ },
102
+ testing: {
103
+ delays: testing?.syncProcessor?.delays,
104
+ },
75
105
  })
76
106
 
77
107
  const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.Leader.MessageToApp>().pipe(
@@ -86,6 +116,8 @@ export const makeLeaderThreadLayer = ({
86
116
  }
87
117
  : { enabled: false as const }
88
118
 
119
+ const applyMutation = yield* makeApplyMutation({ schema, dbReadModel, dbMutationLog })
120
+
89
121
  const ctx = {
90
122
  schema,
91
123
  bootStatusQueue,
@@ -99,7 +131,7 @@ export const makeLeaderThreadLayer = ({
99
131
  shutdownChannel,
100
132
  syncBackend,
101
133
  syncProcessor,
102
- connectedClientSessionPullQueues: yield* makePullQueueSet,
134
+ applyMutation,
103
135
  extraIncomingMessagesQueue,
104
136
  devtools: devtoolsContext,
105
137
  // State will be set during `bootLeaderThread`
@@ -112,7 +144,7 @@ export const makeLeaderThreadLayer = ({
112
144
  const layer = Layer.succeed(LeaderThreadCtx, ctx)
113
145
 
114
146
  ctx.initialState = yield* bootLeaderThread({
115
- dbMissing,
147
+ dbReadModelMissing,
116
148
  initialBlockingSyncContext,
117
149
  devtoolsOptions,
118
150
  }).pipe(Effect.provide(layer))
@@ -122,6 +154,7 @@ export const makeLeaderThreadLayer = ({
122
154
  Effect.withSpan('@livestore/common:leader-thread:boot'),
123
155
  Effect.withSpanScoped('@livestore/common:leader-thread'),
124
156
  UnexpectedError.mapToUnexpectedError,
157
+ Effect.tapCauseLogPretty,
125
158
  Layer.unwrapScoped,
126
159
  )
127
160
 
@@ -177,11 +210,11 @@ const makeInitialBlockingSyncContext = ({
177
210
  * It also starts various background processes (e.g. syncing)
178
211
  */
179
212
  const bootLeaderThread = ({
180
- dbMissing,
213
+ dbReadModelMissing,
181
214
  initialBlockingSyncContext,
182
215
  devtoolsOptions,
183
216
  }: {
184
- dbMissing: boolean
217
+ dbReadModelMissing: boolean
185
218
  initialBlockingSyncContext: InitialBlockingSyncContext
186
219
  devtoolsOptions: DevtoolsOptions
187
220
  }): Effect.Effect<
@@ -192,44 +225,18 @@ const bootLeaderThread = ({
192
225
  Effect.gen(function* () {
193
226
  const { dbMutationLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
194
227
 
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 })
228
+ yield* Mutationlog.initMutationLogDb(dbMutationLog)
223
229
 
224
230
  let migrationsReport: MigrationsReport
225
- if (dbMissing) {
231
+ if (dbReadModelMissing) {
226
232
  const recreateResult = yield* recreateDb
227
233
  migrationsReport = recreateResult.migrationsReport
228
234
  } else {
229
235
  migrationsReport = { migrations: [] }
230
236
  }
231
237
 
232
- yield* Deferred.succeed(dbReady, void 0)
238
+ // NOTE the sync processor depends on the dbs being initialized properly
239
+ const { initialLeaderHead } = yield* syncProcessor.boot
233
240
 
234
241
  if (initialBlockingSyncContext.blockingDeferred !== undefined) {
235
242
  // 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 } }))