@livestore/common 0.3.1-dev.0 → 0.3.2-dev.0

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 (185) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +35 -0
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
  4. package/dist/ClientSessionLeaderThreadProxy.js +6 -0
  5. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
  6. package/dist/adapter-types.d.ts +10 -156
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +5 -49
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/defs.d.ts +20 -0
  11. package/dist/defs.d.ts.map +1 -0
  12. package/dist/defs.js +12 -0
  13. package/dist/defs.js.map +1 -0
  14. package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
  15. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
  16. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  17. package/dist/devtools/devtools-messages-leader.d.ts +26 -24
  18. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  19. package/dist/errors.d.ts +50 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +36 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
  28. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  29. package/dist/leader-thread/LeaderSyncProcessor.js +122 -123
  30. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  31. package/dist/leader-thread/eventlog.d.ts +17 -6
  32. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  33. package/dist/leader-thread/eventlog.js +34 -17
  34. package/dist/leader-thread/eventlog.js.map +1 -1
  35. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  36. package/dist/leader-thread/leader-worker-devtools.js +1 -2
  37. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  38. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  39. package/dist/leader-thread/make-leader-thread-layer.js +37 -7
  40. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  41. package/dist/leader-thread/materialize-event.d.ts +3 -3
  42. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  43. package/dist/leader-thread/materialize-event.js +27 -10
  44. package/dist/leader-thread/materialize-event.js.map +1 -1
  45. package/dist/leader-thread/mod.d.ts +2 -0
  46. package/dist/leader-thread/mod.d.ts.map +1 -1
  47. package/dist/leader-thread/mod.js +2 -0
  48. package/dist/leader-thread/mod.js.map +1 -1
  49. package/dist/leader-thread/recreate-db.d.ts +13 -6
  50. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  51. package/dist/leader-thread/recreate-db.js +1 -3
  52. package/dist/leader-thread/recreate-db.js.map +1 -1
  53. package/dist/leader-thread/types.d.ts +6 -7
  54. package/dist/leader-thread/types.d.ts.map +1 -1
  55. package/dist/make-client-session.d.ts +1 -1
  56. package/dist/make-client-session.d.ts.map +1 -1
  57. package/dist/make-client-session.js +1 -1
  58. package/dist/make-client-session.js.map +1 -1
  59. package/dist/materializer-helper.d.ts +13 -2
  60. package/dist/materializer-helper.d.ts.map +1 -1
  61. package/dist/materializer-helper.js +25 -11
  62. package/dist/materializer-helper.js.map +1 -1
  63. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  64. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  65. package/dist/rematerialize-from-eventlog.js +12 -4
  66. package/dist/rematerialize-from-eventlog.js.map +1 -1
  67. package/dist/schema/EventDef.d.ts +8 -3
  68. package/dist/schema/EventDef.d.ts.map +1 -1
  69. package/dist/schema/EventDef.js +5 -2
  70. package/dist/schema/EventDef.js.map +1 -1
  71. package/dist/schema/EventSequenceNumber.d.ts +20 -2
  72. package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
  73. package/dist/schema/EventSequenceNumber.js +71 -19
  74. package/dist/schema/EventSequenceNumber.js.map +1 -1
  75. package/dist/schema/EventSequenceNumber.test.js +88 -3
  76. package/dist/schema/EventSequenceNumber.test.js.map +1 -1
  77. package/dist/schema/LiveStoreEvent.d.ts +56 -8
  78. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  79. package/dist/schema/LiveStoreEvent.js +34 -8
  80. package/dist/schema/LiveStoreEvent.js.map +1 -1
  81. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
  82. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  83. package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
  84. package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
  85. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  86. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/query-builder/api.d.ts +36 -9
  88. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  89. package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
  90. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  91. package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
  92. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  93. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -86
  94. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
  95. package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
  96. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  97. package/dist/schema/state/sqlite/system-tables.d.ts +380 -432
  98. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  99. package/dist/schema/state/sqlite/system-tables.js +8 -17
  100. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  101. package/dist/schema/state/sqlite/table-def.d.ts +2 -2
  102. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  103. package/dist/schema-management/migrations.d.ts +3 -1
  104. package/dist/schema-management/migrations.d.ts.map +1 -1
  105. package/dist/schema-management/migrations.js.map +1 -1
  106. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  107. package/dist/sql-queries/sql-queries.js +2 -0
  108. package/dist/sql-queries/sql-queries.js.map +1 -1
  109. package/dist/sqlite-db-helper.d.ts +7 -0
  110. package/dist/sqlite-db-helper.d.ts.map +1 -0
  111. package/dist/sqlite-db-helper.js +29 -0
  112. package/dist/sqlite-db-helper.js.map +1 -0
  113. package/dist/sqlite-types.d.ts +72 -0
  114. package/dist/sqlite-types.d.ts.map +1 -0
  115. package/dist/sqlite-types.js +5 -0
  116. package/dist/sqlite-types.js.map +1 -0
  117. package/dist/sync/ClientSessionSyncProcessor.d.ts +12 -3
  118. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  119. package/dist/sync/ClientSessionSyncProcessor.js +37 -19
  120. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  121. package/dist/sync/next/graphology.d.ts.map +1 -1
  122. package/dist/sync/next/graphology.js +0 -6
  123. package/dist/sync/next/graphology.js.map +1 -1
  124. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  125. package/dist/sync/next/rebase-events.js +1 -0
  126. package/dist/sync/next/rebase-events.js.map +1 -1
  127. package/dist/sync/next/test/compact-events.test.js +1 -1
  128. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  129. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  130. package/dist/sync/next/test/event-fixtures.js +12 -3
  131. package/dist/sync/next/test/event-fixtures.js.map +1 -1
  132. package/dist/sync/sync.d.ts +2 -0
  133. package/dist/sync/sync.d.ts.map +1 -1
  134. package/dist/sync/sync.js +3 -0
  135. package/dist/sync/sync.js.map +1 -1
  136. package/dist/sync/syncstate.d.ts +13 -4
  137. package/dist/sync/syncstate.d.ts.map +1 -1
  138. package/dist/sync/syncstate.js +23 -10
  139. package/dist/sync/syncstate.js.map +1 -1
  140. package/dist/sync/syncstate.test.js +17 -17
  141. package/dist/sync/syncstate.test.js.map +1 -1
  142. package/dist/version.d.ts +1 -1
  143. package/dist/version.js +1 -1
  144. package/package.json +7 -6
  145. package/src/ClientSessionLeaderThreadProxy.ts +40 -0
  146. package/src/adapter-types.ts +19 -161
  147. package/src/defs.ts +17 -0
  148. package/src/errors.ts +49 -0
  149. package/src/index.ts +1 -0
  150. package/src/leader-thread/LeaderSyncProcessor.ts +157 -181
  151. package/src/leader-thread/eventlog.ts +78 -54
  152. package/src/leader-thread/leader-worker-devtools.ts +1 -2
  153. package/src/leader-thread/make-leader-thread-layer.ts +52 -8
  154. package/src/leader-thread/materialize-event.ts +33 -12
  155. package/src/leader-thread/mod.ts +2 -0
  156. package/src/leader-thread/recreate-db.ts +99 -91
  157. package/src/leader-thread/types.ts +10 -12
  158. package/src/make-client-session.ts +2 -2
  159. package/src/materializer-helper.ts +45 -19
  160. package/src/rematerialize-from-eventlog.ts +12 -4
  161. package/src/schema/EventDef.ts +16 -4
  162. package/src/schema/EventSequenceNumber.test.ts +120 -3
  163. package/src/schema/EventSequenceNumber.ts +95 -23
  164. package/src/schema/LiveStoreEvent.ts +49 -8
  165. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
  166. package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
  167. package/src/schema/state/sqlite/mod.ts +1 -1
  168. package/src/schema/state/sqlite/query-builder/api.ts +39 -9
  169. package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
  170. package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
  171. package/src/schema/state/sqlite/system-tables.ts +9 -22
  172. package/src/schema/state/sqlite/table-def.ts +2 -2
  173. package/src/schema-management/migrations.ts +3 -1
  174. package/src/sql-queries/sql-queries.ts +2 -0
  175. package/src/sqlite-db-helper.ts +41 -0
  176. package/src/sqlite-types.ts +76 -0
  177. package/src/sync/ClientSessionSyncProcessor.ts +51 -28
  178. package/src/sync/next/graphology.ts +0 -6
  179. package/src/sync/next/rebase-events.ts +1 -0
  180. package/src/sync/next/test/compact-events.test.ts +1 -1
  181. package/src/sync/next/test/event-fixtures.ts +12 -3
  182. package/src/sync/sync.ts +3 -0
  183. package/src/sync/syncstate.test.ts +17 -17
  184. package/src/sync/syncstate.ts +31 -10
  185. package/src/version.ts +1 -1
@@ -14,7 +14,7 @@ import {
14
14
  import { migrateTable } from '../schema-management/migrations.js'
15
15
  import { insertRow, updateRows } from '../sql-queries/sql-queries.js'
16
16
  import type { PreparedBindValues } from '../util.js'
17
- import { prepareBindValues, sql } from '../util.js'
17
+ import { sql } from '../util.js'
18
18
  import { execSql } from './connection.js'
19
19
  import type { InitialSyncInfo } from './types.js'
20
20
  import { LeaderThreadCtx } from './types.js'
@@ -40,66 +40,78 @@ export const initEventlogDb = (dbEventlog: SqliteDb) =>
40
40
  )
41
41
  })
42
42
 
43
- /** Exclusive of the "since event" */
44
- export const getEventsSince = (
45
- since: EventSequenceNumber.EventSequenceNumber,
46
- ): Effect.Effect<ReadonlyArray<LiveStoreEvent.EncodedWithMeta>, never, LeaderThreadCtx> =>
47
- Effect.gen(function* () {
48
- const { dbEventlog, dbState } = yield* LeaderThreadCtx
49
-
50
- const query = eventlogMetaTable.where('seqNumGlobal', '>=', since.global).asSql()
51
- const pendingEventsRaw = dbEventlog.select(query.query, prepareBindValues(query.bindValues, query.query))
52
- const pendingEvents = Schema.decodeUnknownSync(eventlogMetaTable.rowSchema.pipe(Schema.Array))(pendingEventsRaw)
53
-
54
- const sessionChangesetRows = sessionChangesetMetaTable.where('seqNumGlobal', '>=', since.global).asSql()
55
- const sessionChangesetRowsRaw = dbState.select(
56
- sessionChangesetRows.query,
57
- prepareBindValues(sessionChangesetRows.bindValues, sessionChangesetRows.query),
58
- )
59
- const sessionChangesetRowsDecoded = Schema.decodeUnknownSync(
60
- sessionChangesetMetaTable.rowSchema.pipe(Schema.Array),
61
- )(sessionChangesetRowsRaw)
62
-
63
- return pendingEvents
64
- .map((eventlogEvent) => {
65
- const sessionChangeset = sessionChangesetRowsDecoded.find(
66
- (readModelEvent) =>
67
- readModelEvent.seqNumGlobal === eventlogEvent.seqNumGlobal &&
68
- readModelEvent.seqNumClient === eventlogEvent.seqNumClient,
69
- )
70
- return LiveStoreEvent.EncodedWithMeta.make({
71
- name: eventlogEvent.name,
72
- args: eventlogEvent.argsJson,
73
- seqNum: { global: eventlogEvent.seqNumGlobal, client: eventlogEvent.seqNumClient },
74
- parentSeqNum: { global: eventlogEvent.parentSeqNumGlobal, client: eventlogEvent.parentSeqNumClient },
75
- clientId: eventlogEvent.clientId,
76
- sessionId: eventlogEvent.sessionId,
77
- meta: {
78
- sessionChangeset:
79
- sessionChangeset && sessionChangeset.changeset !== null
80
- ? {
81
- _tag: 'sessionChangeset' as const,
82
- data: sessionChangeset.changeset,
83
- debug: sessionChangeset.debug,
84
- }
85
- : { _tag: 'unset' as const },
86
- syncMetadata: eventlogEvent.syncMetadataJson,
87
- },
88
- })
43
+ /**
44
+ * Exclusive of the "since event"
45
+ * Also queries the state db in order to get the SQLite session changeset data.
46
+ */
47
+ export const getEventsSince = ({
48
+ dbEventlog,
49
+ dbState,
50
+ since,
51
+ }: {
52
+ dbEventlog: SqliteDb
53
+ dbState: SqliteDb
54
+ since: EventSequenceNumber.EventSequenceNumber
55
+ }): ReadonlyArray<LiveStoreEvent.EncodedWithMeta> => {
56
+ const pendingEvents = dbEventlog.select(eventlogMetaTable.where('seqNumGlobal', '>=', since.global))
57
+
58
+ const sessionChangesetRowsDecoded = dbState.select(
59
+ sessionChangesetMetaTable.where('seqNumGlobal', '>=', since.global),
60
+ )
61
+
62
+ return pendingEvents
63
+ .map((eventlogEvent) => {
64
+ const sessionChangeset = sessionChangesetRowsDecoded.find(
65
+ (readModelEvent) =>
66
+ readModelEvent.seqNumGlobal === eventlogEvent.seqNumGlobal &&
67
+ readModelEvent.seqNumClient === eventlogEvent.seqNumClient,
68
+ )
69
+ return LiveStoreEvent.EncodedWithMeta.make({
70
+ name: eventlogEvent.name,
71
+ args: eventlogEvent.argsJson,
72
+ seqNum: {
73
+ global: eventlogEvent.seqNumGlobal,
74
+ client: eventlogEvent.seqNumClient,
75
+ rebaseGeneration: eventlogEvent.seqNumRebaseGeneration,
76
+ },
77
+ parentSeqNum: {
78
+ global: eventlogEvent.parentSeqNumGlobal,
79
+ client: eventlogEvent.parentSeqNumClient,
80
+ rebaseGeneration: eventlogEvent.parentSeqNumRebaseGeneration,
81
+ },
82
+ clientId: eventlogEvent.clientId,
83
+ sessionId: eventlogEvent.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: eventlogEvent.syncMetadataJson,
94
+ materializerHashLeader: Option.none(),
95
+ materializerHashSession: Option.none(),
96
+ },
89
97
  })
90
- .filter((_) => EventSequenceNumber.compare(_.seqNum, since) > 0)
91
- .sort((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
92
- })
98
+ })
99
+ .filter((_) => EventSequenceNumber.compare(_.seqNum, since) > 0)
100
+ .sort((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
101
+ }
93
102
 
94
103
  export const getClientHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.EventSequenceNumber => {
95
104
  const res = dbEventlog.select<{
96
105
  seqNumGlobal: EventSequenceNumber.GlobalEventSequenceNumber
97
106
  seqNumClient: EventSequenceNumber.ClientEventSequenceNumber
107
+ seqNumRebaseGeneration: number
98
108
  }>(
99
- sql`select seqNumGlobal, seqNumClient from ${EVENTLOG_META_TABLE} order by seqNumGlobal DESC, seqNumClient DESC limit 1`,
109
+ sql`select seqNumGlobal, seqNumClient, seqNumRebaseGeneration from ${EVENTLOG_META_TABLE} order by seqNumGlobal DESC, seqNumClient DESC limit 1`,
100
110
  )[0]
101
111
 
102
- return res ? { global: res.seqNumGlobal, client: res.seqNumClient } : EventSequenceNumber.ROOT
112
+ return res
113
+ ? { global: res.seqNumGlobal, client: res.seqNumClient, rebaseGeneration: res.seqNumRebaseGeneration }
114
+ : EventSequenceNumber.ROOT
103
115
  }
104
116
 
105
117
  export const getBackendHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.GlobalEventSequenceNumber =>
@@ -143,8 +155,10 @@ export const insertIntoEventlog = (
143
155
  values: {
144
156
  seqNumGlobal: eventEncoded.seqNum.global,
145
157
  seqNumClient: eventEncoded.seqNum.client,
158
+ seqNumRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
146
159
  parentSeqNumGlobal: eventEncoded.parentSeqNum.global,
147
160
  parentSeqNumClient: eventEncoded.parentSeqNum.client,
161
+ parentSeqNumRebaseGeneration: eventEncoded.parentSeqNum.rebaseGeneration,
148
162
  name: eventEncoded.name,
149
163
  argsJson: eventEncoded.args ?? {},
150
164
  clientId,
@@ -154,6 +168,8 @@ export const insertIntoEventlog = (
154
168
  },
155
169
  }),
156
170
  )
171
+
172
+ dbEventlog.debug.head = eventEncoded.seqNum
157
173
  })
158
174
 
159
175
  export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>) =>
@@ -176,7 +192,11 @@ export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.EncodedWi
176
192
  }
177
193
  })
178
194
 
179
- export const getSyncBackendCursorInfo = (remoteHead: EventSequenceNumber.GlobalEventSequenceNumber) =>
195
+ export const getSyncBackendCursorInfo = ({
196
+ remoteHead,
197
+ }: {
198
+ remoteHead: EventSequenceNumber.GlobalEventSequenceNumber
199
+ }) =>
180
200
  Effect.gen(function* () {
181
201
  const { dbEventlog } = yield* LeaderThreadCtx
182
202
 
@@ -193,7 +213,11 @@ export const getSyncBackendCursorInfo = (remoteHead: EventSequenceNumber.GlobalE
193
213
  ).pipe(Effect.andThen(Schema.decode(EventlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
194
214
 
195
215
  return Option.some({
196
- cursor: { global: remoteHead, client: EventSequenceNumber.clientDefault },
216
+ cursor: {
217
+ global: remoteHead,
218
+ client: EventSequenceNumber.clientDefault,
219
+ rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
220
+ },
197
221
  metadata: syncMetadataOption,
198
222
  }) satisfies InitialSyncInfo
199
223
  }).pipe(Effect.withSpan('@livestore/common:eventlog:getSyncBackendCursorInfo', { attributes: { remoteHead } }))
@@ -48,9 +48,8 @@ export const bootDevtools = (options: DevtoolsOptions) =>
48
48
  )
49
49
 
50
50
  const syncState = yield* syncProcessor.syncState
51
- const mergeCounter = syncProcessor.getMergeCounter()
52
51
 
53
- yield* syncProcessor.pull({ cursor: { mergeCounter, eventNum: syncState.localHead } }).pipe(
52
+ yield* syncProcessor.pull({ cursor: syncState.localHead }).pipe(
54
53
  Stream.tap(({ payload }) => sendMessage(Devtools.Leader.SyncPull.make({ payload, liveStoreVersion }))),
55
54
  Stream.runDrain,
56
55
  Effect.forkScoped,
@@ -1,16 +1,18 @@
1
+ import { shouldNeverHappen } from '@livestore/utils'
1
2
  import type { HttpClient, Schema, Scope } from '@livestore/utils/effect'
2
3
  import { Deferred, Effect, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
3
4
 
4
- import type { BootStatus, MakeSqliteDb, SqliteError } from '../adapter-types.js'
5
+ import type { BootStatus, MakeSqliteDb, SqliteDb, SqliteError } from '../adapter-types.js'
5
6
  import { UnexpectedError } from '../adapter-types.js'
6
7
  import type * as Devtools from '../devtools/mod.js'
7
8
  import type { LiveStoreSchema } from '../schema/mod.js'
8
- import { LiveStoreEvent } from '../schema/mod.js'
9
+ import { EventSequenceNumber, LiveStoreEvent } from '../schema/mod.js'
9
10
  import type { InvalidPullError, IsOfflineError, SyncOptions } from '../sync/sync.js'
11
+ import { SyncState } from '../sync/syncstate.js'
10
12
  import { sql } from '../util.js'
11
13
  import * as Eventlog from './eventlog.js'
12
- import { bootDevtools } from './leader-worker-devtools.js'
13
14
  import { makeLeaderSyncProcessor } from './LeaderSyncProcessor.js'
15
+ import { bootDevtools } from './leader-worker-devtools.js'
14
16
  import { makeMaterializeEvent } from './materialize-event.js'
15
17
  import { recreateDb } from './recreate-db.js'
16
18
  import type { ShutdownChannel } from './shutdown-channel.js'
@@ -89,10 +91,8 @@ export const makeLeaderThreadLayer = ({
89
91
 
90
92
  const syncProcessor = yield* makeLeaderSyncProcessor({
91
93
  schema,
92
- dbEventlogMissing,
93
- dbEventlog,
94
94
  dbState,
95
- dbStateMissing,
95
+ initialSyncState: getInitialSyncState({ dbEventlog, dbState, dbEventlogMissing }),
96
96
  initialBlockingSyncContext,
97
97
  onError: syncOptions?.onSyncError ?? 'ignore',
98
98
  params: {
@@ -158,6 +158,48 @@ export const makeLeaderThreadLayer = ({
158
158
  Layer.unwrapScoped,
159
159
  )
160
160
 
161
+ const getInitialSyncState = ({
162
+ dbEventlog,
163
+ dbState,
164
+ dbEventlogMissing,
165
+ }: {
166
+ dbEventlog: SqliteDb
167
+ dbState: SqliteDb
168
+ dbEventlogMissing: boolean
169
+ }) => {
170
+ const initialBackendHead = dbEventlogMissing
171
+ ? EventSequenceNumber.ROOT.global
172
+ : Eventlog.getBackendHeadFromDb(dbEventlog)
173
+
174
+ const initialLocalHead = dbEventlogMissing ? EventSequenceNumber.ROOT : Eventlog.getClientHeadFromDb(dbEventlog)
175
+
176
+ if (initialBackendHead > initialLocalHead.global) {
177
+ return shouldNeverHappen(
178
+ `During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
179
+ )
180
+ }
181
+
182
+ return SyncState.make({
183
+ localHead: initialLocalHead,
184
+ upstreamHead: {
185
+ global: initialBackendHead,
186
+ client: EventSequenceNumber.clientDefault,
187
+ rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
188
+ },
189
+ pending: dbEventlogMissing
190
+ ? []
191
+ : Eventlog.getEventsSince({
192
+ dbEventlog,
193
+ dbState,
194
+ since: {
195
+ global: initialBackendHead,
196
+ client: EventSequenceNumber.clientDefault,
197
+ rebaseGeneration: initialLocalHead.rebaseGeneration,
198
+ },
199
+ }),
200
+ })
201
+ }
202
+
161
203
  const makeInitialBlockingSyncContext = ({
162
204
  initialSyncOptions,
163
205
  bootStatusQueue,
@@ -223,11 +265,13 @@ const bootLeaderThread = ({
223
265
  LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
224
266
  > =>
225
267
  Effect.gen(function* () {
226
- const { dbEventlog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
268
+ const { dbEventlog, bootStatusQueue, syncProcessor, schema, materializeEvent, dbState } = yield* LeaderThreadCtx
227
269
 
228
270
  yield* Eventlog.initEventlogDb(dbEventlog)
229
271
 
230
- const { migrationsReport } = dbStateMissing ? yield* recreateDb : { migrationsReport: { migrations: [] } }
272
+ const { migrationsReport } = dbStateMissing
273
+ ? yield* recreateDb({ dbState, dbEventlog, schema, bootStatusQueue, materializeEvent })
274
+ : { migrationsReport: { migrations: [] } }
231
275
 
232
276
  // NOTE the sync processor depends on the dbs being initialized properly
233
277
  const { initialLeaderHead } = yield* syncProcessor.boot
@@ -1,8 +1,8 @@
1
- import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
2
- import { Effect, ReadonlyArray, Schema } from '@livestore/utils/effect'
1
+ import { isDevEnv, LS_DEV, shouldNeverHappen } from '@livestore/utils'
2
+ import { Effect, Option, ReadonlyArray, Schema } from '@livestore/utils/effect'
3
3
 
4
- import type { SqliteDb } from '../adapter-types.js'
5
- import { getExecArgsFromEvent } from '../materializer-helper.js'
4
+ import { type SqliteDb, UnexpectedError } from '../adapter-types.js'
5
+ import { getExecStatementsFromMaterializer, hashMaterializerResults } from '../materializer-helper.js'
6
6
  import type { LiveStoreSchema } from '../schema/mod.js'
7
7
  import { EventSequenceNumber, getEventDef, SystemTables } from '../schema/mod.js'
8
8
  import { insertRow } from '../sql-queries/index.js'
@@ -13,13 +13,13 @@ import type { MaterializeEvent } from './types.js'
13
13
 
14
14
  export const makeMaterializeEvent = ({
15
15
  schema,
16
- dbState: db,
16
+ dbState,
17
17
  dbEventlog,
18
18
  }: {
19
19
  schema: LiveStoreSchema
20
20
  dbState: SqliteDb
21
21
  dbEventlog: SqliteDb
22
- }): Effect.Effect<MaterializeEvent, never> =>
22
+ }): Effect.Effect<MaterializeEvent, UnexpectedError> =>
23
23
  Effect.gen(function* () {
24
24
  const eventDefSchemaHashMap = new Map(
25
25
  // TODO Running `Schema.hash` can be a bottleneck for larger schemas. There is an opportunity to run this
@@ -35,13 +35,26 @@ export const makeMaterializeEvent = ({
35
35
  const eventName = eventEncoded.name
36
36
  const { eventDef, materializer } = getEventDef(schema, eventName)
37
37
 
38
- const execArgsArr = getExecArgsFromEvent({
38
+ const execArgsArr = getExecStatementsFromMaterializer({
39
39
  eventDef,
40
40
  materializer,
41
- db,
41
+ dbState,
42
42
  event: { decoded: undefined, encoded: eventEncoded },
43
43
  })
44
44
 
45
+ const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
46
+
47
+ if (
48
+ materializerHash._tag === 'Some' &&
49
+ eventEncoded.meta.materializerHashSession._tag === 'Some' &&
50
+ eventEncoded.meta.materializerHashSession.value !== materializerHash.value
51
+ ) {
52
+ yield* UnexpectedError.make({
53
+ cause: `Materializer hash mismatch detected for event "${eventEncoded.name}".`,
54
+ note: `Please make sure your event materializer is a pure function without side effects.`,
55
+ })
56
+ }
57
+
45
58
  // NOTE we might want to bring this back if we want to debug no-op events
46
59
  // const makeExecuteOptions = (statementSql: string, bindValues: any) => ({
47
60
  // onRowsChanged: (rowsChanged: number) => {
@@ -53,26 +66,29 @@ export const makeMaterializeEvent = ({
53
66
 
54
67
  // console.group('[@livestore/common:leader-thread:materializeEvent]', { eventName })
55
68
 
56
- const session = db.session()
69
+ const session = dbState.session()
57
70
 
58
71
  for (const { statementSql, bindValues } of execArgsArr) {
59
72
  // console.debug(eventName, statementSql, bindValues)
60
73
  // TODO use cached prepared statements instead of exec
61
- yield* execSqlPrepared(db, statementSql, bindValues)
74
+ yield* execSqlPrepared(dbState, statementSql, bindValues)
62
75
  }
63
76
 
77
+ dbState.debug.head = eventEncoded.seqNum
78
+
64
79
  const changeset = session.changeset()
65
80
  session.finish()
66
81
 
67
82
  // TODO use prepared statements
68
83
  yield* execSql(
69
- db,
84
+ dbState,
70
85
  ...insertRow({
71
86
  tableName: SystemTables.SESSION_CHANGESET_META_TABLE,
72
87
  columns: SystemTables.sessionChangesetMetaTable.sqliteDef.columns,
73
88
  values: {
74
89
  seqNumGlobal: eventEncoded.seqNum.global,
75
90
  seqNumClient: eventEncoded.seqNum.client,
91
+ seqNumRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
76
92
  // NOTE the changeset will be empty (i.e. null) for no-op events
77
93
  changeset: changeset ?? null,
78
94
  debug: LS_DEV ? execArgsArr : null,
@@ -107,6 +123,7 @@ export const makeMaterializeEvent = ({
107
123
  debug: LS_DEV ? execArgsArr : null,
108
124
  }
109
125
  : { _tag: 'no-op' as const },
126
+ hash: materializerHash,
110
127
  }
111
128
  }).pipe(
112
129
  Effect.withSpan(`@livestore/common:leader-thread:materializeEvent`, {
@@ -135,7 +152,11 @@ export const rollback = ({
135
152
  sql`SELECT * FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE (seqNumGlobal, seqNumClient) IN (${eventNumsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
136
153
  )
137
154
  .map((_) => ({
138
- seqNum: { global: _.seqNumGlobal, client: _.seqNumClient },
155
+ seqNum: {
156
+ global: _.seqNumGlobal,
157
+ client: _.seqNumClient,
158
+ rebaseGeneration: -1, // unused in this code path
159
+ },
139
160
  changeset: _.changeset,
140
161
  debug: _.debug,
141
162
  }))
@@ -4,3 +4,5 @@ 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
6
  export * as Eventlog from './eventlog.js'
7
+ export * from './materialize-event.js'
8
+ export * from './recreate-db.js'
@@ -1,108 +1,116 @@
1
1
  import { casesHandled } from '@livestore/utils'
2
- import type { HttpClient } from '@livestore/utils/effect'
3
2
  import { Effect, Queue } from '@livestore/utils/effect'
4
3
 
5
- import type { InvalidPullError, IsOfflineError, MigrationHooks, MigrationsReport, SqliteError } from '../index.js'
4
+ import type { MigrationsReport } from '../defs.js'
5
+ import type { BootStatus, MigrationHooks, SqliteDb, SqliteError } from '../index.js'
6
6
  import { migrateDb, rematerializeFromEventlog, UnexpectedError } from '../index.js'
7
+ import type { LiveStoreSchema } from '../schema/mod.js'
7
8
  import { configureConnection } from './connection.js'
8
- import { LeaderThreadCtx } from './types.js'
9
-
10
- export const recreateDb: Effect.Effect<
11
- { migrationsReport: MigrationsReport },
12
- UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
13
- LeaderThreadCtx | HttpClient.HttpClient
14
- > = Effect.gen(function* () {
15
- const { dbState, dbEventlog, schema, bootStatusQueue, materializeEvent } = yield* LeaderThreadCtx
16
-
17
- const migrationOptions = schema.state.sqlite.migrations
18
- let migrationsReport: MigrationsReport
19
-
20
- yield* Effect.addFinalizer(
21
- Effect.fn('recreateDb:finalizer')(function* (ex) {
22
- if (ex._tag === 'Failure') dbState.destroy()
23
- }),
24
- )
25
-
26
- // NOTE to speed up the operations below, we're creating a temporary in-memory database
27
- // and later we'll overwrite the persisted database with the new data
28
- // TODO bring back this optimization
29
- // const tmpDb = yield* makeSqliteDb({ _tag: 'in-memory' })
30
- const tmpDb = dbState
31
- yield* configureConnection(tmpDb, { foreignKeys: true })
32
-
33
- const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
34
- Effect.gen(function* () {
35
- yield* Effect.tryAll(() => hooks?.init?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
36
-
37
- const migrationsReport = yield* migrateDb({
38
- db: tmpDb,
39
- schema,
40
- onProgress: ({ done, total }) =>
41
- Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
9
+ import type { MaterializeEvent } from './types.js'
10
+
11
+ export const recreateDb = ({
12
+ dbState,
13
+ dbEventlog,
14
+ schema,
15
+ bootStatusQueue,
16
+ materializeEvent,
17
+ }: {
18
+ dbState: SqliteDb
19
+ dbEventlog: SqliteDb
20
+ schema: LiveStoreSchema
21
+ bootStatusQueue: Queue.Queue<BootStatus>
22
+ materializeEvent: MaterializeEvent
23
+ }): Effect.Effect<{ migrationsReport: MigrationsReport }, UnexpectedError | SqliteError> =>
24
+ Effect.gen(function* () {
25
+ const migrationOptions = schema.state.sqlite.migrations
26
+ let migrationsReport: MigrationsReport
27
+
28
+ yield* Effect.addFinalizer(
29
+ Effect.fn('recreateDb:finalizer')(function* (ex) {
30
+ if (ex._tag === 'Failure') dbState.destroy()
31
+ }),
32
+ )
33
+
34
+ // NOTE to speed up the operations below, we're creating a temporary in-memory database
35
+ // and later we'll overwrite the persisted database with the new data
36
+ // TODO bring back this optimization
37
+ // const tmpDb = yield* makeSqliteDb({ _tag: 'in-memory' })
38
+ const tmpDb = dbState
39
+ yield* configureConnection(tmpDb, { foreignKeys: true })
40
+
41
+ const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
42
+ Effect.gen(function* () {
43
+ yield* Effect.tryAll(() => hooks?.init?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
44
+
45
+ const migrationsReport = yield* migrateDb({
46
+ db: tmpDb,
47
+ schema,
48
+ onProgress: ({ done, total }) =>
49
+ Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
50
+ })
51
+
52
+ yield* Effect.tryAll(() => hooks?.pre?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
53
+
54
+ return { migrationsReport, tmpDb }
42
55
  })
43
56
 
44
- yield* Effect.tryAll(() => hooks?.pre?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
57
+ switch (migrationOptions.strategy) {
58
+ case 'auto': {
59
+ const hooks = migrationOptions.hooks
60
+ const initResult = yield* initDb(hooks)
45
61
 
46
- return { migrationsReport, tmpDb }
47
- })
62
+ migrationsReport = initResult.migrationsReport
48
63
 
49
- switch (migrationOptions.strategy) {
50
- case 'auto': {
51
- const hooks = migrationOptions.hooks
52
- const initResult = yield* initDb(hooks)
64
+ yield* rematerializeFromEventlog({
65
+ // db: initResult.tmpDb,
66
+ dbEventlog,
67
+ schema,
68
+ materializeEvent,
69
+ onProgress: ({ done, total }) =>
70
+ Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
71
+ })
53
72
 
54
- migrationsReport = initResult.migrationsReport
73
+ yield* Effect.tryAll(() => hooks?.post?.(initResult.tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
55
74
 
56
- yield* rematerializeFromEventlog({
57
- // db: initResult.tmpDb,
58
- dbEventlog,
59
- schema,
60
- materializeEvent,
61
- onProgress: ({ done, total }) =>
62
- Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
63
- })
75
+ break
76
+ }
77
+ case 'manual': {
78
+ const oldDbData = dbState.export()
64
79
 
65
- yield* Effect.tryAll(() => hooks?.post?.(initResult.tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
66
-
67
- break
68
- }
69
- case 'manual': {
70
- const oldDbData = dbState.export()
80
+ migrationsReport = { migrations: [] }
71
81
 
72
- migrationsReport = { migrations: [] }
82
+ const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
83
+ UnexpectedError.mapToUnexpectedError,
84
+ )
73
85
 
74
- const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
75
- UnexpectedError.mapToUnexpectedError,
76
- )
86
+ tmpDb.import(newDbData)
77
87
 
78
- tmpDb.import(newDbData)
88
+ // TODO validate schema
79
89
 
80
- // TODO validate schema
81
-
82
- break
83
- }
84
- default: {
85
- casesHandled(migrationOptions)
90
+ break
91
+ }
92
+ default: {
93
+ casesHandled(migrationOptions)
94
+ }
86
95
  }
87
- }
88
-
89
- // TODO bring back
90
- // Import the temporary in-memory database into the persistent database
91
- // yield* Effect.sync(() => db.import(tmpDb)).pipe(
92
- // Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
93
- // )
94
-
95
- // TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
96
- // We've disabled this for now as it made the code too complex, as we often run syncing right after
97
- // so the snapshot is no longer up to date
98
- // const snapshotFromTmpDb = tmpDb.export()
99
-
100
- // TODO bring back
101
- // tmpDb.close()
102
-
103
- return { migrationsReport }
104
- }).pipe(
105
- Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
106
- Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
107
- Effect.withPerformanceMeasure('@livestore/common:leader-thread:recreateDb'),
108
- )
96
+
97
+ // TODO bring back
98
+ // Import the temporary in-memory database into the persistent database
99
+ // yield* Effect.sync(() => db.import(tmpDb)).pipe(
100
+ // Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
101
+ // )
102
+
103
+ // TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
104
+ // We've disabled this for now as it made the code too complex, as we often run syncing right after
105
+ // so the snapshot is no longer up to date
106
+ // const snapshotFromTmpDb = tmpDb.export()
107
+
108
+ // TODO bring back
109
+ // tmpDb.close()
110
+
111
+ return { migrationsReport }
112
+ }).pipe(
113
+ Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
114
+ Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
115
+ Effect.withPerformanceMeasure('@livestore/common:leader-thread:recreateDb'),
116
+ )