@rocicorp/zero 1.11.0-head-86ca5eff-20261014 → 1.11.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 (750) hide show
  1. package/out/_virtual/{_@oxc-project_runtime@0.130.0/helpers → _@oxc-project_runtime@0.154.0/helpers/esm}/usingCtx.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js +2 -1
  3. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  4. package/out/analyze-query/src/bin-analyze.js +5 -3
  5. package/out/analyze-query/src/bin-analyze.js.map +1 -1
  6. package/out/analyze-query/src/bin-transform.js +3 -2
  7. package/out/analyze-query/src/bin-transform.js.map +1 -1
  8. package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
  9. package/out/ast-to-zql/src/bin.js +3 -2
  10. package/out/ast-to-zql/src/bin.js.map +1 -1
  11. package/out/ast-to-zql/src/format.js.map +1 -1
  12. package/out/datadog/src/datadog-log-sink.js +5 -4
  13. package/out/datadog/src/datadog-log-sink.js.map +1 -1
  14. package/out/otel/src/log-options.js.map +1 -1
  15. package/out/replicache/src/btree/node.js +13 -11
  16. package/out/replicache/src/btree/node.js.map +1 -1
  17. package/out/replicache/src/btree/read.js +4 -2
  18. package/out/replicache/src/btree/read.js.map +1 -1
  19. package/out/replicache/src/btree/splice.js.map +1 -1
  20. package/out/replicache/src/btree/write.js +9 -5
  21. package/out/replicache/src/btree/write.js.map +1 -1
  22. package/out/replicache/src/config.js +1 -2
  23. package/out/replicache/src/connection-loop.js.map +1 -1
  24. package/out/replicache/src/dag/chunk.js +1 -1
  25. package/out/replicache/src/dag/gc.js.map +1 -1
  26. package/out/replicache/src/dag/lazy-store.js +8 -7
  27. package/out/replicache/src/dag/lazy-store.js.map +1 -1
  28. package/out/replicache/src/dag/store-impl.js +1 -1
  29. package/out/replicache/src/dag/store-impl.js.map +1 -1
  30. package/out/replicache/src/dag/visitor.js.map +1 -1
  31. package/out/replicache/src/db/commit.js +5 -3
  32. package/out/replicache/src/db/commit.js.map +1 -1
  33. package/out/replicache/src/db/read.js +1 -1
  34. package/out/replicache/src/db/read.js.map +1 -1
  35. package/out/replicache/src/db/rebase.js.map +1 -1
  36. package/out/replicache/src/db/write.js +5 -2
  37. package/out/replicache/src/db/write.js.map +1 -1
  38. package/out/replicache/src/deleted-clients.js +3 -2
  39. package/out/replicache/src/deleted-clients.js.map +1 -1
  40. package/out/replicache/src/expo-sqlite.js +1 -2
  41. package/out/replicache/src/frozen-json.js +1 -1
  42. package/out/replicache/src/frozen-json.js.map +1 -1
  43. package/out/replicache/src/get-default-puller.js +1 -1
  44. package/out/replicache/src/hash.js +1 -1
  45. package/out/replicache/src/hash.js.map +1 -1
  46. package/out/replicache/src/impl.js +1 -2
  47. package/out/replicache/src/index-defs.js +5 -3
  48. package/out/replicache/src/index-defs.js.map +1 -1
  49. package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
  50. package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
  51. package/out/replicache/src/kv/idb-store.js +1 -1
  52. package/out/replicache/src/kv/idb-store.js.map +1 -1
  53. package/out/replicache/src/kv/mem-fallback-store.js +1 -1
  54. package/out/replicache/src/kv/mem-fallback-store.js.map +1 -1
  55. package/out/replicache/src/kv/mem-store.js.map +1 -1
  56. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  57. package/out/replicache/src/kv/op-sqlite/types.js.map +1 -1
  58. package/out/replicache/src/kv/read-impl.js.map +1 -1
  59. package/out/replicache/src/kv/sqlite-store.js +1 -1
  60. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  61. package/out/replicache/src/kv/write-impl-base.js +4 -1
  62. package/out/replicache/src/kv/write-impl-base.js.map +1 -1
  63. package/out/replicache/src/kv/write-impl.js.map +1 -1
  64. package/out/replicache/src/log-options.js +2 -1
  65. package/out/replicache/src/log-options.js.map +1 -1
  66. package/out/replicache/src/make-idb-name.js +1 -1
  67. package/out/replicache/src/new-client-channel.js +9 -7
  68. package/out/replicache/src/new-client-channel.js.map +1 -1
  69. package/out/replicache/src/on-persist-channel.js +1 -1
  70. package/out/replicache/src/on-persist-channel.js.map +1 -1
  71. package/out/replicache/src/op-sqlite.js +1 -2
  72. package/out/replicache/src/pending-mutations.js +2 -1
  73. package/out/replicache/src/pending-mutations.js.map +1 -1
  74. package/out/replicache/src/persist/client-gc.js +2 -2
  75. package/out/replicache/src/persist/client-gc.js.map +1 -1
  76. package/out/replicache/src/persist/client-group-gc.js +2 -2
  77. package/out/replicache/src/persist/client-group-gc.js.map +1 -1
  78. package/out/replicache/src/persist/client-groups.js +1 -1
  79. package/out/replicache/src/persist/client-groups.js.map +1 -1
  80. package/out/replicache/src/persist/clients.js +1 -1
  81. package/out/replicache/src/persist/clients.js.map +1 -1
  82. package/out/replicache/src/persist/collect-idb-databases.js +5 -4
  83. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  84. package/out/replicache/src/persist/gather-mem-only-visitor.js.map +1 -1
  85. package/out/replicache/src/persist/gather-not-cached-visitor.js.map +1 -1
  86. package/out/replicache/src/persist/heartbeat.js +2 -2
  87. package/out/replicache/src/persist/heartbeat.js.map +1 -1
  88. package/out/replicache/src/persist/idb-databases-store.js +1 -1
  89. package/out/replicache/src/persist/idb-databases-store.js.map +1 -1
  90. package/out/replicache/src/persist/make-client-id.js +1 -1
  91. package/out/replicache/src/persist/make-client-id.js.map +1 -1
  92. package/out/replicache/src/persist/persist.js +3 -2
  93. package/out/replicache/src/persist/persist.js.map +1 -1
  94. package/out/replicache/src/persist/refresh.js +8 -5
  95. package/out/replicache/src/persist/refresh.js.map +1 -1
  96. package/out/replicache/src/process-scheduler.js.map +1 -1
  97. package/out/replicache/src/replicache-impl.js +3 -3
  98. package/out/replicache/src/replicache-impl.js.map +1 -1
  99. package/out/replicache/src/scan-iterator.js.map +1 -1
  100. package/out/replicache/src/scan-options.js +7 -6
  101. package/out/replicache/src/scan-options.js.map +1 -1
  102. package/out/replicache/src/sqlite.js +1 -2
  103. package/out/replicache/src/subscriptions.js +1 -1
  104. package/out/replicache/src/subscriptions.js.map +1 -1
  105. package/out/replicache/src/sync/diff.js +3 -1
  106. package/out/replicache/src/sync/diff.js.map +1 -1
  107. package/out/replicache/src/sync/ids.js.map +1 -1
  108. package/out/replicache/src/sync/patch.js +2 -4
  109. package/out/replicache/src/sync/patch.js.map +1 -1
  110. package/out/replicache/src/sync/pull.js +3 -1
  111. package/out/replicache/src/sync/pull.js.map +1 -1
  112. package/out/replicache/src/sync/push.js.map +1 -1
  113. package/out/replicache/src/sync/request-id.js +1 -1
  114. package/out/replicache/src/sync/request-id.js.map +1 -1
  115. package/out/replicache/src/transactions.js +1 -1
  116. package/out/replicache/src/transactions.js.map +1 -1
  117. package/out/shared/src/arrays.js +4 -1
  118. package/out/shared/src/arrays.js.map +1 -1
  119. package/out/shared/src/asserts.js +1 -1
  120. package/out/shared/src/bigint-json.js.map +1 -1
  121. package/out/shared/src/broadcast-channel.js +1 -1
  122. package/out/shared/src/btree-set.js +11 -8
  123. package/out/shared/src/btree-set.js.map +1 -1
  124. package/out/shared/src/cache.js.map +1 -1
  125. package/out/shared/src/config.js +1 -1
  126. package/out/shared/src/custom-key-map.js.map +1 -1
  127. package/out/shared/src/custom-key-set.js.map +1 -1
  128. package/out/shared/src/deep-clone.js +1 -1
  129. package/out/shared/src/document-visible.js.map +1 -1
  130. package/out/shared/src/dotenv.js.map +1 -1
  131. package/out/shared/src/error.js +1 -1
  132. package/out/shared/src/error.js.map +1 -1
  133. package/out/shared/src/hash.js +2 -2
  134. package/out/shared/src/hash.js.map +1 -1
  135. package/out/shared/src/iterables.js.map +1 -1
  136. package/out/shared/src/json-schema.js.map +1 -1
  137. package/out/shared/src/json.js.map +1 -1
  138. package/out/shared/src/logging-test-utils.js +1 -1
  139. package/out/shared/src/logging.js +1 -1
  140. package/out/shared/src/logging.js.map +1 -1
  141. package/out/shared/src/navigator.js +1 -1
  142. package/out/shared/src/options.js +9 -12
  143. package/out/shared/src/options.js.map +1 -1
  144. package/out/shared/src/promise-race.js.map +1 -1
  145. package/out/shared/src/queue.js.map +1 -1
  146. package/out/shared/src/record-proxy.js.map +1 -1
  147. package/out/shared/src/ring-buffer.js.map +1 -1
  148. package/out/shared/src/size-of-value.js +7 -10
  149. package/out/shared/src/size-of-value.js.map +1 -1
  150. package/out/shared/src/sorted-entries.js.map +1 -1
  151. package/out/shared/src/tdigest-schema.js.map +1 -1
  152. package/out/shared/src/tdigest.js.map +1 -1
  153. package/out/shared/src/valita.js +1 -1
  154. package/out/shared/src/valita.js.map +1 -1
  155. package/out/z2s/src/compiler.js +2 -4
  156. package/out/z2s/src/compiler.js.map +1 -1
  157. package/out/z2s/src/sql.js +1 -1
  158. package/out/z2s/src/sql.js.map +1 -1
  159. package/out/zero/package.js +212 -189
  160. package/out/zero/package.js.map +1 -1
  161. package/out/zero/src/adapters/drizzle.js +1 -2
  162. package/out/zero/src/adapters/kysely.js +1 -2
  163. package/out/zero/src/adapters/pg.js +1 -2
  164. package/out/zero/src/adapters/postgresjs.js +1 -2
  165. package/out/zero/src/adapters/prisma.js +1 -2
  166. package/out/zero/src/analyze.js +1 -2
  167. package/out/zero/src/change-protocol/v0.js +1 -2
  168. package/out/zero/src/expo-sqlite.js +1 -2
  169. package/out/zero/src/op-sqlite.js +1 -2
  170. package/out/zero/src/react-native.js.map +1 -1
  171. package/out/zero/src/sqlite.js +1 -2
  172. package/out/zero/src/zero-cache-dev.js +10 -9
  173. package/out/zero/src/zero-cache-dev.js.map +1 -1
  174. package/out/zero/src/zero-out.js +2 -1
  175. package/out/zero/src/zero-out.js.map +1 -1
  176. package/out/zero-cache/src/auth/auth.js +1 -1
  177. package/out/zero-cache/src/auth/auth.js.map +1 -1
  178. package/out/zero-cache/src/auth/jwt.js +4 -1
  179. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  180. package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
  181. package/out/zero-cache/src/auth/read-authorizer.js +3 -2
  182. package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
  183. package/out/zero-cache/src/auth/write-authorizer.js +5 -16
  184. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  185. package/out/zero-cache/src/config/network.js +1 -1
  186. package/out/zero-cache/src/config/network.js.map +1 -1
  187. package/out/zero-cache/src/config/server-context.js.map +1 -1
  188. package/out/zero-cache/src/config/zero-config.js +2 -2
  189. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  190. package/out/zero-cache/src/custom/fetch.js +1 -1
  191. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  192. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  193. package/out/zero-cache/src/db/create.js.map +1 -1
  194. package/out/zero-cache/src/db/index-predicate.js.map +1 -1
  195. package/out/zero-cache/src/db/lite-tables.js.map +1 -1
  196. package/out/zero-cache/src/db/migration-lite.js +1 -1
  197. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  198. package/out/zero-cache/src/db/migration.js +5 -3
  199. package/out/zero-cache/src/db/migration.js.map +1 -1
  200. package/out/zero-cache/src/db/pg-copy-binary.js +4 -3
  201. package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
  202. package/out/zero-cache/src/db/pg-copy.js +0 -1
  203. package/out/zero-cache/src/db/pg-copy.js.map +1 -1
  204. package/out/zero-cache/src/db/pg-to-lite.js +1 -1
  205. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  206. package/out/zero-cache/src/db/run-transaction.js.map +1 -1
  207. package/out/zero-cache/src/db/specs.js +7 -5
  208. package/out/zero-cache/src/db/specs.js.map +1 -1
  209. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
  210. package/out/zero-cache/src/db/transaction-pool.js +5 -3
  211. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  212. package/out/zero-cache/src/db/warmup.js.map +1 -1
  213. package/out/zero-cache/src/observability/events.js.map +1 -1
  214. package/out/zero-cache/src/observability/log-throttle.js.map +1 -1
  215. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  216. package/out/zero-cache/src/scripts/deploy-permissions.js +4 -2
  217. package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
  218. package/out/zero-cache/src/scripts/permissions.js.map +1 -1
  219. package/out/zero-cache/src/server/anonymous-otel-start.js +2 -1
  220. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  221. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  222. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  223. package/out/zero-cache/src/server/logging.js +1 -1
  224. package/out/zero-cache/src/server/logging.js.map +1 -1
  225. package/out/zero-cache/src/server/main.js.map +1 -1
  226. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  227. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  228. package/out/zero-cache/src/server/reaper.js +2 -2
  229. package/out/zero-cache/src/server/reaper.js.map +1 -1
  230. package/out/zero-cache/src/server/replicator.js.map +1 -1
  231. package/out/zero-cache/src/server/runner/run-worker.js +2 -1
  232. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  233. package/out/zero-cache/src/server/runner/runtime.js +2 -1
  234. package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
  235. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  236. package/out/zero-cache/src/server/shadow-syncer.js +1 -1
  237. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  238. package/out/zero-cache/src/server/slot-keeper.js.map +1 -1
  239. package/out/zero-cache/src/server/syncer-assigner.js.map +1 -1
  240. package/out/zero-cache/src/server/syncer.js.map +1 -1
  241. package/out/zero-cache/src/server/worker-dispatcher.js +1 -1
  242. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  243. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  244. package/out/zero-cache/src/services/analyze.js +1 -1
  245. package/out/zero-cache/src/services/analyze.js.map +1 -1
  246. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +1 -1
  247. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  248. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  249. package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -1
  250. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
  251. package/out/zero-cache/src/services/change-source/common/replica-schema.js +3 -3
  252. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  253. package/out/zero-cache/src/services/change-source/custom/change-source.js +1 -1
  254. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  255. package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
  256. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +3 -3
  257. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  258. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +1 -1
  259. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
  260. package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -6
  261. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  262. package/out/zero-cache/src/services/change-source/pg/decommission.js +2 -1
  263. package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
  264. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +5 -3
  265. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  266. package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
  267. package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
  268. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +3 -2
  269. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  270. package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
  271. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js +2 -2
  272. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js.map +1 -1
  273. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js +1 -1
  274. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js.map +1 -1
  275. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +3 -3
  276. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  277. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +1 -1
  278. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  279. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +13 -12
  280. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
  281. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  282. package/out/zero-cache/src/services/change-source/pg/schema/published.js +1 -1
  283. package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
  284. package/out/zero-cache/src/services/change-source/pg/schema/replica-stage-enum.js.map +1 -1
  285. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +9 -6
  286. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  287. package/out/zero-cache/src/services/change-source/pg/schema/validation.js +1 -1
  288. package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
  289. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +1 -1
  290. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -1
  291. package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
  292. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  293. package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
  294. package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
  295. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  296. package/out/zero-cache/src/services/change-source/protocol/current.js +1 -1
  297. package/out/zero-cache/src/services/change-source/protocol/mod.js +1 -2
  298. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
  299. package/out/zero-cache/src/services/change-streamer/backfill-state.js +1 -1
  300. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -1
  301. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  302. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
  303. package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
  304. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  305. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +2 -2
  306. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
  307. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -1
  308. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -1
  309. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  310. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +8 -3
  311. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  312. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +23 -23
  313. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  314. package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
  315. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  316. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts +4 -4
  317. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts.map +1 -1
  318. package/out/zero-cache/src/services/change-streamer/drain-tracker.js +4 -8
  319. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
  320. package/out/zero-cache/src/services/change-streamer/forwarder.js +11 -10
  321. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  322. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js +1 -1
  323. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js.map +1 -1
  324. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +3 -3
  325. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
  326. package/out/zero-cache/src/services/change-streamer/replica-poller.js +1 -1
  327. package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -1
  328. package/out/zero-cache/src/services/change-streamer/restore-admission.js +12 -1
  329. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
  330. package/out/zero-cache/src/services/change-streamer/schema/init.js +1 -1
  331. package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
  332. package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
  333. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  334. package/out/zero-cache/src/services/change-streamer/snapshot-message.js +1 -1
  335. package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
  336. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  337. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +1 -1
  338. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
  339. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js +1 -1
  340. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js.map +1 -1
  341. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -1
  342. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
  343. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +1 -1
  344. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -1
  345. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +1 -3
  346. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
  347. package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
  348. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  349. package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
  350. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  351. package/out/zero-cache/src/services/change-streamer/subscriber.js +2 -2
  352. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  353. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +0 -1
  354. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
  355. package/out/zero-cache/src/services/heapz.js +2 -1
  356. package/out/zero-cache/src/services/heapz.js.map +1 -1
  357. package/out/zero-cache/src/services/http-service.d.ts +6 -0
  358. package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
  359. package/out/zero-cache/src/services/http-service.js +11 -0
  360. package/out/zero-cache/src/services/http-service.js.map +1 -1
  361. package/out/zero-cache/src/services/life-cycle.d.ts +3 -2
  362. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  363. package/out/zero-cache/src/services/life-cycle.js +18 -5
  364. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  365. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js +4 -2
  366. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
  367. package/out/zero-cache/src/services/litestream/commands.js +1 -1
  368. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  369. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +2 -2
  370. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
  371. package/out/zero-cache/src/services/litestream/litestream-controller.js.map +1 -1
  372. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  373. package/out/zero-cache/src/services/litestream/restore-progress.js.map +1 -1
  374. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  375. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  376. package/out/zero-cache/src/services/mutagen/pusher.js +3 -2
  377. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  378. package/out/zero-cache/src/services/plannerz.js +4 -3
  379. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  380. package/out/zero-cache/src/services/profz.js +5 -3
  381. package/out/zero-cache/src/services/profz.js.map +1 -1
  382. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  383. package/out/zero-cache/src/services/replicator/change-log-db.js +3 -2
  384. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
  385. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
  386. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
  387. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  388. package/out/zero-cache/src/services/replicator/drain-handler.d.ts +8 -0
  389. package/out/zero-cache/src/services/replicator/drain-handler.d.ts.map +1 -1
  390. package/out/zero-cache/src/services/replicator/drain-handler.js +38 -4
  391. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
  392. package/out/zero-cache/src/services/replicator/incremental-sync.js +1 -1
  393. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  394. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  395. package/out/zero-cache/src/services/replicator/replication-status.js +4 -3
  396. package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
  397. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  398. package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
  399. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  400. package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
  401. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
  402. package/out/zero-cache/src/services/replicator/schema/backfilling.js +1 -1
  403. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  404. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  405. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  406. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  407. package/out/zero-cache/src/services/replicator/schema/replication-state.js +10 -7
  408. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  409. package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
  410. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +1 -1
  411. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
  412. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +1 -1
  413. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -1
  414. package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
  415. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  416. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  417. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  418. package/out/zero-cache/src/services/runner.js.map +1 -1
  419. package/out/zero-cache/src/services/running-state.js.map +1 -1
  420. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  421. package/out/zero-cache/src/services/statz.js +2 -1
  422. package/out/zero-cache/src/services/statz.js.map +1 -1
  423. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +2 -2
  424. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
  425. package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -1
  426. package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -1
  427. package/out/zero-cache/src/services/view-syncer/client-handler.js +4 -2
  428. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  429. package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
  430. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  431. package/out/zero-cache/src/services/view-syncer/cvr-purger.js +2 -2
  432. package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
  433. package/out/zero-cache/src/services/view-syncer/cvr-store.js +9 -5
  434. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  435. package/out/zero-cache/src/services/view-syncer/cvr.js +3 -2
  436. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  437. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +1 -1
  438. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -1
  439. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  440. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -1
  441. package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -1
  442. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +2 -2
  443. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -1
  444. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +5 -3
  445. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  446. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +6 -4
  447. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  448. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -1
  449. package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -1
  450. package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -1
  451. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
  452. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -1
  453. package/out/zero-cache/src/services/view-syncer/query-stats.js +1 -1
  454. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
  455. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  456. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
  457. package/out/zero-cache/src/services/view-syncer/schema/types.js +1 -1
  458. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  459. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js +7 -1
  460. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js.map +1 -1
  461. package/out/zero-cache/src/services/view-syncer/snapshotter.js +3 -2
  462. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  463. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
  464. package/out/zero-cache/src/services/view-syncer/view-syncer.js +4 -6
  465. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  466. package/out/zero-cache/src/types/error-with-level.js.map +1 -1
  467. package/out/zero-cache/src/types/lexi-version.js.map +1 -1
  468. package/out/zero-cache/src/types/lite.js +1 -1
  469. package/out/zero-cache/src/types/lite.js.map +1 -1
  470. package/out/zero-cache/src/types/pg-data-type.js +1 -1
  471. package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
  472. package/out/zero-cache/src/types/pg.js +6 -4
  473. package/out/zero-cache/src/types/pg.js.map +1 -1
  474. package/out/zero-cache/src/types/poke-chunk.js +2 -2
  475. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  476. package/out/zero-cache/src/types/processes.js +1 -1
  477. package/out/zero-cache/src/types/processes.js.map +1 -1
  478. package/out/zero-cache/src/types/profiler.js.map +1 -1
  479. package/out/zero-cache/src/types/row-key.js.map +1 -1
  480. package/out/zero-cache/src/types/sql.js.map +1 -1
  481. package/out/zero-cache/src/types/streams.js +10 -9
  482. package/out/zero-cache/src/types/streams.js.map +1 -1
  483. package/out/zero-cache/src/types/strings.js.map +1 -1
  484. package/out/zero-cache/src/types/subscription.js.map +1 -1
  485. package/out/zero-cache/src/types/timeout.js +1 -1
  486. package/out/zero-cache/src/types/websocket-handoff.js +1 -1
  487. package/out/zero-cache/src/types/ws.js +1 -1
  488. package/out/zero-cache/src/workers/connection.js +2 -3
  489. package/out/zero-cache/src/workers/connection.js.map +1 -1
  490. package/out/zero-cache/src/workers/mutator.js.map +1 -1
  491. package/out/zero-cache/src/workers/replicator.js +5 -4
  492. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  493. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  494. package/out/zero-cache/src/workers/syncer.js +2 -2
  495. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  496. package/out/zero-client/src/client/active-clients-manager.js +1 -1
  497. package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
  498. package/out/zero-client/src/client/bindings.js +11 -11
  499. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  500. package/out/zero-client/src/client/connection.js +1 -1
  501. package/out/zero-client/src/client/connection.js.map +1 -1
  502. package/out/zero-client/src/client/context.js.map +1 -1
  503. package/out/zero-client/src/client/crud-impl.js +1 -1
  504. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  505. package/out/zero-client/src/client/crud.js +1 -1
  506. package/out/zero-client/src/client/crud.js.map +1 -1
  507. package/out/zero-client/src/client/custom.js +1 -1
  508. package/out/zero-client/src/client/custom.js.map +1 -1
  509. package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
  510. package/out/zero-client/src/client/enable-analytics.js +1 -1
  511. package/out/zero-client/src/client/enable-analytics.js.map +1 -1
  512. package/out/zero-client/src/client/error.js.map +1 -1
  513. package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
  514. package/out/zero-client/src/client/inspector/client.js.map +1 -1
  515. package/out/zero-client/src/client/inspector/html-dialog-prompt.js +1 -1
  516. package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
  517. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  518. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  519. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  520. package/out/zero-client/src/client/ivm-branch.js +1 -3
  521. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  522. package/out/zero-client/src/client/keys.js +2 -1
  523. package/out/zero-client/src/client/keys.js.map +1 -1
  524. package/out/zero-client/src/client/log-options.js.map +1 -1
  525. package/out/zero-client/src/client/make-replicache-mutators.js +5 -3
  526. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  527. package/out/zero-client/src/client/metrics.js +3 -5
  528. package/out/zero-client/src/client/metrics.js.map +1 -1
  529. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  530. package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
  531. package/out/zero-client/src/client/options.js.map +1 -1
  532. package/out/zero-client/src/client/query-manager.js +2 -2
  533. package/out/zero-client/src/client/query-manager.js.map +1 -1
  534. package/out/zero-client/src/client/reload-error-handler.js +4 -3
  535. package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
  536. package/out/zero-client/src/client/version.js +1 -1
  537. package/out/zero-client/src/client/zero-poke-handler.js +1 -1
  538. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  539. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  540. package/out/zero-client/src/client/zero.js +19 -11
  541. package/out/zero-client/src/client/zero.js.map +1 -1
  542. package/out/zero-client/src/mod.js +23 -23
  543. package/out/zero-client/src/util/nanoid.js.map +1 -1
  544. package/out/zero-permissions/src/permissions.js +3 -2
  545. package/out/zero-permissions/src/permissions.js.map +1 -1
  546. package/out/zero-pg/src/mod.js +9 -9
  547. package/out/zero-protocol/src/analyze-query-result.js +1 -1
  548. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  549. package/out/zero-protocol/src/application-error.js.map +1 -1
  550. package/out/zero-protocol/src/ast.js +19 -9
  551. package/out/zero-protocol/src/ast.js.map +1 -1
  552. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  553. package/out/zero-protocol/src/client-schema.js +1 -1
  554. package/out/zero-protocol/src/client-schema.js.map +1 -1
  555. package/out/zero-protocol/src/close-connection.js +1 -1
  556. package/out/zero-protocol/src/close-connection.js.map +1 -1
  557. package/out/zero-protocol/src/connect.js +1 -1
  558. package/out/zero-protocol/src/connect.js.map +1 -1
  559. package/out/zero-protocol/src/custom-queries.js +1 -1
  560. package/out/zero-protocol/src/custom-queries.js.map +1 -1
  561. package/out/zero-protocol/src/data.js +4 -2
  562. package/out/zero-protocol/src/data.js.map +1 -1
  563. package/out/zero-protocol/src/delete-clients.js.map +1 -1
  564. package/out/zero-protocol/src/down.js.map +1 -1
  565. package/out/zero-protocol/src/error.js.map +1 -1
  566. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  567. package/out/zero-protocol/src/inspect-down.js +1 -1
  568. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  569. package/out/zero-protocol/src/inspect-up.js +1 -1
  570. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  571. package/out/zero-protocol/src/mutate-server.js +1 -1
  572. package/out/zero-protocol/src/mutate-server.js.map +1 -1
  573. package/out/zero-protocol/src/mutation-id.js.map +1 -1
  574. package/out/zero-protocol/src/mutation.js +1 -1
  575. package/out/zero-protocol/src/mutation.js.map +1 -1
  576. package/out/zero-protocol/src/mutations-patch.js +1 -1
  577. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  578. package/out/zero-protocol/src/ping.js +1 -1
  579. package/out/zero-protocol/src/ping.js.map +1 -1
  580. package/out/zero-protocol/src/poke.js +1 -1
  581. package/out/zero-protocol/src/poke.js.map +1 -1
  582. package/out/zero-protocol/src/pong.js +1 -1
  583. package/out/zero-protocol/src/pong.js.map +1 -1
  584. package/out/zero-protocol/src/primary-key.js.map +1 -1
  585. package/out/zero-protocol/src/protocol-version.js +1 -0
  586. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  587. package/out/zero-protocol/src/pull.js +1 -1
  588. package/out/zero-protocol/src/pull.js.map +1 -1
  589. package/out/zero-protocol/src/push.js +1 -1
  590. package/out/zero-protocol/src/push.js.map +1 -1
  591. package/out/zero-protocol/src/queries-patch.js +1 -1
  592. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  593. package/out/zero-protocol/src/query-hash-visitor.js.map +1 -1
  594. package/out/zero-protocol/src/query-hash.js +2 -2
  595. package/out/zero-protocol/src/query-server.js +1 -1
  596. package/out/zero-protocol/src/query-server.js.map +1 -1
  597. package/out/zero-protocol/src/row-patch.js.map +1 -1
  598. package/out/zero-protocol/src/up.js.map +1 -1
  599. package/out/zero-protocol/src/update-auth.js.map +1 -1
  600. package/out/zero-protocol/src/version.js.map +1 -1
  601. package/out/zero-react/src/bindings.js +11 -11
  602. package/out/zero-react/src/mod.js +5 -5
  603. package/out/zero-react/src/use-query.js +1 -1
  604. package/out/zero-react/src/use-query.js.map +1 -1
  605. package/out/zero-react/src/zero-provider.d.ts +1 -1
  606. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  607. package/out/zero-react/src/zero-provider.js +2 -1
  608. package/out/zero-react/src/zero-provider.js.map +1 -1
  609. package/out/zero-react/src/zero.js +23 -23
  610. package/out/zero-schema/src/builder/schema-builder.js +3 -2
  611. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  612. package/out/zero-schema/src/builder/table-builder.js +1 -1
  613. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  614. package/out/zero-schema/src/compiled-permissions.js.map +1 -1
  615. package/out/zero-schema/src/name-mapper.js +7 -5
  616. package/out/zero-schema/src/name-mapper.js.map +1 -1
  617. package/out/zero-schema/src/schema-config.js +1 -1
  618. package/out/zero-schema/src/schema-config.js.map +1 -1
  619. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  620. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  621. package/out/zero-server/src/adapters/pg.js.map +1 -1
  622. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  623. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  624. package/out/zero-server/src/closable-db-transaction.js.map +1 -1
  625. package/out/zero-server/src/custom.js +1 -1
  626. package/out/zero-server/src/custom.js.map +1 -1
  627. package/out/zero-server/src/mod.js +8 -8
  628. package/out/zero-server/src/process-mutations.js.map +1 -1
  629. package/out/zero-server/src/push-processor.js.map +1 -1
  630. package/out/zero-server/src/queries/process-queries.js +2 -1
  631. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  632. package/out/zero-server/src/schema.js +7 -7
  633. package/out/zero-server/src/schema.js.map +1 -1
  634. package/out/zero-server/src/zql-database.js.map +1 -1
  635. package/out/zero-solid/src/bindings.js +11 -11
  636. package/out/zero-solid/src/mod.js +5 -5
  637. package/out/zero-solid/src/solid-view.js +1 -1
  638. package/out/zero-solid/src/solid-view.js.map +1 -1
  639. package/out/zero-solid/src/use-connection-state.js +2 -1
  640. package/out/zero-solid/src/use-connection-state.js.map +1 -1
  641. package/out/zero-solid/src/use-query.js.map +1 -1
  642. package/out/zero-solid/src/use-zero-online.js +2 -1
  643. package/out/zero-solid/src/use-zero-online.js.map +1 -1
  644. package/out/zero-solid/src/use-zero.js +3 -2
  645. package/out/zero-solid/src/use-zero.js.map +1 -1
  646. package/out/zero-solid/src/zero.js +23 -23
  647. package/out/zero-types/src/name-mapper.js.map +1 -1
  648. package/out/zql/src/builder/builder.js +15 -13
  649. package/out/zql/src/builder/builder.js.map +1 -1
  650. package/out/zql/src/builder/correlated-predicate-pushdown.js.map +1 -1
  651. package/out/zql/src/builder/debug-delegate.js.map +1 -1
  652. package/out/zql/src/builder/like.js +0 -1
  653. package/out/zql/src/builder/like.js.map +1 -1
  654. package/out/zql/src/ivm/array-view.js.map +1 -1
  655. package/out/zql/src/ivm/cap.js.map +1 -1
  656. package/out/zql/src/ivm/constraint.js +1 -1
  657. package/out/zql/src/ivm/constraint.js.map +1 -1
  658. package/out/zql/src/ivm/data.js.map +1 -1
  659. package/out/zql/src/ivm/deferred-input.js.map +1 -1
  660. package/out/zql/src/ivm/exists.js +20 -18
  661. package/out/zql/src/ivm/exists.js.map +1 -1
  662. package/out/zql/src/ivm/fan-in.js.map +1 -1
  663. package/out/zql/src/ivm/fan-out.js.map +1 -1
  664. package/out/zql/src/ivm/filter-operators.js +1 -1
  665. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  666. package/out/zql/src/ivm/filter-push.js.map +1 -1
  667. package/out/zql/src/ivm/filter.js.map +1 -1
  668. package/out/zql/src/ivm/flipped-join.js +1 -3
  669. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  670. package/out/zql/src/ivm/join-index-tap.js.map +1 -1
  671. package/out/zql/src/ivm/join-index.js.map +1 -1
  672. package/out/zql/src/ivm/join-utils.js +11 -13
  673. package/out/zql/src/ivm/join-utils.js.map +1 -1
  674. package/out/zql/src/ivm/join.js.map +1 -1
  675. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
  676. package/out/zql/src/ivm/memory-source.js +26 -22
  677. package/out/zql/src/ivm/memory-source.js.map +1 -1
  678. package/out/zql/src/ivm/memory-storage.js.map +1 -1
  679. package/out/zql/src/ivm/operator.js +2 -2
  680. package/out/zql/src/ivm/push-accumulated.js +0 -1
  681. package/out/zql/src/ivm/push-accumulated.js.map +1 -1
  682. package/out/zql/src/ivm/skip-yields.js.map +1 -1
  683. package/out/zql/src/ivm/skip.js.map +1 -1
  684. package/out/zql/src/ivm/take-gate.js.map +1 -1
  685. package/out/zql/src/ivm/take.js +1 -1
  686. package/out/zql/src/ivm/take.js.map +1 -1
  687. package/out/zql/src/ivm/union-fan-in.js +1 -1
  688. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  689. package/out/zql/src/ivm/union-fan-out.js.map +1 -1
  690. package/out/zql/src/ivm/view-apply-change.js +1 -1
  691. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  692. package/out/zql/src/mutate/crud.js +3 -2
  693. package/out/zql/src/mutate/crud.js.map +1 -1
  694. package/out/zql/src/planner/planner-builder.js +1 -1
  695. package/out/zql/src/planner/planner-builder.js.map +1 -1
  696. package/out/zql/src/planner/planner-connection.js.map +1 -1
  697. package/out/zql/src/planner/planner-debug.js.map +1 -1
  698. package/out/zql/src/planner/planner-fan-in.js.map +1 -1
  699. package/out/zql/src/planner/planner-fan-out.js.map +1 -1
  700. package/out/zql/src/planner/planner-filter.js.map +1 -1
  701. package/out/zql/src/planner/planner-graph.js +1 -5
  702. package/out/zql/src/planner/planner-graph.js.map +1 -1
  703. package/out/zql/src/planner/planner-join.js +1 -1
  704. package/out/zql/src/planner/planner-join.js.map +1 -1
  705. package/out/zql/src/planner/planner-source.js.map +1 -1
  706. package/out/zql/src/planner/planner-terminus.js.map +1 -1
  707. package/out/zql/src/query/create-builder.js +2 -1
  708. package/out/zql/src/query/create-builder.js.map +1 -1
  709. package/out/zql/src/query/expression.js +1 -1
  710. package/out/zql/src/query/expression.js.map +1 -1
  711. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  712. package/out/zql/src/query/named.js +2 -1
  713. package/out/zql/src/query/named.js.map +1 -1
  714. package/out/zql/src/query/query-delegate-base.js +1 -1
  715. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  716. package/out/zql/src/query/query-impl.js +3 -2
  717. package/out/zql/src/query/query-impl.js.map +1 -1
  718. package/out/zql/src/query/query-registry.js +1 -1
  719. package/out/zql/src/query/query-registry.js.map +1 -1
  720. package/out/zql/src/query/query-transitions.js +1 -1
  721. package/out/zql/src/query/query-transitions.js.map +1 -1
  722. package/out/zql/src/query/runnable-query-impl.js +3 -2
  723. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  724. package/out/zql/src/query/static-query.js.map +1 -1
  725. package/out/zql/src/query/ttl.js +8 -8
  726. package/out/zql/src/query/ttl.js.map +1 -1
  727. package/out/zql/src/query/validate-input.js.map +1 -1
  728. package/out/zqlite/src/database-storage.js +2 -2
  729. package/out/zqlite/src/database-storage.js.map +1 -1
  730. package/out/zqlite/src/db.js +2 -2
  731. package/out/zqlite/src/db.js.map +1 -1
  732. package/out/zqlite/src/explain-queries.js.map +1 -1
  733. package/out/zqlite/src/index-usage.js.map +1 -1
  734. package/out/zqlite/src/internal/sql-inline.js.map +1 -1
  735. package/out/zqlite/src/internal/sql.js.map +1 -1
  736. package/out/zqlite/src/internal/statement-cache.js +1 -1
  737. package/out/zqlite/src/internal/statement-cache.js.map +1 -1
  738. package/out/zqlite/src/mod.js +5 -5
  739. package/out/zqlite/src/pending-delta.js.map +1 -1
  740. package/out/zqlite/src/query-builder.js +1 -1
  741. package/out/zqlite/src/query-builder.js.map +1 -1
  742. package/out/zqlite/src/query-delegate.js.map +1 -1
  743. package/out/zqlite/src/resolve-scalar-subqueries.js +2 -5
  744. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  745. package/out/zqlite/src/sqlite-cost-model.js +3 -2
  746. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  747. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  748. package/out/zqlite/src/table-source.js +1 -1
  749. package/out/zqlite/src/table-source.js.map +1 -1
  750. package/package.json +16 -14
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer.ts"],"sourcesContent":["import type {Enum} from '../../../../shared/src/enum.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {PreSerialized, Sized, Source} from '../../types/streams.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n changeStreamDataSchema,\n type ChangeStreamData,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {BackfillRequest} from '../change-source/protocol/current/upstream.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport {changeSourceTimingsSchema} from '../replicator/reporter/report-schema.ts';\nimport type {Service} from '../service.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport type {SnapshotMessage} from './snapshot-message.ts';\nimport type {LocalReplica, SubscribeDownstream} from './subscribe.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nexport type ChangeTag = ChangeStreamData[1]['tag'];\n\n/**\n * Internally all downstream messages (not just commits) are given a watermark.\n * These are used for internal ordering while replaying stored changes and\n * filtering changes already seen by a subscriber.\n *\n * Only commit watermarks are exposed to subscribers. The JSON string is the\n * canonical serialization shared by storage and live forwarding.\n */\nexport type WatermarkedChange = [\n watermark: string,\n tag: ChangeTag,\n json: string,\n];\n\n/**\n * The ChangeStreamer is the component between replicators (\"subscribers\")\n * and a canonical upstream source of changes (e.g. a Postgres logical\n * replication slot). It facilitates multiple subscribers without incurring\n * the associated upstream expense (e.g. PG replication slots are resource\n * intensive) with a \"forward-store-ack\" procedure.\n *\n * * Changes from the upstream source are immediately **forwarded** to\n * connected subscribers to minimize latency.\n *\n * * They are then **stored** in a separate DB to facilitate catchup\n * of connecting subscribers that are behind.\n *\n * * **Acknowledgements** are sent upstream after they are successfully\n * stored.\n *\n * Unlike Postgres replication slots, in which the progress of a static\n * subscriber is tracked in the replication slot, the ChangeStreamer\n * supports a dynamic set of subscribers (i.e.. zero-caches) that can\n * can continually change.\n *\n * However, it is not the case that the ChangeStreamer needs to support\n * arbitrarily old subscribers. Because the replica is continually\n * backed up to a global location and used to initialize new subscriber\n * tasks, an initial subscription request from a subscriber constitutes\n * a signal for how \"behind\" a new subscriber task can be. This is\n * reflected in the {@link SubscriberContext}, which indicates whether\n * the watermark corresponds to an \"initial\" watermark derived from the\n * replica at task startup.\n *\n * The ChangeStreamer uses a combination of this signal with ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\n */\nexport interface ChangeStreamer {\n /**\n * Subscribes to changes based on the supplied subscriber `ctx`,\n * which indicates the watermark at which the subscriber is up to\n * date. Each result contains parsed, validated data and its approximate\n * serialized transport size.\n */\n subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>>;\n}\n\n// v1: v0.18\n// - Client-side support for JSON_FORMAT. Introduced in 0.18.\n// v2: v0.19\n// - Adds the \"status\" message which is initially used to signal that the\n// subscription is valid (i.e. starting at the requested watermark).\n// v3: v0.20\n// - Adds the \"taskID\" to the subscription context, and support for\n// the backup monitor-mediated \"/snapshot\" request.\n// v4: v0.25\n// - Adds the \"replicaVersion\" and \"minWatermark\" fields to the \"/snapshot\"\n// status request so that a subscriber can verify whether its replica,\n// whether it be restored or existing in a permanent volume, is compatible\n// with the change-streamer.\n// v5: v0.26\n// - Moves relation.keyColumns and relation.replicaIdentity to\n// relation.rowKey: { columns, type }.\n// - Adds `metadata` to `create-table` message\n// - Adds `tableMetadata` to `add-column` message\n// - Adds `table-update-metadata` message\n// v6: v0.26\n// - Adds support for `backfill` messages\n// v6: v1.0.1 (backwards compatible, no version change)\n// - Adds lag reporting to status messages\n// v6: (backwards compatible, no version change)\n// - Adds the optional `commitTimeMs` field to `commit` messages, carrying\n// the upstream commit timestamp for end-to-end serving lag measurement.\n// The stream is parsed in 'passthrough' mode, so an older peer ignores the\n// field, and a newer peer treats its absence (including in changes\n// replayed from the Change DB) as \"no commit time reported\".\n// v7: v1.12\n// - Merges the separate `/snapshot` and `/changes` connections into a single\n// bidirectional `/subscribe` connection (see subscribe.ts). The view-syncer\n// drives the connection with upstream `reserve-snapshot` / `start-subscription`\n// application messages; the change-streamer replies with a `reserved`\n// message and then the change stream on the same socket. This guarantees the\n// replication-manager that reserves the change log is the one that serves the\n// subscription. The legacy `/snapshot` + `/changes` endpoints remain for\n// v4-v6 subscribers during rollout.\n// - Drops the legacy \"initial\" and \"logsChangeStream\" query parameters from the\n// subscriber context\n// v8:\n// - Adds `backfills` to the `start-subscription` context, with which the\n// subscriber reports its pending backfills (and their progress).\n// - Adds `progressMarks` to `backfill` and `backfill-completed` messages,\n// which subscribers use to ignore backfill data that would otherwise\n// create a gap (see `acceptBackfill()`).\n// Both fields are optional in the schemas so that v7 peers (and changes\n// persisted in the change-log by v7 replication-managers) remain compatible,\n// but they should be considered non-optional as of v8.\n\nexport const PROTOCOL_VERSION = 8;\n\nexport type SubscriberContext = {\n /**\n * The supported change-streamer protocol version.\n */\n protocolVersion: number;\n\n /**\n * Task ID. This is used to link the request with a preceding snapshot\n * reservation.\n */\n taskID: string;\n\n /**\n * Subscriber id. This is only used for debugging.\n */\n id: string;\n\n /**\n * The ReplicatorMode of the subscriber. 'backup' indicates that the\n * subscriber is local to the `change-streamer` in the `replication-manager`,\n * while 'serving' indicates that user-facing requests depend on the subscriber.\n */\n mode: ReplicatorMode;\n\n /**\n * The ChangeStreamer will return an Error if the subscriber is\n * on a different replica version (i.e. the initial snapshot associated\n * with the replication slot).\n */\n replicaVersion: string;\n\n /**\n * The watermark up to which the subscriber is up to date.\n * Only changes after the watermark will be streamed.\n */\n watermark: string;\n\n /**\n * Whether the subscriber supports batched WebSocket frames.\n */\n wsBatched?: boolean | undefined;\n\n /**\n * The subscriber's pending backfills as of its `watermark`. Reported by\n * subscribers as of protocol v8. `undefined` for older subscribers.\n */\n backfills?: BackfillRequest[] | undefined;\n\n /**\n * Whether the subscriber understands the `draining` status, i.e. it will\n * switch over to another replication-manager when the current one shuts\n * down. A draining replication-manager waits for drainable subscribers to\n * leave before exiting.\n */\n drainable?: boolean | undefined;\n\n /**\n * Whether the subscriber is switching over from another subscription with\n * a replica that it is currently serving. If the replica version is\n * incompatible, such a subscriber is held (see {@link Status.restore})\n * rather than being immediately rejected. Subscribers that are booting up\n * never set this.\n */\n switchover?: boolean | undefined;\n};\n\n/**\n * The StatusMessage payload for now is empty, but can be extended to\n * include meta-level information in the future.\n */\nexport const statusSchema = v.object({\n tag: v.literal('status'),\n\n lagReport: v\n .object({\n lastTimings: changeSourceTimingsSchema.optional(),\n nextSendTimeMs: v.number(),\n })\n .optional(),\n\n /**\n * Sent by a replication-manager that is shutting down to its (drainable)\n * serving subscribers. The subscriber should switch over to another\n * replication-manager (see {@link SubscriberContext.switchover}) while\n * continuing to consume this subscription until the switchover succeeds.\n */\n draining: v.boolean().optional(),\n\n /**\n * Sent on a {@link SubscriberContext.switchover switchover} subscription\n * to a replication-manager running an incompatible replica version.\n * Rather than being immediately rejected with\n * {@link ErrorType.WrongReplicaVersion}, the subscriber is held until it\n * is granted permission to restore the replication-manager's backup,\n * which limits the number of subscribers that are restoring (and thus not\n * serving) at the same time.\n *\n * * `queued`: the subscriber is waiting for permission to restore. It\n * should continue consuming its existing subscription (if any).\n * * `granted`: the subscriber should (gracefully) restart to restore a\n * new replica.\n */\n restore: v\n .union(\n v.object({state: v.literal('queued'), position: v.number()}),\n v.object({state: v.literal('granted')}),\n )\n .optional(),\n});\n\nexport type Status = v.Infer<typeof statusSchema>;\n\nexport const statusMessageSchema = v.tuple([v.literal('status'), statusSchema]);\n\n/**\n * A StatusMessage will be immediately sent on a (v2+) subscription to\n * indicate that the subscription is valid (i.e. starting at the requested\n * watermark). Invalid subscriptions will instead result in a\n * SubscriptionError as the first message.\n */\nexport type StatusMessage = v.Infer<typeof statusMessageSchema>;\n\nconst subscriptionErrorSchema = v.object({\n type: v.number(), // ErrorType\n message: v.string().optional(),\n /**\n * The replica version of the replication-manager, sent with\n * {@link ErrorType.WrongReplicaVersion} so that a subscriber with a newer\n * replica can avoid restoring an older one.\n */\n replicaVersion: v.string().optional(),\n});\n\nexport type SubscriptionError = v.Infer<typeof subscriptionErrorSchema>;\n\nconst errorSchema = v.tuple([v.literal('error'), subscriptionErrorSchema]);\n\nexport const downstreamSchema = v.union(\n statusMessageSchema,\n changeStreamDataSchema,\n errorSchema,\n);\n\nexport type Error = v.Infer<typeof errorSchema>;\n\nexport function errorTypeToReadableName(val: ErrorType) {\n switch (val) {\n case ErrorType.WrongReplicaVersion:\n return 'WrongReplicaVersion';\n case ErrorType.WatermarkTooOld:\n return 'WatermarkTooOld';\n case ErrorType.Unknown:\n return 'Unknown';\n default:\n return 'Unknown';\n }\n}\n\n/**\n * A stream of transactions, each starting with a {@link Begin} message,\n * containing one or more {@link Data} messages, and ending with a\n * {@link Commit} or {@link Rollback} message. The 'commit' tuple\n * includes a `watermark` that should be stored with the committed\n * data and used for resuming a subscription (e.g. in the\n * {@link SubscriberContext}).\n *\n * A {@link SubscriptionError} indicates an unrecoverable error that requires\n * manual intervention (e.g. configuration / operational error).\n */\nexport type Downstream = v.Infer<typeof downstreamSchema>;\n\n/** A downstream message and its approximate serialized transport size. */\nexport type SizedDownstream = Sized<Downstream>;\n\nexport interface ChangeStreamerService\n extends Omit<ChangeStreamer, 'subscribe'>, Service {\n /**\n * The server-side interface overrides `subscribe()` to return a stream\n * of already-stringified {@link Downstream} payloads or pre-serialized Buffers.\n *\n * If a `downstream` subscription is supplied it is used directly as the\n * returned Source. Otherwise, a new one is created.\n */\n subscribe(\n ctx: SubscriberContext,\n downstream?: Subscription<string | PreSerializedBatch>,\n ): Promise<Source<string | PreSerialized>>;\n\n /**\n * Starts a snapshot reservation to preserve change-log entries while\n * a soon-to-be-subscriber downloads the current replica backup. Once\n * the change-streamer knows that a backup is available\n * (via {@link trackBackupWatermark}), it sends a confirmation\n * SnapshotMessage and reserves the corresponding entries in the change-log\n * until the client with that `taskID` subscribes, or if the snapshot\n * connection terminates.\n */\n startSnapshotReservation(\n taskID: string,\n opts?: {localReplica?: LocalReplica | undefined},\n ): Promise<Source<SnapshotMessage>>;\n\n /**\n * Signals (drainable) serving subscribers to switch over to another\n * replication-manager, and resolves when they have all disconnected.\n * Subscribers continue to be served in the meantime.\n */\n drain?(): Promise<void>;\n\n /**\n * Informs the change-streamer of the watermark up to which the replica has\n * been backed up. This serves two purposes:\n * - It serves as the maximum watermark up to which the change-log can be\n * purged. Note that the change-streamer also takes into account the\n * positions of its subscribers and snapshot reservations when purging\n * changes in the change-log.\n * - In RMv2, this ACK's the upstream change-source to allow its buffer of\n * changes (e.g. the replication_slot) to advance.\n */\n trackBackupWatermark(watermark: string): void;\n}\n"],"mappings":";;;;;;;;AA2MA,IAAa,eAAe,eAAE,OAAO;CACnC,KAAK,eAAE,QAAQ,QAAQ;CAEvB,WAAW,eACR,OAAO;EACN,aAAa,0BAA0B,SAAS;EAChD,gBAAgB,eAAE,OAAO;CAC3B,CAAC,EACA,SAAS;;;;;;;CAQZ,UAAU,eAAE,QAAQ,EAAE,SAAS;;;;;;;;;;;;;;;CAgB/B,SAAS,eACN,MACC,eAAE,OAAO;EAAC,OAAO,eAAE,QAAQ,QAAQ;EAAG,UAAU,eAAE,OAAO;CAAC,CAAC,GAC3D,eAAE,OAAO,EAAC,OAAO,eAAE,QAAQ,SAAS,EAAC,CAAC,CACxC,EACC,SAAS;AACd,CAAC;AAID,IAAa,sBAAsB,eAAE,MAAM,CAAC,eAAE,QAAQ,QAAQ,GAAG,YAAY,CAAC;AAU9E,IAAM,0BAA0B,eAAE,OAAO;CACvC,MAAM,eAAE,OAAO;CACf,SAAS,eAAE,OAAO,EAAE,SAAS;;;;;;CAM7B,gBAAgB,eAAE,OAAO,EAAE,SAAS;AACtC,CAAC;AAID,IAAM,cAAc,eAAE,MAAM,CAAC,eAAE,QAAQ,OAAO,GAAG,uBAAuB,CAAC;AAEzE,IAAa,mBAAmB,eAAE,MAChC,qBACA,wBACA,WACF;AAIA,SAAgB,wBAAwB,KAAgB;CACtD,QAAQ,KAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE,OAAO;CACX;AACF"}
1
+ {"version":3,"file":"change-streamer.js","names":["v"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer.ts"],"sourcesContent":["import type {Enum} from '../../../../shared/src/enum.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {PreSerialized, Sized, Source} from '../../types/streams.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n changeStreamDataSchema,\n type ChangeStreamData,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {BackfillRequest} from '../change-source/protocol/current/upstream.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport {changeSourceTimingsSchema} from '../replicator/reporter/report-schema.ts';\nimport type {Service} from '../service.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport type {SnapshotMessage} from './snapshot-message.ts';\nimport type {LocalReplica, SubscribeDownstream} from './subscribe.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nexport type ChangeTag = ChangeStreamData[1]['tag'];\n\n/**\n * Internally all downstream messages (not just commits) are given a watermark.\n * These are used for internal ordering while replaying stored changes and\n * filtering changes already seen by a subscriber.\n *\n * Only commit watermarks are exposed to subscribers. The JSON string is the\n * canonical serialization shared by storage and live forwarding.\n */\nexport type WatermarkedChange = [\n watermark: string,\n tag: ChangeTag,\n json: string,\n];\n\n/**\n * The ChangeStreamer is the component between replicators (\"subscribers\")\n * and a canonical upstream source of changes (e.g. a Postgres logical\n * replication slot). It facilitates multiple subscribers without incurring\n * the associated upstream expense (e.g. PG replication slots are resource\n * intensive) with a \"forward-store-ack\" procedure.\n *\n * * Changes from the upstream source are immediately **forwarded** to\n * connected subscribers to minimize latency.\n *\n * * They are then **stored** in a separate DB to facilitate catchup\n * of connecting subscribers that are behind.\n *\n * * **Acknowledgements** are sent upstream after they are successfully\n * stored.\n *\n * Unlike Postgres replication slots, in which the progress of a static\n * subscriber is tracked in the replication slot, the ChangeStreamer\n * supports a dynamic set of subscribers (i.e.. zero-caches) that can\n * can continually change.\n *\n * However, it is not the case that the ChangeStreamer needs to support\n * arbitrarily old subscribers. Because the replica is continually\n * backed up to a global location and used to initialize new subscriber\n * tasks, an initial subscription request from a subscriber constitutes\n * a signal for how \"behind\" a new subscriber task can be. This is\n * reflected in the {@link SubscriberContext}, which indicates whether\n * the watermark corresponds to an \"initial\" watermark derived from the\n * replica at task startup.\n *\n * The ChangeStreamer uses a combination of this signal with ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\n */\nexport interface ChangeStreamer {\n /**\n * Subscribes to changes based on the supplied subscriber `ctx`,\n * which indicates the watermark at which the subscriber is up to\n * date. Each result contains parsed, validated data and its approximate\n * serialized transport size.\n */\n subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>>;\n}\n\n// v1: v0.18\n// - Client-side support for JSON_FORMAT. Introduced in 0.18.\n// v2: v0.19\n// - Adds the \"status\" message which is initially used to signal that the\n// subscription is valid (i.e. starting at the requested watermark).\n// v3: v0.20\n// - Adds the \"taskID\" to the subscription context, and support for\n// the backup monitor-mediated \"/snapshot\" request.\n// v4: v0.25\n// - Adds the \"replicaVersion\" and \"minWatermark\" fields to the \"/snapshot\"\n// status request so that a subscriber can verify whether its replica,\n// whether it be restored or existing in a permanent volume, is compatible\n// with the change-streamer.\n// v5: v0.26\n// - Moves relation.keyColumns and relation.replicaIdentity to\n// relation.rowKey: { columns, type }.\n// - Adds `metadata` to `create-table` message\n// - Adds `tableMetadata` to `add-column` message\n// - Adds `table-update-metadata` message\n// v6: v0.26\n// - Adds support for `backfill` messages\n// v6: v1.0.1 (backwards compatible, no version change)\n// - Adds lag reporting to status messages\n// v6: (backwards compatible, no version change)\n// - Adds the optional `commitTimeMs` field to `commit` messages, carrying\n// the upstream commit timestamp for end-to-end serving lag measurement.\n// The stream is parsed in 'passthrough' mode, so an older peer ignores the\n// field, and a newer peer treats its absence (including in changes\n// replayed from the Change DB) as \"no commit time reported\".\n// v7: v1.12\n// - Merges the separate `/snapshot` and `/changes` connections into a single\n// bidirectional `/subscribe` connection (see subscribe.ts). The view-syncer\n// drives the connection with upstream `reserve-snapshot` / `start-subscription`\n// application messages; the change-streamer replies with a `reserved`\n// message and then the change stream on the same socket. This guarantees the\n// replication-manager that reserves the change log is the one that serves the\n// subscription. The legacy `/snapshot` + `/changes` endpoints remain for\n// v4-v6 subscribers during rollout.\n// - Drops the legacy \"initial\" and \"logsChangeStream\" query parameters from the\n// subscriber context\n// v8:\n// - Adds `backfills` to the `start-subscription` context, with which the\n// subscriber reports its pending backfills (and their progress).\n// - Adds `progressMarks` to `backfill` and `backfill-completed` messages,\n// which subscribers use to ignore backfill data that would otherwise\n// create a gap (see `acceptBackfill()`).\n// Both fields are optional in the schemas so that v7 peers (and changes\n// persisted in the change-log by v7 replication-managers) remain compatible,\n// but they should be considered non-optional as of v8.\n\nexport const PROTOCOL_VERSION = 8;\n\nexport type SubscriberContext = {\n /**\n * The supported change-streamer protocol version.\n */\n protocolVersion: number;\n\n /**\n * Task ID. This is used to link the request with a preceding snapshot\n * reservation.\n */\n taskID: string;\n\n /**\n * Subscriber id. This is only used for debugging.\n */\n id: string;\n\n /**\n * The ReplicatorMode of the subscriber. 'backup' indicates that the\n * subscriber is local to the `change-streamer` in the `replication-manager`,\n * while 'serving' indicates that user-facing requests depend on the subscriber.\n */\n mode: ReplicatorMode;\n\n /**\n * The ChangeStreamer will return an Error if the subscriber is\n * on a different replica version (i.e. the initial snapshot associated\n * with the replication slot).\n */\n replicaVersion: string;\n\n /**\n * The watermark up to which the subscriber is up to date.\n * Only changes after the watermark will be streamed.\n */\n watermark: string;\n\n /**\n * Whether the subscriber supports batched WebSocket frames.\n */\n wsBatched?: boolean | undefined;\n\n /**\n * The subscriber's pending backfills as of its `watermark`. Reported by\n * subscribers as of protocol v8. `undefined` for older subscribers.\n */\n backfills?: BackfillRequest[] | undefined;\n\n /**\n * Whether the subscriber understands the `draining` status, i.e. it will\n * switch over to another replication-manager when the current one shuts\n * down. A draining replication-manager waits for drainable subscribers to\n * leave before exiting.\n */\n drainable?: boolean | undefined;\n\n /**\n * Whether the subscriber is switching over from another subscription with\n * a replica that it is currently serving. If the replica version is\n * incompatible, such a subscriber is held (see {@link Status.restore})\n * rather than being immediately rejected. Subscribers that are booting up\n * never set this.\n */\n switchover?: boolean | undefined;\n};\n\n/**\n * The StatusMessage payload for now is empty, but can be extended to\n * include meta-level information in the future.\n */\nexport const statusSchema = v.object({\n tag: v.literal('status'),\n\n lagReport: v\n .object({\n lastTimings: changeSourceTimingsSchema.optional(),\n nextSendTimeMs: v.number(),\n })\n .optional(),\n\n /**\n * Sent by a replication-manager that is shutting down to its (drainable)\n * serving subscribers. The subscriber should switch over to another\n * replication-manager (see {@link SubscriberContext.switchover}) while\n * continuing to consume this subscription until the switchover succeeds.\n */\n draining: v.boolean().optional(),\n\n /**\n * Sent on a {@link SubscriberContext.switchover switchover} subscription\n * to a replication-manager running an incompatible replica version.\n * Rather than being immediately rejected with\n * {@link ErrorType.WrongReplicaVersion}, the subscriber is held until it\n * is granted permission to restore the replication-manager's backup,\n * which limits the number of subscribers that are restoring (and thus not\n * serving) at the same time.\n *\n * * `queued`: the subscriber is waiting for permission to restore. It\n * should continue consuming its existing subscription (if any).\n * * `granted`: the subscriber should (gracefully) restart to restore a\n * new replica.\n */\n restore: v\n .union(\n v.object({state: v.literal('queued'), position: v.number()}),\n v.object({state: v.literal('granted')}),\n )\n .optional(),\n});\n\nexport type Status = v.Infer<typeof statusSchema>;\n\nexport const statusMessageSchema = v.tuple([v.literal('status'), statusSchema]);\n\n/**\n * A StatusMessage will be immediately sent on a (v2+) subscription to\n * indicate that the subscription is valid (i.e. starting at the requested\n * watermark). Invalid subscriptions will instead result in a\n * SubscriptionError as the first message.\n */\nexport type StatusMessage = v.Infer<typeof statusMessageSchema>;\n\nconst subscriptionErrorSchema = v.object({\n type: v.number(), // ErrorType\n message: v.string().optional(),\n /**\n * The replica version of the replication-manager, sent with\n * {@link ErrorType.WrongReplicaVersion} so that a subscriber with a newer\n * replica can avoid restoring an older one.\n */\n replicaVersion: v.string().optional(),\n});\n\nexport type SubscriptionError = v.Infer<typeof subscriptionErrorSchema>;\n\nconst errorSchema = v.tuple([v.literal('error'), subscriptionErrorSchema]);\n\nexport const downstreamSchema = v.union(\n statusMessageSchema,\n changeStreamDataSchema,\n errorSchema,\n);\n\nexport type Error = v.Infer<typeof errorSchema>;\n\nexport function errorTypeToReadableName(val: ErrorType) {\n switch (val) {\n case ErrorType.WrongReplicaVersion:\n return 'WrongReplicaVersion';\n case ErrorType.WatermarkTooOld:\n return 'WatermarkTooOld';\n case ErrorType.Unknown:\n return 'Unknown';\n default:\n return 'Unknown';\n }\n}\n\n/**\n * A stream of transactions, each starting with a {@link Begin} message,\n * containing one or more {@link Data} messages, and ending with a\n * {@link Commit} or {@link Rollback} message. The 'commit' tuple\n * includes a `watermark` that should be stored with the committed\n * data and used for resuming a subscription (e.g. in the\n * {@link SubscriberContext}).\n *\n * A {@link SubscriptionError} indicates an unrecoverable error that requires\n * manual intervention (e.g. configuration / operational error).\n */\nexport type Downstream = v.Infer<typeof downstreamSchema>;\n\n/** A downstream message and its approximate serialized transport size. */\nexport type SizedDownstream = Sized<Downstream>;\n\nexport interface ChangeStreamerService\n extends Omit<ChangeStreamer, 'subscribe'>, Service {\n /**\n * The server-side interface overrides `subscribe()` to return a stream\n * of already-stringified {@link Downstream} payloads or pre-serialized Buffers.\n *\n * If a `downstream` subscription is supplied it is used directly as the\n * returned Source. Otherwise, a new one is created.\n */\n subscribe(\n ctx: SubscriberContext,\n downstream?: Subscription<string | PreSerializedBatch>,\n ): Promise<Source<string | PreSerialized>>;\n\n /**\n * Starts a snapshot reservation to preserve change-log entries while\n * a soon-to-be-subscriber downloads the current replica backup. Once\n * the change-streamer knows that a backup is available\n * (via {@link trackBackupWatermark}), it sends a confirmation\n * SnapshotMessage and reserves the corresponding entries in the change-log\n * until the client with that `taskID` subscribes, or if the snapshot\n * connection terminates.\n */\n startSnapshotReservation(\n taskID: string,\n opts?: {localReplica?: LocalReplica | undefined},\n ): Promise<Source<SnapshotMessage>>;\n\n /**\n * Signals (drainable) serving subscribers to switch over to another\n * replication-manager, and resolves when they have all disconnected.\n * Subscribers continue to be served in the meantime.\n */\n drain?(): Promise<void>;\n\n /**\n * Informs the change-streamer of the watermark up to which the replica has\n * been backed up. This serves two purposes:\n * - It serves as the maximum watermark up to which the change-log can be\n * purged. Note that the change-streamer also takes into account the\n * positions of its subscribers and snapshot reservations when purging\n * changes in the change-log.\n * - In RMv2, this ACK's the upstream change-source to allow its buffer of\n * changes (e.g. the replication_slot) to advance.\n */\n trackBackupWatermark(watermark: string): void;\n}\n"],"mappings":";;;;;;;;AA2MA,IAAa,eAAeA,eAAE,OAAO;CACnC,KAAKA,eAAE,QAAQ,QAAQ;CAEvB,WAAWA,eACR,OAAO;EACN,aAAa,0BAA0B,SAAS;EAChD,gBAAgBA,eAAE,OAAO;CAC3B,CAAC,CAAC,CACD,SAAS;;;;;;;CAQZ,UAAUA,eAAE,QAAQ,CAAC,CAAC,SAAS;;;;;;;;;;;;;;;CAgB/B,SAASA,eACN,MACCA,eAAE,OAAO;EAAC,OAAOA,eAAE,QAAQ,QAAQ;EAAG,UAAUA,eAAE,OAAO;CAAC,CAAC,GAC3DA,eAAE,OAAO,EAAC,OAAOA,eAAE,QAAQ,SAAS,EAAC,CAAC,CACxC,CAAC,CACA,SAAS;AACd,CAAC;AAID,IAAa,sBAAsBA,eAAE,MAAM,CAACA,eAAE,QAAQ,QAAQ,GAAG,YAAY,CAAC;AAU9E,IAAM,0BAA0BA,eAAE,OAAO;CACvC,MAAMA,eAAE,OAAO;CACf,SAASA,eAAE,OAAO,CAAC,CAAC,SAAS;;;;;;CAM7B,gBAAgBA,eAAE,OAAO,CAAC,CAAC,SAAS;AACtC,CAAC;AAID,IAAM,cAAcA,eAAE,MAAM,CAACA,eAAE,QAAQ,OAAO,GAAG,uBAAuB,CAAC;AAEzE,IAAa,mBAAmBA,eAAE,MAChC,qBACA,wBACA,WACF;AAIA,SAAgB,wBAAwB,KAAgB;CACtD,QAAQ,KAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE,OAAO;CACX;AACF"}
@@ -7,10 +7,10 @@ import type { Subscriber } from './subscriber.ts';
7
7
  * change-streamer, i.e. those that switch over to another
8
8
  * replication-manager when this one drains.
9
9
  *
10
- * Upon {@link drain}, drainable subscribers are sent a `draining` status
11
- * (as are drainable subscribers that subscribe while draining), and the
12
- * returned promise resolves once they have all disconnected. Subscribers
13
- * continue to be served in the meantime.
10
+ * Upon {@link drain}, drainable subscribers are sent a `draining` status,
11
+ * and the returned promise resolves once they have all disconnected.
12
+ * Subscribers continue to be served in the meantime. (New subscribers are
13
+ * not accepted while draining.)
14
14
  */
15
15
  export declare class DrainTracker {
16
16
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"drain-tracker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/drain-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAIhD;;;;;;;;;GASG;AACH,qBAAa,YAAY;;IAOvB,YACE,EAAE,EAAE,UAAU,EAEd,aAAa,GAAE,OAAO,WAAyB,EAC/C,eAAe,GAAE,OAAO,aAA6B,EAKtD;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,QAAQ,CACN,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,YAAY,CAAC,MAAM,GAAG,kBAAkB,CAAC,QActD;IAED,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAuBrB;IAED,uEAAuE;IACvE,IAAI,SAEH;CAQF"}
1
+ {"version":3,"file":"drain-tracker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/drain-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAIhD;;;;;;;;;GASG;AACH,qBAAa,YAAY;;IAOvB,YACE,EAAE,EAAE,UAAU,EAEd,aAAa,GAAE,OAAO,WAAyB,EAC/C,eAAe,GAAE,OAAO,aAA6B,EAKtD;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,QAAQ,CACN,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,YAAY,CAAC,MAAM,GAAG,kBAAkB,CAAC,QAWtD;IAED,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAuBrB;IAED,uEAAuE;IACvE,IAAI,SAEH;CAQF"}
@@ -6,10 +6,10 @@ var PROGRESS_LOG_INTERVAL_MS = 3e4;
6
6
  * change-streamer, i.e. those that switch over to another
7
7
  * replication-manager when this one drains.
8
8
  *
9
- * Upon {@link drain}, drainable subscribers are sent a `draining` status
10
- * (as are drainable subscribers that subscribe while draining), and the
11
- * returned promise resolves once they have all disconnected. Subscribers
12
- * continue to be served in the meantime.
9
+ * Upon {@link drain}, drainable subscribers are sent a `draining` status,
10
+ * and the returned promise resolves once they have all disconnected.
11
+ * Subscribers continue to be served in the meantime. (New subscribers are
12
+ * not accepted while draining.)
13
13
  */
14
14
  var DrainTracker = class {
15
15
  #lc;
@@ -28,10 +28,6 @@ var DrainTracker = class {
28
28
  register(subscriber, downstream) {
29
29
  if (!subscriber.drainable) return;
30
30
  this.#subscribers.add(subscriber);
31
- if (this.#draining) subscriber.sendControlStatus({
32
- tag: "status",
33
- draining: true
34
- });
35
31
  downstream.addCloseHandler(() => {
36
32
  if (this.#subscribers.delete(subscriber)) this.#checkDrained();
37
33
  });
@@ -1 +1 @@
1
- {"version":3,"file":"drain-tracker.js","names":["#lc","#subscribers","#setInterval","#clearInterval","#draining","#checkDrained"],"sources":["../../../../../../zero-cache/src/services/change-streamer/drain-tracker.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {Subscriber} from './subscriber.ts';\n\nconst PROGRESS_LOG_INTERVAL_MS = 30_000;\n\n/**\n * Tracks the {@link Subscriber.drainable drainable} subscribers of the\n * change-streamer, i.e. those that switch over to another\n * replication-manager when this one drains.\n *\n * Upon {@link drain}, drainable subscribers are sent a `draining` status\n * (as are drainable subscribers that subscribe while draining), and the\n * returned promise resolves once they have all disconnected. Subscribers\n * continue to be served in the meantime.\n */\nexport class DrainTracker {\n readonly #lc: LogContext;\n readonly #subscribers = new Set<Subscriber>();\n readonly #setInterval: typeof setInterval;\n readonly #clearInterval: typeof clearInterval;\n #draining: Resolver<void> | undefined;\n\n constructor(\n lc: LogContext,\n // Injectable timers, for deterministic testing.\n setIntervalFn: typeof setInterval = setInterval,\n clearIntervalFn: typeof clearInterval = clearInterval,\n ) {\n this.#lc = lc.withContext('component', 'drain-tracker');\n this.#setInterval = setIntervalFn;\n this.#clearInterval = clearIntervalFn;\n }\n\n get draining(): boolean {\n return this.#draining !== undefined;\n }\n\n register(\n subscriber: Subscriber,\n downstream: Subscription<string | PreSerializedBatch>,\n ) {\n if (!subscriber.drainable) {\n return;\n }\n this.#subscribers.add(subscriber);\n if (this.#draining) {\n subscriber.sendControlStatus({tag: 'status', draining: true});\n }\n downstream.addCloseHandler(() => {\n if (this.#subscribers.delete(subscriber)) {\n this.#checkDrained();\n }\n });\n }\n\n drain(): Promise<void> {\n if (!this.#draining) {\n const draining = (this.#draining = resolver<void>());\n this.#lc.info?.(\n `draining: signaling ${this.#subscribers.size} subscribers ` +\n `to switch over`,\n );\n for (const sub of this.#subscribers) {\n sub.sendControlStatus({tag: 'status', draining: true});\n }\n const progress = this.#setInterval(\n () =>\n this.#lc.info?.(\n `draining: waiting for ${this.#subscribers.size} ` +\n `subscribers to switch over`,\n Array.from(this.#subscribers, sub => sub.id),\n ),\n PROGRESS_LOG_INTERVAL_MS,\n );\n void draining.promise.finally(() => this.#clearInterval(progress));\n this.#checkDrained();\n }\n return this.#draining.promise;\n }\n\n /** Resolves any pending drain, e.g. when the change-streamer stops. */\n stop() {\n this.#draining?.resolve();\n }\n\n #checkDrained() {\n if (this.#draining && this.#subscribers.size === 0) {\n this.#lc.info?.(`drained all subscribers`);\n this.#draining.resolve();\n }\n }\n}\n"],"mappings":";;AAMA,IAAM,2BAA2B;;;;;;;;;;;AAYjC,IAAa,eAAb,MAA0B;CACxB;CACA,+BAAwB,IAAI,IAAgB;CAC5C;CACA;CACA;CAEA,YACE,IAEA,gBAAoC,aACpC,kBAAwC,eACxC;EACA,KAAKA,MAAM,GAAG,YAAY,aAAa,eAAe;EACtD,KAAKE,eAAe;EACpB,KAAKC,iBAAiB;CACxB;CAEA,IAAI,WAAoB;EACtB,OAAO,KAAKC,cAAc,KAAA;CAC5B;CAEA,SACE,YACA,YACA;EACA,IAAI,CAAC,WAAW,WACd;EAEF,KAAKH,aAAa,IAAI,UAAU;EAChC,IAAI,KAAKG,WACP,WAAW,kBAAkB;GAAC,KAAK;GAAU,UAAU;EAAI,CAAC;EAE9D,WAAW,sBAAsB;GAC/B,IAAI,KAAKH,aAAa,OAAO,UAAU,GACrC,KAAKI,cAAc;EAEvB,CAAC;CACH;CAEA,QAAuB;EACrB,IAAI,CAAC,KAAKD,WAAW;GACnB,MAAM,WAAY,KAAKA,YAAY,SAAe;GAClD,KAAKJ,IAAI,OACP,uBAAuB,KAAKC,aAAa,KAAK,4BAEhD;GACA,KAAK,MAAM,OAAO,KAAKA,cACrB,IAAI,kBAAkB;IAAC,KAAK;IAAU,UAAU;GAAI,CAAC;GAEvD,MAAM,WAAW,KAAKC,mBAElB,KAAKF,IAAI,OACP,yBAAyB,KAAKC,aAAa,KAAK,8BAEhD,MAAM,KAAK,KAAKA,eAAc,QAAO,IAAI,EAAE,CAC7C,GACF,wBACF;GACA,SAAc,QAAQ,cAAc,KAAKE,eAAe,QAAQ,CAAC;GACjE,KAAKE,cAAc;EACrB;EACA,OAAO,KAAKD,UAAU;CACxB;;CAGA,OAAO;EACL,KAAKA,WAAW,QAAQ;CAC1B;CAEA,gBAAgB;EACd,IAAI,KAAKA,aAAa,KAAKH,aAAa,SAAS,GAAG;GAClD,KAAKD,IAAI,OAAO,yBAAyB;GACzC,KAAKI,UAAU,QAAQ;EACzB;CACF;AACF"}
1
+ {"version":3,"file":"drain-tracker.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/drain-tracker.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {Subscriber} from './subscriber.ts';\n\nconst PROGRESS_LOG_INTERVAL_MS = 30_000;\n\n/**\n * Tracks the {@link Subscriber.drainable drainable} subscribers of the\n * change-streamer, i.e. those that switch over to another\n * replication-manager when this one drains.\n *\n * Upon {@link drain}, drainable subscribers are sent a `draining` status,\n * and the returned promise resolves once they have all disconnected.\n * Subscribers continue to be served in the meantime. (New subscribers are\n * not accepted while draining.)\n */\nexport class DrainTracker {\n readonly #lc: LogContext;\n readonly #subscribers = new Set<Subscriber>();\n readonly #setInterval: typeof setInterval;\n readonly #clearInterval: typeof clearInterval;\n #draining: Resolver<void> | undefined;\n\n constructor(\n lc: LogContext,\n // Injectable timers, for deterministic testing.\n setIntervalFn: typeof setInterval = setInterval,\n clearIntervalFn: typeof clearInterval = clearInterval,\n ) {\n this.#lc = lc.withContext('component', 'drain-tracker');\n this.#setInterval = setIntervalFn;\n this.#clearInterval = clearIntervalFn;\n }\n\n get draining(): boolean {\n return this.#draining !== undefined;\n }\n\n register(\n subscriber: Subscriber,\n downstream: Subscription<string | PreSerializedBatch>,\n ) {\n if (!subscriber.drainable) {\n return;\n }\n this.#subscribers.add(subscriber);\n downstream.addCloseHandler(() => {\n if (this.#subscribers.delete(subscriber)) {\n this.#checkDrained();\n }\n });\n }\n\n drain(): Promise<void> {\n if (!this.#draining) {\n const draining = (this.#draining = resolver<void>());\n this.#lc.info?.(\n `draining: signaling ${this.#subscribers.size} subscribers ` +\n `to switch over`,\n );\n for (const sub of this.#subscribers) {\n sub.sendControlStatus({tag: 'status', draining: true});\n }\n const progress = this.#setInterval(\n () =>\n this.#lc.info?.(\n `draining: waiting for ${this.#subscribers.size} ` +\n `subscribers to switch over`,\n Array.from(this.#subscribers, sub => sub.id),\n ),\n PROGRESS_LOG_INTERVAL_MS,\n );\n void draining.promise.finally(() => this.#clearInterval(progress));\n this.#checkDrained();\n }\n return this.#draining.promise;\n }\n\n /** Resolves any pending drain, e.g. when the change-streamer stops. */\n stop() {\n this.#draining?.resolve();\n }\n\n #checkDrained() {\n if (this.#draining && this.#subscribers.size === 0) {\n this.#lc.info?.(`drained all subscribers`);\n this.#draining.resolve();\n }\n }\n}\n"],"mappings":";;AAMA,IAAM,2BAA2B;;;;;;;;;;;AAYjC,IAAa,eAAb,MAA0B;CACxB;CACA,+BAAwB,IAAI,IAAgB;CAC5C;CACA;CACA;CAEA,YACE,IAEA,gBAAoC,aACpC,kBAAwC,eACxC;EACA,KAAK,MAAM,GAAG,YAAY,aAAa,eAAe;EACtD,KAAK,eAAe;EACpB,KAAK,iBAAiB;CACxB;CAEA,IAAI,WAAoB;EACtB,OAAO,KAAK,cAAc,KAAA;CAC5B;CAEA,SACE,YACA,YACA;EACA,IAAI,CAAC,WAAW,WACd;EAEF,KAAK,aAAa,IAAI,UAAU;EAChC,WAAW,sBAAsB;GAC/B,IAAI,KAAK,aAAa,OAAO,UAAU,GACrC,KAAK,cAAc;EAEvB,CAAC;CACH;CAEA,QAAuB;EACrB,IAAI,CAAC,KAAK,WAAW;GACnB,MAAM,WAAY,KAAK,YAAY,SAAe;GAClD,KAAK,IAAI,OACP,uBAAuB,KAAK,aAAa,KAAK,4BAEhD;GACA,KAAK,MAAM,OAAO,KAAK,cACrB,IAAI,kBAAkB;IAAC,KAAK;IAAU,UAAU;GAAI,CAAC;GAEvD,MAAM,WAAW,KAAK,mBAElB,KAAK,IAAI,OACP,yBAAyB,KAAK,aAAa,KAAK,8BAEhD,MAAM,KAAK,KAAK,eAAc,QAAO,IAAI,EAAE,CAC7C,GACF,wBACF;GACA,SAAc,QAAQ,cAAc,KAAK,eAAe,QAAQ,CAAC;GACjE,KAAK,cAAc;EACrB;EACA,OAAO,KAAK,UAAU;CACxB;;CAGA,OAAO;EACL,KAAK,WAAW,QAAQ;CAC1B;CAEA,gBAAgB;EACd,IAAI,KAAK,aAAa,KAAK,aAAa,SAAS,GAAG;GAClD,KAAK,IAAI,OAAO,yBAAyB;GACzC,KAAK,UAAU,QAAQ;EACzB;CACF;AACF"}
@@ -68,16 +68,18 @@ var Forwarder = class {
68
68
  if (this.#flowControlSlowSubscriberGracePeriodMs && timedOut?.size && slowestOnTime) for (const [sub, stats] of timedOut.entries()) {
69
69
  const rate = stats.processRate < slowestOnTime.processRate ? "lagging" : "catching-up";
70
70
  const laggingDuration = sub.reportChangeRate(now, rate);
71
- if (laggingDuration >= this.#flowControlSlowSubscriberGracePeriodMs) if (sub.mode === "backup") {
72
- this.#backupLagEventCount++;
73
- const grace = this.#flowControlSlowSubscriberGracePeriodMs;
74
- if (this.#lastBackupLagWarnMs === void 0 || now - this.#lastBackupLagWarnMs >= grace) {
75
- this.#lastBackupLagWarnMs = now;
76
- this.#lc.warn?.(`backup subscriber ${sub.id} has lagged for ${laggingDuration}ms (${this.#backupLagEventCount} lag events so far). this is only expected during initial catchup`, { stats });
71
+ if (laggingDuration >= this.#flowControlSlowSubscriberGracePeriodMs) {
72
+ if (sub.mode === "backup") {
73
+ this.#backupLagEventCount++;
74
+ const grace = this.#flowControlSlowSubscriberGracePeriodMs;
75
+ if (this.#lastBackupLagWarnMs === void 0 || now - this.#lastBackupLagWarnMs >= grace) {
76
+ this.#lastBackupLagWarnMs = now;
77
+ this.#lc.warn?.(`backup subscriber ${sub.id} has lagged for ${laggingDuration}ms (${this.#backupLagEventCount} lag events so far). this is only expected during initial catchup`, { stats });
78
+ }
79
+ } else {
80
+ this.#lc.warn?.(`serving subscriber ${sub.id} has lagged for ${laggingDuration}ms. disconnecting`, { stats });
81
+ sub.close(3, `lagging for over ${this.#flowControlSlowSubscriberGracePeriodMs}ms (${laggingDuration}ms)`);
77
82
  }
78
- } else {
79
- this.#lc.warn?.(`serving subscriber ${sub.id} has lagged for ${laggingDuration}ms. disconnecting`, { stats });
80
- sub.close(3, `lagging for over ${this.#flowControlSlowSubscriberGracePeriodMs}ms (${laggingDuration}ms)`);
81
83
  }
82
84
  }
83
85
  }
@@ -188,7 +190,6 @@ var Forwarder = class {
188
190
  this.#inTransaction = false;
189
191
  for (const sub of this.#queued.values()) this.#active.add(sub);
190
192
  this.#queued.clear();
191
- break;
192
193
  }
193
194
  }
194
195
  getAcks() {
@@ -1 +1 @@
1
- {"version":3,"file":"forwarder.js","names":["#lc","#setTimeout","#clearTimeout","#active","#queued","#earlyReleaseOptions","#flowControlSlowSubscriberGracePeriodMs","#flowControlWaits","#flowControlWaitTime","#subscriberStats","#progressMonitor","#currentBroadcast","#backupLagEventCount","#lastBackupLagWarnMs","#flushPendingWithoutTracking","#inTransaction","#pending","#updateActiveSubscribers","#flushScheduled","#flushImmediateId","#drainPending"],"sources":["../../../../../../zero-cache/src/services/change-streamer/forwarder.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {joinIterables, wrapIterable} from '../../../../shared/src/iterables.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {Broadcast, type EarlyReleaseOptions} from './broadcast.ts';\nimport type {ChangeTag, Status, WatermarkedChange} from './change-streamer.ts';\nimport {StreamTooFarBehind} from './error-type-enum.ts';\nimport type {Subscriber, SubscriberStats} from './subscriber.ts';\n\nexport type ProgressMonitorOptions = {\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n // Injectable timers, for deterministic testing.\n setTimeoutFn?: typeof setTimeout | undefined;\n clearTimeoutFn?: typeof clearTimeout | undefined;\n};\n\nconst FORWARD_BATCH_SIZE = 64;\n\nexport class Forwarder {\n readonly #lc: LogContext;\n readonly #setTimeout: typeof setTimeout;\n readonly #clearTimeout: typeof clearTimeout;\n readonly #active = new Set<Subscriber>();\n readonly #queued = new Set<Subscriber>();\n readonly #earlyReleaseOptions: EarlyReleaseOptions | undefined;\n readonly #flowControlSlowSubscriberGracePeriodMs: number | undefined;\n\n readonly #flowControlWaits = getOrCreateCounter(\n 'replication',\n 'flow_control.waits',\n 'Flow-control checkpoints completed, labeled by release mode.',\n );\n readonly #flowControlWaitTime = getOrCreateLatencyHistogram(\n 'replication',\n 'flow_control.wait_duration',\n 'Time replication waits at flow-control checkpoints.',\n );\n #inTransaction = false;\n\n #pending: WatermarkedChange[] = [];\n #flushScheduled = false;\n #flushImmediateId: NodeJS.Immediate | undefined = undefined;\n\n #currentBroadcast: Broadcast | undefined;\n #progressMonitor: NodeJS.Timeout | undefined;\n\n // The backup-replicator is never disconnected as a laggard, so its lag\n // detections are logged (rather than acted upon). Track the running total and\n // throttle the warning to at most once per grace period to avoid spamming the\n // log for the duration of a slow catchup.\n #backupLagEventCount = 0;\n #lastBackupLagWarnMs: number | undefined;\n\n constructor(\n lc: LogContext,\n opts: ProgressMonitorOptions = {\n flowControlConsensusTimeoutProportion: 2.0,\n },\n ) {\n this.#lc = lc.withContext('component', 'subscriber-progress-monitor');\n this.#setTimeout = opts.setTimeoutFn ?? setTimeout;\n this.#clearTimeout = opts.clearTimeoutFn ?? clearTimeout;\n\n this.#earlyReleaseOptions =\n opts.flowControlConsensusTimeoutProportion < 0\n ? undefined\n : {\n consensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n setTimeoutFn: this.#setTimeout,\n clearTimeoutFn: this.#clearTimeout,\n };\n this.#flowControlSlowSubscriberGracePeriodMs =\n opts.flowControlSlowSubscriberGracePeriodMs;\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.active_subscribers',\n 'Active change-stream subscribers receiving live changes.',\n ).addCallback(result => result.observe(this.#active.size));\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.queued_subscribers',\n 'Change-stream subscribers waiting for the current transaction to finish before activation.',\n ).addCallback(result => result.observe(this.#queued.size));\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.pending_messages',\n 'Downstream change-stream messages not yet acked by subscribers.',\n ).addCallback(result =>\n result.observe(this.#subscriberStats().pendingMessages),\n );\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.backlog_messages',\n 'Live change-stream messages buffered while subscribers catch up.',\n ).addCallback(result =>\n result.observe(this.#subscriberStats().backlogMessages),\n );\n\n getOrCreateGauge('replication', 'flow_control.backlog_bytes', {\n description:\n 'Live change-stream bytes buffered while subscribers catch up.',\n unit: 'By',\n }).addCallback(result =>\n result.observe(this.#subscriberStats().backlogBytes),\n );\n\n getOrCreateGauge('replication', 'flow_control.max_backlog_bytes', {\n description:\n 'Maximum live change-stream bytes buffered by a single subscriber.',\n unit: 'By',\n }).addCallback(result =>\n result.observe(this.#subscriberStats().maxBacklogBytes),\n );\n }\n\n startProgressMonitor() {\n clearInterval(this.#progressMonitor);\n this.#progressMonitor = setInterval(\n () => this.checkSubscriberProgress(performance.now()),\n 1000,\n );\n }\n\n /**\n * Runs one progress-monitor tick: samples each active subscriber's change\n * rate, logs slow broadcasts, and disconnects subscribers that have been\n * continuously lagging for longer than the configured grace period.\n *\n * Invoked on an interval by {@link startProgressMonitor()}; exposed (rather\n * than private) so tests can drive a single tick with an explicit `now`.\n */\n checkSubscriberProgress(now: number) {\n let timedOut: Map<Subscriber, SubscriberStats> | undefined;\n let slowestOnTime: SubscriberStats | undefined;\n\n for (const sub of this.#active) {\n const stats = sub.sampleProcessRate(now).getStats();\n if (stats.missedLastTimeout) {\n (timedOut ??= new Map()).set(sub, stats);\n } else if (\n stats.processRate <= (slowestOnTime?.processRate ?? Infinity)\n ) {\n slowestOnTime = stats;\n }\n }\n this.#currentBroadcast?.logSlowRequests(now);\n\n if (\n this.#flowControlSlowSubscriberGracePeriodMs &&\n timedOut?.size &&\n slowestOnTime\n ) {\n for (const [sub, stats] of timedOut.entries()) {\n const rate =\n stats.processRate < slowestOnTime.processRate\n ? 'lagging'\n : 'catching-up';\n const laggingDuration = sub.reportChangeRate(now, rate);\n if (laggingDuration >= this.#flowControlSlowSubscriberGracePeriodMs) {\n if (sub.mode === 'backup') {\n // The backup-replicator can only be detected as a laggard during\n // initial catchup; after that it is required for consensus. If it\n // triggers laggard detection in the former case, just log and\n // continue; as disconnecting it would otherwise kill the\n // change-streamer task altogether.\n this.#backupLagEventCount++;\n const grace = this.#flowControlSlowSubscriberGracePeriodMs;\n if (\n this.#lastBackupLagWarnMs === undefined ||\n now - this.#lastBackupLagWarnMs >= grace\n ) {\n this.#lastBackupLagWarnMs = now;\n this.#lc.warn?.(\n `backup subscriber ${sub.id} has lagged for ${laggingDuration}ms ` +\n `(${this.#backupLagEventCount} lag events so far). ` +\n `this is only expected during initial catchup`,\n {stats},\n );\n }\n } else {\n this.#lc.warn?.(\n `serving subscriber ${sub.id} has lagged for ${laggingDuration}ms. disconnecting`,\n {stats},\n );\n sub.close(\n StreamTooFarBehind,\n `lagging for over ${this.#flowControlSlowSubscriberGracePeriodMs}ms (${laggingDuration}ms)`,\n );\n }\n }\n }\n }\n }\n\n stopProgressMonitor() {\n clearInterval(this.#progressMonitor);\n this.#flushPendingWithoutTracking();\n }\n\n /**\n * `add()` is called in lock step with `Storer.catchup()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n */\n add(sub: Subscriber) {\n this.#flushPendingWithoutTracking();\n if (this.#inTransaction) {\n this.#queued.add(sub);\n } else {\n this.#active.add(sub);\n }\n }\n\n remove(sub: Subscriber) {\n this.#flushPendingWithoutTracking();\n this.#active.delete(sub);\n this.#queued.delete(sub);\n sub.close();\n }\n\n /**\n * `forward()` is called in lockstep with `Storer.store()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n *\n * This version of forward buffers changes across the current event loop turn\n * and dispatches them in batches. The change-streamer should call and await\n * {@link forwardWithFlowControl()} occasionally to avoid memory blowup.\n */\n forward(entry: WatermarkedChange) {\n this.#pending.push(entry);\n\n if (\n this.#queued.size > 0 &&\n (entry[1] === 'commit' || entry[1] === 'rollback')\n ) {\n this.#flushPendingWithoutTracking();\n }\n this.#updateActiveSubscribers(entry[1]);\n\n if (this.#pending.length >= FORWARD_BATCH_SIZE) {\n this.#flushPendingWithoutTracking();\n } else if (!this.#flushScheduled && this.#pending.length > 0) {\n this.#flushScheduled = true;\n this.#flushImmediateId = setImmediate(() =>\n this.#flushPendingWithoutTracking(),\n );\n }\n }\n\n sendStatus(status: Status) {\n this.#flushPendingWithoutTracking();\n for (const sub of this.#active.values()) {\n sub.sendStatus(status);\n }\n }\n\n /**\n * The flow-control-aware equivalent of {@link forward()}, returning a\n * Promise that resolves when replication should continue.\n */\n async forwardWithFlowControl(entry: WatermarkedChange) {\n this.#pending.push(entry);\n\n const batch = this.#drainPending();\n const start = performance.now();\n const broadcast = new Broadcast(\n this.#lc,\n this.#active.values(),\n batch,\n this.#earlyReleaseOptions,\n );\n this.#updateActiveSubscribers(entry[1]);\n\n // set for progress tracking\n this.#currentBroadcast = broadcast;\n\n try {\n await broadcast.done;\n const attributes = {'release.mode': broadcast.releaseMode};\n this.#flowControlWaits.add(1, attributes);\n this.#flowControlWaitTime.recordMs(performance.now() - start, attributes);\n } finally {\n // Technically #currentBroadcast may have changed, so only\n // unset if it if is still the same.\n if (this.#currentBroadcast === broadcast) {\n this.#currentBroadcast = undefined;\n }\n }\n }\n\n #drainPending(): WatermarkedChange[] {\n if (this.#flushImmediateId !== undefined) {\n clearImmediate(this.#flushImmediateId);\n this.#flushImmediateId = undefined;\n }\n this.#flushScheduled = false;\n if (this.#pending.length === 0) {\n return [];\n }\n const batch = this.#pending;\n this.#pending = [];\n return batch;\n }\n\n #flushPendingWithoutTracking() {\n const batch = this.#drainPending();\n if (batch.length > 0) {\n Broadcast.withoutTracking(this.#active.values(), batch);\n }\n }\n\n #subscriberStats() {\n let pendingMessages = 0;\n let backlogMessages = 0;\n let backlogBytes = 0;\n let maxBacklogBytes = 0;\n\n for (const sub of joinIterables(this.#active, this.#queued)) {\n const stats = sub.getStats();\n pendingMessages += stats.pending;\n backlogMessages += stats.backlog;\n backlogBytes += stats.backlogBytes;\n maxBacklogBytes = Math.max(maxBacklogBytes, stats.backlogBytes);\n }\n\n return {pendingMessages, backlogMessages, backlogBytes, maxBacklogBytes};\n }\n\n #updateActiveSubscribers(tag: ChangeTag) {\n switch (tag) {\n case 'begin':\n // While in a Transaction, all added subscribers are \"queued\" so that no\n // messages are forwarded to them. This state corresponds to being queued\n // for catchup in the Storer, which will retrieve historic changes\n // and call catchup() once the current transaction is committed.\n this.#inTransaction = true;\n break;\n case 'commit':\n case 'rollback':\n // Upon commit or rollback, all queued subscribers are transferred to\n // the active set. This means that they can receive messages starting\n // from the next transaction.\n //\n // Note that if catchup is still in progress (in the Storer), these messages\n // will be buffered in the backlog until catchup completes.\n this.#inTransaction = false;\n for (const sub of this.#queued.values()) {\n this.#active.add(sub);\n }\n this.#queued.clear();\n break;\n }\n }\n\n getAcks(): Set<string> {\n return new Set(\n joinIterables(\n wrapIterable(this.#active).map(s => s.acked),\n wrapIterable(this.#queued).map(s => s.acked),\n ),\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"forwarder.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/forwarder.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {joinIterables, wrapIterable} from '../../../../shared/src/iterables.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {Broadcast, type EarlyReleaseOptions} from './broadcast.ts';\nimport type {ChangeTag, Status, WatermarkedChange} from './change-streamer.ts';\nimport {StreamTooFarBehind} from './error-type-enum.ts';\nimport type {Subscriber, SubscriberStats} from './subscriber.ts';\n\nexport type ProgressMonitorOptions = {\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n // Injectable timers, for deterministic testing.\n setTimeoutFn?: typeof setTimeout | undefined;\n clearTimeoutFn?: typeof clearTimeout | undefined;\n};\n\nconst FORWARD_BATCH_SIZE = 64;\n\nexport class Forwarder {\n readonly #lc: LogContext;\n readonly #setTimeout: typeof setTimeout;\n readonly #clearTimeout: typeof clearTimeout;\n readonly #active = new Set<Subscriber>();\n readonly #queued = new Set<Subscriber>();\n readonly #earlyReleaseOptions: EarlyReleaseOptions | undefined;\n readonly #flowControlSlowSubscriberGracePeriodMs: number | undefined;\n\n readonly #flowControlWaits = getOrCreateCounter(\n 'replication',\n 'flow_control.waits',\n 'Flow-control checkpoints completed, labeled by release mode.',\n );\n readonly #flowControlWaitTime = getOrCreateLatencyHistogram(\n 'replication',\n 'flow_control.wait_duration',\n 'Time replication waits at flow-control checkpoints.',\n );\n #inTransaction = false;\n\n #pending: WatermarkedChange[] = [];\n #flushScheduled = false;\n #flushImmediateId: NodeJS.Immediate | undefined = undefined;\n\n #currentBroadcast: Broadcast | undefined;\n #progressMonitor: NodeJS.Timeout | undefined;\n\n // The backup-replicator is never disconnected as a laggard, so its lag\n // detections are logged (rather than acted upon). Track the running total and\n // throttle the warning to at most once per grace period to avoid spamming the\n // log for the duration of a slow catchup.\n #backupLagEventCount = 0;\n #lastBackupLagWarnMs: number | undefined;\n\n constructor(\n lc: LogContext,\n opts: ProgressMonitorOptions = {\n flowControlConsensusTimeoutProportion: 2.0,\n },\n ) {\n this.#lc = lc.withContext('component', 'subscriber-progress-monitor');\n this.#setTimeout = opts.setTimeoutFn ?? setTimeout;\n this.#clearTimeout = opts.clearTimeoutFn ?? clearTimeout;\n\n this.#earlyReleaseOptions =\n opts.flowControlConsensusTimeoutProportion < 0\n ? undefined\n : {\n consensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n setTimeoutFn: this.#setTimeout,\n clearTimeoutFn: this.#clearTimeout,\n };\n this.#flowControlSlowSubscriberGracePeriodMs =\n opts.flowControlSlowSubscriberGracePeriodMs;\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.active_subscribers',\n 'Active change-stream subscribers receiving live changes.',\n ).addCallback(result => result.observe(this.#active.size));\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.queued_subscribers',\n 'Change-stream subscribers waiting for the current transaction to finish before activation.',\n ).addCallback(result => result.observe(this.#queued.size));\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.pending_messages',\n 'Downstream change-stream messages not yet acked by subscribers.',\n ).addCallback(result =>\n result.observe(this.#subscriberStats().pendingMessages),\n );\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.backlog_messages',\n 'Live change-stream messages buffered while subscribers catch up.',\n ).addCallback(result =>\n result.observe(this.#subscriberStats().backlogMessages),\n );\n\n getOrCreateGauge('replication', 'flow_control.backlog_bytes', {\n description:\n 'Live change-stream bytes buffered while subscribers catch up.',\n unit: 'By',\n }).addCallback(result =>\n result.observe(this.#subscriberStats().backlogBytes),\n );\n\n getOrCreateGauge('replication', 'flow_control.max_backlog_bytes', {\n description:\n 'Maximum live change-stream bytes buffered by a single subscriber.',\n unit: 'By',\n }).addCallback(result =>\n result.observe(this.#subscriberStats().maxBacklogBytes),\n );\n }\n\n startProgressMonitor() {\n clearInterval(this.#progressMonitor);\n this.#progressMonitor = setInterval(\n () => this.checkSubscriberProgress(performance.now()),\n 1000,\n );\n }\n\n /**\n * Runs one progress-monitor tick: samples each active subscriber's change\n * rate, logs slow broadcasts, and disconnects subscribers that have been\n * continuously lagging for longer than the configured grace period.\n *\n * Invoked on an interval by {@link startProgressMonitor()}; exposed (rather\n * than private) so tests can drive a single tick with an explicit `now`.\n */\n checkSubscriberProgress(now: number) {\n let timedOut: Map<Subscriber, SubscriberStats> | undefined;\n let slowestOnTime: SubscriberStats | undefined;\n\n for (const sub of this.#active) {\n const stats = sub.sampleProcessRate(now).getStats();\n if (stats.missedLastTimeout) {\n (timedOut ??= new Map()).set(sub, stats);\n } else if (\n stats.processRate <= (slowestOnTime?.processRate ?? Infinity)\n ) {\n slowestOnTime = stats;\n }\n }\n this.#currentBroadcast?.logSlowRequests(now);\n\n if (\n this.#flowControlSlowSubscriberGracePeriodMs &&\n timedOut?.size &&\n slowestOnTime\n ) {\n for (const [sub, stats] of timedOut.entries()) {\n const rate =\n stats.processRate < slowestOnTime.processRate\n ? 'lagging'\n : 'catching-up';\n const laggingDuration = sub.reportChangeRate(now, rate);\n if (laggingDuration >= this.#flowControlSlowSubscriberGracePeriodMs) {\n if (sub.mode === 'backup') {\n // The backup-replicator can only be detected as a laggard during\n // initial catchup; after that it is required for consensus. If it\n // triggers laggard detection in the former case, just log and\n // continue; as disconnecting it would otherwise kill the\n // change-streamer task altogether.\n this.#backupLagEventCount++;\n const grace = this.#flowControlSlowSubscriberGracePeriodMs;\n if (\n this.#lastBackupLagWarnMs === undefined ||\n now - this.#lastBackupLagWarnMs >= grace\n ) {\n this.#lastBackupLagWarnMs = now;\n this.#lc.warn?.(\n `backup subscriber ${sub.id} has lagged for ${laggingDuration}ms ` +\n `(${this.#backupLagEventCount} lag events so far). ` +\n `this is only expected during initial catchup`,\n {stats},\n );\n }\n } else {\n this.#lc.warn?.(\n `serving subscriber ${sub.id} has lagged for ${laggingDuration}ms. disconnecting`,\n {stats},\n );\n sub.close(\n StreamTooFarBehind,\n `lagging for over ${this.#flowControlSlowSubscriberGracePeriodMs}ms (${laggingDuration}ms)`,\n );\n }\n }\n }\n }\n }\n\n stopProgressMonitor() {\n clearInterval(this.#progressMonitor);\n this.#flushPendingWithoutTracking();\n }\n\n /**\n * `add()` is called in lock step with `Storer.catchup()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n */\n add(sub: Subscriber) {\n this.#flushPendingWithoutTracking();\n if (this.#inTransaction) {\n this.#queued.add(sub);\n } else {\n this.#active.add(sub);\n }\n }\n\n remove(sub: Subscriber) {\n this.#flushPendingWithoutTracking();\n this.#active.delete(sub);\n this.#queued.delete(sub);\n sub.close();\n }\n\n /**\n * `forward()` is called in lockstep with `Storer.store()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n *\n * This version of forward buffers changes across the current event loop turn\n * and dispatches them in batches. The change-streamer should call and await\n * {@link forwardWithFlowControl()} occasionally to avoid memory blowup.\n */\n forward(entry: WatermarkedChange) {\n this.#pending.push(entry);\n\n if (\n this.#queued.size > 0 &&\n (entry[1] === 'commit' || entry[1] === 'rollback')\n ) {\n this.#flushPendingWithoutTracking();\n }\n this.#updateActiveSubscribers(entry[1]);\n\n if (this.#pending.length >= FORWARD_BATCH_SIZE) {\n this.#flushPendingWithoutTracking();\n } else if (!this.#flushScheduled && this.#pending.length > 0) {\n this.#flushScheduled = true;\n this.#flushImmediateId = setImmediate(() =>\n this.#flushPendingWithoutTracking(),\n );\n }\n }\n\n sendStatus(status: Status) {\n this.#flushPendingWithoutTracking();\n for (const sub of this.#active.values()) {\n sub.sendStatus(status);\n }\n }\n\n /**\n * The flow-control-aware equivalent of {@link forward()}, returning a\n * Promise that resolves when replication should continue.\n */\n async forwardWithFlowControl(entry: WatermarkedChange) {\n this.#pending.push(entry);\n\n const batch = this.#drainPending();\n const start = performance.now();\n const broadcast = new Broadcast(\n this.#lc,\n this.#active.values(),\n batch,\n this.#earlyReleaseOptions,\n );\n this.#updateActiveSubscribers(entry[1]);\n\n // set for progress tracking\n this.#currentBroadcast = broadcast;\n\n try {\n await broadcast.done;\n const attributes = {'release.mode': broadcast.releaseMode};\n this.#flowControlWaits.add(1, attributes);\n this.#flowControlWaitTime.recordMs(performance.now() - start, attributes);\n } finally {\n // Technically #currentBroadcast may have changed, so only\n // unset if it if is still the same.\n if (this.#currentBroadcast === broadcast) {\n this.#currentBroadcast = undefined;\n }\n }\n }\n\n #drainPending(): WatermarkedChange[] {\n if (this.#flushImmediateId !== undefined) {\n clearImmediate(this.#flushImmediateId);\n this.#flushImmediateId = undefined;\n }\n this.#flushScheduled = false;\n if (this.#pending.length === 0) {\n return [];\n }\n const batch = this.#pending;\n this.#pending = [];\n return batch;\n }\n\n #flushPendingWithoutTracking() {\n const batch = this.#drainPending();\n if (batch.length > 0) {\n Broadcast.withoutTracking(this.#active.values(), batch);\n }\n }\n\n #subscriberStats() {\n let pendingMessages = 0;\n let backlogMessages = 0;\n let backlogBytes = 0;\n let maxBacklogBytes = 0;\n\n for (const sub of joinIterables(this.#active, this.#queued)) {\n const stats = sub.getStats();\n pendingMessages += stats.pending;\n backlogMessages += stats.backlog;\n backlogBytes += stats.backlogBytes;\n maxBacklogBytes = Math.max(maxBacklogBytes, stats.backlogBytes);\n }\n\n return {pendingMessages, backlogMessages, backlogBytes, maxBacklogBytes};\n }\n\n #updateActiveSubscribers(tag: ChangeTag) {\n switch (tag) {\n case 'begin':\n // While in a Transaction, all added subscribers are \"queued\" so that no\n // messages are forwarded to them. This state corresponds to being queued\n // for catchup in the Storer, which will retrieve historic changes\n // and call catchup() once the current transaction is committed.\n this.#inTransaction = true;\n break;\n case 'commit':\n case 'rollback':\n // Upon commit or rollback, all queued subscribers are transferred to\n // the active set. This means that they can receive messages starting\n // from the next transaction.\n //\n // Note that if catchup is still in progress (in the Storer), these messages\n // will be buffered in the backlog until catchup completes.\n this.#inTransaction = false;\n for (const sub of this.#queued.values()) {\n this.#active.add(sub);\n }\n this.#queued.clear();\n break;\n }\n }\n\n getAcks(): Set<string> {\n return new Set(\n joinIterables(\n wrapIterable(this.#active).map(s => s.acked),\n wrapIterable(this.#queued).map(s => s.acked),\n ),\n );\n }\n}\n"],"mappings":"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"}
@@ -16,7 +16,7 @@ var SERVER_TIMEOUT_SECONDS = 60;
16
16
  * Client-side timeout, slightly above the server timeout so that litestream,
17
17
  * rather than the client, ends a slow request.
18
18
  */
19
- var CLIENT_TIMEOUT_MS = (SERVER_TIMEOUT_SECONDS + 5) * 1e3;
19
+ var CLIENT_TIMEOUT_MS = 65e3;
20
20
  var ERROR_LOG_INTERVAL_MS = 6e4;
21
21
  /**
22
22
  * Initial delay before retrying a failed request, doubled on each consecutive
@@ -1 +1 @@
1
- {"version":3,"file":"litestream-sync-requester.js","names":["#lc","#state","#config","#litestream","#readLocalWatermark","#stream","#timer","#retryTimer","#inFlight","#requestSync","#retryDelayMs","#publish","#scheduleRetry","#lastErrorLogMs","#backedUp"],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream-sync-requester.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {Database, type Statement} from '../../../../zqlite/src/db.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {LitestreamController} from '../litestream/litestream-controller.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\n/**\n * How long litestream waits for a requested sync (and its upload) before\n * ending the request. A request that times out is simply retried on a later\n * tick: litestream only abandons the *wait*, never the sync or upload itself,\n * so this bounds request lifetime without affecting backup progress.\n */\nconst SERVER_TIMEOUT_SECONDS = 60;\n\n/**\n * Client-side timeout, slightly above the server timeout so that litestream,\n * rather than the client, ends a slow request.\n */\nconst CLIENT_TIMEOUT_MS = (SERVER_TIMEOUT_SECONDS + 5) * 1000;\n\nconst ERROR_LOG_INTERVAL_MS = 60_000;\n\n/**\n * Initial delay before retrying a failed request, doubled on each consecutive\n * failure until it reaches the configured interval, after which the regular\n * ticks alone retry, so that a busy litestream sees no more requests than the\n * interval allows. The first request is typically issued before litestream's\n * control socket is listening; retrying quickly (rather than after a full\n * interval) keeps the initial backup confirmation, which gates readiness, from\n * waiting out an interval.\n */\nconst MIN_RETRY_DELAY_MS = 250;\n\nconst localWatermarkSchema = v.object({\n stateVersion: v.string(),\n writeTimeMs: v.number(),\n});\n\ntype LocalWatermark = v.Infer<typeof localWatermarkSchema>;\n\n/** The subset of {@link LitestreamController} used by the requester. */\nexport type SyncController = Pick<LitestreamController, 'sync' | 'close'>;\n\nexport type SyncRequesterConfig = {\n /**\n * Interval between sync requests. This paces how often litestream seals\n * (and uploads) a new LTX file.\n */\n intervalMs: number;\n};\n\n/**\n * The LitestreamSyncRequester drives litestream v5 backups and tracks which\n * replica watermark is durably backed up, emitting it as a stream of\n * {@link BackedUpWatermark}s.\n *\n * It periodically reads the replica's current watermark `W` and then asks\n * litestream to sync and wait for the upload (`POST /sync {wait: true}`).\n * litestream satisfies the request with a sync pass that *starts after* the\n * request, and a pass syncs up to the end of the WAL, so a successful response\n * means that every transaction committed before the request, including the\n * one that wrote `W`, has been uploaded to the backup. No part of the backup\n * needs to be read to determine this.\n *\n * Requests continue even when `W` is already backed up, so that changes that\n * don't advance the watermark (e.g. change-log cleanup or `PRAGMA optimize`)\n * are also backed up at this cadence rather than only at litestream's slower\n * backstop interval. A request on an idle replica is a local no-op for\n * litestream: there is nothing to seal or upload, and no backup reads.\n *\n * At most one request is outstanding at a time. Failed or timed-out requests\n * are retried with a backoff that starts short and, once it grows to the\n * interval, defers to the regular ticks. Nothing is published until a request\n * succeeds.\n */\nexport class LitestreamSyncRequester {\n readonly #lc: LogContext;\n readonly #state = new RunningState('litestream-sync-requester');\n readonly #config: SyncRequesterConfig;\n readonly #litestream: SyncController;\n readonly #readLocalWatermark: Statement;\n readonly #stream: Subscription<BackedUpWatermark>;\n\n #timer: NodeJS.Timeout | undefined;\n #retryTimer: NodeJS.Timeout | undefined;\n #retryDelayMs = MIN_RETRY_DELAY_MS;\n #inFlight: Promise<void> | undefined;\n #backedUp: string | undefined;\n #lastErrorLogMs = 0;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n config: SyncRequesterConfig,\n litestream: SyncController = new LitestreamController(lc, replicaFile),\n ) {\n this.#lc = lc.withContext('component', 'litestream-sync-requester');\n this.#config = config;\n this.#litestream = litestream;\n\n const db = new Database(this.#lc, replicaFile, {readonly: true});\n this.#readLocalWatermark = db.prepare(\n `SELECT stateVersion, writeTimeMs FROM \"_zero.replicationState\"`,\n );\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n clearInterval(this.#timer);\n clearTimeout(this.#retryTimer);\n this.#state.stop(this.#lc);\n this.#litestream.close();\n db.close();\n },\n });\n }\n\n /**\n * Starts requesting syncs and pushes backed-up watermarks to the returned\n * stream. Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#timer === undefined, `Already called start()`);\n this.#lc.info?.(\n `requesting litestream syncs every ${this.#config.intervalMs} ms`,\n );\n this.#timer = setInterval(this.tick, this.#config.intervalMs);\n void this.tick();\n return this.#stream;\n }\n\n /**\n * Requests a sync unless a request is already outstanding. Resolves when\n * this tick's request (or the outstanding one) completes.\n * Exported for testing.\n */\n readonly tick = (): Promise<void> => {\n if (!this.#state.shouldRun() || this.#inFlight) {\n return this.#inFlight ?? Promise.resolve();\n }\n let local: LocalWatermark;\n try {\n local = v.parse(this.#readLocalWatermark.get(), localWatermarkSchema);\n } catch (e) {\n this.#lc.warn?.(`unable to read local watermark`, e);\n return Promise.resolve();\n }\n this.#inFlight = this.#requestSync(local).finally(() => {\n this.#inFlight = undefined;\n });\n return this.#inFlight;\n };\n\n async #requestSync(local: LocalWatermark) {\n // `local` must be read before the request is issued: litestream only\n // guarantees that transactions committed before the request are synced.\n try {\n const result = await this.#litestream.sync(\n {\n wait: true,\n timeoutMs: CLIENT_TIMEOUT_MS,\n serverTimeoutSeconds: SERVER_TIMEOUT_SECONDS,\n },\n this.#state.signal,\n );\n this.#retryDelayMs = MIN_RETRY_DELAY_MS;\n this.#publish(local, Date.now());\n this.#lc.debug?.(`backed up watermark ${local.stateVersion}`, result);\n } catch (e) {\n if (!this.#state.shouldRun()) {\n return; // shutting down\n }\n // Expected while litestream is starting up or busy with a long sync or\n // upload (e.g. the initial snapshot of a large replica); the request is\n // retried after a backoff, or on the next tick if that comes first.\n this.#scheduleRetry();\n const now = Date.now();\n if (now - this.#lastErrorLogMs >= ERROR_LOG_INTERVAL_MS) {\n this.#lastErrorLogMs = now;\n this.#lc.info?.(\n `backup of watermark ${local.stateVersion} not yet confirmed. retrying`,\n e,\n );\n }\n }\n }\n\n #scheduleRetry() {\n clearTimeout(this.#retryTimer);\n const delayMs = this.#retryDelayMs;\n if (delayMs >= this.#config.intervalMs) {\n return; // backed off to the interval; the regular ticks retry\n }\n this.#retryDelayMs = delayMs * 2;\n this.#retryTimer = setTimeout(this.tick, delayMs);\n }\n\n #publish({stateVersion, writeTimeMs}: LocalWatermark, backupTimeMs: number) {\n if (this.#backedUp !== undefined && stateVersion <= this.#backedUp) {\n return;\n }\n this.#backedUp = stateVersion;\n this.#stream.push({watermark: stateVersion, writeTimeMs, backupTimeMs});\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"litestream-sync-requester.js","names":["v","v.parse"],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream-sync-requester.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {Database, type Statement} from '../../../../zqlite/src/db.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {LitestreamController} from '../litestream/litestream-controller.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\n/**\n * How long litestream waits for a requested sync (and its upload) before\n * ending the request. A request that times out is simply retried on a later\n * tick: litestream only abandons the *wait*, never the sync or upload itself,\n * so this bounds request lifetime without affecting backup progress.\n */\nconst SERVER_TIMEOUT_SECONDS = 60;\n\n/**\n * Client-side timeout, slightly above the server timeout so that litestream,\n * rather than the client, ends a slow request.\n */\nconst CLIENT_TIMEOUT_MS = (SERVER_TIMEOUT_SECONDS + 5) * 1000;\n\nconst ERROR_LOG_INTERVAL_MS = 60_000;\n\n/**\n * Initial delay before retrying a failed request, doubled on each consecutive\n * failure until it reaches the configured interval, after which the regular\n * ticks alone retry, so that a busy litestream sees no more requests than the\n * interval allows. The first request is typically issued before litestream's\n * control socket is listening; retrying quickly (rather than after a full\n * interval) keeps the initial backup confirmation, which gates readiness, from\n * waiting out an interval.\n */\nconst MIN_RETRY_DELAY_MS = 250;\n\nconst localWatermarkSchema = v.object({\n stateVersion: v.string(),\n writeTimeMs: v.number(),\n});\n\ntype LocalWatermark = v.Infer<typeof localWatermarkSchema>;\n\n/** The subset of {@link LitestreamController} used by the requester. */\nexport type SyncController = Pick<LitestreamController, 'sync' | 'close'>;\n\nexport type SyncRequesterConfig = {\n /**\n * Interval between sync requests. This paces how often litestream seals\n * (and uploads) a new LTX file.\n */\n intervalMs: number;\n};\n\n/**\n * The LitestreamSyncRequester drives litestream v5 backups and tracks which\n * replica watermark is durably backed up, emitting it as a stream of\n * {@link BackedUpWatermark}s.\n *\n * It periodically reads the replica's current watermark `W` and then asks\n * litestream to sync and wait for the upload (`POST /sync {wait: true}`).\n * litestream satisfies the request with a sync pass that *starts after* the\n * request, and a pass syncs up to the end of the WAL, so a successful response\n * means that every transaction committed before the request, including the\n * one that wrote `W`, has been uploaded to the backup. No part of the backup\n * needs to be read to determine this.\n *\n * Requests continue even when `W` is already backed up, so that changes that\n * don't advance the watermark (e.g. change-log cleanup or `PRAGMA optimize`)\n * are also backed up at this cadence rather than only at litestream's slower\n * backstop interval. A request on an idle replica is a local no-op for\n * litestream: there is nothing to seal or upload, and no backup reads.\n *\n * At most one request is outstanding at a time. Failed or timed-out requests\n * are retried with a backoff that starts short and, once it grows to the\n * interval, defers to the regular ticks. Nothing is published until a request\n * succeeds.\n */\nexport class LitestreamSyncRequester {\n readonly #lc: LogContext;\n readonly #state = new RunningState('litestream-sync-requester');\n readonly #config: SyncRequesterConfig;\n readonly #litestream: SyncController;\n readonly #readLocalWatermark: Statement;\n readonly #stream: Subscription<BackedUpWatermark>;\n\n #timer: NodeJS.Timeout | undefined;\n #retryTimer: NodeJS.Timeout | undefined;\n #retryDelayMs = MIN_RETRY_DELAY_MS;\n #inFlight: Promise<void> | undefined;\n #backedUp: string | undefined;\n #lastErrorLogMs = 0;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n config: SyncRequesterConfig,\n litestream: SyncController = new LitestreamController(lc, replicaFile),\n ) {\n this.#lc = lc.withContext('component', 'litestream-sync-requester');\n this.#config = config;\n this.#litestream = litestream;\n\n const db = new Database(this.#lc, replicaFile, {readonly: true});\n this.#readLocalWatermark = db.prepare(\n `SELECT stateVersion, writeTimeMs FROM \"_zero.replicationState\"`,\n );\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n clearInterval(this.#timer);\n clearTimeout(this.#retryTimer);\n this.#state.stop(this.#lc);\n this.#litestream.close();\n db.close();\n },\n });\n }\n\n /**\n * Starts requesting syncs and pushes backed-up watermarks to the returned\n * stream. Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#timer === undefined, `Already called start()`);\n this.#lc.info?.(\n `requesting litestream syncs every ${this.#config.intervalMs} ms`,\n );\n this.#timer = setInterval(this.tick, this.#config.intervalMs);\n void this.tick();\n return this.#stream;\n }\n\n /**\n * Requests a sync unless a request is already outstanding. Resolves when\n * this tick's request (or the outstanding one) completes.\n * Exported for testing.\n */\n readonly tick = (): Promise<void> => {\n if (!this.#state.shouldRun() || this.#inFlight) {\n return this.#inFlight ?? Promise.resolve();\n }\n let local: LocalWatermark;\n try {\n local = v.parse(this.#readLocalWatermark.get(), localWatermarkSchema);\n } catch (e) {\n this.#lc.warn?.(`unable to read local watermark`, e);\n return Promise.resolve();\n }\n this.#inFlight = this.#requestSync(local).finally(() => {\n this.#inFlight = undefined;\n });\n return this.#inFlight;\n };\n\n async #requestSync(local: LocalWatermark) {\n // `local` must be read before the request is issued: litestream only\n // guarantees that transactions committed before the request are synced.\n try {\n const result = await this.#litestream.sync(\n {\n wait: true,\n timeoutMs: CLIENT_TIMEOUT_MS,\n serverTimeoutSeconds: SERVER_TIMEOUT_SECONDS,\n },\n this.#state.signal,\n );\n this.#retryDelayMs = MIN_RETRY_DELAY_MS;\n this.#publish(local, Date.now());\n this.#lc.debug?.(`backed up watermark ${local.stateVersion}`, result);\n } catch (e) {\n if (!this.#state.shouldRun()) {\n return; // shutting down\n }\n // Expected while litestream is starting up or busy with a long sync or\n // upload (e.g. the initial snapshot of a large replica); the request is\n // retried after a backoff, or on the next tick if that comes first.\n this.#scheduleRetry();\n const now = Date.now();\n if (now - this.#lastErrorLogMs >= ERROR_LOG_INTERVAL_MS) {\n this.#lastErrorLogMs = now;\n this.#lc.info?.(\n `backup of watermark ${local.stateVersion} not yet confirmed. retrying`,\n e,\n );\n }\n }\n }\n\n #scheduleRetry() {\n clearTimeout(this.#retryTimer);\n const delayMs = this.#retryDelayMs;\n if (delayMs >= this.#config.intervalMs) {\n return; // backed off to the interval; the regular ticks retry\n }\n this.#retryDelayMs = delayMs * 2;\n this.#retryTimer = setTimeout(this.tick, delayMs);\n }\n\n #publish({stateVersion, writeTimeMs}: LocalWatermark, backupTimeMs: number) {\n if (this.#backedUp !== undefined && stateVersion <= this.#backedUp) {\n return;\n }\n this.#backedUp = stateVersion;\n this.#stream.push({watermark: stateVersion, writeTimeMs, backupTimeMs});\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAM,yBAAyB;;;;;AAM/B,IAAM,oBAAA;AAEN,IAAM,wBAAwB;;;;;;;;;;AAW9B,IAAM,qBAAqB;AAE3B,IAAM,uBAAuBA,eAAE,OAAO;CACpC,cAAcA,eAAE,OAAO;CACvB,aAAaA,eAAE,OAAO;AACxB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AAuCD,IAAa,0BAAb,MAAqC;CACnC;CACA,SAAkB,IAAI,aAAa,2BAA2B;CAC9D;CACA;CACA;CACA;CAEA;CACA;CACA,gBAAgB;CAChB;CACA;CACA,kBAAkB;CAElB,YACE,IACA,aACA,QACA,aAA6B,IAAI,qBAAqB,IAAI,WAAW,GACrE;EACA,KAAK,MAAM,GAAG,YAAY,aAAa,2BAA2B;EAClE,KAAK,UAAU;EACf,KAAK,cAAc;EAEnB,MAAM,KAAK,IAAI,SAAS,KAAK,KAAK,aAAa,EAAC,UAAU,KAAI,CAAC;EAC/D,KAAK,sBAAsB,GAAG,QAC5B,gEACF;EACA,KAAK,UAAU,aAAa,OAA0B,EACpD,eAAe;GACb,cAAc,KAAK,MAAM;GACzB,aAAa,KAAK,WAAW;GAC7B,KAAK,OAAO,KAAK,KAAK,GAAG;GACzB,KAAK,YAAY,MAAM;GACvB,GAAG,MAAM;EACX,EACF,CAAC;CACH;;;;;CAMA,QAAmC;EACjC,OAAO,KAAK,WAAW,KAAA,GAAW,wBAAwB;EAC1D,KAAK,IAAI,OACP,qCAAqC,KAAK,QAAQ,WAAW,IAC/D;EACA,KAAK,SAAS,YAAY,KAAK,MAAM,KAAK,QAAQ,UAAU;EAC5D,KAAU,KAAK;EACf,OAAO,KAAK;CACd;;;;;;CAOA,aAAqC;EACnC,IAAI,CAAC,KAAK,OAAO,UAAU,KAAK,KAAK,WACnC,OAAO,KAAK,aAAa,QAAQ,QAAQ;EAE3C,IAAI;EACJ,IAAI;GACF,QAAQC,MAAQ,KAAK,oBAAoB,IAAI,GAAG,oBAAoB;EACtE,SAAS,GAAG;GACV,KAAK,IAAI,OAAO,kCAAkC,CAAC;GACnD,OAAO,QAAQ,QAAQ;EACzB;EACA,KAAK,YAAY,KAAK,aAAa,KAAK,CAAC,CAAC,cAAc;GACtD,KAAK,YAAY,KAAA;EACnB,CAAC;EACD,OAAO,KAAK;CACd;CAEA,MAAM,aAAa,OAAuB;EAGxC,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,YAAY,KACpC;IACE,MAAM;IACN,WAAW;IACX,sBAAsB;GACxB,GACA,KAAK,OAAO,MACd;GACA,KAAK,gBAAgB;GACrB,KAAK,SAAS,OAAO,KAAK,IAAI,CAAC;GAC/B,KAAK,IAAI,QAAQ,uBAAuB,MAAM,gBAAgB,MAAM;EACtE,SAAS,GAAG;GACV,IAAI,CAAC,KAAK,OAAO,UAAU,GACzB;GAKF,KAAK,eAAe;GACpB,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,MAAM,KAAK,mBAAmB,uBAAuB;IACvD,KAAK,kBAAkB;IACvB,KAAK,IAAI,OACP,uBAAuB,MAAM,aAAa,+BAC1C,CACF;GACF;EACF;CACF;CAEA,iBAAiB;EACf,aAAa,KAAK,WAAW;EAC7B,MAAM,UAAU,KAAK;EACrB,IAAI,WAAW,KAAK,QAAQ,YAC1B;EAEF,KAAK,gBAAgB,UAAU;EAC/B,KAAK,cAAc,WAAW,KAAK,MAAM,OAAO;CAClD;CAEA,SAAS,EAAC,cAAc,eAA8B,cAAsB;EAC1E,IAAI,KAAK,cAAc,KAAA,KAAa,gBAAgB,KAAK,WACvD;EAEF,KAAK,YAAY;EACjB,KAAK,QAAQ,KAAK;GAAC,WAAW;GAAc;GAAa;EAAY,CAAC;CACxE;AACF"}
@@ -35,7 +35,7 @@ var BACKUP_VERIFICATION_SLACK_MS = 6e4;
35
35
  * staleness clock is reset the moment a purge is confirmed against a real
36
36
  * upload.
37
37
  */
38
- var WEDGED_SHUTDOWN_GRACE_MS = 15 * 6e4;
38
+ var WEDGED_SHUTDOWN_GRACE_MS = 9e5;
39
39
  var GiB = 1024 ** 3;
40
40
  /**
41
41
  * How long the BackupMonitor waits for the *first* restorable backup to appear
@@ -51,7 +51,7 @@ var GiB = 1024 ** 3;
51
51
  * 50GB) so that it never fires before the platform would restart a view-syncer
52
52
  * that is waiting on the backup.
53
53
  */
54
- var INITIAL_BACKUP_GRACE_MS_PER_UNIT = 60 * 6e4;
54
+ var INITIAL_BACKUP_GRACE_MS_PER_UNIT = 36e5;
55
55
  var INITIAL_BACKUP_GRACE_UNIT_BYTES = 50 * GiB;
56
56
  /**
57
57
  * The Litestream3PrometheusPoller polls the litestream "/metrics" endpoint to
@@ -310,6 +310,6 @@ var Litestream3PrometheusPoller = class {
310
310
  }
311
311
  };
312
312
  //#endregion
313
- export { Litestream3PrometheusPoller };
313
+ export { BACKUP_VERIFICATION_SLACK_MS, CHECK_INTERVAL_MS, INITIAL_BACKUP_GRACE_MS_PER_UNIT, Litestream3PrometheusPoller, WEDGED_SHUTDOWN_GRACE_MS };
314
314
 
315
315
  //# sourceMappingURL=litestream3-prometheus-poller.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"litestream3-prometheus-poller.js","names":["#lc","#replicaFile","#backupURL","#metricsEndpoint","#state","#stream","#watermarks","#verifyBackupState","#purgesBlocked","#checkMetricsTimer","#runStartTime","#confirmRestorableBackup","#lastVerifiedUploadTime","#verifyBackupStateTimed","#checkWatermarks","#publishVerifiedWatermarks","#checkInitialBackupDeadline","#fetchWatermarks","#lastWatermark","#latestBackupTime","#maxBackupBefore","#confirmedDurable","#backupStaleSince","#shutDownOnWedgedBackup","#initialBackupGraceMs","#shutDownOnMissingInitialBackup"],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream3-prometheus-poller.ts"],"sourcesContent":["import {statSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport parsePrometheusTextFormat from 'parse-prometheus-text-format';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {versionFromLexi} from '../../types/lexi-version.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {\n litestreamBackupVerificationDuration,\n litestreamMonitorMetricAttrs,\n} from '../litestream/metrics.ts';\nimport {RunningState, UnrecoverableError} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nexport const CHECK_INTERVAL_MS = 10_000;\n\n/**\n * Allowance for clock skew between the machine reporting litestream metrics\n * and the timestamps reported by the backup destination (e.g. S3).\n */\nexport const BACKUP_VERIFICATION_SLACK_MS = 60_000;\n\n/**\n * How long the actual backup state may remain *continuously* behind the\n * backup progress claimed by litestream metrics before the backup is\n * considered genuinely wedged and the process is shut down.\n *\n * A wedged backup is dangerous: litestream believes it is making durable\n * progress when it is not, which can silently corrupt the backup (the WAL\n * format only makes *some* such gaps detectable). Once that is confirmed to\n * be the persistent state, continuing to run is worse than crashing, so the\n * process exits loudly (non-zero, with a logged error) and the wedged backup\n * destination becomes the priority to investigate.\n *\n * The change-log is conservatively *not* purged for the entire time the\n * backup is stale, so the only cost of a generous grace period is unbounded\n * change-log growth. We therefore err well on the side of slack: the backup\n * must stay stuck across many {@link CHECK_INTERVAL_MS} checks before we tear\n * the server down, so that a transient hiccup (a slow or briefly unreachable\n * destination, or litestream restarting) does not trigger a shutdown. The\n * staleness clock is reset the moment a purge is confirmed against a real\n * upload.\n */\nexport const WEDGED_SHUTDOWN_GRACE_MS = 15 * 60_000; // 15 minutes\n\nconst GiB = 1024 ** 3;\n\n/**\n * How long the BackupMonitor waits for the *first* restorable backup to appear\n * before concluding that the backup pipeline is broken and shutting the process\n * down (see {@link BackupMonitor.#shutDownOnMissingInitialBackup}).\n *\n * On a fresh stack the first backup is not restorable until the initial\n * Postgres->replica sync completes and litestream finishes uploading the\n * initial snapshot — a multipart upload that is not listable in the destination\n * until it is committed. The producer cannot observe that sub-snapshot progress,\n * so this is necessarily a generous timeout. It is scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance (~1 hour per\n * 50GB) so that it never fires before the platform would restart a view-syncer\n * that is waiting on the backup.\n */\nexport const INITIAL_BACKUP_GRACE_MS_PER_UNIT = 60 * 60_000; // 1 hour ...\nconst INITIAL_BACKUP_GRACE_UNIT_BYTES = 50 * GiB; // ... per 50 GiB\n\n/**\n * How often the BackupMonitor re-checks whether the first restorable backup has\n * appeared while a view-syncer holds a snapshot reservation open during a cold\n * start.\n */\nexport const RESTORABLE_BACKUP_POLL_INTERVAL_MS = 10_000;\n\n/**\n * Returns the time of the most recent object actually uploaded to the\n * backup replica destination (e.g. as determined by listing the snapshots\n * and WAL segments in S3). Rejects if the backup state cannot be determined.\n *\n * See `getLastBackupTime()` in `../litestream/commands.ts` for the\n * production implementation.\n */\nexport type BackupStateVerifier = () => Promise<Date>;\n\n/**\n * The Litestream3PrometheusPoller polls the litestream \"/metrics\" endpoint to\n * track the watermark (label) value of the `litestream_replica_progress` gauge\n * and emits verified watermarks to a stream of {@link BackedUpWatermark}s.\n *\n * See: https://github.com/rocicorp/litestream/pull/3\n *\n * Because the watermarks reported by litestream metrics reflect what\n * litestream *believes* has been backed up (which has been observed to\n * diverge from reality when uploads silently fail), the cleanup watermark is\n * only published after verifying it against the actual backup state in the\n * replica destination via a {@link BackupStateVerifier}. If the\n * actual backup state stays behind the claimed progress for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}, the backup is treated as wedged and the\n * process is shut down rather than risk corrupting the backup.\n */\nexport class Litestream3PrometheusPoller {\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #backupURL: string;\n readonly #metricsEndpoint: string;\n readonly #state = new RunningState('litestream3-prometheus-poller');\n\n readonly #stream: Subscription<BackedUpWatermark>;\n readonly #watermarks = new Map<string, Date>();\n\n readonly #verifyBackupState: BackupStateVerifier;\n readonly #purgesBlocked = getOrCreateCounter('replica', 'purge_blocked', {\n description:\n 'Number of change-log purges blocked because the actual backup state ' +\n '(as listed from the replica destination) could not be verified, or ' +\n 'is older than the backup progress claimed by litestream metrics. ' +\n 'A steadily increasing value indicates a wedged or failing backup. ' +\n 'The \"backup-wedged\" reason is emitted once just before the process ' +\n 'shuts itself down due to a persistently stale backup.',\n });\n\n #lastWatermark: string = '';\n #latestBackupTime: Date | null = null;\n // The time of the most recent object actually verified in the backup\n // destination, or `null` if none has ever been verified. Only assigned from a\n // successful verifyBackupState() and never cleared, so a non-null value also\n // means a restorable backup has been confirmed to exist (which is what gates\n // the snapshot `status` signal and satisfies the initial-backup deadline).\n #lastVerifiedUploadTime: Date | null = null;\n // Epoch ms at which the actual backup state was first observed to be\n // continuously behind the watermark litestream claims to have backed up,\n // or `null` while the backup is keeping up. Reset whenever a purge is\n // confirmed against a real upload.\n #backupStaleSince: number | null = null;\n // Epoch ms at which run() started, used to bound how long to wait for the\n // first restorable backup to appear. `null` until run() is called.\n #runStartTime: number | null = null;\n #checkMetricsTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n backupURL: string,\n metricsEndpoint: string,\n verifyBackupState: BackupStateVerifier,\n ) {\n this.#lc = lc.withContext('component', 'litestream3-prometheus-poller');\n this.#replicaFile = replicaFile;\n this.#backupURL = backupURL;\n this.#metricsEndpoint = metricsEndpoint;\n this.#verifyBackupState = verifyBackupState;\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n clearInterval(this.#checkMetricsTimer);\n this.#state.stop(this.#lc);\n },\n });\n }\n\n /**\n * Starts polling for watermarks and pushes new ones to returned stream.\n * Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#runStartTime === null, `Already called start()`);\n this.#runStartTime = Date.now();\n this.#lc.info?.(`monitoring backups at ${this.#metricsEndpoint}`);\n this.#checkMetricsTimer = setInterval(\n this.checkVerifyAndPublishWatermarks,\n CHECK_INTERVAL_MS,\n );\n return this.#stream;\n }\n\n /**\n * Verifies whether a restorable backup actually exists in the destination\n * (as opposed to what litestream metrics merely claim). On success the\n * verified upload time is cached — seeding the cleanup-verification fast path\n * ({@link #confirmedDurable}) — and the initial-backup deadline is satisfied.\n * Returns false if no backup is listable yet (e.g. the initial snapshot is\n * still uploading).\n */\n async #confirmRestorableBackup(): Promise<boolean> {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n return true;\n } catch (e) {\n this.#lc.info?.(`backup not yet restorable at ${this.#backupURL}`, e);\n return false;\n }\n }\n\n // Exported for testing\n readonly checkVerifyAndPublishWatermarks = async () => {\n try {\n await this.#checkWatermarks();\n } catch (e) {\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n }\n try {\n await this.#publishVerifiedWatermarks();\n } catch (e) {\n this.#lc.warn?.(`error scheduling cleanup`, e);\n }\n try {\n await this.#checkInitialBackupDeadline();\n } catch (e) {\n this.#lc.warn?.(`error checking initial backup deadline`, e);\n }\n };\n\n async *#fetchWatermarks(): AsyncGenerator<{\n watermark: string;\n time: Date;\n name?: string | undefined;\n }> {\n const metricsEndpoint = this.#metricsEndpoint;\n const signal = this.#state.signal;\n let resp;\n try {\n resp = await fetch(metricsEndpoint, {signal});\n } catch (e) {\n if (signal.aborted) {\n // not an error.\n return;\n }\n // Treat exceptions from fetch (e.g. network errors) as non-fatal, and simply\n // log them and skip the watermark check until the next interval.\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n return;\n }\n if (!resp.ok) {\n this.#lc.warn?.(\n `unable to fetch metrics at ${this.#metricsEndpoint}: ${await resp.text()}`,\n );\n return;\n }\n\n const families = parsePrometheusTextFormat(await resp.text());\n for (const family of families) {\n if (\n family.type === 'GAUGE' &&\n family.name === 'litestream_replica_progress'\n ) {\n for (const metric of family.metrics) {\n const watermark = metric.labels?.watermark;\n const name = metric.labels?.name;\n const time = new Date(parseFloat(metric.value) * 1000);\n if (watermark) {\n try {\n versionFromLexi(watermark); // Sanity check: Ensure that it is valid.\n yield {watermark, time, name};\n } catch (e) {\n this.#lc.warn?.(\n `ignoring invalid watermark ${watermark}`,\n {metric},\n e,\n );\n }\n }\n }\n }\n }\n }\n\n async #checkWatermarks() {\n for await (const {watermark, name, time} of this.#fetchWatermarks()) {\n if (watermark > this.#lastWatermark && !this.#watermarks.has(watermark)) {\n this.#lc.info?.(\n `replicated watermark=${watermark} to ${name}` +\n ` at ${time.toISOString()}.`,\n );\n this.#watermarks.set(watermark, time);\n this.#latestBackupTime = time;\n }\n }\n return this.#latestBackupTime;\n }\n\n async #publishVerifiedWatermarks() {\n const maxBackup = this.#maxBackupBefore(Date.now());\n if (!maxBackup) {\n return;\n }\n const {watermark: maxWatermark, backupTime: claimedTime} = maxBackup;\n // Purge guard: the watermarks (and their backup times) come from\n // litestream metrics, which are exported when litestream *believes*\n // an upload succeeded, and have been observed to advance even when\n // nothing was actually written to the backup destination. Purging the\n // change-log based on a falsely advancing watermark permanently breaks\n // the ability to restore + catch up. Before advancing the cleanup\n // watermark, verify it against the actual backup state: a claimed\n // backup time is only trusted if an object was actually uploaded to\n // the replica destination at (or after) that time, modulo clock skew.\n if (!this.#confirmedDurable(claimedTime)) {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n } catch (e) {\n this.#purgesBlocked.add(1, {reason: 'verification-failed'});\n // Skipping the purge is safe: the change-log just grows.\n this.#lc.warn?.(\n `unable to verify backup state. skipping change-log cleanup ` +\n `up to watermark ${maxWatermark} ` +\n `(claimed backup time ${claimedTime.toISOString()})`,\n e,\n );\n return;\n }\n }\n // Watermarks whose backup time isn't yet confirmed durable remain in the\n // map and are re-evaluated at the next check.\n const lastUpload = must(this.#lastVerifiedUploadTime);\n const verifiedBackup = this.#maxBackupBefore(\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS,\n );\n if (!verifiedBackup) {\n this.#purgesBlocked.add(1, {reason: 'backup-stale'});\n const now = Date.now();\n if (this.#backupStaleSince === null) {\n this.#backupStaleSince = now;\n }\n const staleForMs = now - this.#backupStaleSince;\n this.#lc.warn?.(\n `blocked change-log cleanup up to watermark ${maxWatermark}: ` +\n `litestream claims it was backed up at ` +\n `${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded to ${this.#backupURL} was at ` +\n `${lastUpload.toISOString()}. ` +\n `The backup has been stale for ${staleForMs} ms.`,\n );\n if (staleForMs >= WEDGED_SHUTDOWN_GRACE_MS) {\n this.#shutDownOnWedgedBackup(staleForMs, claimedTime, lastUpload);\n }\n return;\n }\n // The cleanup watermark advanced past a real upload, so the backup is\n // keeping up: clear the staleness clock.\n this.#backupStaleSince = null;\n const {watermark: verifiedWatermark, backupTime} = verifiedBackup;\n for (const watermark of this.#watermarks.keys()) {\n if (watermark <= verifiedWatermark) {\n this.#watermarks.delete(watermark);\n }\n }\n this.#stream.push({\n watermark: verifiedWatermark,\n backupTimeMs: backupTime.getTime(),\n });\n this.#lastWatermark = verifiedWatermark;\n }\n\n async #verifyBackupStateTimed(): Promise<Date> {\n const start = performance.now();\n let result: 'success' | 'error' = 'error';\n try {\n const ret = await this.#verifyBackupState();\n result = 'success';\n return ret;\n } finally {\n litestreamBackupVerificationDuration().recordMs(\n performance.now() - start,\n {\n ...litestreamMonitorMetricAttrs(\n this.#backupURL,\n 'legacy',\n 'replication_manager',\n ),\n result,\n },\n );\n }\n }\n\n /**\n * Called when the backup has been continuously stale for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}. Continuing to run risks corrupting the\n * backup, so the process is shut down by rejecting the {@link run()}\n * promise. The exit is non-zero and logged at ERROR for alerting; on\n * restart the monitor re-verifies, so a still-wedged backup keeps the\n * process down until the destination is fixed.\n */\n #shutDownOnWedgedBackup(\n staleForMs: number,\n claimedTime: Date,\n lastUpload: Date,\n ) {\n this.#purgesBlocked.add(1, {reason: 'backup-wedged'});\n const err = new UnrecoverableError(\n `backup at ${this.#backupURL} is wedged: litestream claims a backup ` +\n `at ${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded was at ${lastUpload.toISOString()}, and the backup has ` +\n `not advanced for ${staleForMs} ms (grace period ` +\n `${WEDGED_SHUTDOWN_GRACE_MS} ms). Shutting down to avoid corrupting ` +\n `the backup; investigate the backup destination.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * On a fresh stack, fails loudly if no restorable backup has appeared within\n * a generous, replica-size-scaled grace period (see\n * {@link INITIAL_BACKUP_GRACE_MS_PER_UNIT}). This moves the \"give up\" decision\n * onto the replication-manager — the producer responsible for creating the\n * backup — instead of leaving view-syncers to wait (and the platform to keep\n * restarting them) while the real fault is here. Does nothing once a\n * restorable backup has been confirmed.\n */\n async #checkInitialBackupDeadline() {\n if (this.#lastVerifiedUploadTime !== null || this.#runStartTime === null) {\n return;\n }\n const elapsed = Date.now() - this.#runStartTime;\n if (elapsed < this.#initialBackupGraceMs()) {\n return;\n }\n // The deadline has elapsed without a confirmed backup. Do one definitive\n // check against the destination before giving up, in case a backup landed\n // but was never exercised by a reservation or a cleanup verification.\n if (await this.#confirmRestorableBackup()) {\n return;\n }\n this.#shutDownOnMissingInitialBackup(elapsed);\n }\n\n /**\n * The grace period for the first restorable backup, scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance. Falls back to\n * the minimum (one unit) if the replica file size cannot be determined.\n */\n #initialBackupGraceMs(): number {\n let bytes = 0;\n try {\n bytes = statSync(this.#replicaFile).size;\n } catch {\n // Replica file not present/readable yet; fall back to the minimum grace.\n }\n const units = Math.max(\n 1,\n Math.ceil(bytes / INITIAL_BACKUP_GRACE_UNIT_BYTES),\n );\n return units * INITIAL_BACKUP_GRACE_MS_PER_UNIT;\n }\n\n /**\n * Called when no restorable backup has appeared within\n * {@link #initialBackupGraceMs}. Continuing to run is pointless — no\n * view-syncer can restore — and the fault is the backup pipeline, so the\n * process exits non-zero with a logged error for alerting, mirroring\n * {@link #shutDownOnWedgedBackup}.\n */\n #shutDownOnMissingInitialBackup(elapsedMs: number) {\n const err = new UnrecoverableError(\n `no restorable backup has appeared at ${this.#backupURL} within ` +\n `${elapsedMs} ms of startup (grace period ` +\n `${this.#initialBackupGraceMs()} ms). The initial backup pipeline ` +\n `appears to be broken; shutting down so that the replication-manager ` +\n `is flagged as the cause rather than the view-syncers waiting on it. ` +\n `Investigate litestream replication to ${this.#backupURL}.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * Returns the newest watermark whose backup time is at or before `cutoff`\n * (epoch ms), or `''` if there is none.\n */\n #maxBackupBefore(\n cutoff: number,\n ): {watermark: string; backupTime: Date} | null {\n let max = '';\n let maxBackupTime: Date | undefined;\n for (const [watermark, backupTime] of this.#watermarks) {\n if (backupTime.getTime() <= cutoff && watermark > max) {\n max = watermark;\n maxBackupTime = backupTime;\n }\n }\n if (!maxBackupTime) {\n return null;\n }\n return {watermark: max, backupTime: maxBackupTime};\n }\n\n /**\n * Returns `true` if the actual backup state, as last verified against the\n * backup destination, confirms that data claimed to be backed up at\n * `claimedTime` is durable (i.e. an object was actually uploaded at or\n * after `claimedTime`, allowing {@link BACKUP_VERIFICATION_SLACK_MS} of\n * clock skew).\n */\n #confirmedDurable(claimedTime: Date): boolean {\n const lastUpload = this.#lastVerifiedUploadTime;\n return (\n lastUpload !== null &&\n claimedTime.getTime() <=\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"litestream3-prometheus-poller.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream3-prometheus-poller.ts"],"sourcesContent":["import {statSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport parsePrometheusTextFormat from 'parse-prometheus-text-format';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {versionFromLexi} from '../../types/lexi-version.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {\n litestreamBackupVerificationDuration,\n litestreamMonitorMetricAttrs,\n} from '../litestream/metrics.ts';\nimport {RunningState, UnrecoverableError} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nexport const CHECK_INTERVAL_MS = 10_000;\n\n/**\n * Allowance for clock skew between the machine reporting litestream metrics\n * and the timestamps reported by the backup destination (e.g. S3).\n */\nexport const BACKUP_VERIFICATION_SLACK_MS = 60_000;\n\n/**\n * How long the actual backup state may remain *continuously* behind the\n * backup progress claimed by litestream metrics before the backup is\n * considered genuinely wedged and the process is shut down.\n *\n * A wedged backup is dangerous: litestream believes it is making durable\n * progress when it is not, which can silently corrupt the backup (the WAL\n * format only makes *some* such gaps detectable). Once that is confirmed to\n * be the persistent state, continuing to run is worse than crashing, so the\n * process exits loudly (non-zero, with a logged error) and the wedged backup\n * destination becomes the priority to investigate.\n *\n * The change-log is conservatively *not* purged for the entire time the\n * backup is stale, so the only cost of a generous grace period is unbounded\n * change-log growth. We therefore err well on the side of slack: the backup\n * must stay stuck across many {@link CHECK_INTERVAL_MS} checks before we tear\n * the server down, so that a transient hiccup (a slow or briefly unreachable\n * destination, or litestream restarting) does not trigger a shutdown. The\n * staleness clock is reset the moment a purge is confirmed against a real\n * upload.\n */\nexport const WEDGED_SHUTDOWN_GRACE_MS = 15 * 60_000; // 15 minutes\n\nconst GiB = 1024 ** 3;\n\n/**\n * How long the BackupMonitor waits for the *first* restorable backup to appear\n * before concluding that the backup pipeline is broken and shutting the process\n * down (see {@link BackupMonitor.#shutDownOnMissingInitialBackup}).\n *\n * On a fresh stack the first backup is not restorable until the initial\n * Postgres->replica sync completes and litestream finishes uploading the\n * initial snapshot — a multipart upload that is not listable in the destination\n * until it is committed. The producer cannot observe that sub-snapshot progress,\n * so this is necessarily a generous timeout. It is scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance (~1 hour per\n * 50GB) so that it never fires before the platform would restart a view-syncer\n * that is waiting on the backup.\n */\nexport const INITIAL_BACKUP_GRACE_MS_PER_UNIT = 60 * 60_000; // 1 hour ...\nconst INITIAL_BACKUP_GRACE_UNIT_BYTES = 50 * GiB; // ... per 50 GiB\n\n/**\n * How often the BackupMonitor re-checks whether the first restorable backup has\n * appeared while a view-syncer holds a snapshot reservation open during a cold\n * start.\n */\nexport const RESTORABLE_BACKUP_POLL_INTERVAL_MS = 10_000;\n\n/**\n * Returns the time of the most recent object actually uploaded to the\n * backup replica destination (e.g. as determined by listing the snapshots\n * and WAL segments in S3). Rejects if the backup state cannot be determined.\n *\n * See `getLastBackupTime()` in `../litestream/commands.ts` for the\n * production implementation.\n */\nexport type BackupStateVerifier = () => Promise<Date>;\n\n/**\n * The Litestream3PrometheusPoller polls the litestream \"/metrics\" endpoint to\n * track the watermark (label) value of the `litestream_replica_progress` gauge\n * and emits verified watermarks to a stream of {@link BackedUpWatermark}s.\n *\n * See: https://github.com/rocicorp/litestream/pull/3\n *\n * Because the watermarks reported by litestream metrics reflect what\n * litestream *believes* has been backed up (which has been observed to\n * diverge from reality when uploads silently fail), the cleanup watermark is\n * only published after verifying it against the actual backup state in the\n * replica destination via a {@link BackupStateVerifier}. If the\n * actual backup state stays behind the claimed progress for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}, the backup is treated as wedged and the\n * process is shut down rather than risk corrupting the backup.\n */\nexport class Litestream3PrometheusPoller {\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #backupURL: string;\n readonly #metricsEndpoint: string;\n readonly #state = new RunningState('litestream3-prometheus-poller');\n\n readonly #stream: Subscription<BackedUpWatermark>;\n readonly #watermarks = new Map<string, Date>();\n\n readonly #verifyBackupState: BackupStateVerifier;\n readonly #purgesBlocked = getOrCreateCounter('replica', 'purge_blocked', {\n description:\n 'Number of change-log purges blocked because the actual backup state ' +\n '(as listed from the replica destination) could not be verified, or ' +\n 'is older than the backup progress claimed by litestream metrics. ' +\n 'A steadily increasing value indicates a wedged or failing backup. ' +\n 'The \"backup-wedged\" reason is emitted once just before the process ' +\n 'shuts itself down due to a persistently stale backup.',\n });\n\n #lastWatermark: string = '';\n #latestBackupTime: Date | null = null;\n // The time of the most recent object actually verified in the backup\n // destination, or `null` if none has ever been verified. Only assigned from a\n // successful verifyBackupState() and never cleared, so a non-null value also\n // means a restorable backup has been confirmed to exist (which is what gates\n // the snapshot `status` signal and satisfies the initial-backup deadline).\n #lastVerifiedUploadTime: Date | null = null;\n // Epoch ms at which the actual backup state was first observed to be\n // continuously behind the watermark litestream claims to have backed up,\n // or `null` while the backup is keeping up. Reset whenever a purge is\n // confirmed against a real upload.\n #backupStaleSince: number | null = null;\n // Epoch ms at which run() started, used to bound how long to wait for the\n // first restorable backup to appear. `null` until run() is called.\n #runStartTime: number | null = null;\n #checkMetricsTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n backupURL: string,\n metricsEndpoint: string,\n verifyBackupState: BackupStateVerifier,\n ) {\n this.#lc = lc.withContext('component', 'litestream3-prometheus-poller');\n this.#replicaFile = replicaFile;\n this.#backupURL = backupURL;\n this.#metricsEndpoint = metricsEndpoint;\n this.#verifyBackupState = verifyBackupState;\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n clearInterval(this.#checkMetricsTimer);\n this.#state.stop(this.#lc);\n },\n });\n }\n\n /**\n * Starts polling for watermarks and pushes new ones to returned stream.\n * Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#runStartTime === null, `Already called start()`);\n this.#runStartTime = Date.now();\n this.#lc.info?.(`monitoring backups at ${this.#metricsEndpoint}`);\n this.#checkMetricsTimer = setInterval(\n this.checkVerifyAndPublishWatermarks,\n CHECK_INTERVAL_MS,\n );\n return this.#stream;\n }\n\n /**\n * Verifies whether a restorable backup actually exists in the destination\n * (as opposed to what litestream metrics merely claim). On success the\n * verified upload time is cached — seeding the cleanup-verification fast path\n * ({@link #confirmedDurable}) — and the initial-backup deadline is satisfied.\n * Returns false if no backup is listable yet (e.g. the initial snapshot is\n * still uploading).\n */\n async #confirmRestorableBackup(): Promise<boolean> {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n return true;\n } catch (e) {\n this.#lc.info?.(`backup not yet restorable at ${this.#backupURL}`, e);\n return false;\n }\n }\n\n // Exported for testing\n readonly checkVerifyAndPublishWatermarks = async () => {\n try {\n await this.#checkWatermarks();\n } catch (e) {\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n }\n try {\n await this.#publishVerifiedWatermarks();\n } catch (e) {\n this.#lc.warn?.(`error scheduling cleanup`, e);\n }\n try {\n await this.#checkInitialBackupDeadline();\n } catch (e) {\n this.#lc.warn?.(`error checking initial backup deadline`, e);\n }\n };\n\n async *#fetchWatermarks(): AsyncGenerator<{\n watermark: string;\n time: Date;\n name?: string | undefined;\n }> {\n const metricsEndpoint = this.#metricsEndpoint;\n const signal = this.#state.signal;\n let resp;\n try {\n resp = await fetch(metricsEndpoint, {signal});\n } catch (e) {\n if (signal.aborted) {\n // not an error.\n return;\n }\n // Treat exceptions from fetch (e.g. network errors) as non-fatal, and simply\n // log them and skip the watermark check until the next interval.\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n return;\n }\n if (!resp.ok) {\n this.#lc.warn?.(\n `unable to fetch metrics at ${this.#metricsEndpoint}: ${await resp.text()}`,\n );\n return;\n }\n\n const families = parsePrometheusTextFormat(await resp.text());\n for (const family of families) {\n if (\n family.type === 'GAUGE' &&\n family.name === 'litestream_replica_progress'\n ) {\n for (const metric of family.metrics) {\n const watermark = metric.labels?.watermark;\n const name = metric.labels?.name;\n const time = new Date(parseFloat(metric.value) * 1000);\n if (watermark) {\n try {\n versionFromLexi(watermark); // Sanity check: Ensure that it is valid.\n yield {watermark, time, name};\n } catch (e) {\n this.#lc.warn?.(\n `ignoring invalid watermark ${watermark}`,\n {metric},\n e,\n );\n }\n }\n }\n }\n }\n }\n\n async #checkWatermarks() {\n for await (const {watermark, name, time} of this.#fetchWatermarks()) {\n if (watermark > this.#lastWatermark && !this.#watermarks.has(watermark)) {\n this.#lc.info?.(\n `replicated watermark=${watermark} to ${name}` +\n ` at ${time.toISOString()}.`,\n );\n this.#watermarks.set(watermark, time);\n this.#latestBackupTime = time;\n }\n }\n return this.#latestBackupTime;\n }\n\n async #publishVerifiedWatermarks() {\n const maxBackup = this.#maxBackupBefore(Date.now());\n if (!maxBackup) {\n return;\n }\n const {watermark: maxWatermark, backupTime: claimedTime} = maxBackup;\n // Purge guard: the watermarks (and their backup times) come from\n // litestream metrics, which are exported when litestream *believes*\n // an upload succeeded, and have been observed to advance even when\n // nothing was actually written to the backup destination. Purging the\n // change-log based on a falsely advancing watermark permanently breaks\n // the ability to restore + catch up. Before advancing the cleanup\n // watermark, verify it against the actual backup state: a claimed\n // backup time is only trusted if an object was actually uploaded to\n // the replica destination at (or after) that time, modulo clock skew.\n if (!this.#confirmedDurable(claimedTime)) {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n } catch (e) {\n this.#purgesBlocked.add(1, {reason: 'verification-failed'});\n // Skipping the purge is safe: the change-log just grows.\n this.#lc.warn?.(\n `unable to verify backup state. skipping change-log cleanup ` +\n `up to watermark ${maxWatermark} ` +\n `(claimed backup time ${claimedTime.toISOString()})`,\n e,\n );\n return;\n }\n }\n // Watermarks whose backup time isn't yet confirmed durable remain in the\n // map and are re-evaluated at the next check.\n const lastUpload = must(this.#lastVerifiedUploadTime);\n const verifiedBackup = this.#maxBackupBefore(\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS,\n );\n if (!verifiedBackup) {\n this.#purgesBlocked.add(1, {reason: 'backup-stale'});\n const now = Date.now();\n if (this.#backupStaleSince === null) {\n this.#backupStaleSince = now;\n }\n const staleForMs = now - this.#backupStaleSince;\n this.#lc.warn?.(\n `blocked change-log cleanup up to watermark ${maxWatermark}: ` +\n `litestream claims it was backed up at ` +\n `${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded to ${this.#backupURL} was at ` +\n `${lastUpload.toISOString()}. ` +\n `The backup has been stale for ${staleForMs} ms.`,\n );\n if (staleForMs >= WEDGED_SHUTDOWN_GRACE_MS) {\n this.#shutDownOnWedgedBackup(staleForMs, claimedTime, lastUpload);\n }\n return;\n }\n // The cleanup watermark advanced past a real upload, so the backup is\n // keeping up: clear the staleness clock.\n this.#backupStaleSince = null;\n const {watermark: verifiedWatermark, backupTime} = verifiedBackup;\n for (const watermark of this.#watermarks.keys()) {\n if (watermark <= verifiedWatermark) {\n this.#watermarks.delete(watermark);\n }\n }\n this.#stream.push({\n watermark: verifiedWatermark,\n backupTimeMs: backupTime.getTime(),\n });\n this.#lastWatermark = verifiedWatermark;\n }\n\n async #verifyBackupStateTimed(): Promise<Date> {\n const start = performance.now();\n let result: 'success' | 'error' = 'error';\n try {\n const ret = await this.#verifyBackupState();\n result = 'success';\n return ret;\n } finally {\n litestreamBackupVerificationDuration().recordMs(\n performance.now() - start,\n {\n ...litestreamMonitorMetricAttrs(\n this.#backupURL,\n 'legacy',\n 'replication_manager',\n ),\n result,\n },\n );\n }\n }\n\n /**\n * Called when the backup has been continuously stale for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}. Continuing to run risks corrupting the\n * backup, so the process is shut down by rejecting the {@link run()}\n * promise. The exit is non-zero and logged at ERROR for alerting; on\n * restart the monitor re-verifies, so a still-wedged backup keeps the\n * process down until the destination is fixed.\n */\n #shutDownOnWedgedBackup(\n staleForMs: number,\n claimedTime: Date,\n lastUpload: Date,\n ) {\n this.#purgesBlocked.add(1, {reason: 'backup-wedged'});\n const err = new UnrecoverableError(\n `backup at ${this.#backupURL} is wedged: litestream claims a backup ` +\n `at ${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded was at ${lastUpload.toISOString()}, and the backup has ` +\n `not advanced for ${staleForMs} ms (grace period ` +\n `${WEDGED_SHUTDOWN_GRACE_MS} ms). Shutting down to avoid corrupting ` +\n `the backup; investigate the backup destination.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * On a fresh stack, fails loudly if no restorable backup has appeared within\n * a generous, replica-size-scaled grace period (see\n * {@link INITIAL_BACKUP_GRACE_MS_PER_UNIT}). This moves the \"give up\" decision\n * onto the replication-manager — the producer responsible for creating the\n * backup — instead of leaving view-syncers to wait (and the platform to keep\n * restarting them) while the real fault is here. Does nothing once a\n * restorable backup has been confirmed.\n */\n async #checkInitialBackupDeadline() {\n if (this.#lastVerifiedUploadTime !== null || this.#runStartTime === null) {\n return;\n }\n const elapsed = Date.now() - this.#runStartTime;\n if (elapsed < this.#initialBackupGraceMs()) {\n return;\n }\n // The deadline has elapsed without a confirmed backup. Do one definitive\n // check against the destination before giving up, in case a backup landed\n // but was never exercised by a reservation or a cleanup verification.\n if (await this.#confirmRestorableBackup()) {\n return;\n }\n this.#shutDownOnMissingInitialBackup(elapsed);\n }\n\n /**\n * The grace period for the first restorable backup, scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance. Falls back to\n * the minimum (one unit) if the replica file size cannot be determined.\n */\n #initialBackupGraceMs(): number {\n let bytes = 0;\n try {\n bytes = statSync(this.#replicaFile).size;\n } catch {\n // Replica file not present/readable yet; fall back to the minimum grace.\n }\n const units = Math.max(\n 1,\n Math.ceil(bytes / INITIAL_BACKUP_GRACE_UNIT_BYTES),\n );\n return units * INITIAL_BACKUP_GRACE_MS_PER_UNIT;\n }\n\n /**\n * Called when no restorable backup has appeared within\n * {@link #initialBackupGraceMs}. Continuing to run is pointless — no\n * view-syncer can restore — and the fault is the backup pipeline, so the\n * process exits non-zero with a logged error for alerting, mirroring\n * {@link #shutDownOnWedgedBackup}.\n */\n #shutDownOnMissingInitialBackup(elapsedMs: number) {\n const err = new UnrecoverableError(\n `no restorable backup has appeared at ${this.#backupURL} within ` +\n `${elapsedMs} ms of startup (grace period ` +\n `${this.#initialBackupGraceMs()} ms). The initial backup pipeline ` +\n `appears to be broken; shutting down so that the replication-manager ` +\n `is flagged as the cause rather than the view-syncers waiting on it. ` +\n `Investigate litestream replication to ${this.#backupURL}.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * Returns the newest watermark whose backup time is at or before `cutoff`\n * (epoch ms), or `''` if there is none.\n */\n #maxBackupBefore(\n cutoff: number,\n ): {watermark: string; backupTime: Date} | null {\n let max = '';\n let maxBackupTime: Date | undefined;\n for (const [watermark, backupTime] of this.#watermarks) {\n if (backupTime.getTime() <= cutoff && watermark > max) {\n max = watermark;\n maxBackupTime = backupTime;\n }\n }\n if (!maxBackupTime) {\n return null;\n }\n return {watermark: max, backupTime: maxBackupTime};\n }\n\n /**\n * Returns `true` if the actual backup state, as last verified against the\n * backup destination, confirms that data claimed to be backed up at\n * `claimedTime` is durable (i.e. an object was actually uploaded at or\n * after `claimedTime`, allowing {@link BACKUP_VERIFICATION_SLACK_MS} of\n * clock skew).\n */\n #confirmedDurable(claimedTime: Date): boolean {\n const lastUpload = this.#lastVerifiedUploadTime;\n return (\n lastUpload !== null &&\n claimedTime.getTime() <=\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS\n );\n }\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { Subscription } from "../../types/subscription.js";
5
5
  import { assert } from "console";
6
6
  import { clearInterval } from "timers";
7
7
  //#region ../zero-cache/src/services/change-streamer/replica-poller.ts
8
- var CHECK_INTERVAL_MS = 10 * 1e3;
8
+ var CHECK_INTERVAL_MS = 1e4;
9
9
  /**
10
10
  * The single-node equivalent of a backup monitor polls the replica file every
11
11
  * 30 seconds and schedules cleanup when the watermark
@@ -1 +1 @@
1
- {"version":3,"file":"replica-poller.js","names":["#lc","#replicaFile","#stream","#pollReplicaTimer","#pollReplica","#lastWatermark"],"sources":["../../../../../../zero-cache/src/services/change-streamer/replica-poller.ts"],"sourcesContent":["import {assert} from 'console';\nimport {clearInterval} from 'timers';\nimport type {LogContext} from '@rocicorp/logger';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {getReplicationState} from '../replicator/schema/replication-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nconst CHECK_INTERVAL_MS = 10 * 1000;\n\n/**\n * The single-node equivalent of a backup monitor polls the replica file every\n * 30 seconds and schedules cleanup when the watermark\n * (i.e. stateVersion) moves forward.\n */\nexport class ReplicaPoller {\n readonly id = 'replica-poller';\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #stream: Subscription<BackedUpWatermark>;\n #pollReplicaTimer: NodeJS.Timeout | undefined;\n\n #lastWatermark: string = '';\n\n constructor(lc: LogContext, replicaFile: string) {\n this.#lc = lc.withContext('component', this.id);\n this.#replicaFile = replicaFile;\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => clearInterval(this.#pollReplicaTimer),\n });\n }\n\n start(): Source<BackedUpWatermark> {\n assert(this.#pollReplicaTimer === undefined, `Already called start()`);\n this.#lc.info?.(`starting replica monitor`);\n // Perform the first poll immediately to avoid unnecessarily waiting\n // during single-node development.\n this.#pollReplica();\n this.#pollReplicaTimer = setInterval(this.#pollReplica, CHECK_INTERVAL_MS);\n return this.#stream;\n }\n\n readonly #pollReplica = () => {\n const db = new Database(this.#lc, this.#replicaFile);\n try {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n if (stateVersion !== this.#lastWatermark) {\n this.#lastWatermark = stateVersion;\n this.#lc.debug?.(`replicated up to watermark ${stateVersion}`);\n this.#stream.push({watermark: stateVersion, backupTimeMs: Date.now()});\n }\n } catch (e) {\n this.#lc.error?.(`Unable to read watermark from replica`, e);\n } finally {\n db.close();\n }\n };\n}\n"],"mappings":";;;;;;;AAUA,IAAM,oBAAoB,KAAK;;;;;;AAO/B,IAAa,gBAAb,MAA2B;CACzB,KAAc;CACd;CACA;CACA;CACA;CAEA,iBAAyB;CAEzB,YAAY,IAAgB,aAAqB;EAC/C,KAAKA,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAKC,eAAe;EACpB,KAAKC,UAAU,aAAa,OAA0B,EACpD,eAAe,cAAc,KAAKC,iBAAiB,EACrD,CAAC;CACH;CAEA,QAAmC;EACjC,OAAO,KAAKA,sBAAsB,KAAA,GAAW,wBAAwB;EACrE,KAAKH,IAAI,OAAO,0BAA0B;EAG1C,KAAKI,aAAa;EAClB,KAAKD,oBAAoB,YAAY,KAAKC,cAAc,iBAAiB;EACzE,OAAO,KAAKF;CACd;CAEA,qBAA8B;EAC5B,MAAM,KAAK,IAAI,SAAS,KAAKF,KAAK,KAAKC,YAAY;EACnD,IAAI;GACF,MAAM,EAAC,iBAAgB,oBAAoB,IAAI,gBAAgB,EAAE,CAAC;GAClE,IAAI,iBAAiB,KAAKI,gBAAgB;IACxC,KAAKA,iBAAiB;IACtB,KAAKL,IAAI,QAAQ,8BAA8B,cAAc;IAC7D,KAAKE,QAAQ,KAAK;KAAC,WAAW;KAAc,cAAc,KAAK,IAAI;IAAC,CAAC;GACvE;EACF,SAAS,GAAG;GACV,KAAKF,IAAI,QAAQ,yCAAyC,CAAC;EAC7D,UAAU;GACR,GAAG,MAAM;EACX;CACF;AACF"}
1
+ {"version":3,"file":"replica-poller.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/replica-poller.ts"],"sourcesContent":["import {assert} from 'console';\nimport {clearInterval} from 'timers';\nimport type {LogContext} from '@rocicorp/logger';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {getReplicationState} from '../replicator/schema/replication-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nconst CHECK_INTERVAL_MS = 10 * 1000;\n\n/**\n * The single-node equivalent of a backup monitor polls the replica file every\n * 30 seconds and schedules cleanup when the watermark\n * (i.e. stateVersion) moves forward.\n */\nexport class ReplicaPoller {\n readonly id = 'replica-poller';\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #stream: Subscription<BackedUpWatermark>;\n #pollReplicaTimer: NodeJS.Timeout | undefined;\n\n #lastWatermark: string = '';\n\n constructor(lc: LogContext, replicaFile: string) {\n this.#lc = lc.withContext('component', this.id);\n this.#replicaFile = replicaFile;\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => clearInterval(this.#pollReplicaTimer),\n });\n }\n\n start(): Source<BackedUpWatermark> {\n assert(this.#pollReplicaTimer === undefined, `Already called start()`);\n this.#lc.info?.(`starting replica monitor`);\n // Perform the first poll immediately to avoid unnecessarily waiting\n // during single-node development.\n this.#pollReplica();\n this.#pollReplicaTimer = setInterval(this.#pollReplica, CHECK_INTERVAL_MS);\n return this.#stream;\n }\n\n readonly #pollReplica = () => {\n const db = new Database(this.#lc, this.#replicaFile);\n try {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n if (stateVersion !== this.#lastWatermark) {\n this.#lastWatermark = stateVersion;\n this.#lc.debug?.(`replicated up to watermark ${stateVersion}`);\n this.#stream.push({watermark: stateVersion, backupTimeMs: Date.now()});\n }\n } catch (e) {\n this.#lc.error?.(`Unable to read watermark from replica`, e);\n } finally {\n db.close();\n }\n };\n}\n"],"mappings":";;;;;;;AAUA,IAAM,oBAAoB;;;;;;AAO1B,IAAa,gBAAb,MAA2B;CACzB,KAAc;CACd;CACA;CACA;CACA;CAEA,iBAAyB;CAEzB,YAAY,IAAgB,aAAqB;EAC/C,KAAK,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAK,eAAe;EACpB,KAAK,UAAU,aAAa,OAA0B,EACpD,eAAe,cAAc,KAAK,iBAAiB,EACrD,CAAC;CACH;CAEA,QAAmC;EACjC,OAAO,KAAK,sBAAsB,KAAA,GAAW,wBAAwB;EACrE,KAAK,IAAI,OAAO,0BAA0B;EAG1C,KAAK,aAAa;EAClB,KAAK,oBAAoB,YAAY,KAAK,cAAc,iBAAiB;EACzE,OAAO,KAAK;CACd;CAEA,qBAA8B;EAC5B,MAAM,KAAK,IAAI,SAAS,KAAK,KAAK,KAAK,YAAY;EACnD,IAAI;GACF,MAAM,EAAC,iBAAgB,oBAAoB,IAAI,gBAAgB,EAAE,CAAC;GAClE,IAAI,iBAAiB,KAAK,gBAAgB;IACxC,KAAK,iBAAiB;IACtB,KAAK,IAAI,QAAQ,8BAA8B,cAAc;IAC7D,KAAK,QAAQ,KAAK;KAAC,WAAW;KAAc,cAAc,KAAK,IAAI;IAAC,CAAC;GACvE;EACF,SAAS,GAAG;GACV,KAAK,IAAI,QAAQ,yCAAyC,CAAC;EAC7D,UAAU;GACR,GAAG,MAAM;EACX;CACF;AACF"}
@@ -1,4 +1,15 @@
1
1
  import { getOrCreateGauge } from "../../observability/metrics.js";
2
+ //#region ../zero-cache/src/services/change-streamer/restore-admission.ts
3
+ /**
4
+ * The default time to wait for a subscriber that was granted permission to
5
+ * restore to reconnect (i.e. to open a replacement reservation) after its
6
+ * held connection closes, or after its replacement reservation closes
7
+ * without subscribing (e.g. to retry a failed restore). This covers the
8
+ * subscriber's process exit and restart (e.g. a container restart, the
9
+ * backoff of which starts at 10 seconds in Kubernetes), but not the restore
10
+ * itself, which is tracked by the liveness of its snapshot reservation.
11
+ */
12
+ var DEFAULT_PENDING_GRANT_TTL_MS = 9e4;
2
13
  /**
3
14
  * The default time for which a replacement continues to be counted after its
4
15
  * subscriber has caught up, which covers the time for the subscriber to start
@@ -312,6 +323,6 @@ var RestoreAdmission = class {
312
323
  }
313
324
  };
314
325
  //#endregion
315
- export { DEFAULT_READINESS_GRACE_PERIOD_MS, RestoreAdmission };
326
+ export { DEFAULT_PENDING_GRANT_TTL_MS, DEFAULT_READINESS_GRACE_PERIOD_MS, RestoreAdmission };
316
327
 
317
328
  //# sourceMappingURL=restore-admission.js.map