@rocicorp/zero 1.9.1-canary.1 → 1.10.0-canary.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 (344) hide show
  1. package/out/analyze-query/src/analyze-cli.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  3. package/out/replicache/src/db/commit.js +1 -4
  4. package/out/replicache/src/db/commit.js.map +1 -1
  5. package/out/replicache/src/db/read.d.ts +1 -0
  6. package/out/replicache/src/db/read.d.ts.map +1 -1
  7. package/out/replicache/src/db/read.js +1 -4
  8. package/out/replicache/src/db/read.js.map +1 -1
  9. package/out/replicache/src/db/write.d.ts +2 -1
  10. package/out/replicache/src/db/write.d.ts.map +1 -1
  11. package/out/replicache/src/db/write.js +4 -51
  12. package/out/replicache/src/db/write.js.map +1 -1
  13. package/out/replicache/src/persist/clients.d.ts +1 -0
  14. package/out/replicache/src/persist/clients.d.ts.map +1 -1
  15. package/out/replicache/src/persist/clients.js +4 -31
  16. package/out/replicache/src/persist/clients.js.map +1 -1
  17. package/out/replicache/src/process-env.d.ts +28 -0
  18. package/out/replicache/src/process-env.d.ts.map +1 -0
  19. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  20. package/out/replicache/src/replicache-impl.js.map +1 -1
  21. package/out/replicache/src/scan-iterator.js +2 -14
  22. package/out/replicache/src/scan-iterator.js.map +1 -1
  23. package/out/replicache/src/subscriptions.d.ts +1 -0
  24. package/out/replicache/src/subscriptions.d.ts.map +1 -1
  25. package/out/replicache/src/subscriptions.js +2 -10
  26. package/out/replicache/src/subscriptions.js.map +1 -1
  27. package/out/replicache/src/transactions.d.ts +1 -0
  28. package/out/replicache/src/transactions.d.ts.map +1 -1
  29. package/out/replicache/src/transactions.js +2 -4
  30. package/out/replicache/src/transactions.js.map +1 -1
  31. package/out/zero/package.js +2 -2
  32. package/out/zero/package.js.map +1 -1
  33. package/out/zero-cache/src/config/normalize.d.ts +1 -0
  34. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  35. package/out/zero-cache/src/config/normalize.js +10 -1
  36. package/out/zero-cache/src/config/normalize.js.map +1 -1
  37. package/out/zero-cache/src/config/zero-config.d.ts +43 -1
  38. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  39. package/out/zero-cache/src/config/zero-config.js +139 -20
  40. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  41. package/out/zero-cache/src/db/migration.js +1 -1
  42. package/out/zero-cache/src/observability/metrics.d.ts +5 -0
  43. package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
  44. package/out/zero-cache/src/observability/metrics.js +15 -1
  45. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  46. package/out/zero-cache/src/server/backup-watermark-poller.js +31 -0
  47. package/out/zero-cache/src/server/backup-watermark-poller.js.map +1 -0
  48. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  49. package/out/zero-cache/src/server/change-streamer.js +90 -30
  50. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  51. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  52. package/out/zero-cache/src/server/main.js +5 -15
  53. package/out/zero-cache/src/server/main.js.map +1 -1
  54. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  55. package/out/zero-cache/src/server/replicator.js +46 -3
  56. package/out/zero-cache/src/server/replicator.js.map +1 -1
  57. package/out/zero-cache/src/server/runner/run-worker.js +2 -1
  58. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  59. package/out/zero-cache/src/server/worker-urls.d.ts +1 -1
  60. package/out/zero-cache/src/server/worker-urls.js +2 -2
  61. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  62. package/out/zero-cache/src/services/change-source/change-source.d.ts +4 -2
  63. package/out/zero-cache/src/services/change-source/change-source.d.ts.map +1 -1
  64. package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +35 -0
  65. package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -0
  66. package/out/zero-cache/src/services/change-source/common/replica-restore.js +34 -0
  67. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -0
  68. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts +2 -0
  69. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  70. package/out/zero-cache/src/services/change-source/common/replica-schema.js +73 -1
  71. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  72. package/out/zero-cache/src/services/change-source/custom/change-source.d.ts +2 -6
  73. package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
  74. package/out/zero-cache/src/services/change-source/custom/change-source.js +16 -7
  75. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  76. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +4 -7
  77. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  78. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +7 -7
  79. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  80. package/out/zero-cache/src/services/change-source/pg/change-source.js +75 -23
  81. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  82. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +9 -1
  83. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  84. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +34 -4
  85. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  86. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +1 -1
  87. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +4 -2
  88. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
  89. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +7 -4
  90. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  91. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts +0 -5
  92. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  93. package/out/zero-cache/src/services/change-source/pg/schema/init.js +29 -41
  94. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  95. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +35 -16
  96. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  97. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +77 -7
  98. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  99. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +1 -1
  100. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
  101. package/out/zero-cache/src/services/change-source/protocol/current/data.js +1 -12
  102. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  103. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +3 -3
  104. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.d.ts +6 -0
  105. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.d.ts.map +1 -0
  106. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.js +21 -0
  107. package/out/zero-cache/src/services/change-source/protocol/current/schema-change-tags.js.map +1 -0
  108. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts +5 -6
  109. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
  110. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +32 -9
  111. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  112. package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts +15 -5
  113. package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts.map +1 -1
  114. package/out/zero-cache/src/services/change-streamer/backup-monitor.js +66 -0
  115. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -0
  116. package/out/zero-cache/src/services/change-streamer/broadcast.d.ts +68 -78
  117. package/out/zero-cache/src/services/change-streamer/broadcast.d.ts.map +1 -1
  118. package/out/zero-cache/src/services/change-streamer/broadcast.js +84 -92
  119. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  120. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +19 -0
  121. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -0
  122. package/out/zero-cache/src/services/change-streamer/change-log-codec.js +50 -0
  123. package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -0
  124. package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts +141 -0
  125. package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts.map +1 -0
  126. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +270 -0
  127. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -0
  128. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.d.ts +19 -0
  129. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.d.ts.map +1 -0
  130. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js +39 -0
  131. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -0
  132. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -4
  133. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  134. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +15 -20
  135. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  136. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +69 -24
  137. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  138. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +330 -63
  139. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  140. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +55 -13
  141. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  142. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  143. package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts +8 -0
  144. package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts.map +1 -1
  145. package/out/zero-cache/src/services/change-streamer/forwarder.d.ts +14 -3
  146. package/out/zero-cache/src/services/change-streamer/forwarder.d.ts.map +1 -1
  147. package/out/zero-cache/src/services/change-streamer/forwarder.js +41 -11
  148. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  149. package/out/zero-cache/src/services/change-streamer/{litestream3-backup-monitor.d.ts → litestream3-prometheus-poller.d.ts} +19 -45
  150. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.d.ts.map +1 -0
  151. package/out/zero-cache/src/services/change-streamer/{litestream3-backup-monitor.js → litestream3-prometheus-poller.js} +57 -171
  152. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -0
  153. package/out/zero-cache/src/services/change-streamer/replica-poller.d.ts +15 -0
  154. package/out/zero-cache/src/services/change-streamer/replica-poller.d.ts.map +1 -0
  155. package/out/zero-cache/src/services/change-streamer/replica-poller.js +55 -0
  156. package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -0
  157. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +5 -1
  158. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
  159. package/out/zero-cache/src/services/change-streamer/schema/tables.js +2 -1
  160. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  161. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +14 -0
  162. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -0
  163. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +86 -0
  164. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -0
  165. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +4 -1
  166. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
  167. package/out/zero-cache/src/services/change-streamer/snapshot.js +39 -14
  168. package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
  169. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts +75 -0
  170. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts.map +1 -0
  171. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +261 -0
  172. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -0
  173. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.d.ts +121 -0
  174. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.d.ts.map +1 -0
  175. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js +391 -0
  176. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -0
  177. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts +62 -0
  178. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts.map +1 -0
  179. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +159 -0
  180. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -0
  181. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts +109 -0
  182. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.d.ts.map +1 -0
  183. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +258 -0
  184. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -0
  185. package/out/zero-cache/src/services/change-streamer/storer.d.ts +22 -12
  186. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  187. package/out/zero-cache/src/services/change-streamer/storer.js +93 -117
  188. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  189. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +61 -10
  190. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  191. package/out/zero-cache/src/services/change-streamer/subscriber.js +101 -8
  192. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  193. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts +34 -0
  194. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts.map +1 -0
  195. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +86 -0
  196. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -0
  197. package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
  198. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  199. package/out/zero-cache/src/services/life-cycle.js +3 -2
  200. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  201. package/out/zero-cache/src/services/litestream/commands.d.ts +12 -11
  202. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  203. package/out/zero-cache/src/services/litestream/commands.js +50 -140
  204. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  205. package/out/zero-cache/src/services/litestream/config-v5.yml +42 -0
  206. package/out/zero-cache/src/services/litestream/metrics.d.ts +4 -4
  207. package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -1
  208. package/out/zero-cache/src/services/litestream/metrics.js +7 -10
  209. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  210. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts +57 -0
  211. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts.map +1 -0
  212. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js +137 -0
  213. package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js.map +1 -0
  214. package/out/zero-cache/src/services/replicator/change-log-cookies.d.ts +169 -0
  215. package/out/zero-cache/src/services/replicator/change-log-cookies.d.ts.map +1 -0
  216. package/out/zero-cache/src/services/replicator/change-log-cookies.js +434 -0
  217. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -0
  218. package/out/zero-cache/src/services/replicator/change-log-db.d.ts +358 -0
  219. package/out/zero-cache/src/services/replicator/change-log-db.d.ts.map +1 -0
  220. package/out/zero-cache/src/services/replicator/change-log-db.js +477 -0
  221. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -0
  222. package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts +55 -0
  223. package/out/zero-cache/src/services/replicator/change-log-stream-writer.d.ts.map +1 -0
  224. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +150 -0
  225. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -0
  226. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  227. package/out/zero-cache/src/services/replicator/change-processor.js +44 -17
  228. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  229. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +1 -1
  230. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  231. package/out/zero-cache/src/services/replicator/incremental-sync.js +11 -4
  232. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  233. package/out/zero-cache/src/services/replicator/replicator.d.ts +1 -1
  234. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  235. package/out/zero-cache/src/services/replicator/replicator.js +2 -2
  236. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  237. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts +119 -0
  238. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts.map +1 -0
  239. package/out/zero-cache/src/services/replicator/schema/backfilling.js +214 -0
  240. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -0
  241. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +3 -3
  242. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  243. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  244. package/out/zero-cache/src/services/replicator/schema/replication-state.d.ts +1 -1
  245. package/out/zero-cache/src/services/replicator/schema/replication-state.d.ts.map +1 -1
  246. package/out/zero-cache/src/services/replicator/schema/replication-state.js +9 -5
  247. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  248. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts +131 -0
  249. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts.map +1 -0
  250. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +413 -0
  251. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -0
  252. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.d.ts +120 -0
  253. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.d.ts.map +1 -0
  254. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +283 -0
  255. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -0
  256. package/out/zero-cache/src/services/replicator/write-worker.js +9 -6
  257. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  258. package/out/zero-cache/src/services/run-ast.d.ts.map +1 -1
  259. package/out/zero-cache/src/services/run-ast.js +2 -1
  260. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  261. package/out/zero-cache/src/services/running-state.d.ts +1 -1
  262. package/out/zero-cache/src/services/running-state.d.ts.map +1 -1
  263. package/out/zero-cache/src/services/running-state.js +1 -0
  264. package/out/zero-cache/src/services/running-state.js.map +1 -1
  265. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts +9 -0
  266. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts.map +1 -1
  267. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js +9 -0
  268. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -1
  269. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  270. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +18 -3
  271. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  272. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +11 -1
  273. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  274. package/out/zero-cache/src/services/view-syncer/view-syncer.js +48 -7
  275. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  276. package/out/zero-cache/src/types/processes.d.ts +1 -2
  277. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  278. package/out/zero-cache/src/types/processes.js +1 -25
  279. package/out/zero-cache/src/types/processes.js.map +1 -1
  280. package/out/zero-cache/src/types/streams.d.ts +10 -0
  281. package/out/zero-cache/src/types/streams.d.ts.map +1 -1
  282. package/out/zero-cache/src/types/streams.js +25 -4
  283. package/out/zero-cache/src/types/streams.js.map +1 -1
  284. package/out/zero-cache/src/workers/connection.d.ts +1 -0
  285. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  286. package/out/zero-cache/src/workers/connection.js +25 -3
  287. package/out/zero-cache/src/workers/connection.js.map +1 -1
  288. package/out/zero-cache/src/workers/replicator.d.ts +25 -0
  289. package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
  290. package/out/zero-cache/src/workers/replicator.js +33 -1
  291. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  292. package/out/zero-cache/src/workers/syncer.d.ts +12 -0
  293. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  294. package/out/zero-cache/src/workers/syncer.js +75 -0
  295. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  296. package/out/zero-client/src/client/version.js +1 -1
  297. package/out/zero-schema/src/builder/table-builder.d.ts +23 -0
  298. package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
  299. package/out/zero-schema/src/builder/table-builder.js +18 -0
  300. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  301. package/out/zero-types/src/schema.d.ts +11 -0
  302. package/out/zero-types/src/schema.d.ts.map +1 -1
  303. package/out/zql/src/query/query-impl.d.ts +2 -2
  304. package/out/zql/src/query/query-impl.d.ts.map +1 -1
  305. package/out/zql/src/query/query-impl.js.map +1 -1
  306. package/out/zql/src/query/query.d.ts +83 -14
  307. package/out/zql/src/query/query.d.ts.map +1 -1
  308. package/out/zql/src/query/static-query.d.ts +1 -1
  309. package/out/zql/src/query/static-query.d.ts.map +1 -1
  310. package/out/zqlite/src/resolve-scalar-subqueries.d.ts +15 -0
  311. package/out/zqlite/src/resolve-scalar-subqueries.d.ts.map +1 -1
  312. package/out/zqlite/src/resolve-scalar-subqueries.js +20 -13
  313. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  314. package/package.json +2 -2
  315. package/out/replicache/src/db/index.js +0 -91
  316. package/out/replicache/src/db/index.js.map +0 -1
  317. package/out/replicache/src/lazy.d.ts +0 -2
  318. package/out/replicache/src/lazy.d.ts.map +0 -1
  319. package/out/replicache/src/lazy.js +0 -12
  320. package/out/replicache/src/lazy.js.map +0 -1
  321. package/out/zero-cache/src/server/backup-watermark-reader.js +0 -35
  322. package/out/zero-cache/src/server/backup-watermark-reader.js.map +0 -1
  323. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +0 -1
  324. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +0 -1
  325. package/out/zero-cache/src/services/change-streamer/replica-monitor.d.ts +0 -16
  326. package/out/zero-cache/src/services/change-streamer/replica-monitor.d.ts.map +0 -1
  327. package/out/zero-cache/src/services/change-streamer/replica-monitor.js +0 -54
  328. package/out/zero-cache/src/services/change-streamer/replica-monitor.js.map +0 -1
  329. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts +0 -25
  330. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts.map +0 -1
  331. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js +0 -159
  332. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js.map +0 -1
  333. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts +0 -44
  334. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts.map +0 -1
  335. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +0 -65
  336. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js.map +0 -1
  337. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts +0 -11
  338. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts.map +0 -1
  339. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js +0 -74
  340. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js.map +0 -1
  341. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts +0 -42
  342. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts.map +0 -1
  343. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js +0 -117
  344. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js.map +0 -1
@@ -0,0 +1,413 @@
1
+ import { unreachable } from "../../../../shared/src/asserts.js";
2
+ import { getOrCreateCounter, getOrCreateGauge, getOrCreateLatencyHistogram, getOrCreateValueHistogram } from "../../observability/metrics.js";
3
+ import { readCookieRowCounts } from "./change-log-cookies.js";
4
+ import { versionFromLexi } from "../../types/lexi-version.js";
5
+ import { CHANGE_LOG_STREAM_TABLE, readChangeLogHead, readChangeLogMeta } from "./change-log-db.js";
6
+ import { extractChangeSubstring } from "../change-streamer/change-log-codec.js";
7
+ import { ChangeLogTransactionHasher } from "../change-streamer/change-log-transaction-hash.js";
8
+ import { stat } from "node:fs/promises";
9
+ //#region ../zero-cache/src/services/replicator/sqlite-change-log-observability.ts
10
+ /**
11
+ * Observability for the SQLite change log.
12
+ *
13
+ * Everything here now runs in the **change-streamer** process, which is where
14
+ * the writer lives. The module stays beside the rest of the change-log code
15
+ * under `services/replicator/` for the same reason `change-log-db.ts` and
16
+ * `change-log-stream-writer.ts` do: moving files would obscure the topology
17
+ * diff, and the writer's home is a property of who calls it rather than of
18
+ * where it sits. Metric names are unchanged from 7H — only their emitting
19
+ * process, and therefore their resource attributes, moved.
20
+ */
21
+ /**
22
+ * Reports the reconciliation of the change-log database against the watermark
23
+ * the stream connection resumes from. This runs once per stream connection, so
24
+ * a reconnect storm is visible here as well as a restart.
25
+ *
26
+ * A wipe is not an error — it is how every abnormal change-log state is
27
+ * collapsed into a correct one — but each wipe costs the retention window, so
28
+ * every reconnecting subscriber below the head gets `too-old`. The `gap` reason
29
+ * in particular means the log neither held the resume watermark nor could be
30
+ * truncated to it, so a nonzero rate in steady state is an alert, not a
31
+ * statistic.
32
+ */
33
+ function recordSQLiteChangeLogReconcile(lc, result) {
34
+ switch (result.action) {
35
+ case "truncated":
36
+ getOrCreateCounter("replica", "sqlite_change_log.reconcile_truncated_rows", "Phantom change-log rows removed at reconciliation, i.e. rows above the watermark the change stream resumed from.").add(result.rows);
37
+ break;
38
+ case "reseeded":
39
+ getOrCreateCounter("replica", "sqlite_change_log.reconcile_wipes", "Change-log wipes at reconciliation, by reason. Each one resets the retention window, so subscribers below the head get `too-old`.").add(1, { reason: result.reason });
40
+ break;
41
+ case "none": break;
42
+ default: unreachable(result);
43
+ }
44
+ if (result.cookiesStale) getOrCreateCounter("replica", "sqlite_change_log.cookie_reseed", "Change-log cookie sets invalidated at reconciliation, by the reconcile action that invalidated them.").add(1, { action: result.action });
45
+ lc.debug?.("SQLite change-log reconciliation", { sqliteChangeLogReconcile: result });
46
+ }
47
+ /**
48
+ * Reads the meta row and the head. The head is an index-optimized `max()`, so
49
+ * unlike {@link getSQLiteChangeLogInfo} this does not scan the log.
50
+ *
51
+ * Call this after reconciliation, which is the only point at which the head is
52
+ * known to be the resume watermark.
53
+ */
54
+ function getSQLiteChangeLogStartupInfo(changeLog) {
55
+ const meta = readChangeLogMeta(changeLog);
56
+ const head = readChangeLogHead(changeLog);
57
+ if (head === null) throw new Error("the SQLite change log must contain its seed transaction");
58
+ return {
59
+ ...meta,
60
+ headWatermark: head
61
+ };
62
+ }
63
+ /**
64
+ * Extends {@link getSQLiteChangeLogStartupInfo} with the retained row and
65
+ * estimated byte totals. Computing these scans the entire change-log table, so
66
+ * only call it when the totals are consumed, i.e. when the writer (and thus the
67
+ * observer) is enabled.
68
+ */
69
+ function getSQLiteChangeLogInfo(changeLog) {
70
+ const startup = getSQLiteChangeLogStartupInfo(changeLog);
71
+ const aggregate = changeLog.prepare(`
72
+ SELECT count(*) AS "rows",
73
+ coalesce(sum("estimatedBytes"), 0) AS "estimatedBytes"
74
+ FROM "${CHANGE_LOG_STREAM_TABLE}"
75
+ `).get();
76
+ return {
77
+ ...startup,
78
+ rows: aggregate.rows,
79
+ estimatedBytes: aggregate.estimatedBytes,
80
+ cookieRows: readCookieRowCounts(changeLog)
81
+ };
82
+ }
83
+ function logSQLiteChangeLogStartup(lc, file, info) {
84
+ lc.info?.("SQLite change-log startup", { sqliteChangeLog: {
85
+ file,
86
+ epoch: info.epoch,
87
+ generation: info.generation,
88
+ replicaID: info.replicaID,
89
+ schemaVersion: info.schemaVersion,
90
+ seedWatermark: info.seedWatermark,
91
+ seededAtMs: info.seededAtMs,
92
+ headWatermark: info.headWatermark
93
+ } });
94
+ }
95
+ /**
96
+ * The on-disk footprint of a SQLite database: its main file plus its `-wal` and
97
+ * `-wal2` sidecars, whichever exist. `-shm` is excluded — it is a
98
+ * shared-memory index rather than retained bytes.
99
+ *
100
+ * A missing file counts as zero rather than warning. Which sidecars exist
101
+ * depends on the journal mode, and the change-log database is absent entirely
102
+ * until the writer creates it.
103
+ */
104
+ async function sqliteFileBytes(lc, file) {
105
+ return (await Promise.all([
106
+ file,
107
+ `${file}-wal`,
108
+ `${file}-wal2`
109
+ ].map(async (path) => {
110
+ try {
111
+ return (await stat(path)).size;
112
+ } catch (e) {
113
+ if (e.code !== "ENOENT") lc.warn?.(`unable to stat ${path} for size metrics`, e);
114
+ return 0;
115
+ }
116
+ }))).reduce((total, size) => total + size, 0);
117
+ }
118
+ var TRANSACTION_ROW_BUCKETS = [
119
+ 2,
120
+ 3,
121
+ 5,
122
+ 10,
123
+ 25,
124
+ 50,
125
+ 100,
126
+ 250,
127
+ 1e3,
128
+ 5e3
129
+ ];
130
+ /**
131
+ * Records writer metrics in the change-streamer process. State changes only
132
+ * after the corresponding write succeeds, except receivedHead, which advances
133
+ * when a commit message arrives so that transient lag behind the log is
134
+ * observable.
135
+ */
136
+ var SQLiteChangeLogObserver = class {
137
+ #lc;
138
+ #messageProcessing = getOrCreateLatencyHistogram("replica", "sqlite_change_log.message_processing_duration", "Time to write a replication message to the SQLite change log.");
139
+ #logCommitDuration = getOrCreateLatencyHistogram("replica", "sqlite_change_log.log_commit_duration", "Time to append the commit row to the SQLite change log and commit its transaction. This is on the forward path: it precedes the forward of that transaction's commit message.");
140
+ #transactionRows = getOrCreateValueHistogram("replica", "sqlite_change_log.transaction_rows", {
141
+ description: "Rows stored per committed SQLite change-log transaction.",
142
+ unit: "{row}",
143
+ bucketBoundaries: TRANSACTION_ROW_BUCKETS
144
+ });
145
+ #rollbackCounter = getOrCreateCounter("replica", "sqlite_change_log.rollbacks", "SQLite change-log transactions rolled back before commit.");
146
+ #invariantFailureCounter = getOrCreateCounter("replica", "sqlite_change_log.invariant_failures", "Detected SQLite change-log writer invariant failures.");
147
+ #hashComparisonCounter = getOrCreateCounter("replica", "sqlite_change_log.hash_comparisons", "Ephemeral transaction hash comparisons between the received change stream and SQLite change-log writes.");
148
+ #hashUnpairedCounter = getOrCreateCounter("replica", "sqlite_change_log.hash_unpaired", "Committed change-log transactions without both ephemeral hashes available for comparison.");
149
+ #cookieMutationCounter = getOrCreateCounter("replica", "sqlite_change_log.cookie_mutations", "Change-log cookie mutations folded from schema changes, by operation.");
150
+ #receivedHead;
151
+ #sqliteHead;
152
+ #rows;
153
+ #estimatedBytes;
154
+ #cookieRows;
155
+ #cookieMutations = 0;
156
+ #transactionWatermark;
157
+ #sourceHasher;
158
+ #sourcePos = 0;
159
+ #sourceCommitHash;
160
+ #rollbacks = 0;
161
+ #invariantFailures = 0;
162
+ #hashMatches = 0;
163
+ #hashMismatches = 0;
164
+ #hashUnpaired = 0;
165
+ constructor(lc, info) {
166
+ this.#lc = lc.withContext("component", "sqlite-change-log-observer");
167
+ this.#receivedHead = info.headWatermark;
168
+ this.#sqliteHead = info.headWatermark;
169
+ this.#rows = info.rows;
170
+ this.#estimatedBytes = info.estimatedBytes;
171
+ this.#cookieRows = info.cookieRows;
172
+ getOrCreateGauge("replica", "sqlite_change_log.rows", "Rows retained in the SQLite change log.").addCallback((result) => result.observe(this.#rows));
173
+ getOrCreateGauge("replica", "sqlite_change_log.cookie_rows", "Cookie rows retained in the SQLite change log, by cookie table.").addCallback((result) => {
174
+ result.observe(this.#cookieRows.tableMetadata, { table: "tableMetadata" });
175
+ result.observe(this.#cookieRows.backfilling, { table: "backfilling" });
176
+ });
177
+ getOrCreateGauge("replica", "sqlite_change_log.retained_bytes", {
178
+ description: "Estimated UTF-8 payload bytes retained in the SQLite change log.",
179
+ unit: "By"
180
+ }).addCallback((result) => result.observe(this.#estimatedBytes));
181
+ getOrCreateGauge("replica", "sqlite_change_log.head", "SQLite change-log head converted from its lexicographic watermark.").addCallback((result) => {
182
+ const head = watermarkValue(this.#sqliteHead);
183
+ if (head !== void 0) result.observe(head);
184
+ });
185
+ getOrCreateGauge("replica", "sqlite_change_log.head_lag", "Distance from the latest received commit to the SQLite change-log head. The log commits before the forward, so this is zero except within a transaction.").addCallback((result) => {
186
+ const lag = watermarkDistance(this.#receivedHead, this.#sqliteHead);
187
+ if (lag !== void 0) result.observe(lag);
188
+ });
189
+ }
190
+ /**
191
+ * Called as each message arrives, before it is written. Hashes the *received*
192
+ * stream, which is compared against the hash the writer computes over what it
193
+ * *stored*.
194
+ */
195
+ messageReceived(data, json) {
196
+ const tag = data[1].tag;
197
+ if (data[0] === "begin") {
198
+ if (this.#sourceHasher !== void 0) this.#invariantFailure("Received change-log begin while a source hash is already open", { receivedWatermark: data[2].commitWatermark });
199
+ const watermark = data[2].commitWatermark;
200
+ this.#sourceHasher = new ChangeLogTransactionHasher();
201
+ this.#sourcePos = 0;
202
+ this.#sourceCommitHash = void 0;
203
+ this.#sourceHasher.add({
204
+ watermark,
205
+ pos: this.#sourcePos,
206
+ tag,
207
+ change: extractChangeSubstring(json, tag),
208
+ precommit: null
209
+ });
210
+ return;
211
+ }
212
+ if (tag === "rollback") {
213
+ this.#resetSourceHash();
214
+ return;
215
+ }
216
+ if (data[0] === "commit") this.#receivedHead = data[2].watermark;
217
+ const watermark = this.#transactionWatermark;
218
+ if (this.#sourceHasher === void 0 || watermark === void 0) {
219
+ this.#invariantFailure("Received change-log message without an observed transaction", { tag });
220
+ return;
221
+ }
222
+ const commitWatermark = data[0] === "commit" ? data[2].watermark : watermark;
223
+ this.#sourceHasher.add({
224
+ watermark: commitWatermark,
225
+ pos: ++this.#sourcePos,
226
+ tag,
227
+ change: extractChangeSubstring(json, tag),
228
+ precommit: data[0] === "commit" ? watermark : null
229
+ });
230
+ if (data[0] === "commit") {
231
+ this.#sourceCommitHash = this.#sourceHasher.digest();
232
+ this.#sourceHasher = void 0;
233
+ }
234
+ }
235
+ /** Called after the message has been written to the log. */
236
+ messageProcessed(data, commit, durationMs) {
237
+ const tag = data[1].tag;
238
+ this.#messageProcessing.recordMs(durationMs, {
239
+ tag,
240
+ outcome: "success"
241
+ });
242
+ if (data[0] === "begin") {
243
+ if (this.#transactionWatermark !== void 0) this.#invariantFailure("SQLite change-log transaction already open", {
244
+ openWatermark: this.#transactionWatermark,
245
+ receivedWatermark: data[2].commitWatermark
246
+ });
247
+ this.#transactionWatermark = data[2].commitWatermark;
248
+ return;
249
+ }
250
+ if (tag === "rollback") {
251
+ this.#recordRollback("upstream");
252
+ this.#transactionWatermark = void 0;
253
+ this.#resetSourceHash();
254
+ return;
255
+ }
256
+ if (data[0] !== "commit") return;
257
+ const watermark = data[2].watermark;
258
+ if (commit !== null) this.#logCommitDuration.recordMs(commit.logCommitMs);
259
+ if (this.#transactionWatermark !== watermark) this.#invariantFailure("SQLite change-log commit does not match the observed begin", {
260
+ openWatermark: this.#transactionWatermark,
261
+ commitWatermark: watermark
262
+ });
263
+ if (commit === null) {
264
+ this.#invariantFailure("SQLite change-log commit result is missing writer statistics", { commitWatermark: watermark });
265
+ this.#recordUnpaired(watermark, "missing-sqlite-hash");
266
+ } else {
267
+ if (commit.watermark !== watermark) this.#invariantFailure("SQLite change-log commit result has an unexpected watermark", {
268
+ commitWatermark: watermark,
269
+ resultWatermark: commit.watermark
270
+ });
271
+ this.#rows += commit.stats.rows;
272
+ this.#estimatedBytes += commit.stats.estimatedBytes;
273
+ this.#transactionRows.record(commit.stats.rows);
274
+ this.#compareHashes(watermark, this.#sourceCommitHash, commit.stats.hash);
275
+ }
276
+ if (watermark < this.#sqliteHead) this.#invariantFailure("SQLite change-log head regressed", {
277
+ previousHead: this.#sqliteHead,
278
+ commitWatermark: watermark
279
+ });
280
+ this.#sqliteHead = watermark;
281
+ this.#transactionWatermark = void 0;
282
+ this.#resetSourceHash();
283
+ }
284
+ messageFailed(data, error, durationMs) {
285
+ const tag = data[1].tag;
286
+ this.#messageProcessing.recordMs(durationMs, {
287
+ tag,
288
+ outcome: "error"
289
+ });
290
+ if (data[0] === "commit") this.#recordUnpaired(data[2].watermark, "sqlite-commit-failed");
291
+ if (error instanceof Error && error.name === "ChangeLogStreamInvariantError") this.#invariantFailure(error.message);
292
+ if (this.#transactionWatermark !== void 0 || data[0] === "begin") this.#recordRollback("processing-error");
293
+ this.#transactionWatermark = void 0;
294
+ this.#resetSourceHash();
295
+ }
296
+ /**
297
+ * A committed transaction folded schema changes into the cookie jar. Reported
298
+ * separately from {@link messageProcessed} because it costs a read of the
299
+ * cookie tables, which is only worth taking when they actually moved.
300
+ */
301
+ cookiesMutated(ops, rows) {
302
+ for (const op of ops) {
303
+ this.#cookieMutations++;
304
+ this.#cookieMutationCounter.add(1, { op });
305
+ }
306
+ this.#cookieRows = rows;
307
+ }
308
+ /**
309
+ * A missed truncate-above surfaces as a constraint violation on the writer's
310
+ * plain `INSERT`, which the fail-soft policy would otherwise absorb quietly.
311
+ * Classifying it as an invariant failure is what keeps a reconciliation bug
312
+ * from looking like a lost cache.
313
+ */
314
+ invariantFailure(message, details) {
315
+ this.#invariantFailure(message, details);
316
+ }
317
+ /** The source interrupted the stream mid-transaction. */
318
+ abort() {
319
+ if (this.#transactionWatermark !== void 0) {
320
+ this.#recordRollback("source-interruption");
321
+ this.#transactionWatermark = void 0;
322
+ }
323
+ this.#resetSourceHash();
324
+ }
325
+ /** Reconciliation moved the head, so the observer's totals are restated. */
326
+ reconciled(info) {
327
+ this.#receivedHead = info.headWatermark;
328
+ this.#sqliteHead = info.headWatermark;
329
+ this.#rows = info.rows;
330
+ this.#estimatedBytes = info.estimatedBytes;
331
+ this.#cookieRows = info.cookieRows;
332
+ this.#transactionWatermark = void 0;
333
+ this.#resetSourceHash();
334
+ }
335
+ state() {
336
+ return {
337
+ receivedHead: this.#receivedHead,
338
+ sqliteHead: this.#sqliteHead,
339
+ headLag: watermarkDistance(this.#receivedHead, this.#sqliteHead),
340
+ rows: this.#rows,
341
+ estimatedBytes: this.#estimatedBytes,
342
+ cookieRows: this.#cookieRows,
343
+ cookieMutations: this.#cookieMutations,
344
+ rollbacks: this.#rollbacks,
345
+ invariantFailures: this.#invariantFailures,
346
+ hashMatches: this.#hashMatches,
347
+ hashMismatches: this.#hashMismatches,
348
+ hashUnpaired: this.#hashUnpaired
349
+ };
350
+ }
351
+ #recordRollback(reason) {
352
+ this.#rollbacks++;
353
+ this.#rollbackCounter.add(1, { reason });
354
+ }
355
+ #invariantFailure(message, details) {
356
+ this.#invariantFailures++;
357
+ this.#invariantFailureCounter.add(1);
358
+ this.#lc.error?.(message, details);
359
+ }
360
+ #compareHashes(watermark, sourceHash, sqliteHash) {
361
+ if (sourceHash === void 0) {
362
+ this.#recordUnpaired(watermark, "missing-source-hash");
363
+ return;
364
+ }
365
+ if (sourceHash === sqliteHash) {
366
+ this.#hashMatches++;
367
+ this.#hashComparisonCounter.add(1, { outcome: "match" });
368
+ return;
369
+ }
370
+ this.#hashMismatches++;
371
+ this.#hashComparisonCounter.add(1, { outcome: "mismatch" });
372
+ this.#lc.error?.("SQLite change-log transaction hash mismatch", { sqliteChangeLogHashComparison: {
373
+ watermark,
374
+ sourceHash: sourceHash.slice(0, 16),
375
+ sqliteHash: sqliteHash.slice(0, 16)
376
+ } });
377
+ }
378
+ #recordUnpaired(watermark, reason) {
379
+ this.#hashUnpaired++;
380
+ this.#hashUnpairedCounter.add(1, { reason });
381
+ this.#lc.warn?.("SQLite change-log transaction hash was not compared", { sqliteChangeLogHashComparison: {
382
+ watermark,
383
+ reason
384
+ } });
385
+ }
386
+ #resetSourceHash() {
387
+ this.#sourceHasher = void 0;
388
+ this.#sourcePos = 0;
389
+ this.#sourceCommitHash = void 0;
390
+ }
391
+ };
392
+ function watermarkValue(watermark) {
393
+ try {
394
+ const value = Number(versionFromLexi(watermark));
395
+ return Number.isFinite(value) ? value : void 0;
396
+ } catch {
397
+ return;
398
+ }
399
+ }
400
+ function watermarkDistance(receivedHead, sqliteHead) {
401
+ if (receivedHead <= sqliteHead) return 0;
402
+ try {
403
+ const distance = versionFromLexi(receivedHead) - versionFromLexi(sqliteHead);
404
+ const value = Number(distance);
405
+ return Number.isFinite(value) ? value : void 0;
406
+ } catch {
407
+ return;
408
+ }
409
+ }
410
+ //#endregion
411
+ export { SQLiteChangeLogObserver, getSQLiteChangeLogInfo, logSQLiteChangeLogStartup, recordSQLiteChangeLogReconcile, sqliteFileBytes };
412
+
413
+ //# sourceMappingURL=sqlite-change-log-observability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-change-log-observability.js","names":["#lc","#messageProcessing","#logCommitDuration","#transactionRows","#rollbackCounter","#invariantFailureCounter","#hashComparisonCounter","#hashUnpairedCounter","#cookieMutationCounter","#receivedHead","#sqliteHead","#rows","#estimatedBytes","#cookieRows","#sourceHasher","#invariantFailure","#sourcePos","#sourceCommitHash","#resetSourceHash","#transactionWatermark","#recordRollback","#recordUnpaired","#compareHashes","#cookieMutations","#rollbacks","#invariantFailures","#hashMatches","#hashMismatches","#hashUnpaired"],"sources":["../../../../../../zero-cache/src/services/replicator/sqlite-change-log-observability.ts"],"sourcesContent":["/**\n * Observability for the SQLite change log.\n *\n * Everything here now runs in the **change-streamer** process, which is where\n * the writer lives. The module stays beside the rest of the change-log code\n * under `services/replicator/` for the same reason `change-log-db.ts` and\n * `change-log-stream-writer.ts` do: moving files would obscure the topology\n * diff, and the writer's home is a property of who calls it rather than of\n * where it sits. Metric names are unchanged from 7H — only their emitting\n * process, and therefore their resource attributes, moved.\n */\n\nimport {stat} from 'node:fs/promises';\nimport type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../../shared/src/asserts.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateLatencyHistogram,\n getOrCreateValueHistogram,\n} from '../../observability/metrics.ts';\nimport {versionFromLexi} from '../../types/lexi-version.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {extractChangeSubstring} from '../change-streamer/change-log-codec.ts';\nimport {ChangeLogTransactionHasher} from '../change-streamer/change-log-transaction-hash.ts';\nimport {\n readCookieRowCounts,\n type CookieOpTag,\n type CookieRowCounts,\n} from './change-log-cookies.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n readChangeLogHead,\n readChangeLogMeta,\n type ChangeLogMeta,\n type ReconcileResult,\n} from './change-log-db.ts';\nimport type {ChangeLogStreamTransactionStats} from './change-log-stream-writer.ts';\n\n/**\n * Reports the reconciliation of the change-log database against the watermark\n * the stream connection resumes from. This runs once per stream connection, so\n * a reconnect storm is visible here as well as a restart.\n *\n * A wipe is not an error — it is how every abnormal change-log state is\n * collapsed into a correct one — but each wipe costs the retention window, so\n * every reconnecting subscriber below the head gets `too-old`. The `gap` reason\n * in particular means the log neither held the resume watermark nor could be\n * truncated to it, so a nonzero rate in steady state is an alert, not a\n * statistic.\n */\nexport function recordSQLiteChangeLogReconcile(\n lc: LogContext,\n result: ReconcileResult,\n): void {\n switch (result.action) {\n case 'truncated':\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.reconcile_truncated_rows',\n 'Phantom change-log rows removed at reconciliation, i.e. rows above ' +\n 'the watermark the change stream resumed from.',\n ).add(result.rows);\n break;\n case 'reseeded':\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.reconcile_wipes',\n 'Change-log wipes at reconciliation, by reason. Each one resets the ' +\n 'retention window, so subscribers below the head get `too-old`.',\n ).add(1, {reason: result.reason});\n break;\n case 'none':\n break;\n default:\n unreachable(result);\n }\n if (result.cookiesStale) {\n // The invariant-17 counter: after any reconcile that moved the head, the\n // log's cookie set has to be replaced with the one that belongs to the\n // resume watermark. A nonzero `truncated` series in steady state means\n // phantom truncation is happening on a shard where it should not.\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.cookie_reseed',\n 'Change-log cookie sets invalidated at reconciliation, by the ' +\n 'reconcile action that invalidated them.',\n ).add(1, {action: result.action});\n }\n lc.debug?.('SQLite change-log reconciliation', {\n sqliteChangeLogReconcile: result,\n });\n}\n\n/**\n * What the log says about itself. Nothing here reads the replica: after the\n * writer moved to the change-streamer, the replica's state version is a\n * consequence of this log rather than a fact about it.\n */\nexport type SQLiteChangeLogStartupInfo = ChangeLogMeta & {\n readonly headWatermark: string;\n};\n\nexport type SQLiteChangeLogInfo = SQLiteChangeLogStartupInfo & {\n readonly rows: number;\n readonly estimatedBytes: number;\n readonly cookieRows: CookieRowCounts;\n};\n\n/**\n * Reads the meta row and the head. The head is an index-optimized `max()`, so\n * unlike {@link getSQLiteChangeLogInfo} this does not scan the log.\n *\n * Call this after reconciliation, which is the only point at which the head is\n * known to be the resume watermark.\n */\nexport function getSQLiteChangeLogStartupInfo(\n changeLog: Database,\n): SQLiteChangeLogStartupInfo {\n const meta = readChangeLogMeta(changeLog);\n const head = readChangeLogHead(changeLog);\n if (head === null) {\n throw new Error('the SQLite change log must contain its seed transaction');\n }\n return {...meta, headWatermark: head};\n}\n\n/**\n * Extends {@link getSQLiteChangeLogStartupInfo} with the retained row and\n * estimated byte totals. Computing these scans the entire change-log table, so\n * only call it when the totals are consumed, i.e. when the writer (and thus the\n * observer) is enabled.\n */\nexport function getSQLiteChangeLogInfo(\n changeLog: Database,\n): SQLiteChangeLogInfo {\n const startup = getSQLiteChangeLogStartupInfo(changeLog);\n const aggregate = changeLog\n .prepare(/*sql*/ `\n SELECT count(*) AS \"rows\",\n coalesce(sum(\"estimatedBytes\"), 0) AS \"estimatedBytes\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n `)\n .get<{rows: number; estimatedBytes: number}>();\n\n return {\n ...startup,\n rows: aggregate.rows,\n estimatedBytes: aggregate.estimatedBytes,\n cookieRows: readCookieRowCounts(changeLog),\n };\n}\n\nexport function logSQLiteChangeLogStartup(\n lc: LogContext,\n file: string,\n info: SQLiteChangeLogStartupInfo,\n): void {\n lc.info?.('SQLite change-log startup', {\n sqliteChangeLog: {\n file,\n epoch: info.epoch,\n generation: info.generation,\n replicaID: info.replicaID,\n schemaVersion: info.schemaVersion,\n seedWatermark: info.seedWatermark,\n seededAtMs: info.seededAtMs,\n headWatermark: info.headWatermark,\n },\n });\n}\n\n/**\n * The on-disk footprint of a SQLite database: its main file plus its `-wal` and\n * `-wal2` sidecars, whichever exist. `-shm` is excluded — it is a\n * shared-memory index rather than retained bytes.\n *\n * A missing file counts as zero rather than warning. Which sidecars exist\n * depends on the journal mode, and the change-log database is absent entirely\n * until the writer creates it.\n */\nexport async function sqliteFileBytes(\n lc: LogContext,\n file: string,\n): Promise<number> {\n const sizes = await Promise.all(\n [file, `${file}-wal`, `${file}-wal2`].map(async path => {\n try {\n return (await stat(path)).size;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n lc.warn?.(`unable to stat ${path} for size metrics`, e);\n }\n return 0;\n }\n }),\n );\n return sizes.reduce((total, size) => total + size, 0);\n}\n\n/** What the writer reports back for a committed transaction. */\nexport type ChangeLogCommit = {\n readonly watermark: string;\n readonly stats: ChangeLogStreamTransactionStats;\n /**\n * Appending the commit row and committing the log's transaction. This is the\n * cost that sits on the forward path, once per upstream transaction.\n */\n readonly logCommitMs: number;\n};\n\nexport type SQLiteChangeLogObservabilityState = {\n readonly receivedHead: string;\n readonly sqliteHead: string;\n readonly headLag: number | undefined;\n readonly rows: number;\n readonly estimatedBytes: number;\n readonly cookieRows: CookieRowCounts;\n readonly cookieMutations: number;\n readonly rollbacks: number;\n readonly invariantFailures: number;\n readonly hashMatches: number;\n readonly hashMismatches: number;\n readonly hashUnpaired: number;\n};\n\nconst TRANSACTION_ROW_BUCKETS = [2, 3, 5, 10, 25, 50, 100, 250, 1000, 5000];\n\n/**\n * Records writer metrics in the change-streamer process. State changes only\n * after the corresponding write succeeds, except receivedHead, which advances\n * when a commit message arrives so that transient lag behind the log is\n * observable.\n */\nexport class SQLiteChangeLogObserver {\n readonly #lc: LogContext;\n readonly #messageProcessing = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.message_processing_duration',\n 'Time to write a replication message to the SQLite change log.',\n );\n // The commit that sits on the forward path. There is no second commit to\n // order against any more: the replica's belongs to a subscriber downstream of\n // this loop, so `sqlite_change_log.replica_commit_duration` and the phantom\n // window it measured are gone. A failed commit is still timed, by\n // `message_processing_duration` keyed `tag=\"commit\"`.\n readonly #logCommitDuration = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.log_commit_duration',\n 'Time to append the commit row to the SQLite change log and commit its ' +\n 'transaction. This is on the forward path: it precedes the forward of ' +\n \"that transaction's commit message.\",\n );\n readonly #transactionRows = getOrCreateValueHistogram(\n 'replica',\n 'sqlite_change_log.transaction_rows',\n {\n description: 'Rows stored per committed SQLite change-log transaction.',\n unit: '{row}',\n bucketBoundaries: TRANSACTION_ROW_BUCKETS,\n },\n );\n readonly #rollbackCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.rollbacks',\n 'SQLite change-log transactions rolled back before commit.',\n );\n readonly #invariantFailureCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.invariant_failures',\n 'Detected SQLite change-log writer invariant failures.',\n );\n readonly #hashComparisonCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.hash_comparisons',\n 'Ephemeral transaction hash comparisons between the received change stream and SQLite change-log writes.',\n );\n readonly #hashUnpairedCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.hash_unpaired',\n 'Committed change-log transactions without both ephemeral hashes available for comparison.',\n );\n // Near zero at steady state: cookie state only moves on schema changes.\n readonly #cookieMutationCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.cookie_mutations',\n 'Change-log cookie mutations folded from schema changes, by operation.',\n );\n\n #receivedHead: string;\n #sqliteHead: string;\n #rows: number;\n #estimatedBytes: number;\n #cookieRows: CookieRowCounts;\n #cookieMutations = 0;\n #transactionWatermark: string | undefined;\n #sourceHasher: ChangeLogTransactionHasher | undefined;\n #sourcePos = 0;\n #sourceCommitHash: string | undefined;\n #rollbacks = 0;\n #invariantFailures = 0;\n #hashMatches = 0;\n #hashMismatches = 0;\n #hashUnpaired = 0;\n\n constructor(lc: LogContext, info: SQLiteChangeLogInfo) {\n this.#lc = lc.withContext('component', 'sqlite-change-log-observer');\n // Reconciliation has just put the head at the resume watermark, which is\n // also the point the stream is about to be received from.\n this.#receivedHead = info.headWatermark;\n this.#sqliteHead = info.headWatermark;\n this.#rows = info.rows;\n this.#estimatedBytes = info.estimatedBytes;\n this.#cookieRows = info.cookieRows;\n\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.rows',\n 'Rows retained in the SQLite change log.',\n ).addCallback(result => result.observe(this.#rows));\n // Nonzero with no backfill in flight is a leaked cookie, i.e. a backfill\n // that will be re-requested forever.\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.cookie_rows',\n 'Cookie rows retained in the SQLite change log, by cookie table.',\n ).addCallback(result => {\n result.observe(this.#cookieRows.tableMetadata, {table: 'tableMetadata'});\n result.observe(this.#cookieRows.backfilling, {table: 'backfilling'});\n });\n getOrCreateGauge('replica', 'sqlite_change_log.retained_bytes', {\n description:\n 'Estimated UTF-8 payload bytes retained in the SQLite change log.',\n unit: 'By',\n }).addCallback(result => result.observe(this.#estimatedBytes));\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.head',\n 'SQLite change-log head converted from its lexicographic watermark.',\n ).addCallback(result => {\n const head = watermarkValue(this.#sqliteHead);\n if (head !== undefined) {\n result.observe(head);\n }\n });\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.head_lag',\n 'Distance from the latest received commit to the SQLite change-log ' +\n 'head. The log commits before the forward, so this is zero except ' +\n 'within a transaction.',\n ).addCallback(result => {\n const lag = watermarkDistance(this.#receivedHead, this.#sqliteHead);\n if (lag !== undefined) {\n result.observe(lag);\n }\n });\n }\n\n /**\n * Called as each message arrives, before it is written. Hashes the *received*\n * stream, which is compared against the hash the writer computes over what it\n * *stored*.\n */\n messageReceived(data: ChangeStreamData, json: string): void {\n const tag = data[1].tag;\n if (data[0] === 'begin') {\n if (this.#sourceHasher !== undefined) {\n this.#invariantFailure(\n 'Received change-log begin while a source hash is already open',\n {receivedWatermark: data[2].commitWatermark},\n );\n }\n const watermark = data[2].commitWatermark;\n this.#sourceHasher = new ChangeLogTransactionHasher();\n this.#sourcePos = 0;\n this.#sourceCommitHash = undefined;\n this.#sourceHasher.add({\n watermark,\n pos: this.#sourcePos,\n tag,\n change: extractChangeSubstring(json, tag),\n precommit: null,\n });\n return;\n }\n\n if (tag === 'rollback') {\n this.#resetSourceHash();\n return;\n }\n\n if (data[0] === 'commit') {\n this.#receivedHead = data[2].watermark;\n }\n\n const watermark = this.#transactionWatermark;\n if (this.#sourceHasher === undefined || watermark === undefined) {\n this.#invariantFailure(\n 'Received change-log message without an observed transaction',\n {tag},\n );\n return;\n }\n\n const commitWatermark =\n data[0] === 'commit' ? data[2].watermark : watermark;\n this.#sourceHasher.add({\n watermark: commitWatermark,\n pos: ++this.#sourcePos,\n tag,\n change: extractChangeSubstring(json, tag),\n precommit: data[0] === 'commit' ? watermark : null,\n });\n if (data[0] === 'commit') {\n this.#sourceCommitHash = this.#sourceHasher.digest();\n this.#sourceHasher = undefined;\n }\n }\n\n /** Called after the message has been written to the log. */\n messageProcessed(\n data: ChangeStreamData,\n commit: ChangeLogCommit | null,\n durationMs: number,\n ): void {\n const tag = data[1].tag;\n this.#messageProcessing.recordMs(durationMs, {tag, outcome: 'success'});\n\n if (data[0] === 'begin') {\n if (this.#transactionWatermark !== undefined) {\n this.#invariantFailure('SQLite change-log transaction already open', {\n openWatermark: this.#transactionWatermark,\n receivedWatermark: data[2].commitWatermark,\n });\n }\n this.#transactionWatermark = data[2].commitWatermark;\n return;\n }\n\n if (tag === 'rollback') {\n this.#recordRollback('upstream');\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n return;\n }\n\n if (data[0] !== 'commit') {\n return;\n }\n\n const watermark = data[2].watermark;\n if (commit !== null) {\n this.#logCommitDuration.recordMs(commit.logCommitMs);\n }\n if (this.#transactionWatermark !== watermark) {\n this.#invariantFailure(\n 'SQLite change-log commit does not match the observed begin',\n {openWatermark: this.#transactionWatermark, commitWatermark: watermark},\n );\n }\n if (commit === null) {\n this.#invariantFailure(\n 'SQLite change-log commit result is missing writer statistics',\n {commitWatermark: watermark},\n );\n this.#recordUnpaired(watermark, 'missing-sqlite-hash');\n } else {\n if (commit.watermark !== watermark) {\n this.#invariantFailure(\n 'SQLite change-log commit result has an unexpected watermark',\n {commitWatermark: watermark, resultWatermark: commit.watermark},\n );\n }\n this.#rows += commit.stats.rows;\n this.#estimatedBytes += commit.stats.estimatedBytes;\n this.#transactionRows.record(commit.stats.rows);\n this.#compareHashes(watermark, this.#sourceCommitHash, commit.stats.hash);\n }\n if (watermark < this.#sqliteHead) {\n this.#invariantFailure('SQLite change-log head regressed', {\n previousHead: this.#sqliteHead,\n commitWatermark: watermark,\n });\n }\n this.#sqliteHead = watermark;\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n messageFailed(\n data: ChangeStreamData,\n error: unknown,\n durationMs: number,\n ): void {\n const tag = data[1].tag;\n this.#messageProcessing.recordMs(durationMs, {tag, outcome: 'error'});\n if (data[0] === 'commit') {\n // No commit duration here: a failed commit produces no statistics, so\n // there is nothing to attribute. The attempt is timed by\n // `message_processing_duration{tag=\"commit\",outcome=\"error\"}` above.\n this.#recordUnpaired(data[2].watermark, 'sqlite-commit-failed');\n }\n if (\n error instanceof Error &&\n error.name === 'ChangeLogStreamInvariantError'\n ) {\n this.#invariantFailure(error.message);\n }\n if (this.#transactionWatermark !== undefined || data[0] === 'begin') {\n this.#recordRollback('processing-error');\n }\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n /**\n * A committed transaction folded schema changes into the cookie jar. Reported\n * separately from {@link messageProcessed} because it costs a read of the\n * cookie tables, which is only worth taking when they actually moved.\n */\n cookiesMutated(ops: readonly CookieOpTag[], rows: CookieRowCounts): void {\n for (const op of ops) {\n this.#cookieMutations++;\n this.#cookieMutationCounter.add(1, {op});\n }\n this.#cookieRows = rows;\n }\n\n /**\n * A missed truncate-above surfaces as a constraint violation on the writer's\n * plain `INSERT`, which the fail-soft policy would otherwise absorb quietly.\n * Classifying it as an invariant failure is what keeps a reconciliation bug\n * from looking like a lost cache.\n */\n invariantFailure(message: string, details?: Record<string, unknown>): void {\n this.#invariantFailure(message, details);\n }\n\n /** The source interrupted the stream mid-transaction. */\n abort(): void {\n if (this.#transactionWatermark !== undefined) {\n this.#recordRollback('source-interruption');\n this.#transactionWatermark = undefined;\n }\n this.#resetSourceHash();\n }\n\n /** Reconciliation moved the head, so the observer's totals are restated. */\n reconciled(info: SQLiteChangeLogInfo): void {\n this.#receivedHead = info.headWatermark;\n this.#sqliteHead = info.headWatermark;\n this.#rows = info.rows;\n this.#estimatedBytes = info.estimatedBytes;\n this.#cookieRows = info.cookieRows;\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n state(): SQLiteChangeLogObservabilityState {\n return {\n receivedHead: this.#receivedHead,\n sqliteHead: this.#sqliteHead,\n headLag: watermarkDistance(this.#receivedHead, this.#sqliteHead),\n rows: this.#rows,\n estimatedBytes: this.#estimatedBytes,\n cookieRows: this.#cookieRows,\n cookieMutations: this.#cookieMutations,\n rollbacks: this.#rollbacks,\n invariantFailures: this.#invariantFailures,\n hashMatches: this.#hashMatches,\n hashMismatches: this.#hashMismatches,\n hashUnpaired: this.#hashUnpaired,\n };\n }\n\n #recordRollback(\n reason: 'upstream' | 'processing-error' | 'source-interruption',\n ) {\n this.#rollbacks++;\n this.#rollbackCounter.add(1, {reason});\n }\n\n #invariantFailure(message: string, details?: Record<string, unknown>) {\n this.#invariantFailures++;\n this.#invariantFailureCounter.add(1);\n this.#lc.error?.(message, details);\n }\n\n #compareHashes(\n watermark: string,\n sourceHash: string | undefined,\n sqliteHash: string,\n ): void {\n if (sourceHash === undefined) {\n this.#recordUnpaired(watermark, 'missing-source-hash');\n return;\n }\n if (sourceHash === sqliteHash) {\n this.#hashMatches++;\n this.#hashComparisonCounter.add(1, {outcome: 'match'});\n return;\n }\n this.#hashMismatches++;\n this.#hashComparisonCounter.add(1, {outcome: 'mismatch'});\n this.#lc.error?.('SQLite change-log transaction hash mismatch', {\n sqliteChangeLogHashComparison: {\n watermark,\n sourceHash: sourceHash.slice(0, 16),\n sqliteHash: sqliteHash.slice(0, 16),\n },\n });\n }\n\n #recordUnpaired(\n watermark: string,\n reason:\n | 'missing-source-hash'\n | 'missing-sqlite-hash'\n | 'sqlite-commit-failed',\n ): void {\n this.#hashUnpaired++;\n this.#hashUnpairedCounter.add(1, {reason});\n this.#lc.warn?.('SQLite change-log transaction hash was not compared', {\n sqliteChangeLogHashComparison: {watermark, reason},\n });\n }\n\n #resetSourceHash(): void {\n this.#sourceHasher = undefined;\n this.#sourcePos = 0;\n this.#sourceCommitHash = undefined;\n }\n}\n\nfunction watermarkValue(watermark: string): number | undefined {\n try {\n const value = Number(versionFromLexi(watermark));\n return Number.isFinite(value) ? value : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction watermarkDistance(\n receivedHead: string,\n sqliteHead: string,\n): number | undefined {\n if (receivedHead <= sqliteHead) {\n return 0;\n }\n try {\n const distance =\n versionFromLexi(receivedHead) - versionFromLexi(sqliteHead);\n const value = Number(distance);\n return Number.isFinite(value) ? value : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Deletes aged-out transactions from the change-log database, one bounded write
3
+ * transaction at a time.
4
+ *
5
+ * The purger is a pure function of one database: it takes the floors its caller
6
+ * has already gated on and narrows them, never widens them, and it reads no
7
+ * replication state. That is what makes it testable without a scheduler and
8
+ * what lets slice 9 run it from the change-streamer's stream loop, between the
9
+ * writer's commits, on the writer's own connection.
10
+ */
11
+ import type { Database } from '../../../../zqlite/src/db.ts';
12
+ /**
13
+ * The eligible set, as three ANDed bounds. Only the `externalFloor` is a safety
14
+ * property; the other two make the log serviceable and make purge conservative.
15
+ *
16
+ * `"watermark" < @externalFloor` is deliberately strict. A follower that lost
17
+ * its replica restores it from S3 at watermark `W` and then asks this log for
18
+ * everything after `W`, which requires the `commit` row *at* `W` to still be
19
+ * here (`SQLITE_CHANGE_LOG_BOUNDARY_SQL`). Deleting it returns `too-old` to the
20
+ * very subscriber the floor exists to protect.
21
+ *
22
+ * The head cap reads `max("watermark")` from the log's own database, which
23
+ * keeps the purger a pure function of one file. The cap is inert in normal
24
+ * operation: the floor is derived from a backup of a replica that consumes this
25
+ * loop's output, and the log commits before that output is forwarded, so
26
+ * `externalFloor <= head` holds by construction. It engages only on a *stale*
27
+ * log — writer stopped, mode flipped to `off`, another task took over — where
28
+ * deleting to a floor that has run past a frozen log would empty the file. Note
29
+ * that head can be a phantom — committed here but not yet re-deliverable by a
30
+ * resumed stream — for as long as it takes the watermark the stream would
31
+ * resume from to advance past it. That is harmless precisely because head is
32
+ * never a purge candidate, and it is why the purger asserts nothing about
33
+ * anything outside its own file.
34
+ *
35
+ * Ordered by `("writeTimeMs", "watermark")` so it rides the partial index,
36
+ * which carries one entry per transaction rather than one per change. The
37
+ * `LIMIT` keeps a drain from re-scanning the remaining history on every batch,
38
+ * which would be quadratic in the number of eligible transactions and would do
39
+ * it inside the `BEGIN IMMEDIATE`, holding the write lock alongside the delete.
40
+ *
41
+ * Exported so the focused query-plan test covers the production query.
42
+ */
43
+ export declare const SQLITE_CHANGE_LOG_ELIGIBLE_COMMITS_SQL = "\n SELECT \"watermark\"\n FROM \"_zero.changeLogStream\"\n WHERE \"writeTimeMs\" IS NOT NULL\n AND \"writeTimeMs\" < @retentionCutoffMs\n AND \"watermark\" < @externalFloor\n AND \"watermark\" < (SELECT max(\"watermark\") FROM \"_zero.changeLogStream\")\n ORDER BY \"writeTimeMs\", \"watermark\"\n LIMIT @limit\n";
44
+ /**
45
+ * Counts the rows at `@watermark`, capped at `@limit`. The cap is what keeps
46
+ * batch sizing O(budget): this runs per candidate inside the
47
+ * `BEGIN IMMEDIATE`, and the transaction being sized can be arbitrarily
48
+ * large — an uncapped count of a multi-million-row backfill would rescan
49
+ * every remaining row on every tear batch, holding the write lock (and the
50
+ * appending path behind it) for the duration, quadratically over the drain.
51
+ * The caller only needs to know whether the transaction fits its remaining
52
+ * budget, so it passes `remaining + 1` and reads a count above `remaining`
53
+ * as "does not fit".
54
+ *
55
+ * Exported so the focused tests cover the production query.
56
+ */
57
+ export declare const SQLITE_CHANGE_LOG_COUNT_TRANSACTION_SQL = "\n SELECT count(*) AS \"rows\" FROM (\n SELECT 1\n FROM \"_zero.changeLogStream\"\n WHERE \"watermark\" = @watermark\n LIMIT @limit\n )\n";
58
+ export type PurgeBatchResult = {
59
+ readonly deletedRows: number;
60
+ /**
61
+ * The watermark through which whole transactions were removed. `undefined`
62
+ * when the batch only tore rows off the minimum transaction without
63
+ * completing it — in that case `deletedRows > 0` and `moreEligible` is true.
64
+ */
65
+ readonly deletedThrough: string | undefined;
66
+ /** True while a tear is in progress, as well as while whole transactions remain eligible. */
67
+ readonly moreEligible: boolean;
68
+ };
69
+ export type PurgeBatchOptions = {
70
+ /**
71
+ * The floor the caller has already gated on subscriber ACKs and snapshot
72
+ * reservations — normally the confirmed S3 backup watermark. Transactions at
73
+ * or above it are NOT durable anywhere else: the change log is excluded from
74
+ * the litestream backup, so this is the boundary a follower restores to and
75
+ * then catches up from. Compared strictly (`<`), so the transaction *at* the
76
+ * floor survives as that follower's catchup boundary. The purger narrows this
77
+ * input and never widens it.
78
+ */
79
+ readonly externalFloor: string;
80
+ /**
81
+ * A *minimum* retention, ANDed on top of `externalFloor` rather than an
82
+ * alternative to it. Which of the two binds depends on the backup interval.
83
+ */
84
+ readonly retentionCutoffMs: number;
85
+ /** Starting chunk size. Not the bound — `maxDurationMs` is. */
86
+ readonly maxRows: number;
87
+ /**
88
+ * The statement duration target, enforced by feedback rather than by
89
+ * interruption: SQLite cannot yield mid-statement, so the purger measures
90
+ * each delete and shrinks the next batch's row budget when one overruns
91
+ * (see {@link SQLiteChangeLogPurger.budgetRows}) — which means the first
92
+ * delete after a restart, sized by `maxRows` alone, can still overrun it.
93
+ * The target is this consequential because purge shares the writer's
94
+ * connection: there is no lock to arbitrate and no `busy_timeout` to bound
95
+ * the wait — a long statement blocks the appending path, and the forward
96
+ * behind it, directly and for its full duration. That is why this is a
97
+ * precondition of purging from the stream loop rather than a tuning knob.
98
+ */
99
+ readonly maxDurationMs: number;
100
+ };
101
+ export declare class SQLiteChangeLogPurger {
102
+ #private;
103
+ /**
104
+ * @param db a writable connection to a change log the writer has already
105
+ * created and reconciled — in slice 9, the writer's own connection.
106
+ * Statements are prepared eagerly, so a caller that may run before the
107
+ * log exists opens the connection lazily and treats construction failure
108
+ * as a skipped cycle.
109
+ */
110
+ constructor(db: Database, now?: () => number);
111
+ /** The row budget the next batch would use. Exposed for tests and metrics. */
112
+ get budgetRows(): number | undefined;
113
+ /**
114
+ * Removes one bounded batch of aged-out history, in one `BEGIN IMMEDIATE`
115
+ * transaction. Callers loop while `moreEligible`, releasing whatever guards
116
+ * they hold between batches.
117
+ */
118
+ purgeBatch(opts: PurgeBatchOptions): PurgeBatchResult;
119
+ }
120
+ //# sourceMappingURL=sqlite-change-log-purger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-change-log-purger.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/sqlite-change-log-purger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,8BAA8B,CAAC;AAGtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,sCAAsC,2UASlD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uCAAuC,4JAOnD,CAAC;AAuCF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAWF,qBAAa,qBAAqB;;IAahC;;;;;;OAMG;gBAED,EAAE,EAAE,QAAQ,EACZ,GAAG,GAAE,MAAM,MAA0C;IAcvD,8EAA8E;IAC9E,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;IAED;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,gBAAgB;CAwMtD"}