@livestore/common 0.5.0-dev.0 → 0.5.0-dev.1

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 (302) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  3. package/dist/MaterializationJournal.d.ts +2 -2
  4. package/dist/MaterializationJournal.d.ts.map +1 -1
  5. package/dist/MaterializationJournal.js +7 -7
  6. package/dist/StateHead.d.ts +1 -1
  7. package/dist/StateHead.d.ts.map +1 -1
  8. package/dist/StateHead.js +4 -4
  9. package/dist/WorkerTransportError.d.ts.map +1 -1
  10. package/dist/__tests__/fixture.d.ts +7 -7
  11. package/dist/__tests__/fixture.js +2 -2
  12. package/dist/adapter-types.d.ts +4 -0
  13. package/dist/adapter-types.d.ts.map +1 -1
  14. package/dist/adapter-types.js +4 -4
  15. package/dist/bounded-collections.d.ts.map +1 -1
  16. package/dist/bounded-collections.js.map +1 -1
  17. package/dist/debug-info.d.ts.map +1 -1
  18. package/dist/debug-info.js +9 -8
  19. package/dist/debug-info.js.map +1 -1
  20. package/dist/defs.d.ts +4 -4
  21. package/dist/defs.js +2 -2
  22. package/dist/devtools/devtools-compatibility.test.js +1 -1
  23. package/dist/devtools/devtools-messages-client-session.d.ts +4 -4
  24. package/dist/devtools/devtools-messages-client-session.js +4 -4
  25. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  26. package/dist/devtools/devtools-messages-common.js +1 -1
  27. package/dist/devtools/devtools-messages-leader.d.ts +9 -9
  28. package/dist/devtools/devtools-messages-leader.js +4 -4
  29. package/dist/devtools/devtools-messages.js +3 -3
  30. package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
  31. package/dist/devtools/mod.d.ts.map +1 -1
  32. package/dist/devtools/mod.js +3 -3
  33. package/dist/errors.d.ts +4 -4
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +1 -1
  36. package/dist/errors.js.map +1 -1
  37. package/dist/index.js +22 -22
  38. package/dist/leader-thread/LeaderSyncProcessor.d.ts +27 -6
  39. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  40. package/dist/leader-thread/LeaderSyncProcessor.js +65 -55
  41. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  42. package/dist/leader-thread/RejectedPushError.d.ts +9 -9
  43. package/dist/leader-thread/RejectedPushError.d.ts.map +1 -1
  44. package/dist/leader-thread/RejectedPushError.js +1 -1
  45. package/dist/leader-thread/connection.d.ts.map +1 -1
  46. package/dist/leader-thread/connection.js +2 -2
  47. package/dist/leader-thread/eventlog.d.ts +11 -1
  48. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  49. package/dist/leader-thread/eventlog.js +13 -13
  50. package/dist/leader-thread/eventlog.js.map +1 -1
  51. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  52. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  53. package/dist/leader-thread/leader-worker-devtools.js +4 -4
  54. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  55. package/dist/leader-thread/make-leader-thread-layer.d.ts +6 -0
  56. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  57. package/dist/leader-thread/make-leader-thread-layer.js +43 -29
  58. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  59. package/dist/leader-thread/make-leader-thread-layer.test.js +2 -2
  60. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  61. package/dist/leader-thread/materialize-event.js +9 -9
  62. package/dist/leader-thread/mod.js +9 -9
  63. package/dist/leader-thread/recreate-db.d.ts +5 -1
  64. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  65. package/dist/leader-thread/recreate-db.js +21 -25
  66. package/dist/leader-thread/recreate-db.js.map +1 -1
  67. package/dist/leader-thread/shutdown-channel.js +1 -1
  68. package/dist/leader-thread/stream-events.d.ts.map +1 -1
  69. package/dist/leader-thread/stream-events.js +3 -5
  70. package/dist/leader-thread/stream-events.js.map +1 -1
  71. package/dist/leader-thread/types.d.ts +6 -4
  72. package/dist/leader-thread/types.d.ts.map +1 -1
  73. package/dist/leader-thread/types.js +5 -1
  74. package/dist/leader-thread/types.js.map +1 -1
  75. package/dist/make-client-session.d.ts +1 -1
  76. package/dist/make-client-session.d.ts.map +1 -1
  77. package/dist/make-client-session.js +2 -2
  78. package/dist/materializer-helper.d.ts.map +1 -1
  79. package/dist/materializer-helper.js +4 -4
  80. package/dist/materializer-helper.js.map +1 -1
  81. package/dist/otel.d.ts.map +1 -1
  82. package/dist/rematerialize-from-eventlog.d.ts +3 -1
  83. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  84. package/dist/rematerialize-from-eventlog.js +16 -9
  85. package/dist/rematerialize-from-eventlog.js.map +1 -1
  86. package/dist/schema/EventDef/define.d.ts +2 -2
  87. package/dist/schema/EventDef/define.d.ts.map +1 -1
  88. package/dist/schema/EventDef/define.test.js +1 -1
  89. package/dist/schema/EventDef/deprecated.d.ts +1 -1
  90. package/dist/schema/EventDef/deprecated.d.ts.map +1 -1
  91. package/dist/schema/EventDef/deprecated.test.js +2 -2
  92. package/dist/schema/EventDef/facts.d.ts.map +1 -1
  93. package/dist/schema/EventDef/materializer.d.ts +2 -2
  94. package/dist/schema/EventDef/materializer.d.ts.map +1 -1
  95. package/dist/schema/EventDef/mod.js +5 -5
  96. package/dist/schema/EventSequenceNumber/client.d.ts +3 -8
  97. package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -1
  98. package/dist/schema/EventSequenceNumber/client.js +12 -15
  99. package/dist/schema/EventSequenceNumber/client.js.map +1 -1
  100. package/dist/schema/EventSequenceNumber/mod.js +2 -2
  101. package/dist/schema/EventSequenceNumber.test.js +1 -1
  102. package/dist/schema/LiveStoreEvent/client.d.ts +8 -8
  103. package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -1
  104. package/dist/schema/LiveStoreEvent/client.js +1 -1
  105. package/dist/schema/LiveStoreEvent/client.js.map +1 -1
  106. package/dist/schema/LiveStoreEvent/client.test.js +2 -2
  107. package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -1
  108. package/dist/schema/LiveStoreEvent/global.js +1 -1
  109. package/dist/schema/LiveStoreEvent/input.d.ts +1 -1
  110. package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -1
  111. package/dist/schema/LiveStoreEvent/mod.js +4 -4
  112. package/dist/schema/events.js +1 -1
  113. package/dist/schema/mod.js +11 -11
  114. package/dist/schema/schema.d.ts +1 -1
  115. package/dist/schema/schema.d.ts.map +1 -1
  116. package/dist/schema/schema.js +3 -3
  117. package/dist/schema/state/mod.js +2 -2
  118. package/dist/schema/state/sqlite/client-document-def.d.ts +1 -1
  119. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  120. package/dist/schema/state/sqlite/client-document-def.js +8 -8
  121. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  122. package/dist/schema/state/sqlite/client-document-def.test.js +24 -10
  123. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  124. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  125. package/dist/schema/state/sqlite/column-annotations.test.js +1 -1
  126. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  127. package/dist/schema/state/sqlite/column-def.js +17 -3
  128. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  129. package/dist/schema/state/sqlite/column-def.test.js +60 -5
  130. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  131. package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -1
  132. package/dist/schema/state/sqlite/column-spec.js +1 -1
  133. package/dist/schema/state/sqlite/column-spec.test.js +2 -2
  134. package/dist/schema/state/sqlite/column-spec.test.js.map +1 -1
  135. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts +3 -0
  136. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts.map +1 -0
  137. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js +5 -0
  138. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js.map +1 -0
  139. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts +5 -0
  140. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts.map +1 -0
  141. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js +183 -0
  142. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js.map +1 -0
  143. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts +2 -0
  144. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts.map +1 -0
  145. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js +142 -0
  146. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js.map +1 -0
  147. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +1 -6
  148. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  149. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -82
  150. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  151. package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -1
  152. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
  153. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -1
  154. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  155. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +1 -1
  156. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  157. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -1
  158. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts +3 -0
  159. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts.map +1 -0
  160. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js +6 -0
  161. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js.map +1 -0
  162. package/dist/schema/state/sqlite/db-schema/mod.js +2 -2
  163. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  164. package/dist/schema/state/sqlite/mod.js +10 -9
  165. package/dist/schema/state/sqlite/mod.js.map +1 -1
  166. package/dist/schema/state/sqlite/persistence.d.ts.map +1 -1
  167. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  168. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
  169. package/dist/schema/state/sqlite/query-builder/astToSql.js +2 -7
  170. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
  171. package/dist/schema/state/sqlite/query-builder/impl.d.ts +1 -3
  172. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  173. package/dist/schema/state/sqlite/query-builder/impl.js +2 -2
  174. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  175. package/dist/schema/state/sqlite/query-builder/impl.test.js +2 -2
  176. package/dist/schema/state/sqlite/query-builder/mod.js +2 -2
  177. package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -1
  178. package/dist/schema/state/sqlite/schema-helpers.js +1 -1
  179. package/dist/schema/state/sqlite/schema-helpers.test.js +2 -2
  180. package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +3 -3
  181. package/dist/schema/state/sqlite/system-tables/mod.js +2 -2
  182. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +60 -17
  183. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -1
  184. package/dist/schema/state/sqlite/system-tables/state-tables.js +14 -5
  185. package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -1
  186. package/dist/schema/state/sqlite/table-def.d.ts +1 -1
  187. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  188. package/dist/schema/state/sqlite/table-def.js +24 -8
  189. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  190. package/dist/schema/state/sqlite/table-def.test.js +54 -1
  191. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  192. package/dist/schema/unknown-events.d.ts.map +1 -1
  193. package/dist/schema/unknown-events.js +1 -1
  194. package/dist/schema/unknown-events.js.map +1 -1
  195. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +26 -4
  196. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js.map +1 -1
  197. package/dist/schema-management/common.d.ts.map +1 -1
  198. package/dist/schema-management/common.js +2 -2
  199. package/dist/schema-management/migrations.d.ts +4 -4
  200. package/dist/schema-management/migrations.d.ts.map +1 -1
  201. package/dist/schema-management/migrations.js +9 -9
  202. package/dist/schema-management/migrations.js.map +1 -1
  203. package/dist/schema-management/validate-schema.d.ts.map +1 -1
  204. package/dist/schema-management/validate-schema.js +1 -1
  205. package/dist/session-id-symbol.d.ts.map +1 -1
  206. package/dist/sql-queries/index.js +3 -3
  207. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  208. package/dist/sql-queries/sql-queries.js +3 -3
  209. package/dist/sql-queries/sql-queries.js.map +1 -1
  210. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  211. package/dist/sql-queries/sql-query-builder.js +1 -1
  212. package/dist/sql-queries/types.d.ts.map +1 -1
  213. package/dist/sqlite-db-helper.d.ts +2 -2
  214. package/dist/sqlite-db-helper.d.ts.map +1 -1
  215. package/dist/sqlite-db-helper.js +2 -2
  216. package/dist/sync/ClientSessionSyncProcessor.d.ts +3 -3
  217. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  218. package/dist/sync/ClientSessionSyncProcessor.js +7 -7
  219. package/dist/sync/errors.js +1 -1
  220. package/dist/sync/index.js +6 -6
  221. package/dist/sync/mock-sync-backend.d.ts.map +1 -1
  222. package/dist/sync/mock-sync-backend.js +4 -4
  223. package/dist/sync/mock-sync-backend.test.js +2 -2
  224. package/dist/sync/next/compact-events.d.ts.map +1 -1
  225. package/dist/sync/next/compact-events.js +3 -3
  226. package/dist/sync/next/facts.d.ts.map +1 -1
  227. package/dist/sync/next/facts.js +1 -1
  228. package/dist/sync/next/history-dag-common.d.ts +1 -1
  229. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  230. package/dist/sync/next/history-dag-common.js +1 -1
  231. package/dist/sync/next/history-dag.d.ts.map +1 -1
  232. package/dist/sync/next/history-dag.js +3 -3
  233. package/dist/sync/next/mod.js +5 -5
  234. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  235. package/dist/sync/next/rebase-events.js +2 -2
  236. package/dist/sync/next/test/compact-events.calculator.test.js +4 -4
  237. package/dist/sync/next/test/compact-events.test.js +4 -4
  238. package/dist/sync/next/test/event-fixtures.d.ts +1 -1
  239. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  240. package/dist/sync/next/test/event-fixtures.js +5 -5
  241. package/dist/sync/next/test/history-dag-common.test.js +2 -2
  242. package/dist/sync/next/test/mod.js +1 -1
  243. package/dist/sync/sync-backend-kv.js +1 -1
  244. package/dist/sync/sync-backend.d.ts.map +1 -1
  245. package/dist/sync/sync-backend.js +1 -1
  246. package/dist/sync/sync.js +2 -2
  247. package/dist/sync/syncstate.d.ts +3 -3
  248. package/dist/sync/syncstate.d.ts.map +1 -1
  249. package/dist/sync/syncstate.js +2 -2
  250. package/dist/sync/syncstate.js.map +1 -1
  251. package/dist/sync/syncstate.test.js +3 -3
  252. package/dist/sync/transport-chunking.d.ts.map +1 -1
  253. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  254. package/dist/sync/validate-push-payload.js +2 -2
  255. package/dist/testing/event-factory.d.ts.map +1 -1
  256. package/dist/testing/event-factory.js +2 -2
  257. package/dist/testing/event-factory.js.map +1 -1
  258. package/dist/testing/mod.js +1 -1
  259. package/dist/util.d.ts.map +1 -1
  260. package/dist/util.js +1 -0
  261. package/dist/util.js.map +1 -1
  262. package/dist/version.d.ts.map +1 -1
  263. package/dist/version.js.map +1 -1
  264. package/package.json +40 -39
  265. package/src/adapter-types.ts +4 -0
  266. package/src/debug-info.ts +11 -8
  267. package/src/defs.ts +2 -2
  268. package/src/leader-thread/LeaderSyncProcessor.ts +121 -97
  269. package/src/leader-thread/eventlog.ts +13 -5
  270. package/src/leader-thread/leader-worker-devtools.ts +1 -1
  271. package/src/leader-thread/make-leader-thread-layer.ts +49 -23
  272. package/src/leader-thread/recreate-db.ts +26 -25
  273. package/src/leader-thread/stream-events.ts +1 -3
  274. package/src/leader-thread/types.ts +5 -0
  275. package/src/materializer-helper.ts +1 -1
  276. package/src/rematerialize-from-eventlog.ts +28 -13
  277. package/src/schema/EventSequenceNumber/client.ts +12 -15
  278. package/src/schema/schema.ts +1 -1
  279. package/src/schema/state/sqlite/client-document-def.test.ts +23 -7
  280. package/src/schema/state/sqlite/client-document-def.ts +2 -2
  281. package/src/schema/state/sqlite/column-def.test.ts +84 -3
  282. package/src/schema/state/sqlite/column-def.ts +16 -1
  283. package/src/schema/state/sqlite/db-schema/ast/fingerprint-digest.ts +6 -0
  284. package/src/schema/state/sqlite/db-schema/ast/fingerprint.test.ts +209 -0
  285. package/src/schema/state/sqlite/db-schema/ast/fingerprint.ts +225 -0
  286. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +1 -90
  287. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +1 -0
  288. package/src/schema/state/sqlite/db-schema/has-json-string-encoding.ts +9 -0
  289. package/src/schema/state/sqlite/mod.ts +2 -1
  290. package/src/schema/state/sqlite/query-builder/astToSql.ts +1 -8
  291. package/src/schema/state/sqlite/system-tables/state-tables.ts +13 -2
  292. package/src/schema/state/sqlite/table-def.test.ts +66 -0
  293. package/src/schema/state/sqlite/table-def.ts +21 -5
  294. package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +33 -2
  295. package/src/schema-management/migrations.ts +7 -6
  296. package/src/sql-queries/sql-queries.ts +1 -1
  297. package/src/util.ts +1 -0
  298. package/dist/schema/state/sqlite/db-schema/hash.d.ts +0 -2
  299. package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +0 -1
  300. package/dist/schema/state/sqlite/db-schema/hash.js +0 -15
  301. package/dist/schema/state/sqlite/db-schema/hash.js.map +0 -1
  302. package/src/schema/state/sqlite/db-schema/hash.ts +0 -13
@@ -15,6 +15,7 @@ import {
15
15
  Option,
16
16
  Predicate,
17
17
  Queue,
18
+ Random,
18
19
  ReadonlyArray,
19
20
  References,
20
21
  Result,
@@ -49,8 +50,7 @@ import {
49
50
  StaleRebaseGenerationError,
50
51
  } from './RejectedPushError.ts'
51
52
  import type { ShutdownChannel } from './shutdown-channel.ts'
52
- import type { InitialBlockingSyncContext } from './types.ts'
53
- import { LeaderThreadCtx } from './types.ts'
53
+ import type { InitialBlockingSyncContext, MaterializeEvent } from './types.ts'
54
54
 
55
55
  export const TypeId = '~@livestore/common/LeaderSyncProcessor' as const
56
56
  export type TypeId = typeof TypeId
@@ -120,14 +120,15 @@ export interface Service {
120
120
  readonly boot: Effect.Effect<
121
121
  { initialLeaderHead: EventSequenceNumber.Client.Composite },
122
122
  never,
123
- LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
123
+ Scope.Scope | HttpClient.HttpClient
124
124
  >
125
125
  readonly syncState: Subscribable.Subscribable<SyncState.SyncState>
126
126
  }
127
127
 
128
128
  interface Options {
129
129
  readonly schema: LiveStoreSchema
130
- readonly dbState: SqliteDb
130
+ /** Complete runtime capabilities supplied before the processor is constructed. */
131
+ readonly runtime: Runtime
131
132
  readonly initialBlockingSyncContext: InitialBlockingSyncContext
132
133
  /** Initial sync state rehydrated from the persisted eventlog or initial sync state */
133
134
  readonly initialSyncState: SyncState.SyncState
@@ -197,16 +198,33 @@ interface Options {
197
198
  readonly testing: {
198
199
  readonly delays?: {
199
200
  readonly localPushProcessing?: Effect.Effect<void>
201
+ readonly backendPullReconnect?: (fallbackMs: number) => number
200
202
  }
201
203
  readonly hooks?: {
202
204
  readonly localPushAdmitted?: (events: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>) => Effect.Effect<void>
205
+ /** Observes connectivity after the offline wait has subscribed, allowing tests to synchronize without sleeps. */
206
+ readonly backendPullReconnectWait?: (state: { isConnected: boolean; fallbackMs: number }) => Effect.Effect<void>
203
207
  }
204
208
  }
205
209
  }
206
210
 
211
+ /**
212
+ * Runtime capabilities used by sync processing. Keeping these explicit prevents the processor from
213
+ * depending on the outward-facing leader aggregate that contains the processor itself.
214
+ */
215
+ interface Runtime {
216
+ readonly dbState: SqliteDb
217
+ readonly dbEventlog: SqliteDb
218
+ readonly materializeEvent: MaterializeEvent
219
+ readonly syncBackend: SyncBackend.SyncBackend | undefined
220
+ readonly shutdownChannel: ShutdownChannel
221
+ readonly devtoolsLatch: Latch.Latch | undefined
222
+ readonly span: Tracer.Span | undefined
223
+ }
224
+
207
225
  export const make = Effect.fnUntraced(function* ({
208
226
  schema,
209
- dbState,
227
+ runtime,
210
228
  initialBlockingSyncContext,
211
229
  initialSyncState,
212
230
  onError,
@@ -216,6 +234,7 @@ export const make = Effect.fnUntraced(function* ({
216
234
  testing,
217
235
  }: Options) {
218
236
  const stateHead = yield* StateHead.StateHead
237
+ const { dbState, dbEventlog, devtoolsLatch, materializeEvent, shutdownChannel, span, syncBackend } = runtime
219
238
  const syncBackendPushQueue = yield* TxQueue.unbounded<LiveStoreEvent.Client.EncodedWithMeta>()
220
239
  const localPushBatchSize = params.localPushBatchSize ?? 10
221
240
  const backendPushBatchSize = params.backendPushBatchSize ?? 50
@@ -226,17 +245,7 @@ export const make = Effect.fnUntraced(function* ({
226
245
  schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
227
246
 
228
247
  const connectedClientSessionPullQueues = yield* makePullQueueSet
229
-
230
- // This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
231
- const ctxRef = {
232
- current: undefined as
233
- | undefined
234
- | {
235
- span: Tracer.Span
236
- devtoolsLatch: Latch.Latch | undefined
237
- services: Context.Context<LeaderThreadCtx>
238
- },
239
- }
248
+ const materializeEventsBatch = makeMaterializeEventsBatch({ dbState, dbEventlog, materializeEvent })
240
249
 
241
250
  type LocalPushQueueItem = [
242
251
  event: LiveStoreEvent.Client.EncodedWithMeta,
@@ -311,7 +320,7 @@ export const make = Effect.fnUntraced(function* ({
311
320
 
312
321
  // Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
313
322
  // It's important that we filter after acquiring the localPushBackendPullMutex, otherwise we might filter with the old generation
314
- const [droppedItems, filteredItems] = ReadonlyArray.partition(batchItems, (batchItem) =>
323
+ const [filteredItems, droppedItems] = ReadonlyArray.partition(batchItems, (batchItem) =>
315
324
  batchItem[0].seqNum.rebaseGeneration >= currentRebaseGeneration
316
325
  ? Result.succeed(batchItem)
317
326
  : Result.fail(batchItem),
@@ -381,7 +390,7 @@ export const make = Effect.fnUntraced(function* ({
381
390
  const remainingLocalPushes = yield* snapshotTxQueue(localPushesQueue)
382
391
  if (LS_DEV === true && remainingLocalPushes.length > 0) {
383
392
  console.log('localPushesQueue is not empty', remainingLocalPushes.length)
384
- // oxlint-disable-next-line eslint(no-debugger) -- intentional breakpoint for unexpected queue state
393
+ // oxlint-disable-next-line no-debugger -- intentional breakpoint for unexpected queue state
385
394
  debugger
386
395
  }
387
396
 
@@ -458,13 +467,13 @@ export const make = Effect.fnUntraced(function* ({
458
467
 
459
468
  const backgroundBackendPulling = Effect.fn('@livestore/common:LeaderSyncProcessor:backend-pulling')(function* ({
460
469
  restartBackendPushing,
470
+ onPullSuccess,
461
471
  }: {
462
472
  restartBackendPushing: (
463
473
  filteredRebasedPending: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
464
- ) => Effect.Effect<void, never, LeaderThreadCtx | HttpClient.HttpClient>
474
+ ) => Effect.Effect<void, never, HttpClient.HttpClient>
475
+ onPullSuccess: () => void
465
476
  }) {
466
- const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
467
-
468
477
  if (syncBackend === undefined) return
469
478
 
470
479
  let pullMutexHeld = false
@@ -482,8 +491,8 @@ export const make = Effect.fnUntraced(function* ({
482
491
  pageInfo: SyncBackend.PullResPageInfo,
483
492
  ) =>
484
493
  Effect.gen(function* () {
485
- if (ctxRef.current?.devtoolsLatch !== undefined) {
486
- yield* ctxRef.current.devtoolsLatch.await
494
+ if (devtoolsLatch !== undefined) {
495
+ yield* devtoolsLatch.await
487
496
  }
488
497
 
489
498
  if (newEvents.length === 0) {
@@ -540,7 +549,7 @@ export const make = Effect.fnUntraced(function* ({
540
549
 
541
550
  if (mergeResult.rollbackEvents.length > 0) {
542
551
  yield* rollback({
543
- dbState: db,
552
+ dbState,
544
553
  dbEventlog,
545
554
  eventNumsToRollback: mergeResult.rollbackEvents.map((_) => _.seqNum),
546
555
  })
@@ -576,12 +585,12 @@ export const make = Effect.fnUntraced(function* ({
576
585
  EventSequenceNumber.Client.isEqual(event.seqNum, confirmedEvent.seqNum),
577
586
  ),
578
587
  )
579
- yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(Effect.orDieDebugger)
588
+ yield* Eventlog.updateSyncMetadataForDb(dbEventlog, confirmedNewEvents).pipe(Effect.orDieDebugger)
580
589
  }
581
590
  }
582
591
 
583
592
  // Removes the changeset rows which are no longer needed as we'll never have to rollback beyond this point
584
- trimChangesetRows(db, newBackendHead)
593
+ trimChangesetRows(dbState, newBackendHead)
585
594
 
586
595
  // The backend merge may advance or rebase the authoritative head. Realign the admission
587
596
  // fence now so newly arriving pushes are validated against that history, not the pre-pull head.
@@ -605,7 +614,9 @@ export const make = Effect.fnUntraced(function* ({
605
614
  })
606
615
 
607
616
  const syncState = yield* Effect.fromNullishOr(yield* SubscriptionRef.get(syncStateSref)).pipe(Effect.orDieDebugger)
608
- const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
617
+ const cursorInfo = yield* Eventlog.getSyncBackendCursorInfoForDb(dbEventlog, {
618
+ remoteHead: syncState.upstreamHead.global,
619
+ })
609
620
 
610
621
  const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
611
622
 
@@ -613,6 +624,8 @@ export const make = Effect.fnUntraced(function* ({
613
624
  // TODO only take from queue while connected
614
625
  Stream.tap(({ batch, pageInfo }) =>
615
626
  Effect.gen(function* () {
627
+ // Any response (including an empty batch) proves backend contact, even with stale connectivity.
628
+ onPullSuccess()
616
629
  // NOTE we only want to take process events when the sync backend is connected
617
630
  // (e.g. needed for simulating being offline)
618
631
  // TODO remove when there's a better way to handle this in stream above
@@ -642,7 +655,6 @@ export const make = Effect.fnUntraced(function* ({
642
655
  })
643
656
 
644
657
  const backgroundBackendPushing = Effect.gen(function* () {
645
- const { syncBackend } = yield* LeaderThreadCtx
646
658
  if (syncBackend === undefined) return
647
659
 
648
660
  while (true) {
@@ -652,8 +664,8 @@ export const make = Effect.fnUntraced(function* ({
652
664
 
653
665
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
654
666
 
655
- if (ctxRef.current?.devtoolsLatch !== undefined) {
656
- yield* ctxRef.current.devtoolsLatch.await
667
+ if (devtoolsLatch !== undefined) {
668
+ yield* devtoolsLatch.await
657
669
  }
658
670
 
659
671
  yield* Effect.spanEvent('backend-push', {
@@ -732,14 +744,14 @@ export const make = Effect.fnUntraced(function* ({
732
744
  }).pipe(Effect.uninterruptible),
733
745
  )
734
746
 
735
- yield* Effect.all(deferreds.map(Deferred.await))
747
+ yield* Effect.forEach(deferreds, Deferred.await)
736
748
  }).pipe(
737
749
  Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
738
750
  attributes: {
739
751
  batchSize: newEvents.length,
740
752
  batch: TRACE_VERBOSE === true ? newEvents : undefined,
741
753
  },
742
- links: ctxRef.current?.span !== undefined ? [{ span: ctxRef.current.span, attributes: {} }] : undefined,
754
+ links: span !== undefined ? [{ span, attributes: {} }] : undefined,
743
755
  }),
744
756
  )
745
757
 
@@ -747,16 +759,6 @@ export const make = Effect.fnUntraced(function* ({
747
759
  [TypeId]: TypeId,
748
760
  // Starts various background loops
749
761
  boot: Effect.gen(function* () {
750
- const span = yield* Effect.currentSpan.pipe(Effect.orDie)
751
- const { devtools, shutdownChannel } = yield* LeaderThreadCtx
752
- const services = yield* Effect.context<LeaderThreadCtx>()
753
-
754
- ctxRef.current = {
755
- span,
756
- devtoolsLatch: devtools.enabled === true ? devtools.syncBackendLatch : undefined,
757
- services,
758
- }
759
-
760
762
  /** State transitions need to happen atomically, so we use a Ref to track the state */
761
763
  yield* SubscriptionRef.set(syncStateSref, initialSyncState)
762
764
 
@@ -772,7 +774,7 @@ export const make = Effect.fnUntraced(function* ({
772
774
  }
773
775
 
774
776
  const handleBackendIdMismatchError = (error: BackendIdMismatchError) =>
775
- handleBackendIdMismatch({ error, onBackendIdMismatch, shutdownChannel })
777
+ handleBackendIdMismatch({ error, onBackendIdMismatch, shutdownChannel, dbEventlog, dbState })
776
778
 
777
779
  const maybeShutdownOnError = (cause: Cause.Cause<UnknownError | MaterializeError>) =>
778
780
  Effect.gen(function* () {
@@ -803,7 +805,12 @@ export const make = Effect.fnUntraced(function* ({
803
805
 
804
806
  yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
805
807
 
808
+ let offlinePullFallbackMs = 1_000
809
+
806
810
  yield* backgroundBackendPulling({
811
+ onPullSuccess: () => {
812
+ offlinePullFallbackMs = 1_000
813
+ },
807
814
  restartBackendPushing: (filteredRebasedPending) =>
808
815
  Effect.gen(function* () {
809
816
  // Stop current pushing fiber
@@ -817,10 +824,34 @@ export const make = Effect.fnUntraced(function* ({
817
824
  yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
818
825
  }),
819
826
  }).pipe(
827
+ Effect.tapError((error) => {
828
+ if (error._tag !== 'IsOfflineError' || syncBackend === undefined) return Effect.void
829
+ const isConnected = syncBackend.isConnected
830
+ return Effect.gen(function* () {
831
+ // Wake immediately on reconnection. Probe on a capped fallback timer if connectivity is stale.
832
+ const jitter = yield* Random.nextBetween(0.8, 1.2)
833
+ const fallbackMs =
834
+ testing.delays?.backendPullReconnect?.(offlinePullFallbackMs * jitter) ?? offlinePullFallbackMs * jitter
835
+ offlinePullFallbackMs = Math.min(offlinePullFallbackMs * 2, 30_000)
836
+ // A stale initial true is not a reconnect. Watch false→true even while the fallback timer is pending.
837
+ const waitHook = testing.hooks?.backendPullReconnectWait
838
+ const changes = SubscriptionRef.changes(isConnected)
839
+ const observedChanges =
840
+ waitHook === undefined
841
+ ? changes
842
+ : changes.pipe(Stream.tap((value) => waitHook({ isConnected: value, fallbackMs })))
843
+ const reconnected = yield* observedChanges.pipe(
844
+ Stream.dropWhile((value) => value === true),
845
+ Stream.filter((value) => value === true),
846
+ Stream.runHead,
847
+ Effect.as(true),
848
+ Effect.timeoutOrElse({ duration: fallbackMs, orElse: () => Effect.succeed(false) }),
849
+ )
850
+ if (reconnected === true) offlinePullFallbackMs = 1_000
851
+ })
852
+ }),
820
853
  Effect.retry({
821
- // Retry pulling when we've lost connection to the sync backend
822
- // We're using `until` with a refinement instead of `while` to narrow `IsOfflineError` out of the error type.
823
- // See https://github.com/Effect-TS/effect/issues/6122
854
+ // The unbounded offline-only retry keeps IsOfflineError out of the terminal error channel.
824
855
  until: (error): error is Exclude<typeof error, IsOfflineError> => error._tag !== 'IsOfflineError',
825
856
  }),
826
857
  Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
@@ -875,12 +906,7 @@ export const make = Effect.fnUntraced(function* ({
875
906
  ),
876
907
  pull: ({ cursor }) =>
877
908
  Effect.gen(function* () {
878
- const queue = yield* Effect.fromNullishOr(ctxRef.current?.services).pipe(
879
- Effect.orDieDebugger,
880
- Effect.flatMap((services) =>
881
- connectedClientSessionPullQueues.makeQueue(cursor).pipe(Effect.provide(services)),
882
- ),
883
- )
909
+ const queue = yield* connectedClientSessionPullQueues.makeQueue(cursor)
884
910
  return Stream.fromQueue(queue)
885
911
  }).pipe(Stream.unwrap),
886
912
  /*
@@ -897,11 +923,7 @@ export const make = Effect.fnUntraced(function* ({
897
923
  - full new state db snapshot in the "rebase" case
898
924
  - downside: importing the snapshot is expensive
899
925
  */
900
- pullQueue: ({ cursor }) =>
901
- Effect.fromNullishOr(ctxRef.current?.services).pipe(
902
- Effect.orDieDebugger,
903
- Effect.flatMap((services) => connectedClientSessionPullQueues.makeQueue(cursor).pipe(Effect.provide(services))),
904
- ),
926
+ pullQueue: ({ cursor }) => connectedClientSessionPullQueues.makeQueue(cursor),
905
927
  syncState: Subscribable.make({
906
928
  get: SubscriptionRef.get(syncStateSref).pipe(Effect.flatMap(Effect.fromNullishOr), Effect.orDieDebugger),
907
929
  changes: SubscriptionRef.changes(syncStateSref).pipe(Stream.filter(Predicate.isNotUndefined)),
@@ -911,46 +933,50 @@ export const make = Effect.fnUntraced(function* ({
911
933
 
912
934
  export const layer = (options: Options) => Layer.effect(LeaderSyncProcessor, make(options))
913
935
 
914
- type MaterializeEventsBatch = (_: {
915
- batchItems: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>
916
- }) => Effect.Effect<void, MaterializeError, LeaderThreadCtx>
917
-
918
936
  // TODO how to handle errors gracefully
919
- const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems }) =>
920
- Effect.gen(function* () {
921
- const { dbState: db, dbEventlog, materializeEvent } = yield* LeaderThreadCtx
937
+ const makeMaterializeEventsBatch =
938
+ ({
939
+ dbState,
940
+ dbEventlog,
941
+ materializeEvent,
942
+ }: {
943
+ dbState: SqliteDb
944
+ dbEventlog: SqliteDb
945
+ materializeEvent: MaterializeEvent
946
+ }) =>
947
+ ({ batchItems }: { batchItems: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta> }) =>
948
+ Effect.gen(function* () {
949
+ // NOTE We always start a transaction to ensure consistency between db and eventlog (even for single-item batches)
950
+ dbState.execute('BEGIN TRANSACTION', undefined) // Start the transaction
951
+ dbEventlog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
922
952
 
923
- // NOTE We always start a transaction to ensure consistency between db and eventlog (even for single-item batches)
924
- db.execute('BEGIN TRANSACTION', undefined) // Start the transaction
925
- dbEventlog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
953
+ yield* Effect.addFinalizer((exit) =>
954
+ Effect.gen(function* () {
955
+ if (Exit.isSuccess(exit) === true) return
926
956
 
927
- yield* Effect.addFinalizer((exit) =>
928
- Effect.gen(function* () {
929
- if (Exit.isSuccess(exit) === true) return
957
+ // Rollback in case of an error
958
+ dbState.execute('ROLLBACK', undefined)
959
+ dbEventlog.execute('ROLLBACK', undefined)
960
+ }),
961
+ )
962
+
963
+ for (let i = 0; i < batchItems.length; i++) {
964
+ const { sessionChangeset, hash } = yield* materializeEvent(batchItems[i]!)
965
+ batchItems[i]!.meta.sessionChangeset = sessionChangeset
966
+ batchItems[i]!.meta.materializerHashLeader = hash
967
+ }
930
968
 
931
- // Rollback in case of an error
932
- db.execute('ROLLBACK', undefined)
933
- dbEventlog.execute('ROLLBACK', undefined)
969
+ dbState.execute('COMMIT', undefined) // Commit the transaction
970
+ dbEventlog.execute('COMMIT', undefined) // Commit the transaction
971
+ }).pipe(
972
+ Effect.uninterruptible,
973
+ Effect.scoped,
974
+ Effect.withSpan('@livestore/common:LeaderSyncProcessor:materializeEventItems', {
975
+ attributes: { batchSize: batchItems.length },
934
976
  }),
977
+ Effect.tapCauseLogPretty,
935
978
  )
936
979
 
937
- for (let i = 0; i < batchItems.length; i++) {
938
- const { sessionChangeset, hash } = yield* materializeEvent(batchItems[i]!)
939
- batchItems[i]!.meta.sessionChangeset = sessionChangeset
940
- batchItems[i]!.meta.materializerHashLeader = hash
941
- }
942
-
943
- db.execute('COMMIT', undefined) // Commit the transaction
944
- dbEventlog.execute('COMMIT', undefined) // Commit the transaction
945
- }).pipe(
946
- Effect.uninterruptible,
947
- Effect.scoped,
948
- Effect.withSpan('@livestore/common:LeaderSyncProcessor:materializeEventItems', {
949
- attributes: { batchSize: batchItems.length },
950
- }),
951
- Effect.tapCauseLogPretty,
952
- )
953
-
954
980
  const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.Client.Composite) => {
955
981
  // Since we're using the session changeset rows to query for the current head,
956
982
  // we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
@@ -960,11 +986,7 @@ const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.Client.Com
960
986
  interface PullQueueSet {
961
987
  makeQueue: (
962
988
  cursor: EventSequenceNumber.Client.Composite,
963
- ) => Effect.Effect<
964
- Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>,
965
- never,
966
- Scope.Scope | LeaderThreadCtx
967
- >
989
+ ) => Effect.Effect<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>, never, Scope.Scope>
968
990
  offer: (item: {
969
991
  payload: typeof SyncState.PayloadUpstream.Type
970
992
  leaderHead: EventSequenceNumber.Client.Composite
@@ -1176,13 +1198,15 @@ const handleBackendIdMismatch = Effect.fn('@livestore/common:LeaderSyncProcessor
1176
1198
  error,
1177
1199
  onBackendIdMismatch,
1178
1200
  shutdownChannel,
1201
+ dbEventlog,
1202
+ dbState,
1179
1203
  }: {
1180
1204
  error: BackendIdMismatchError
1181
1205
  onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
1182
1206
  shutdownChannel: ShutdownChannel
1207
+ dbEventlog: SqliteDb
1208
+ dbState: SqliteDb
1183
1209
  }) {
1184
- const { dbEventlog, dbState } = yield* LeaderThreadCtx
1185
-
1186
1210
  if (onBackendIdMismatch === 'reset') {
1187
1211
  yield* Effect.logWarning(
1188
1212
  'Sync backend identity changed (backend was reset). Clearing local storage and shutting down.',
@@ -258,9 +258,13 @@ export const insertIntoEventlog = (
258
258
  })
259
259
 
260
260
  export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>) =>
261
- Effect.gen(function* () {
262
- const { dbEventlog } = yield* LeaderThreadCtx
261
+ LeaderThreadCtx.pipe(Effect.flatMap(({ dbEventlog }) => updateSyncMetadataForDb(dbEventlog, items)))
263
262
 
263
+ export const updateSyncMetadataForDb = (
264
+ dbEventlog: SqliteDb,
265
+ items: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
266
+ ) =>
267
+ Effect.gen(function* () {
264
268
  // TODO try to do this in a single query
265
269
  for (let i = 0; i < items.length; i++) {
266
270
  const event = items[i]!
@@ -277,10 +281,14 @@ export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.Client.En
277
281
  }
278
282
  })
279
283
 
280
- export const getSyncBackendCursorInfo = ({ remoteHead }: { remoteHead: EventSequenceNumber.Global.Type }) =>
281
- Effect.gen(function* () {
282
- const { dbEventlog } = yield* LeaderThreadCtx
284
+ export const getSyncBackendCursorInfo = (args: { remoteHead: EventSequenceNumber.Global.Type }) =>
285
+ LeaderThreadCtx.pipe(Effect.flatMap(({ dbEventlog }) => getSyncBackendCursorInfoForDb(dbEventlog, args)))
283
286
 
287
+ export const getSyncBackendCursorInfoForDb = (
288
+ dbEventlog: SqliteDb,
289
+ { remoteHead }: { remoteHead: EventSequenceNumber.Global.Type },
290
+ ) =>
291
+ Effect.gen(function* () {
284
292
  if (remoteHead === EventSequenceNumber.Client.ROOT.global) return Option.none()
285
293
 
286
294
  const EventlogQuerySchema = Schema.Struct({
@@ -42,7 +42,7 @@ export const bootDevtools = Effect.fn('@livestore/common:leader-thread:devtools:
42
42
  }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
43
43
 
44
44
  const bootResult = yield* options.boot.pipe(
45
- Effect.map(Option.some),
45
+ Effect.asSome,
46
46
  Effect.catchIf(isDevtoolsViteNotInstalledError, (error) =>
47
47
  Effect.logWarning(`[@livestore/devtools] ${error.message} Devtools will be disabled.`).pipe(
48
48
  Effect.as(Option.none()),
@@ -31,11 +31,12 @@ import type * as StateHead from '../StateHead.ts'
31
31
  import type { SyncBackend, SyncOptions } from '../sync/sync.ts'
32
32
  import { SyncState } from '../sync/syncstate.ts'
33
33
  import { sql } from '../util.ts'
34
+ import { configureConnection } from './connection.ts'
34
35
  import * as Eventlog from './eventlog.ts'
35
36
  import { bootDevtools } from './leader-worker-devtools.ts'
36
37
  import * as LeaderSyncProcessor from './LeaderSyncProcessor.ts'
37
38
  import { makeMaterializeEvent } from './materialize-event.ts'
38
- import { recreateDb } from './recreate-db.ts'
39
+ import { hasCompletedState, recreateDb } from './recreate-db.ts'
39
40
  import type { ShutdownChannel } from './shutdown-channel.ts'
40
41
  import type {
41
42
  DevtoolsContext,
@@ -64,14 +65,17 @@ export interface MakeLeaderThreadLayerParams {
64
65
  params?: {
65
66
  localPushBatchSize?: number
66
67
  backendPushBatchSize?: number
68
+ stateRebuildBatchSize?: number
67
69
  }
68
70
  testing?: {
69
71
  syncProcessor?: {
70
72
  delays?: {
71
73
  localPushProcessing?: Effect.Effect<void>
74
+ backendPullReconnect?: (fallbackMs: number) => number
72
75
  }
73
76
  hooks?: {
74
77
  localPushAdmitted?: (events: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>) => Effect.Effect<void>
78
+ backendPullReconnectWait?: (state: { isConnected: boolean; fallbackMs: number }) => Effect.Effect<void>
75
79
  }
76
80
  }
77
81
  }
@@ -99,9 +103,7 @@ export const makeLeaderThreadLayer = ({
99
103
  > =>
100
104
  Effect.gen(function* () {
101
105
  const syncPayloadDecoded =
102
- syncPayloadEncoded === undefined
103
- ? undefined
104
- : yield* Schema.decodeUnknownEffect(syncPayloadSchema)(syncPayloadEncoded)
106
+ syncPayloadEncoded === undefined ? undefined : yield* Schema.decodeEffect(syncPayloadSchema)(syncPayloadEncoded)
105
107
 
106
108
  const bootStatusQueue = yield* Effect.acquireRelease(Queue.unbounded<BootStatus>(), Queue.shutdown)
107
109
 
@@ -112,8 +114,20 @@ export const makeLeaderThreadLayer = ({
112
114
 
113
115
  const dbEventlogMissing = !hasEventlogTables(dbEventlog)
114
116
 
115
- // Either happens on initial boot or if schema changes
116
- const dbStateMissing = !hasStateTables(dbState)
117
+ const stateNeedsRebuild = !hasCompletedState(dbState)
118
+
119
+ if (stateNeedsRebuild === true) {
120
+ // Import also clears hook-created objects while preserving the open connection.
121
+ yield* Effect.acquireUseRelease(
122
+ makeSqliteDb({ _tag: 'in-memory' }),
123
+ (emptyDb) =>
124
+ configureConnection(emptyDb, { foreignKeys: false }).pipe(
125
+ Effect.andThen(Effect.sync(() => dbState.import(emptyDb))),
126
+ UnknownError.mapToUnknownError,
127
+ ),
128
+ (emptyDb) => Effect.sync(() => emptyDb.close()),
129
+ )
130
+ }
117
131
 
118
132
  yield* Eventlog.initEventlogDb(dbEventlog)
119
133
 
@@ -171,13 +185,39 @@ export const makeLeaderThreadLayer = ({
171
185
  // Recreate state database if needed BEFORE creating sync processor
172
186
  // This ensures all system tables exist before any queries are made
173
187
  const { migrationsReport } =
174
- dbStateMissing === true
175
- ? yield* recreateDb({ dbState, dbEventlog, schema, bootStatusQueue, materializeEvent })
188
+ stateNeedsRebuild === true
189
+ ? yield* recreateDb({
190
+ dbState,
191
+ dbEventlog,
192
+ schema,
193
+ bootStatusQueue,
194
+ materializeEvent,
195
+ ...omitUndefineds({ stateRebuildBatchSize: params?.stateRebuildBatchSize }),
196
+ })
176
197
  : { migrationsReport: { migrations: [] } }
177
198
 
199
+ const devtoolsContext =
200
+ devtoolsOptions.enabled === true
201
+ ? {
202
+ enabled: true as const,
203
+ syncBackendLatch: yield* Latch.make(true),
204
+ syncBackendLatchState: yield* SubscriptionRef.make<{ latchClosed: boolean }>({ latchClosed: false }),
205
+ }
206
+ : { enabled: false as const }
207
+
208
+ const span = yield* Effect.currentSpan.pipe(Effect.option, Effect.map(Option.getOrUndefined))
209
+
178
210
  const syncProcessor = yield* LeaderSyncProcessor.make({
179
211
  schema,
180
- dbState,
212
+ runtime: {
213
+ dbState,
214
+ dbEventlog,
215
+ materializeEvent,
216
+ syncBackend,
217
+ shutdownChannel,
218
+ devtoolsLatch: devtoolsContext.enabled === true ? devtoolsContext.syncBackendLatch : undefined,
219
+ span,
220
+ },
181
221
  initialSyncState: getInitialSyncState({ dbEventlog, dbState, dbEventlogMissing }),
182
222
  initialBlockingSyncContext,
183
223
  onError: syncOptions?.onSyncError ?? 'ignore',
@@ -199,15 +239,6 @@ export const makeLeaderThreadLayer = ({
199
239
  Queue.shutdown,
200
240
  )
201
241
 
202
- const devtoolsContext =
203
- devtoolsOptions.enabled === true
204
- ? {
205
- enabled: true as const,
206
- syncBackendLatch: yield* Latch.make(true),
207
- syncBackendLatchState: yield* SubscriptionRef.make<{ latchClosed: boolean }>({ latchClosed: false }),
208
- }
209
- : { enabled: false as const }
210
-
211
242
  const networkStatus = yield* makeNetworkStatusSubscribable({ syncBackend, devtoolsContext })
212
243
 
213
244
  const ctx = {
@@ -256,11 +287,6 @@ const hasEventlogTables = (db: SqliteDb) => {
256
287
  return ReadonlyArray.every(SystemTables.eventlogSystemTables, (_) => tableNames.has(_.sqliteDef.name))
257
288
  }
258
289
 
259
- const hasStateTables = (db: SqliteDb) => {
260
- const tableNames = new Set(db.select<{ name: string }>(sql`select name from sqlite_master`).map((_) => _.name))
261
- return ReadonlyArray.every(SystemTables.stateSystemTables, (_) => tableNames.has(_.sqliteDef.name))
262
- }
263
-
264
290
  const getInitialSyncState = ({
265
291
  dbEventlog,
266
292
  dbState,