@rocicorp/zero 1.10.0-canary.0 → 1.10.0-canary.2

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 (408) hide show
  1. package/out/replicache/src/db/commit.js +1 -4
  2. package/out/replicache/src/db/commit.js.map +1 -1
  3. package/out/replicache/src/db/read.d.ts +1 -0
  4. package/out/replicache/src/db/read.d.ts.map +1 -1
  5. package/out/replicache/src/db/read.js +1 -4
  6. package/out/replicache/src/db/read.js.map +1 -1
  7. package/out/replicache/src/db/write.d.ts +2 -1
  8. package/out/replicache/src/db/write.d.ts.map +1 -1
  9. package/out/replicache/src/db/write.js +4 -51
  10. package/out/replicache/src/db/write.js.map +1 -1
  11. package/out/replicache/src/persist/clients.d.ts +1 -0
  12. package/out/replicache/src/persist/clients.d.ts.map +1 -1
  13. package/out/replicache/src/persist/clients.js +4 -31
  14. package/out/replicache/src/persist/clients.js.map +1 -1
  15. package/out/replicache/src/process-env.d.ts +28 -0
  16. package/out/replicache/src/process-env.d.ts.map +1 -0
  17. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  18. package/out/replicache/src/replicache-impl.js.map +1 -1
  19. package/out/replicache/src/scan-iterator.js +2 -14
  20. package/out/replicache/src/scan-iterator.js.map +1 -1
  21. package/out/replicache/src/subscriptions.d.ts +1 -0
  22. package/out/replicache/src/subscriptions.d.ts.map +1 -1
  23. package/out/replicache/src/subscriptions.js +2 -10
  24. package/out/replicache/src/subscriptions.js.map +1 -1
  25. package/out/replicache/src/transactions.d.ts +1 -0
  26. package/out/replicache/src/transactions.d.ts.map +1 -1
  27. package/out/replicache/src/transactions.js +2 -4
  28. package/out/replicache/src/transactions.js.map +1 -1
  29. package/out/z2s/src/compiler.d.ts.map +1 -1
  30. package/out/z2s/src/compiler.js +1 -22
  31. package/out/z2s/src/compiler.js.map +1 -1
  32. package/out/zero/package.js +3 -3
  33. package/out/zero/package.js.map +1 -1
  34. package/out/zero-cache/src/config/normalize.d.ts +1 -0
  35. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  36. package/out/zero-cache/src/config/normalize.js +10 -1
  37. package/out/zero-cache/src/config/normalize.js.map +1 -1
  38. package/out/zero-cache/src/config/zero-config.d.ts +52 -1
  39. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  40. package/out/zero-cache/src/config/zero-config.js +171 -22
  41. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  42. package/out/zero-cache/src/custom/fetch.js +1 -1
  43. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  44. package/out/zero-cache/src/db/delete-lite-db.js +2 -1
  45. package/out/zero-cache/src/db/delete-lite-db.js.map +1 -1
  46. package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
  47. package/out/zero-cache/src/db/migration-lite.js +5 -2
  48. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  49. package/out/zero-cache/src/db/migration.d.ts.map +1 -1
  50. package/out/zero-cache/src/db/migration.js +4 -2
  51. package/out/zero-cache/src/db/migration.js.map +1 -1
  52. package/out/zero-cache/src/db/sqlite-corruption.d.ts +3 -2
  53. package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -1
  54. package/out/zero-cache/src/db/sqlite-corruption.js +13 -8
  55. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
  56. package/out/zero-cache/src/observability/metrics.d.ts +6 -1
  57. package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
  58. package/out/zero-cache/src/observability/metrics.js +20 -2
  59. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  60. package/out/zero-cache/src/server/backup-watermark-poller.js +31 -0
  61. package/out/zero-cache/src/server/backup-watermark-poller.js.map +1 -0
  62. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  63. package/out/zero-cache/src/server/change-streamer.js +91 -31
  64. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  65. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  66. package/out/zero-cache/src/server/main.js +6 -16
  67. package/out/zero-cache/src/server/main.js.map +1 -1
  68. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  69. package/out/zero-cache/src/server/replicator.js +48 -5
  70. package/out/zero-cache/src/server/replicator.js.map +1 -1
  71. package/out/zero-cache/src/server/runner/run-worker.js +2 -1
  72. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  73. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  74. package/out/zero-cache/src/server/syncer.js +1 -1
  75. package/out/zero-cache/src/server/syncer.js.map +1 -1
  76. package/out/zero-cache/src/server/worker-urls.d.ts +1 -1
  77. package/out/zero-cache/src/server/worker-urls.js +2 -2
  78. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  79. package/out/zero-cache/src/services/change-source/change-source.d.ts +4 -2
  80. package/out/zero-cache/src/services/change-source/change-source.d.ts.map +1 -1
  81. package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +35 -0
  82. package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -0
  83. package/out/zero-cache/src/services/change-source/common/replica-restore.js +34 -0
  84. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -0
  85. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts +2 -0
  86. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  87. package/out/zero-cache/src/services/change-source/common/replica-schema.js +73 -1
  88. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  89. package/out/zero-cache/src/services/change-source/custom/change-source.d.ts +2 -6
  90. package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/change-source/custom/change-source.js +14 -5
  92. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  93. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +4 -7
  94. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  95. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +7 -7
  96. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  97. package/out/zero-cache/src/services/change-source/pg/change-source.js +82 -25
  98. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  99. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +9 -1
  100. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  101. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +12 -4
  102. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  103. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +45 -2
  104. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
  105. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +59 -8
  106. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  107. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +4 -2
  108. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
  109. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +7 -4
  110. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  111. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts +0 -5
  112. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  113. package/out/zero-cache/src/services/change-source/pg/schema/init.js +29 -41
  114. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  115. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +35 -16
  116. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  117. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +77 -7
  118. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  119. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +2 -1
  120. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
  121. package/out/zero-cache/src/services/change-source/protocol/current/data.js +5 -13
  122. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  123. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +6 -3
  124. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
  125. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.d.ts +6 -0
  126. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.d.ts.map +1 -0
  127. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.js +21 -0
  128. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.js.map +1 -0
  129. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts +1 -0
  130. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts.map +1 -1
  131. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +1 -0
  132. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
  133. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts +5 -6
  134. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
  135. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +32 -9
  136. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  137. package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts +15 -5
  138. package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts.map +1 -1
  139. package/out/zero-cache/src/services/change-streamer/backup-monitor.js +66 -0
  140. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -0
  141. package/out/zero-cache/src/services/change-streamer/broadcast.d.ts +68 -78
  142. package/out/zero-cache/src/services/change-streamer/broadcast.d.ts.map +1 -1
  143. package/out/zero-cache/src/services/change-streamer/broadcast.js +84 -92
  144. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  145. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +19 -0
  146. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -0
  147. package/out/zero-cache/src/services/change-streamer/change-log-codec.js +50 -0
  148. package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -0
  149. package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts +141 -0
  150. package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts.map +1 -0
  151. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +270 -0
  152. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -0
  153. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.d.ts +19 -0
  154. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.d.ts.map +1 -0
  155. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js +39 -0
  156. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -0
  157. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -4
  158. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  159. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +12 -17
  160. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  161. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +69 -24
  162. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  163. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +333 -66
  164. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  165. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +56 -13
  166. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  167. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  168. package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts +8 -0
  169. package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts.map +1 -1
  170. package/out/zero-cache/src/services/change-streamer/forwarder.d.ts +14 -3
  171. package/out/zero-cache/src/services/change-streamer/forwarder.d.ts.map +1 -1
  172. package/out/zero-cache/src/services/change-streamer/forwarder.js +41 -11
  173. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  174. package/out/zero-cache/src/services/change-streamer/{litestream3-backup-monitor.d.ts → litestream3-prometheus-poller.d.ts} +19 -45
  175. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.d.ts.map +1 -0
  176. package/out/zero-cache/src/services/change-streamer/{litestream3-backup-monitor.js → litestream3-prometheus-poller.js} +57 -171
  177. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -0
  178. package/out/zero-cache/src/services/change-streamer/replica-poller.d.ts +15 -0
  179. package/out/zero-cache/src/services/change-streamer/replica-poller.d.ts.map +1 -0
  180. package/out/zero-cache/src/services/change-streamer/replica-poller.js +55 -0
  181. package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -0
  182. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +5 -1
  183. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
  184. package/out/zero-cache/src/services/change-streamer/schema/tables.js +2 -1
  185. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  186. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +14 -0
  187. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -0
  188. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +86 -0
  189. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -0
  190. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +4 -1
  191. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
  192. package/out/zero-cache/src/services/change-streamer/snapshot.js +39 -14
  193. package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
  194. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts +75 -0
  195. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts.map +1 -0
  196. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +261 -0
  197. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -0
  198. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.d.ts +121 -0
  199. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.d.ts.map +1 -0
  200. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js +391 -0
  201. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -0
  202. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts +62 -0
  203. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts.map +1 -0
  204. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +159 -0
  205. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -0
  206. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts +109 -0
  207. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts.map +1 -0
  208. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +258 -0
  209. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -0
  210. package/out/zero-cache/src/services/change-streamer/storer.d.ts +28 -12
  211. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  212. package/out/zero-cache/src/services/change-streamer/storer.js +116 -121
  213. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  214. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +61 -10
  215. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  216. package/out/zero-cache/src/services/change-streamer/subscriber.js +101 -8
  217. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  218. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts +34 -0
  219. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts.map +1 -0
  220. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +86 -0
  221. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -0
  222. package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
  223. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  224. package/out/zero-cache/src/services/life-cycle.js +4 -3
  225. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  226. package/out/zero-cache/src/services/litestream/commands.d.ts +12 -11
  227. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  228. package/out/zero-cache/src/services/litestream/commands.js +107 -148
  229. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  230. package/out/zero-cache/src/services/litestream/config-v5.yml +42 -0
  231. package/out/zero-cache/src/services/litestream/config.yml +3 -2
  232. package/out/zero-cache/src/services/litestream/metrics.d.ts +4 -4
  233. package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -1
  234. package/out/zero-cache/src/services/litestream/metrics.js +7 -10
  235. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  236. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts +57 -0
  237. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts.map +1 -0
  238. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js +137 -0
  239. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js.map +1 -0
  240. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  241. package/out/zero-cache/src/services/mutagen/mutagen.js +4 -1
  242. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  243. package/out/zero-cache/src/services/replicator/change-log-cookies.d.ts +169 -0
  244. package/out/zero-cache/src/services/replicator/change-log-cookies.d.ts.map +1 -0
  245. package/out/zero-cache/src/services/replicator/change-log-cookies.js +434 -0
  246. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -0
  247. package/out/zero-cache/src/services/replicator/change-log-db.d.ts +358 -0
  248. package/out/zero-cache/src/services/replicator/change-log-db.d.ts.map +1 -0
  249. package/out/zero-cache/src/services/replicator/change-log-db.js +477 -0
  250. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -0
  251. package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts +55 -0
  252. package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts.map +1 -0
  253. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +150 -0
  254. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -0
  255. package/out/zero-cache/src/services/replicator/change-processor.d.ts +6 -0
  256. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  257. package/out/zero-cache/src/services/replicator/change-processor.js +70 -25
  258. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  259. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +1 -1
  260. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  261. package/out/zero-cache/src/services/replicator/incremental-sync.js +19 -6
  262. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  263. package/out/zero-cache/src/services/replicator/notifier.d.ts +0 -22
  264. package/out/zero-cache/src/services/replicator/notifier.d.ts.map +1 -1
  265. package/out/zero-cache/src/services/replicator/notifier.js +7 -1
  266. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  267. package/out/zero-cache/src/services/replicator/replicator.d.ts +17 -2
  268. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  269. package/out/zero-cache/src/services/replicator/replicator.js +9 -3
  270. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  271. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +27 -0
  272. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
  273. package/out/zero-cache/src/services/replicator/reporter/recorder.js +32 -0
  274. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  275. package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts +1 -1
  276. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts +119 -0
  277. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts.map +1 -0
  278. package/out/zero-cache/src/services/replicator/schema/backfilling.js +214 -0
  279. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -0
  280. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +3 -3
  281. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  282. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  283. package/out/zero-cache/src/services/replicator/schema/replication-state.d.ts +1 -1
  284. package/out/zero-cache/src/services/replicator/schema/replication-state.d.ts.map +1 -1
  285. package/out/zero-cache/src/services/replicator/schema/replication-state.js +9 -5
  286. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  287. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts +131 -0
  288. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts.map +1 -0
  289. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +413 -0
  290. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -0
  291. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.d.ts +120 -0
  292. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.d.ts.map +1 -0
  293. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +283 -0
  294. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -0
  295. package/out/zero-cache/src/services/replicator/write-worker.js +23 -12
  296. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  297. package/out/zero-cache/src/services/run-ast.d.ts.map +1 -1
  298. package/out/zero-cache/src/services/run-ast.js +2 -1
  299. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  300. package/out/zero-cache/src/services/running-state.d.ts +1 -1
  301. package/out/zero-cache/src/services/running-state.d.ts.map +1 -1
  302. package/out/zero-cache/src/services/running-state.js +1 -0
  303. package/out/zero-cache/src/services/running-state.js.map +1 -1
  304. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts +53 -0
  305. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts.map +1 -0
  306. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js +68 -0
  307. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -0
  308. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  309. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +18 -3
  310. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  311. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  312. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  313. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +11 -1
  314. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  315. package/out/zero-cache/src/services/view-syncer/view-syncer.js +64 -6
  316. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  317. package/out/zero-cache/src/types/processes.d.ts +1 -2
  318. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  319. package/out/zero-cache/src/types/processes.js +1 -25
  320. package/out/zero-cache/src/types/processes.js.map +1 -1
  321. package/out/zero-cache/src/types/streams.d.ts +21 -0
  322. package/out/zero-cache/src/types/streams.d.ts.map +1 -1
  323. package/out/zero-cache/src/types/streams.js +25 -4
  324. package/out/zero-cache/src/types/streams.js.map +1 -1
  325. package/out/zero-cache/src/types/subscription.d.ts +11 -0
  326. package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
  327. package/out/zero-cache/src/types/subscription.js +27 -0
  328. package/out/zero-cache/src/types/subscription.js.map +1 -1
  329. package/out/zero-cache/src/workers/connection.d.ts +1 -0
  330. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  331. package/out/zero-cache/src/workers/connection.js +24 -2
  332. package/out/zero-cache/src/workers/connection.js.map +1 -1
  333. package/out/zero-cache/src/workers/replicator.d.ts +25 -0
  334. package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
  335. package/out/zero-cache/src/workers/replicator.js +33 -1
  336. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  337. package/out/zero-cache/src/workers/syncer.d.ts +12 -0
  338. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  339. package/out/zero-cache/src/workers/syncer.js +75 -0
  340. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  341. package/out/zero-client/src/client/version.js +1 -1
  342. package/out/zero-client/src/client/zero.d.ts +6 -7
  343. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  344. package/out/zero-client/src/client/zero.js +156 -101
  345. package/out/zero-client/src/client/zero.js.map +1 -1
  346. package/out/zero-schema/src/builder/table-builder.d.ts +23 -0
  347. package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
  348. package/out/zero-schema/src/builder/table-builder.js +18 -0
  349. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  350. package/out/zero-server/src/custom.js +10 -3
  351. package/out/zero-server/src/custom.js.map +1 -1
  352. package/out/zero-server/src/schema.d.ts +8 -0
  353. package/out/zero-server/src/schema.d.ts.map +1 -1
  354. package/out/zero-server/src/schema.js +63 -24
  355. package/out/zero-server/src/schema.js.map +1 -1
  356. package/out/zero-types/src/schema.d.ts +11 -0
  357. package/out/zero-types/src/schema.d.ts.map +1 -1
  358. package/out/zql/src/mutate/crud.d.ts +10 -8
  359. package/out/zql/src/mutate/crud.d.ts.map +1 -1
  360. package/out/zql/src/mutate/crud.js.map +1 -1
  361. package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
  362. package/out/zql/src/query/query-delegate-base.js +5 -1
  363. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  364. package/out/zql/src/query/query-impl.d.ts +2 -2
  365. package/out/zql/src/query/query-impl.d.ts.map +1 -1
  366. package/out/zql/src/query/query-impl.js.map +1 -1
  367. package/out/zql/src/query/query.d.ts +83 -14
  368. package/out/zql/src/query/query.d.ts.map +1 -1
  369. package/out/zql/src/query/static-query.d.ts +1 -1
  370. package/out/zql/src/query/static-query.d.ts.map +1 -1
  371. package/out/zqlite/src/db.js +2 -2
  372. package/out/zqlite/src/resolve-scalar-subqueries.d.ts +15 -0
  373. package/out/zqlite/src/resolve-scalar-subqueries.d.ts.map +1 -1
  374. package/out/zqlite/src/resolve-scalar-subqueries.js +33 -14
  375. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  376. package/out/zqlite/src/sqlite-cost-model.js +5 -5
  377. package/out/zqlite/src/table-source.js +3 -3
  378. package/package.json +3 -3
  379. package/out/replicache/src/db/index.js +0 -91
  380. package/out/replicache/src/db/index.js.map +0 -1
  381. package/out/replicache/src/lazy.d.ts +0 -2
  382. package/out/replicache/src/lazy.d.ts.map +0 -1
  383. package/out/replicache/src/lazy.js +0 -12
  384. package/out/replicache/src/lazy.js.map +0 -1
  385. package/out/zero-cache/src/server/backup-watermark-reader.js +0 -35
  386. package/out/zero-cache/src/server/backup-watermark-reader.js.map +0 -1
  387. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +0 -1
  388. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +0 -1
  389. package/out/zero-cache/src/services/change-streamer/replica-monitor.d.ts +0 -16
  390. package/out/zero-cache/src/services/change-streamer/replica-monitor.d.ts.map +0 -1
  391. package/out/zero-cache/src/services/change-streamer/replica-monitor.js +0 -54
  392. package/out/zero-cache/src/services/change-streamer/replica-monitor.js.map +0 -1
  393. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts +0 -25
  394. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts.map +0 -1
  395. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js +0 -159
  396. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js.map +0 -1
  397. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts +0 -44
  398. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts.map +0 -1
  399. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +0 -65
  400. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js.map +0 -1
  401. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts +0 -11
  402. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts.map +0 -1
  403. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js +0 -74
  404. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js.map +0 -1
  405. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts +0 -42
  406. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts.map +0 -1
  407. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js +0 -117
  408. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js.map +0 -1
@@ -0,0 +1,159 @@
1
+ import { assert } from "../../../../shared/src/asserts.js";
2
+ import { AbortError } from "../../../../shared/src/abort-error.js";
3
+ import { Database } from "../../../../zqlite/src/db.js";
4
+ import { CHANGE_LOG_STREAM_TABLE } from "../replicator/change-log-db.js";
5
+ import { reconstructWatermarkedChange } from "./change-log-codec.js";
6
+ //#region ../zero-cache/src/services/change-streamer/sqlite-change-log-reader.ts
7
+ /**
8
+ * Seeks to the final position so validating a boundary does not scan every row
9
+ * of a large transaction. Exported so the focused query-plan test covers the
10
+ * production subquery.
11
+ */
12
+ var SQLITE_CHANGE_LOG_BOUNDARY_SQL = `
13
+ SELECT
14
+ "precommit" = @fromWatermark AND
15
+ "tag" = 'commit' AS "boundaryExists"
16
+ FROM "${CHANGE_LOG_STREAM_TABLE}"
17
+ WHERE "watermark" = @fromWatermark
18
+ ORDER BY "pos" DESC
19
+ LIMIT 1
20
+ `;
21
+ /**
22
+ * The log describes its own bounds; nothing here reads the replica.
23
+ * `max("watermark")` is the head because a transaction's rows commit atomically,
24
+ * so the log never ends mid-transaction.
25
+ *
26
+ * `min` and `max` are deliberately kept as *separate* scalar subqueries: SQLite
27
+ * applies its single-aggregate index optimization only when the aggregate is
28
+ * alone in its query, so `SELECT min(x), max(x) FROM t` degrades to a full scan
29
+ * while two scalar subqueries are two index seeks. The query-plan test pins
30
+ * this.
31
+ *
32
+ * Exported so that test can cover the production query.
33
+ */
34
+ var SQLITE_CHANGE_LOG_PLAN_SQL = `
35
+ SELECT
36
+ (SELECT max("watermark") FROM "${CHANGE_LOG_STREAM_TABLE}")
37
+ AS "headWatermark",
38
+ (SELECT min("watermark") FROM "${CHANGE_LOG_STREAM_TABLE}")
39
+ AS "minWatermark",
40
+ coalesce((${SQLITE_CHANGE_LOG_BOUNDARY_SQL}), 0) AS "boundaryExists"
41
+ `;
42
+ /** Exported so the focused query-plan test covers the production query. */
43
+ var SQLITE_CHANGE_LOG_READ_BATCH_SQL = `
44
+ SELECT
45
+ "watermark",
46
+ "pos",
47
+ "tag",
48
+ "change"
49
+ FROM "${CHANGE_LOG_STREAM_TABLE}"
50
+ WHERE ("watermark", "pos") > (?, ?)
51
+ AND "watermark" <= ?
52
+ ORDER BY "watermark", "pos"
53
+ LIMIT ?
54
+ `;
55
+ var TABLE_EXISTS_SQL = `
56
+ SELECT 1 FROM "sqlite_master" WHERE "type" = 'table' AND "name" = ?
57
+ `;
58
+ /**
59
+ * Reads the change log without retaining a SQLite snapshot while a consumer
60
+ * processes a batch.
61
+ *
62
+ * The log lives in its own database beside the replica
63
+ * (`changeLogFileName(replicaFile)`), which this reader opens read-only; it
64
+ * never opens the replica. The connection therefore keeps the journal mode
65
+ * configured by the change-log writer. Callers pin a head with {@link plan}
66
+ * before passing it to {@link read}.
67
+ *
68
+ * A change-streamer can start before the replicator has created the log, so the
69
+ * stream table may not exist yet. Statements are prepared on first use rather
70
+ * than in the constructor, and {@link plan} reports `not-ready` until they can
71
+ * be; a reader constructed too early becomes usable once the writer catches up.
72
+ */
73
+ var SQLiteChangeLogReader = class {
74
+ #db;
75
+ #statements;
76
+ #closed = false;
77
+ constructor(lc, changeLogFile) {
78
+ this.#db = new Database(lc.withContext("component", "sqlite-change-log-reader"), changeLogFile, { readonly: true });
79
+ }
80
+ plan(fromWatermark) {
81
+ this.#assertOpen();
82
+ const statements = this.#prepare();
83
+ if (statements === void 0) return { kind: "not-ready" };
84
+ const row = statements.plan.get({ fromWatermark });
85
+ if (row.headWatermark === null || row.minWatermark === null) return { kind: "not-ready" };
86
+ const { headWatermark, minWatermark } = row;
87
+ if (fromWatermark > headWatermark) return {
88
+ kind: "ahead",
89
+ headWatermark
90
+ };
91
+ if (fromWatermark < minWatermark || row.boundaryExists === 0) return {
92
+ kind: "too-old",
93
+ minWatermark,
94
+ headWatermark
95
+ };
96
+ return {
97
+ kind: "range",
98
+ minWatermark,
99
+ headWatermark
100
+ };
101
+ }
102
+ async *read(fromWatermark, throughWatermark, batchSize, signal) {
103
+ assert(Number.isSafeInteger(batchSize) && batchSize > 0, "SQLite change log batch size must be a positive safe integer");
104
+ this.#assertOpen();
105
+ const statements = this.#prepare();
106
+ assert(statements !== void 0, "SQLite change log must be ready before it is read");
107
+ let lastWatermark = fromWatermark;
108
+ let lastPos = Number.MAX_SAFE_INTEGER;
109
+ let lastTag;
110
+ while (true) {
111
+ this.#throwIfAborted(signal);
112
+ const rows = statements.readBatch.all(lastWatermark, lastPos, throughWatermark, batchSize);
113
+ this.#throwIfAborted(signal);
114
+ if (rows.length === 0) break;
115
+ const changes = rows.map(reconstructWatermarkedChange);
116
+ const last = rows.at(-1);
117
+ assert(last !== void 0, "non-empty batch must have a final row");
118
+ lastWatermark = last.watermark;
119
+ lastPos = last.pos;
120
+ lastTag = last.tag;
121
+ yield changes;
122
+ }
123
+ if (fromWatermark !== throughWatermark) assert(lastWatermark === throughWatermark && lastTag === "commit", () => `SQLite change log did not end at pinned head ${throughWatermark}`);
124
+ }
125
+ close() {
126
+ if (this.#closed) return;
127
+ this.#closed = true;
128
+ this.#db.close();
129
+ }
130
+ [Symbol.dispose]() {
131
+ this.close();
132
+ }
133
+ /**
134
+ * Prepares the statements once the stream table exists, memoizing them.
135
+ * Returns `undefined` while the log has not been created, which is a normal
136
+ * startup state rather than an error.
137
+ */
138
+ #prepare() {
139
+ if (this.#statements === void 0 && this.#streamTableExists()) this.#statements = {
140
+ plan: this.#db.prepare(SQLITE_CHANGE_LOG_PLAN_SQL),
141
+ readBatch: this.#db.prepare(SQLITE_CHANGE_LOG_READ_BATCH_SQL)
142
+ };
143
+ return this.#statements;
144
+ }
145
+ #streamTableExists() {
146
+ return this.#db.prepare(TABLE_EXISTS_SQL).get(CHANGE_LOG_STREAM_TABLE) !== void 0;
147
+ }
148
+ #assertOpen() {
149
+ if (this.#closed) throw new AbortError("SQLite change log reader is closed");
150
+ }
151
+ #throwIfAborted(signal) {
152
+ this.#assertOpen();
153
+ if (signal?.aborted) throw new AbortError("SQLite change log read aborted");
154
+ }
155
+ };
156
+ //#endregion
157
+ export { SQLITE_CHANGE_LOG_BOUNDARY_SQL, SQLITE_CHANGE_LOG_PLAN_SQL, SQLiteChangeLogReader };
158
+
159
+ //# sourceMappingURL=sqlite-change-log-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-change-log-reader.js","names":["#db","#assertOpen","#prepare","#throwIfAborted","#closed","#statements","#streamTableExists"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-reader.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {Database, type Statement} from '../../../../zqlite/src/db.ts';\nimport {CHANGE_LOG_STREAM_TABLE} from '../replicator/change-log-db.ts';\nimport {\n reconstructWatermarkedChange,\n type ChangeLogEntry,\n} from './change-log-codec.ts';\nimport type {WatermarkedChange} from './change-streamer.ts';\n\nexport type CatchupPlan =\n | {\n readonly kind: 'range';\n readonly minWatermark: string;\n readonly headWatermark: string;\n }\n | {readonly kind: 'ahead'; readonly headWatermark: string}\n | {\n readonly kind: 'too-old';\n readonly minWatermark: string;\n readonly headWatermark: string;\n }\n // The log exists as a file but has no content to serve: the replicator has\n // not created or reconciled it yet. Subscriber selection treats this as\n // \"decline and fall back to PG catchup\", which is why it is a plan rather\n // than an error.\n | {readonly kind: 'not-ready'};\n\ntype PlanRow = {\n readonly headWatermark: string | null;\n readonly minWatermark: string | null;\n readonly boundaryExists: number;\n};\n\ntype ChangeLogRow = ChangeLogEntry & {\n readonly pos: number;\n};\n\ntype PreparedStatements = {\n readonly plan: Statement;\n readonly readBatch: Statement;\n};\n\n/**\n * Seeks to the final position so validating a boundary does not scan every row\n * of a large transaction. Exported so the focused query-plan test covers the\n * production subquery.\n */\nexport const SQLITE_CHANGE_LOG_BOUNDARY_SQL = /*sql*/ `\n SELECT\n \"precommit\" = @fromWatermark AND\n \"tag\" = 'commit' AS \"boundaryExists\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @fromWatermark\n ORDER BY \"pos\" DESC\n LIMIT 1\n`;\n\n/**\n * The log describes its own bounds; nothing here reads the replica.\n * `max(\"watermark\")` is the head because a transaction's rows commit atomically,\n * so the log never ends mid-transaction.\n *\n * `min` and `max` are deliberately kept as *separate* scalar subqueries: SQLite\n * applies its single-aggregate index optimization only when the aggregate is\n * alone in its query, so `SELECT min(x), max(x) FROM t` degrades to a full scan\n * while two scalar subqueries are two index seeks. The query-plan test pins\n * this.\n *\n * Exported so that test can cover the production query.\n */\nexport const SQLITE_CHANGE_LOG_PLAN_SQL = /*sql*/ `\n SELECT\n (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"headWatermark\",\n (SELECT min(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"minWatermark\",\n coalesce((${SQLITE_CHANGE_LOG_BOUNDARY_SQL}), 0) AS \"boundaryExists\"\n`;\n\n/** Exported so the focused query-plan test covers the production query. */\nexport const SQLITE_CHANGE_LOG_READ_BATCH_SQL = /*sql*/ `\n SELECT\n \"watermark\",\n \"pos\",\n \"tag\",\n \"change\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE (\"watermark\", \"pos\") > (?, ?)\n AND \"watermark\" <= ?\n ORDER BY \"watermark\", \"pos\"\n LIMIT ?\n`;\n\nconst TABLE_EXISTS_SQL = /*sql*/ `\n SELECT 1 FROM \"sqlite_master\" WHERE \"type\" = 'table' AND \"name\" = ?\n`;\n\n/**\n * Reads the change log without retaining a SQLite snapshot while a consumer\n * processes a batch.\n *\n * The log lives in its own database beside the replica\n * (`changeLogFileName(replicaFile)`), which this reader opens read-only; it\n * never opens the replica. The connection therefore keeps the journal mode\n * configured by the change-log writer. Callers pin a head with {@link plan}\n * before passing it to {@link read}.\n *\n * A change-streamer can start before the replicator has created the log, so the\n * stream table may not exist yet. Statements are prepared on first use rather\n * than in the constructor, and {@link plan} reports `not-ready` until they can\n * be; a reader constructed too early becomes usable once the writer catches up.\n */\nexport class SQLiteChangeLogReader implements Disposable {\n readonly #db: Database;\n #statements: PreparedStatements | undefined;\n #closed = false;\n\n constructor(lc: LogContext, changeLogFile: string) {\n this.#db = new Database(\n lc.withContext('component', 'sqlite-change-log-reader'),\n changeLogFile,\n {readonly: true},\n );\n }\n\n plan(fromWatermark: string): CatchupPlan {\n this.#assertOpen();\n const statements = this.#prepare();\n if (statements === undefined) {\n return {kind: 'not-ready'};\n }\n const row = statements.plan.get<PlanRow>({fromWatermark});\n if (row.headWatermark === null || row.minWatermark === null) {\n return {kind: 'not-ready'};\n }\n const {headWatermark, minWatermark} = row;\n\n if (fromWatermark > headWatermark) {\n return {kind: 'ahead', headWatermark};\n }\n if (fromWatermark < minWatermark || row.boundaryExists === 0) {\n return {kind: 'too-old', minWatermark, headWatermark};\n }\n return {kind: 'range', minWatermark, headWatermark};\n }\n\n async *read(\n fromWatermark: string,\n throughWatermark: string,\n batchSize: number,\n signal?: AbortSignal | undefined,\n ): AsyncIterable<readonly WatermarkedChange[]> {\n assert(\n Number.isSafeInteger(batchSize) && batchSize > 0,\n 'SQLite change log batch size must be a positive safe integer',\n );\n this.#assertOpen();\n const statements = this.#prepare();\n assert(\n statements !== undefined,\n 'SQLite change log must be ready before it is read',\n );\n\n let lastWatermark = fromWatermark;\n // The first query must be strictly after the requested transaction. Every\n // real stream position is far below this sentinel.\n let lastPos = Number.MAX_SAFE_INTEGER;\n let lastTag: string | undefined;\n\n while (true) {\n this.#throwIfAborted(signal);\n const rows = statements.readBatch.all<ChangeLogRow>(\n lastWatermark,\n lastPos,\n throughWatermark,\n batchSize,\n );\n this.#throwIfAborted(signal);\n\n if (rows.length === 0) {\n break;\n }\n\n const changes = rows.map(reconstructWatermarkedChange);\n const last = rows.at(-1);\n assert(last !== undefined, 'non-empty batch must have a final row');\n lastWatermark = last.watermark;\n lastPos = last.pos;\n lastTag = last.tag;\n\n // all() has exhausted the SELECT and closed its implicit read\n // transaction. This yield therefore cannot pin the WAL while subscriber\n // flow control is pending.\n yield changes;\n }\n\n // `throughWatermark` was pinned from `max(\"watermark\")` of this same table,\n // so reaching it no longer depends on the log agreeing with the replica's\n // `_zero.replicationState`, which is what this assertion used to guard.\n // What remains is a concurrent purge of the head transaction, which the\n // purge policy (never past the latest durable transaction) does not do.\n if (fromWatermark !== throughWatermark) {\n assert(\n lastWatermark === throughWatermark && lastTag === 'commit',\n () =>\n `SQLite change log did not end at pinned head ${throughWatermark}`,\n );\n }\n }\n\n close(): void {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#db.close();\n }\n\n [Symbol.dispose](): void {\n this.close();\n }\n\n /**\n * Prepares the statements once the stream table exists, memoizing them.\n * Returns `undefined` while the log has not been created, which is a normal\n * startup state rather than an error.\n */\n #prepare(): PreparedStatements | undefined {\n if (this.#statements === undefined && this.#streamTableExists()) {\n this.#statements = {\n plan: this.#db.prepare(SQLITE_CHANGE_LOG_PLAN_SQL),\n readBatch: this.#db.prepare(SQLITE_CHANGE_LOG_READ_BATCH_SQL),\n };\n }\n return this.#statements;\n }\n\n #streamTableExists(): boolean {\n return (\n this.#db\n .prepare(TABLE_EXISTS_SQL)\n .get<{1: number} | undefined>(CHANGE_LOG_STREAM_TABLE) !== undefined\n );\n }\n\n #assertOpen(): void {\n if (this.#closed) {\n throw new AbortError('SQLite change log reader is closed');\n }\n }\n\n #throwIfAborted(signal: AbortSignal | undefined): void {\n this.#assertOpen();\n if (signal?.aborted) {\n throw new AbortError('SQLite change log read aborted');\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAiDA,IAAa,iCAAyC;;;;UAI5C,wBAAwB;;;;;;;;;;;;;;;;;;AAmBlC,IAAa,6BAAqC;;qCAEb,wBAAwB;;qCAExB,wBAAwB;;gBAE7C,+BAA+B;;;AAI/C,IAAa,mCAA2C;;;;;;UAM9C,wBAAwB;;;;;;AAOlC,IAAM,mBAA2B;;;;;;;;;;;;;;;;;;AAmBjC,IAAa,wBAAb,MAAyD;CACvD;CACA;CACA,UAAU;CAEV,YAAY,IAAgB,eAAuB;EACjD,KAAKA,MAAM,IAAI,SACb,GAAG,YAAY,aAAa,0BAA0B,GACtD,eACA,EAAC,UAAU,KAAI,CACjB;CACF;CAEA,KAAK,eAAoC;EACvC,KAAKC,YAAY;EACjB,MAAM,aAAa,KAAKC,SAAS;EACjC,IAAI,eAAe,KAAA,GACjB,OAAO,EAAC,MAAM,YAAW;EAE3B,MAAM,MAAM,WAAW,KAAK,IAAa,EAAC,cAAa,CAAC;EACxD,IAAI,IAAI,kBAAkB,QAAQ,IAAI,iBAAiB,MACrD,OAAO,EAAC,MAAM,YAAW;EAE3B,MAAM,EAAC,eAAe,iBAAgB;EAEtC,IAAI,gBAAgB,eAClB,OAAO;GAAC,MAAM;GAAS;EAAa;EAEtC,IAAI,gBAAgB,gBAAgB,IAAI,mBAAmB,GACzD,OAAO;GAAC,MAAM;GAAW;GAAc;EAAa;EAEtD,OAAO;GAAC,MAAM;GAAS;GAAc;EAAa;CACpD;CAEA,OAAO,KACL,eACA,kBACA,WACA,QAC6C;EAC7C,OACE,OAAO,cAAc,SAAS,KAAK,YAAY,GAC/C,8DACF;EACA,KAAKD,YAAY;EACjB,MAAM,aAAa,KAAKC,SAAS;EACjC,OACE,eAAe,KAAA,GACf,mDACF;EAEA,IAAI,gBAAgB;EAGpB,IAAI,UAAU,OAAO;EACrB,IAAI;EAEJ,OAAO,MAAM;GACX,KAAKC,gBAAgB,MAAM;GAC3B,MAAM,OAAO,WAAW,UAAU,IAChC,eACA,SACA,kBACA,SACF;GACA,KAAKA,gBAAgB,MAAM;GAE3B,IAAI,KAAK,WAAW,GAClB;GAGF,MAAM,UAAU,KAAK,IAAI,4BAA4B;GACrD,MAAM,OAAO,KAAK,GAAG,EAAE;GACvB,OAAO,SAAS,KAAA,GAAW,uCAAuC;GAClE,gBAAgB,KAAK;GACrB,UAAU,KAAK;GACf,UAAU,KAAK;GAKf,MAAM;EACR;EAOA,IAAI,kBAAkB,kBACpB,OACE,kBAAkB,oBAAoB,YAAY,gBAEhD,gDAAgD,kBACpD;CAEJ;CAEA,QAAc;EACZ,IAAI,KAAKC,SACP;EAEF,KAAKA,UAAU;EACf,KAAKJ,IAAI,MAAM;CACjB;CAEA,CAAC,OAAO,WAAiB;EACvB,KAAK,MAAM;CACb;;;;;;CAOA,WAA2C;EACzC,IAAI,KAAKK,gBAAgB,KAAA,KAAa,KAAKC,mBAAmB,GAC5D,KAAKD,cAAc;GACjB,MAAM,KAAKL,IAAI,QAAQ,0BAA0B;GACjD,WAAW,KAAKA,IAAI,QAAQ,gCAAgC;EAC9D;EAEF,OAAO,KAAKK;CACd;CAEA,qBAA8B;EAC5B,OACE,KAAKL,IACF,QAAQ,gBAAgB,EACxB,IAA6B,uBAAuB,MAAM,KAAA;CAEjE;CAEA,cAAoB;EAClB,IAAI,KAAKI,SACP,MAAM,IAAI,WAAW,oCAAoC;CAE7D;CAEA,gBAAgB,QAAuC;EACrD,KAAKH,YAAY;EACjB,IAAI,QAAQ,SACV,MAAM,IAAI,WAAW,gCAAgC;CAEzD;AACF"}
@@ -0,0 +1,109 @@
1
+ import type { LogContext } from '@rocicorp/logger';
2
+ import type { Database } from '../../../../zqlite/src/db.ts';
3
+ import type { ChangeStreamData } from '../change-source/protocol/current/downstream.ts';
4
+ import { type ChangeLogIdentity, type ChangeLogResumePoint } from '../replicator/change-log-db.ts';
5
+ import { type SQLiteChangeLogObservabilityState } from '../replicator/sqlite-change-log-observability.ts';
6
+ export type SQLiteChangeLogWriterOptions = {
7
+ /** The replica file the log is named after, i.e. `${replicaFile}-change-log`. */
8
+ replicaFile: string;
9
+ /** The replica whose contents this log belongs to. */
10
+ identity: ChangeLogIdentity;
11
+ /**
12
+ * Called after each transaction's log commit, with the log's new head. This
13
+ * is what releases the catchup barrier: after the writer moved in-process,
14
+ * the head advances here rather than at a subscriber's ACK.
15
+ */
16
+ onCommit?: ((watermark: string) => void) | undefined;
17
+ /**
18
+ * Called when the writer disables itself and deletes the file. The reader is
19
+ * cached on the service, so a delete that leaves it open would leave an
20
+ * in-process reader serving an unlinked inode while every new open sees
21
+ * nothing.
22
+ */
23
+ onDisabled?: (() => void) | undefined;
24
+ /** Called when reconciliation replaces the file, to close cached readers. */
25
+ onRebuilt?: (() => void) | undefined;
26
+ /** Overridable for tests. */
27
+ now?: (() => number) | undefined;
28
+ };
29
+ /**
30
+ * Appends the canonical change stream to the SQLite change log, from the
31
+ * change-streamer's own stream loop.
32
+ *
33
+ * ### Ordering
34
+ *
35
+ * The log's commit for a transaction precedes the forward of that transaction's
36
+ * `commit` message, and nothing `await`s in between. Everything that can
37
+ * advance the watermark the stream would resume from — the storer's Postgres
38
+ * commit, `lastWatermark`, the upstream ACK — runs strictly after it, so the
39
+ * log's head is always at or above the resume point. A *hole* is therefore
40
+ * unreachable at any crash timing, and a *phantom* is the normal state, which is
41
+ * what makes truncate-above the whole of reconciliation.
42
+ *
43
+ * ### Fail-soft
44
+ *
45
+ * The log is a catchup cache: nothing in it is not either already durable in the
46
+ * replica's litestream backup or re-derivable from the replication slot. A write
47
+ * failure must therefore not stop replication for the shard. Corruption is
48
+ * rebuilt once at open; any other write error — a full disk, an I/O error, a
49
+ * permissions problem — logs, disables the writer, deletes the file, closes the
50
+ * reader, and lets the stream continue. The cost is catchup reach, and reach is
51
+ * recoverable: `too-old` sends a subscriber to a restore.
52
+ *
53
+ * One carve-out keeps that from hiding the bug class this topology introduces. A
54
+ * missed truncate-above surfaces as `SQLITE_CONSTRAINT` on the writer's plain
55
+ * `INSERT`, which is "any write error other than corruption" and would be
56
+ * absorbed quietly. It is counted as an invariant failure and alerted on before
57
+ * failing soft.
58
+ */
59
+ export declare class SQLiteChangeLogWriter {
60
+ #private;
61
+ constructor(lc: LogContext, opts: SQLiteChangeLogWriterOptions);
62
+ /** False once the writer has failed soft, or after {@link close}. */
63
+ get enabled(): boolean;
64
+ /**
65
+ * The writer's own connection, which is the one purge runs on (§3.3): one
66
+ * process appends *and* deletes, on one handle, with no second writable
67
+ * connection and no cross-process barrier. `undefined` until the stream
68
+ * loop's first reconcile creates the log, and again once the writer has
69
+ * failed soft and deleted the file — both of which a purge cycle treats as
70
+ * "skip", not as an error.
71
+ */
72
+ get connection(): Database | undefined;
73
+ /** Exposed for tests and for the integration assertions on the metrics. */
74
+ state(): SQLiteChangeLogObservabilityState | undefined;
75
+ /**
76
+ * Opens the log if necessary and reconciles it with the Postgres resume
77
+ * point. Returns the resume point stored in the log.
78
+ *
79
+ * Returns `undefined` when the writer is disabled. The caller must then use
80
+ * another source.
81
+ *
82
+ * This runs for every connection because Postgres supplies a new resume point
83
+ * after each reconnect. The log can contain transactions above that point.
84
+ * Reconciliation removes them before Postgres sends them again.
85
+ *
86
+ * `resumeFrom` includes cookies because the cookies must match the resume
87
+ * watermark.
88
+ */
89
+ reconcile(resumeFrom: ChangeLogResumePoint): ChangeLogResumePoint | undefined;
90
+ /**
91
+ * Reconciles the log without a Postgres resume point.
92
+ *
93
+ * A valid log uses its own head and cookies. A new or invalid log uses the
94
+ * replica values returned by `seed`.
95
+ */
96
+ reconcileFromLog(seed: () => ChangeLogResumePoint): ChangeLogResumePoint | undefined;
97
+ /**
98
+ * Writes one stream message, committing at transaction boundaries. Never
99
+ * throws: a write failure disables the writer instead of failing the stream.
100
+ */
101
+ write(change: ChangeStreamData, json: string): void;
102
+ /**
103
+ * Discards the open transaction, if any. Called when the change stream is
104
+ * interrupted mid-transaction.
105
+ */
106
+ abort(): void;
107
+ close(): void;
108
+ }
109
+ //# sourceMappingURL=sqlite-change-log-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-change-log-writer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAGjD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,8BAA8B,CAAC;AAM3D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iDAAiD,CAAC;AAKtF,OAAO,EAUL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAML,KAAK,iCAAiC,EACvC,MAAM,kDAAkD,CAAC;AAE1D,MAAM,MAAM,4BAA4B,GAAG;IACzC,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACrD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACtC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACrC,6BAA6B;IAC7B,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,qBAAqB;;gBAUpB,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,4BAA4B;IAM9D,qEAAqE;IACrE,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;;;;OAOG;IACH,IAAI,UAAU,IAAI,QAAQ,GAAG,SAAS,CAErC;IAED,2EAA2E;IAC3E,KAAK,IAAI,iCAAiC,GAAG,SAAS;IAItD;;;;;;;;;;;;;OAaG;IACH,SAAS,CACP,UAAU,EAAE,oBAAoB,GAC/B,oBAAoB,GAAG,SAAS;IAInC;;;;;OAKG;IACH,gBAAgB,CACd,IAAI,EAAE,MAAM,oBAAoB,GAC/B,oBAAoB,GAAG,SAAS;IA6GnC;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAkEnD;;;OAGG;IACH,KAAK,IAAI,IAAI;IAab,KAAK,IAAI,IAAI;CAwCd"}
@@ -0,0 +1,258 @@
1
+ import { must } from "../../../../shared/src/must.js";
2
+ import { readCookieRowCounts, readCookies } from "../replicator/change-log-cookies.js";
3
+ import { StatementRunner } from "../../db/statements.js";
4
+ import { isSQLiteCorruption, logSQLiteCorruptionDiagnostics } from "../../db/sqlite-corruption.js";
5
+ import { ChangeLogRebuildRequired, changeLogFileName, changeLogWipeReason, deleteChangeLogDB, openChangeLogDBForWriting, readChangeLogHead, rebuildChangeLogDBForWriting, reconcileChangeLog } from "../replicator/change-log-db.js";
6
+ import { ChangeLogStreamWriter } from "../replicator/change-log-stream-writer.js";
7
+ import { SQLiteChangeLogObserver, getSQLiteChangeLogInfo, logSQLiteChangeLogStartup, recordSQLiteChangeLogReconcile } from "../replicator/sqlite-change-log-observability.js";
8
+ import { SqliteError } from "@rocicorp/zero-sqlite3";
9
+ //#region ../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts
10
+ /**
11
+ * Appends the canonical change stream to the SQLite change log, from the
12
+ * change-streamer's own stream loop.
13
+ *
14
+ * ### Ordering
15
+ *
16
+ * The log's commit for a transaction precedes the forward of that transaction's
17
+ * `commit` message, and nothing `await`s in between. Everything that can
18
+ * advance the watermark the stream would resume from — the storer's Postgres
19
+ * commit, `lastWatermark`, the upstream ACK — runs strictly after it, so the
20
+ * log's head is always at or above the resume point. A *hole* is therefore
21
+ * unreachable at any crash timing, and a *phantom* is the normal state, which is
22
+ * what makes truncate-above the whole of reconciliation.
23
+ *
24
+ * ### Fail-soft
25
+ *
26
+ * The log is a catchup cache: nothing in it is not either already durable in the
27
+ * replica's litestream backup or re-derivable from the replication slot. A write
28
+ * failure must therefore not stop replication for the shard. Corruption is
29
+ * rebuilt once at open; any other write error — a full disk, an I/O error, a
30
+ * permissions problem — logs, disables the writer, deletes the file, closes the
31
+ * reader, and lets the stream continue. The cost is catchup reach, and reach is
32
+ * recoverable: `too-old` sends a subscriber to a restore.
33
+ *
34
+ * One carve-out keeps that from hiding the bug class this topology introduces. A
35
+ * missed truncate-above surfaces as `SQLITE_CONSTRAINT` on the writer's plain
36
+ * `INSERT`, which is "any write error other than corruption" and would be
37
+ * absorbed quietly. It is counted as an invariant failure and alerted on before
38
+ * failing soft.
39
+ */
40
+ var SQLiteChangeLogWriter = class {
41
+ #lc;
42
+ #opts;
43
+ #now;
44
+ #db;
45
+ #writer;
46
+ #observer;
47
+ #disabled = false;
48
+ constructor(lc, opts) {
49
+ this.#lc = lc.withContext("component", "sqlite-change-log-writer");
50
+ this.#opts = opts;
51
+ this.#now = opts.now ?? Date.now;
52
+ }
53
+ /** False once the writer has failed soft, or after {@link close}. */
54
+ get enabled() {
55
+ return !this.#disabled;
56
+ }
57
+ /**
58
+ * The writer's own connection, which is the one purge runs on (§3.3): one
59
+ * process appends *and* deletes, on one handle, with no second writable
60
+ * connection and no cross-process barrier. `undefined` until the stream
61
+ * loop's first reconcile creates the log, and again once the writer has
62
+ * failed soft and deleted the file — both of which a purge cycle treats as
63
+ * "skip", not as an error.
64
+ */
65
+ get connection() {
66
+ return this.#db;
67
+ }
68
+ /** Exposed for tests and for the integration assertions on the metrics. */
69
+ state() {
70
+ return this.#observer?.state();
71
+ }
72
+ /**
73
+ * Opens the log if necessary and reconciles it with the Postgres resume
74
+ * point. Returns the resume point stored in the log.
75
+ *
76
+ * Returns `undefined` when the writer is disabled. The caller must then use
77
+ * another source.
78
+ *
79
+ * This runs for every connection because Postgres supplies a new resume point
80
+ * after each reconnect. The log can contain transactions above that point.
81
+ * Reconciliation removes them before Postgres sends them again.
82
+ *
83
+ * `resumeFrom` includes cookies because the cookies must match the resume
84
+ * watermark.
85
+ */
86
+ reconcile(resumeFrom) {
87
+ return this.#reconcile(() => resumeFrom);
88
+ }
89
+ /**
90
+ * Reconciles the log without a Postgres resume point.
91
+ *
92
+ * A valid log uses its own head and cookies. A new or invalid log uses the
93
+ * replica values returned by `seed`.
94
+ */
95
+ reconcileFromLog(seed) {
96
+ return this.#reconcile((db) => this.#resumeFromLog(db, seed));
97
+ }
98
+ #reconcile(resolve) {
99
+ if (this.#disabled) return;
100
+ const { replicaFile, identity } = this.#opts;
101
+ const nowMs = this.#now();
102
+ const anchor = (db) => ({
103
+ ...resolve(db),
104
+ identity,
105
+ nowMs
106
+ });
107
+ try {
108
+ const db = this.#db;
109
+ if (db === void 0) {
110
+ const opened = openChangeLogDBForWriting(this.#lc, replicaFile, anchor);
111
+ this.#db = opened.db;
112
+ if (opened.replacedFile) this.#opts.onRebuilt?.();
113
+ recordSQLiteChangeLogReconcile(this.#lc, opened.result);
114
+ const info = getSQLiteChangeLogInfo(opened.db);
115
+ logSQLiteChangeLogStartup(this.#lc, changeLogFileName(replicaFile), info);
116
+ this.#observer = new SQLiteChangeLogObserver(this.#lc, info);
117
+ } else {
118
+ const resolved = anchor(db);
119
+ let result;
120
+ try {
121
+ result = reconcileChangeLog(this.#lc, db, resolved, { rebuildInsteadOfUnboundedWork: true });
122
+ } catch (e) {
123
+ if (!(e instanceof ChangeLogRebuildRequired)) throw e;
124
+ this.#writer = void 0;
125
+ this.#db = void 0;
126
+ db.close();
127
+ this.#opts.onRebuilt?.();
128
+ const rebuilt = rebuildChangeLogDBForWriting(this.#lc, replicaFile, resolved, e);
129
+ this.#db = rebuilt.db;
130
+ result = rebuilt.result;
131
+ }
132
+ recordSQLiteChangeLogReconcile(this.#lc, result);
133
+ if (result.action !== "none") this.#observer?.reconciled(getSQLiteChangeLogInfo(must(this.#db, "the SQLite change log is not open")));
134
+ }
135
+ const reconciledDB = must(this.#db, "the SQLite change log is not open");
136
+ this.#writer = new ChangeLogStreamWriter(new StatementRunner(reconciledDB));
137
+ return {
138
+ resumeWatermark: must(readChangeLogHead(reconciledDB), "the SQLite change log has no head after reconciliation"),
139
+ cookies: readCookies(reconciledDB)
140
+ };
141
+ } catch (e) {
142
+ this.#failSoft("reconciling the SQLite change log", e);
143
+ return;
144
+ }
145
+ }
146
+ /**
147
+ * Returns the resume point supplied by the log.
148
+ *
149
+ * A valid log uses its own head and cookies. A log that must be recreated uses
150
+ * the replica values returned by `seed`.
151
+ */
152
+ #resumeFromLog(db, seed) {
153
+ if (changeLogWipeReason(db, this.#opts.identity) !== void 0) return seed();
154
+ const head = readChangeLogHead(db);
155
+ return head === null ? seed() : {
156
+ resumeWatermark: head,
157
+ cookies: readCookies(db)
158
+ };
159
+ }
160
+ /**
161
+ * Writes one stream message, committing at transaction boundaries. Never
162
+ * throws: a write failure disables the writer instead of failing the stream.
163
+ */
164
+ write(change, json) {
165
+ const writer = this.#writer;
166
+ const db = this.#db;
167
+ if (this.#disabled || writer === void 0 || db === void 0) return;
168
+ const observer = this.#observer;
169
+ observer?.messageReceived(change, json);
170
+ const [type, msg] = change;
171
+ const startedAt = performance.now();
172
+ try {
173
+ let commit = null;
174
+ switch (type) {
175
+ case "begin":
176
+ writer.begin(change[2].commitWatermark, json);
177
+ break;
178
+ case "commit": {
179
+ const { watermark } = change[2];
180
+ const commitStart = performance.now();
181
+ commit = {
182
+ watermark,
183
+ stats: writer.commit(watermark, json, this.#now()),
184
+ logCommitMs: performance.now() - commitStart
185
+ };
186
+ break;
187
+ }
188
+ case "rollback":
189
+ writer.rollback();
190
+ break;
191
+ default:
192
+ writer.append(json, msg);
193
+ break;
194
+ }
195
+ observer?.messageProcessed(change, commit, performance.now() - startedAt);
196
+ if (commit !== null && commit.stats.cookieMutations.length > 0) observer?.cookiesMutated(commit.stats.cookieMutations, readCookieRowCounts(db));
197
+ if (commit !== null) this.#opts.onCommit?.(commit.watermark);
198
+ } catch (e) {
199
+ observer?.messageFailed(change, e, performance.now() - startedAt);
200
+ if (isConstraintViolation(e)) observer?.invariantFailure("SQLite change-log insert violated a constraint, which means a transaction above the resume watermark was not truncated", {
201
+ tag: msg.tag,
202
+ error: String(e)
203
+ });
204
+ this.#failSoft("writing to the SQLite change log", e);
205
+ }
206
+ }
207
+ /**
208
+ * Discards the open transaction, if any. Called when the change stream is
209
+ * interrupted mid-transaction.
210
+ */
211
+ abort() {
212
+ const writer = this.#writer;
213
+ if (this.#disabled || writer === void 0) return;
214
+ this.#observer?.abort();
215
+ try {
216
+ writer.rollback();
217
+ } catch (e) {
218
+ this.#failSoft("rolling back the SQLite change log", e);
219
+ }
220
+ }
221
+ close() {
222
+ this.#disabled = true;
223
+ this.#writer = void 0;
224
+ this.#observer = void 0;
225
+ const db = this.#db;
226
+ this.#db = void 0;
227
+ try {
228
+ db?.close();
229
+ } catch (e) {
230
+ this.#lc.warn?.("error closing the SQLite change log", e);
231
+ }
232
+ }
233
+ /**
234
+ * Logs, disables the writer, deletes the file, and closes the reader, leaving
235
+ * replication running. Disabling is for the life of the process: a log that
236
+ * failed a write is not re-opened until the task restarts, so the behavior a
237
+ * subscriber sees is the same as `sqliteChangeLogMode=off`.
238
+ */
239
+ #failSoft(what, e) {
240
+ const file = changeLogFileName(this.#opts.replicaFile);
241
+ if (isSQLiteCorruption(e)) logSQLiteCorruptionDiagnostics(this.#lc, "change-log", file, e);
242
+ this.#lc.error?.(`error ${what}: disabling the writer and deleting ${file}. Replication continues; catchup reach is lost until this task restarts.`, e);
243
+ this.close();
244
+ try {
245
+ deleteChangeLogDB(this.#opts.replicaFile);
246
+ } catch (deleteError) {
247
+ this.#lc.error?.(`unable to delete ${file}`, deleteError);
248
+ }
249
+ this.#opts.onDisabled?.();
250
+ }
251
+ };
252
+ function isConstraintViolation(e) {
253
+ return e instanceof SqliteError && e.code.startsWith("SQLITE_CONSTRAINT");
254
+ }
255
+ //#endregion
256
+ export { SQLiteChangeLogWriter };
257
+
258
+ //# sourceMappingURL=sqlite-change-log-writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-change-log-writer.js","names":["#lc","#opts","#now","#disabled","#db","#observer","#reconcile","#resumeFromLog","#writer","#failSoft"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-writer.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {must} from '../../../../shared/src/must.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {\n readCookieRowCounts,\n readCookies,\n} from '../replicator/change-log-cookies.ts';\nimport {\n ChangeLogRebuildRequired,\n changeLogFileName,\n changeLogWipeReason,\n deleteChangeLogDB,\n openChangeLogDBForWriting,\n readChangeLogHead,\n rebuildChangeLogDBForWriting,\n reconcileChangeLog,\n type ChangeLogAnchor,\n type ChangeLogIdentity,\n type ChangeLogResumePoint,\n type ReconcileResult,\n} from '../replicator/change-log-db.ts';\nimport {ChangeLogStreamWriter} from '../replicator/change-log-stream-writer.ts';\nimport {\n getSQLiteChangeLogInfo,\n logSQLiteChangeLogStartup,\n recordSQLiteChangeLogReconcile,\n SQLiteChangeLogObserver,\n type ChangeLogCommit,\n type SQLiteChangeLogObservabilityState,\n} from '../replicator/sqlite-change-log-observability.ts';\n\nexport type SQLiteChangeLogWriterOptions = {\n /** The replica file the log is named after, i.e. `${replicaFile}-change-log`. */\n replicaFile: string;\n /** The replica whose contents this log belongs to. */\n identity: ChangeLogIdentity;\n /**\n * Called after each transaction's log commit, with the log's new head. This\n * is what releases the catchup barrier: after the writer moved in-process,\n * the head advances here rather than at a subscriber's ACK.\n */\n onCommit?: ((watermark: string) => void) | undefined;\n /**\n * Called when the writer disables itself and deletes the file. The reader is\n * cached on the service, so a delete that leaves it open would leave an\n * in-process reader serving an unlinked inode while every new open sees\n * nothing.\n */\n onDisabled?: (() => void) | undefined;\n /** Called when reconciliation replaces the file, to close cached readers. */\n onRebuilt?: (() => void) | undefined;\n /** Overridable for tests. */\n now?: (() => number) | undefined;\n};\n\n/**\n * Appends the canonical change stream to the SQLite change log, from the\n * change-streamer's own stream loop.\n *\n * ### Ordering\n *\n * The log's commit for a transaction precedes the forward of that transaction's\n * `commit` message, and nothing `await`s in between. Everything that can\n * advance the watermark the stream would resume from — the storer's Postgres\n * commit, `lastWatermark`, the upstream ACK — runs strictly after it, so the\n * log's head is always at or above the resume point. A *hole* is therefore\n * unreachable at any crash timing, and a *phantom* is the normal state, which is\n * what makes truncate-above the whole of reconciliation.\n *\n * ### Fail-soft\n *\n * The log is a catchup cache: nothing in it is not either already durable in the\n * replica's litestream backup or re-derivable from the replication slot. A write\n * failure must therefore not stop replication for the shard. Corruption is\n * rebuilt once at open; any other write error — a full disk, an I/O error, a\n * permissions problem — logs, disables the writer, deletes the file, closes the\n * reader, and lets the stream continue. The cost is catchup reach, and reach is\n * recoverable: `too-old` sends a subscriber to a restore.\n *\n * One carve-out keeps that from hiding the bug class this topology introduces. A\n * missed truncate-above surfaces as `SQLITE_CONSTRAINT` on the writer's plain\n * `INSERT`, which is \"any write error other than corruption\" and would be\n * absorbed quietly. It is counted as an invariant failure and alerted on before\n * failing soft.\n */\nexport class SQLiteChangeLogWriter {\n readonly #lc: LogContext;\n readonly #opts: SQLiteChangeLogWriterOptions;\n readonly #now: () => number;\n\n #db: Database | undefined;\n #writer: ChangeLogStreamWriter | undefined;\n #observer: SQLiteChangeLogObserver | undefined;\n #disabled = false;\n\n constructor(lc: LogContext, opts: SQLiteChangeLogWriterOptions) {\n this.#lc = lc.withContext('component', 'sqlite-change-log-writer');\n this.#opts = opts;\n this.#now = opts.now ?? Date.now;\n }\n\n /** False once the writer has failed soft, or after {@link close}. */\n get enabled(): boolean {\n return !this.#disabled;\n }\n\n /**\n * The writer's own connection, which is the one purge runs on (§3.3): one\n * process appends *and* deletes, on one handle, with no second writable\n * connection and no cross-process barrier. `undefined` until the stream\n * loop's first reconcile creates the log, and again once the writer has\n * failed soft and deleted the file — both of which a purge cycle treats as\n * \"skip\", not as an error.\n */\n get connection(): Database | undefined {\n return this.#db;\n }\n\n /** Exposed for tests and for the integration assertions on the metrics. */\n state(): SQLiteChangeLogObservabilityState | undefined {\n return this.#observer?.state();\n }\n\n /**\n * Opens the log if necessary and reconciles it with the Postgres resume\n * point. Returns the resume point stored in the log.\n *\n * Returns `undefined` when the writer is disabled. The caller must then use\n * another source.\n *\n * This runs for every connection because Postgres supplies a new resume point\n * after each reconnect. The log can contain transactions above that point.\n * Reconciliation removes them before Postgres sends them again.\n *\n * `resumeFrom` includes cookies because the cookies must match the resume\n * watermark.\n */\n reconcile(\n resumeFrom: ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n return this.#reconcile(() => resumeFrom);\n }\n\n /**\n * Reconciles the log without a Postgres resume point.\n *\n * A valid log uses its own head and cookies. A new or invalid log uses the\n * replica values returned by `seed`.\n */\n reconcileFromLog(\n seed: () => ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n return this.#reconcile(db => this.#resumeFromLog(db, seed));\n }\n\n #reconcile(\n resolve: (db: Database) => ChangeLogResumePoint,\n ): ChangeLogResumePoint | undefined {\n if (this.#disabled) {\n return undefined;\n }\n const {replicaFile, identity} = this.#opts;\n const nowMs = this.#now();\n const anchor = (db: Database): ChangeLogAnchor => ({\n ...resolve(db),\n identity,\n nowMs,\n });\n try {\n const db = this.#db;\n if (db === undefined) {\n const opened = openChangeLogDBForWriting(this.#lc, replicaFile, anchor);\n this.#db = opened.db;\n if (opened.replacedFile) {\n this.#opts.onRebuilt?.();\n }\n recordSQLiteChangeLogReconcile(this.#lc, opened.result);\n const info = getSQLiteChangeLogInfo(opened.db);\n logSQLiteChangeLogStartup(\n this.#lc,\n changeLogFileName(replicaFile),\n info,\n );\n this.#observer = new SQLiteChangeLogObserver(this.#lc, info);\n } else {\n const resolved = anchor(db);\n let result: ReconcileResult;\n try {\n result = reconcileChangeLog(this.#lc, db, resolved, {\n rebuildInsteadOfUnboundedWork: true,\n });\n } catch (e) {\n if (!(e instanceof ChangeLogRebuildRequired)) {\n throw e;\n }\n // Close every handle to the old inode before unlinking it. The writer\n // is reconstructed below against the replacement connection.\n this.#writer = undefined;\n this.#db = undefined;\n db.close();\n this.#opts.onRebuilt?.();\n const rebuilt = rebuildChangeLogDBForWriting(\n this.#lc,\n replicaFile,\n resolved,\n e,\n );\n this.#db = rebuilt.db;\n result = rebuilt.result;\n }\n recordSQLiteChangeLogReconcile(this.#lc, result);\n if (result.action !== 'none') {\n this.#observer?.reconciled(\n getSQLiteChangeLogInfo(\n must(this.#db, 'the SQLite change log is not open'),\n ),\n );\n }\n }\n // A reseed drops and recreates the stream table, so the append statements\n // are prepared fresh against whatever the reconciliation left behind.\n const reconciledDB = must(this.#db, 'the SQLite change log is not open');\n this.#writer = new ChangeLogStreamWriter(\n new StatementRunner(reconciledDB),\n );\n // Read both values after reconciliation. A truncate or reseed updates the\n // head and cookies in the same transaction.\n return {\n resumeWatermark: must(\n readChangeLogHead(reconciledDB),\n 'the SQLite change log has no head after reconciliation',\n ),\n cookies: readCookies(reconciledDB),\n };\n } catch (e) {\n this.#failSoft('reconciling the SQLite change log', e);\n return undefined;\n }\n }\n\n /**\n * Returns the resume point supplied by the log.\n *\n * A valid log uses its own head and cookies. A log that must be recreated uses\n * the replica values returned by `seed`.\n */\n #resumeFromLog(\n db: Database,\n seed: () => ChangeLogResumePoint,\n ): ChangeLogResumePoint {\n if (changeLogWipeReason(db, this.#opts.identity) !== undefined) {\n return seed();\n }\n const head = readChangeLogHead(db);\n // An empty log has no resume point, so use the replica seed.\n return head === null\n ? seed()\n : {resumeWatermark: head, cookies: readCookies(db)};\n }\n\n /**\n * Writes one stream message, committing at transaction boundaries. Never\n * throws: a write failure disables the writer instead of failing the stream.\n */\n write(change: ChangeStreamData, json: string): void {\n const writer = this.#writer;\n const db = this.#db;\n if (this.#disabled || writer === undefined || db === undefined) {\n return;\n }\n const observer = this.#observer;\n observer?.messageReceived(change, json);\n const [type, msg] = change;\n const startedAt = performance.now();\n try {\n let commit: ChangeLogCommit | null = null;\n switch (type) {\n case 'begin':\n writer.begin(change[2].commitWatermark, json);\n break;\n case 'commit': {\n const {watermark} = change[2];\n const commitStart = performance.now();\n const stats = writer.commit(watermark, json, this.#now());\n commit = {\n watermark,\n stats,\n logCommitMs: performance.now() - commitStart,\n };\n break;\n }\n case 'rollback':\n writer.rollback();\n break;\n default:\n writer.append(json, msg);\n break;\n }\n observer?.messageProcessed(change, commit, performance.now() - startedAt);\n if (commit !== null && commit.stats.cookieMutations.length > 0) {\n // Only re-counted when a schema change actually moved the cookie jar,\n // which keeps the count off the forward path of every other\n // transaction. The counts are read after the commit, so they are what\n // a reader of the file would see.\n observer?.cookiesMutated(\n commit.stats.cookieMutations,\n readCookieRowCounts(db),\n );\n }\n if (commit !== null) {\n // The head advanced, and it is durable. Releasing the barrier here is\n // what makes its poll interval a backstop rather than the mechanism.\n this.#opts.onCommit?.(commit.watermark);\n }\n } catch (e) {\n observer?.messageFailed(change, e, performance.now() - startedAt);\n if (isConstraintViolation(e)) {\n // Reconciliation should have truncated whatever this collided with.\n // Report it as an invariant failure before failing soft, so that a\n // truncate-above bug cannot look like a lost cache.\n observer?.invariantFailure(\n 'SQLite change-log insert violated a constraint, which means a ' +\n 'transaction above the resume watermark was not truncated',\n {tag: msg.tag, error: String(e)},\n );\n }\n this.#failSoft('writing to the SQLite change log', e);\n }\n }\n\n /**\n * Discards the open transaction, if any. Called when the change stream is\n * interrupted mid-transaction.\n */\n abort(): void {\n const writer = this.#writer;\n if (this.#disabled || writer === undefined) {\n return;\n }\n this.#observer?.abort();\n try {\n writer.rollback();\n } catch (e) {\n this.#failSoft('rolling back the SQLite change log', e);\n }\n }\n\n close(): void {\n this.#disabled = true;\n this.#writer = undefined;\n this.#observer = undefined;\n const db = this.#db;\n this.#db = undefined;\n try {\n db?.close();\n } catch (e) {\n this.#lc.warn?.('error closing the SQLite change log', e);\n }\n }\n\n /**\n * Logs, disables the writer, deletes the file, and closes the reader, leaving\n * replication running. Disabling is for the life of the process: a log that\n * failed a write is not re-opened until the task restarts, so the behavior a\n * subscriber sees is the same as `sqliteChangeLogMode=off`.\n */\n #failSoft(what: string, e: unknown): void {\n const file = changeLogFileName(this.#opts.replicaFile);\n if (isSQLiteCorruption(e)) {\n // Open-time corruption is rebuilt by openChangeLogDBForWriting. Reaching\n // here means the file went bad under an open handle, which the same\n // delete-and-reseed answers -- on the next process start rather than now.\n logSQLiteCorruptionDiagnostics(this.#lc, 'change-log', file, e);\n }\n this.#lc.error?.(\n `error ${what}: disabling the writer and deleting ${file}. ` +\n `Replication continues; catchup reach is lost until this task restarts.`,\n e,\n );\n this.close();\n try {\n deleteChangeLogDB(this.#opts.replicaFile);\n } catch (deleteError) {\n this.#lc.error?.(`unable to delete ${file}`, deleteError);\n }\n this.#opts.onDisabled?.();\n }\n}\n\nfunction isConstraintViolation(e: unknown): boolean {\n return e instanceof SqliteError && e.code.startsWith('SQLITE_CONSTRAINT');\n}\n"],"mappings":"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"}