@rocicorp/zero 1.6.0-canary.11 → 1.6.0-canary.13

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 (659) hide show
  1. package/README.md +3 -28
  2. package/out/_virtual/{_@oxc-project_runtime@0.130.0 → _@oxc-project_runtime@0.122.0}/helpers/usingCtx.js +1 -1
  3. package/out/_virtual/_rolldown/runtime.js +1 -12
  4. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  5. package/out/analyze-query/src/bin-analyze.js +1 -6
  6. package/out/analyze-query/src/bin-analyze.js.map +1 -1
  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.map +1 -1
  10. package/out/ast-to-zql/src/format.js.map +1 -1
  11. package/out/datadog/src/datadog-log-sink.js.map +1 -1
  12. package/out/otel/src/enabled.js.map +1 -1
  13. package/out/otel/src/log-options.js.map +1 -1
  14. package/out/otel/src/maybe-time.js.map +1 -1
  15. package/out/otel/src/span.js.map +1 -1
  16. package/out/replicache/src/async-iterable-to-array.js.map +1 -1
  17. package/out/replicache/src/bg-interval.js.map +1 -1
  18. package/out/replicache/src/btree/diff.js.map +1 -1
  19. package/out/replicache/src/btree/node.js.map +1 -1
  20. package/out/replicache/src/btree/read.js.map +1 -1
  21. package/out/replicache/src/btree/splice.js.map +1 -1
  22. package/out/replicache/src/btree/write.js +3 -6
  23. package/out/replicache/src/btree/write.js.map +1 -1
  24. package/out/replicache/src/call-default-fetch.js.map +1 -1
  25. package/out/replicache/src/connection-loop-delegates.js.map +1 -1
  26. package/out/replicache/src/connection-loop.js.map +1 -1
  27. package/out/replicache/src/cookies.js.map +1 -1
  28. package/out/replicache/src/dag/chunk.js.map +1 -1
  29. package/out/replicache/src/dag/gc.js.map +1 -1
  30. package/out/replicache/src/dag/key.js.map +1 -1
  31. package/out/replicache/src/dag/lazy-store.js.map +1 -1
  32. package/out/replicache/src/dag/store-impl.js.map +1 -1
  33. package/out/replicache/src/dag/store.js.map +1 -1
  34. package/out/replicache/src/dag/visitor.js.map +1 -1
  35. package/out/replicache/src/db/commit.js.map +1 -1
  36. package/out/replicache/src/db/index.js.map +1 -1
  37. package/out/replicache/src/db/read.js.map +1 -1
  38. package/out/replicache/src/db/rebase.js.map +1 -1
  39. package/out/replicache/src/db/write.js.map +1 -1
  40. package/out/replicache/src/deleted-clients.js.map +1 -1
  41. package/out/replicache/src/error-responses.js.map +1 -1
  42. package/out/replicache/src/frozen-json.js.map +1 -1
  43. package/out/replicache/src/get-default-puller.js.map +1 -1
  44. package/out/replicache/src/get-default-pusher.js.map +1 -1
  45. package/out/replicache/src/get-kv-store-provider.js.map +1 -1
  46. package/out/replicache/src/hash.js.map +1 -1
  47. package/out/replicache/src/http-request-info.js.map +1 -1
  48. package/out/replicache/src/index-defs.js.map +1 -1
  49. package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
  50. package/out/replicache/src/kv/idb-store-with-mem-fallback.js.map +1 -1
  51. package/out/replicache/src/kv/idb-store.js.map +1 -1
  52. package/out/replicache/src/kv/mem-store.js.map +1 -1
  53. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  54. package/out/replicache/src/kv/read-impl.js.map +1 -1
  55. package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
  56. package/out/replicache/src/kv/sqlite-store.js +1 -4
  57. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  58. package/out/replicache/src/kv/throw-if-closed.js.map +1 -1
  59. package/out/replicache/src/kv/write-impl-base.js.map +1 -1
  60. package/out/replicache/src/kv/write-impl.js.map +1 -1
  61. package/out/replicache/src/lazy.js.map +1 -1
  62. package/out/replicache/src/log-options.js.map +1 -1
  63. package/out/replicache/src/make-idb-name.js.map +1 -1
  64. package/out/replicache/src/new-client-channel.js.map +1 -1
  65. package/out/replicache/src/on-persist-channel.js.map +1 -1
  66. package/out/replicache/src/patch-operation.js.map +1 -1
  67. package/out/replicache/src/pending-mutations.js.map +1 -1
  68. package/out/replicache/src/persist/client-gc.js.map +1 -1
  69. package/out/replicache/src/persist/client-group-gc.js.map +1 -1
  70. package/out/replicache/src/persist/client-groups.js +0 -40
  71. package/out/replicache/src/persist/client-groups.js.map +1 -1
  72. package/out/replicache/src/persist/clients.js +0 -28
  73. package/out/replicache/src/persist/clients.js.map +1 -1
  74. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  75. package/out/replicache/src/persist/gather-mem-only-visitor.js.map +1 -1
  76. package/out/replicache/src/persist/gather-not-cached-visitor.js.map +1 -1
  77. package/out/replicache/src/persist/heartbeat.js.map +1 -1
  78. package/out/replicache/src/persist/idb-databases-store-db-name.js.map +1 -1
  79. package/out/replicache/src/persist/idb-databases-store.js.map +1 -1
  80. package/out/replicache/src/persist/make-client-id.js.map +1 -1
  81. package/out/replicache/src/persist/persist.js.map +1 -1
  82. package/out/replicache/src/persist/refresh.js.map +1 -1
  83. package/out/replicache/src/process-scheduler.js.map +1 -1
  84. package/out/replicache/src/pusher.js.map +1 -1
  85. package/out/replicache/src/replicache-impl.js.map +1 -1
  86. package/out/replicache/src/report-error.js.map +1 -1
  87. package/out/replicache/src/request-idle.js.map +1 -1
  88. package/out/replicache/src/scan-iterator.js.map +1 -1
  89. package/out/replicache/src/scan-options.js.map +1 -1
  90. package/out/replicache/src/set-interval-with-signal.js.map +1 -1
  91. package/out/replicache/src/subscriptions.js.map +1 -1
  92. package/out/replicache/src/sync/diff.js.map +1 -1
  93. package/out/replicache/src/sync/ids.js.map +1 -1
  94. package/out/replicache/src/sync/patch.js.map +1 -1
  95. package/out/replicache/src/sync/pull-error.js.map +1 -1
  96. package/out/replicache/src/sync/pull.js.map +1 -1
  97. package/out/replicache/src/sync/push.js.map +1 -1
  98. package/out/replicache/src/sync/request-id.js.map +1 -1
  99. package/out/replicache/src/to-error.js.map +1 -1
  100. package/out/replicache/src/transaction-closed-error.js.map +1 -1
  101. package/out/replicache/src/transactions.js.map +1 -1
  102. package/out/replicache/src/with-transactions.js.map +1 -1
  103. package/out/shared/src/abort-error.js.map +1 -1
  104. package/out/shared/src/arrays.js.map +1 -1
  105. package/out/shared/src/asserts.js.map +1 -1
  106. package/out/shared/src/bigint-json.js.map +1 -1
  107. package/out/shared/src/binary-search.js.map +1 -1
  108. package/out/shared/src/broadcast-channel.js.map +1 -1
  109. package/out/shared/src/browser-env.js.map +1 -1
  110. package/out/shared/src/btree-set.js.map +1 -1
  111. package/out/shared/src/cache.js.map +1 -1
  112. package/out/shared/src/centroid.js.map +1 -1
  113. package/out/shared/src/custom-key-map.js.map +1 -1
  114. package/out/shared/src/custom-key-set.js.map +1 -1
  115. package/out/shared/src/deep-clone.js.map +1 -1
  116. package/out/shared/src/deep-merge.js.map +1 -1
  117. package/out/shared/src/document-visible.js.map +1 -1
  118. package/out/shared/src/dotenv.js.map +1 -1
  119. package/out/shared/src/error.js.map +1 -1
  120. package/out/shared/src/hash.js.map +1 -1
  121. package/out/shared/src/iterables.js.map +1 -1
  122. package/out/shared/src/json-schema.js.map +1 -1
  123. package/out/shared/src/json.js.map +1 -1
  124. package/out/shared/src/logging-test-utils.js.map +1 -1
  125. package/out/shared/src/logging.js.map +1 -1
  126. package/out/shared/src/map.js.map +1 -1
  127. package/out/shared/src/must.js.map +1 -1
  128. package/out/shared/src/object-traversal.js.map +1 -1
  129. package/out/shared/src/objects.js.map +1 -1
  130. package/out/shared/src/options.js.map +1 -1
  131. package/out/shared/src/parse-big-int.js.map +1 -1
  132. package/out/shared/src/promise-race.js.map +1 -1
  133. package/out/shared/src/queue.d.ts.map +1 -1
  134. package/out/shared/src/queue.js +21 -15
  135. package/out/shared/src/queue.js.map +1 -1
  136. package/out/shared/src/rand.js.map +1 -1
  137. package/out/shared/src/random-uint64.js.map +1 -1
  138. package/out/shared/src/random-values.js.map +1 -1
  139. package/out/shared/src/record-proxy.js.map +1 -1
  140. package/out/shared/src/resolved-promises.js.map +1 -1
  141. package/out/shared/src/sentinels.js.map +1 -1
  142. package/out/shared/src/set-utils.js.map +1 -1
  143. package/out/shared/src/size-of-value.js.map +1 -1
  144. package/out/shared/src/sleep.js.map +1 -1
  145. package/out/shared/src/sorted-entries.js.map +1 -1
  146. package/out/shared/src/string-compare.js.map +1 -1
  147. package/out/shared/src/subscribable.js.map +1 -1
  148. package/out/shared/src/tdigest-schema.js.map +1 -1
  149. package/out/shared/src/tdigest.js.map +1 -1
  150. package/out/shared/src/valita.js.map +1 -1
  151. package/out/z2s/src/compiler.js.map +1 -1
  152. package/out/z2s/src/sql.js.map +1 -1
  153. package/out/zero/package.js +23 -23
  154. package/out/zero/package.js.map +1 -1
  155. package/out/zero/src/build-schema.js.map +1 -1
  156. package/out/zero/src/zero-cache-dev.js.map +1 -1
  157. package/out/zero/src/zero-out.js.map +1 -1
  158. package/out/zero-cache/src/auth/auth.js.map +1 -1
  159. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  160. package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
  161. package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
  162. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  163. package/out/zero-cache/src/config/network.js.map +1 -1
  164. package/out/zero-cache/src/config/normalize.js.map +1 -1
  165. package/out/zero-cache/src/config/server-context.js.map +1 -1
  166. package/out/zero-cache/src/config/zero-config.js +0 -5
  167. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  168. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  169. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  170. package/out/zero-cache/src/db/create.js.map +1 -1
  171. package/out/zero-cache/src/db/delete-lite-db.js.map +1 -1
  172. package/out/zero-cache/src/db/lite-tables.js.map +1 -1
  173. package/out/zero-cache/src/db/migration-lite.js +0 -19
  174. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  175. package/out/zero-cache/src/db/migration.js +0 -19
  176. package/out/zero-cache/src/db/migration.js.map +1 -1
  177. package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
  178. package/out/zero-cache/src/db/pg-copy.js.map +1 -1
  179. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  180. package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
  181. package/out/zero-cache/src/db/run-transaction.js.map +1 -1
  182. package/out/zero-cache/src/db/specs.js.map +1 -1
  183. package/out/zero-cache/src/db/statements.js.map +1 -1
  184. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  185. package/out/zero-cache/src/db/warmup.js.map +1 -1
  186. package/out/zero-cache/src/observability/events.js.map +1 -1
  187. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  188. package/out/zero-cache/src/scripts/decommission.js.map +1 -1
  189. package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
  190. package/out/zero-cache/src/scripts/permissions.js.map +1 -1
  191. package/out/zero-cache/src/server/anonymous-otel-start.js +10 -11
  192. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  193. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  194. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  195. package/out/zero-cache/src/server/logging.js.map +1 -1
  196. package/out/zero-cache/src/server/main.js.map +1 -1
  197. package/out/zero-cache/src/server/mutator.js.map +1 -1
  198. package/out/zero-cache/src/server/otel-diag-logger.js.map +1 -1
  199. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  200. package/out/zero-cache/src/server/otel-start.js +1 -1
  201. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  202. package/out/zero-cache/src/server/priority-op.js.map +1 -1
  203. package/out/zero-cache/src/server/reaper.js.map +1 -1
  204. package/out/zero-cache/src/server/replicator.js.map +1 -1
  205. package/out/zero-cache/src/server/runner/main.js.map +1 -1
  206. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  207. package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
  208. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  209. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  210. package/out/zero-cache/src/server/syncer.js.map +1 -1
  211. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  212. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  213. package/out/zero-cache/src/services/analyze.d.ts.map +1 -1
  214. package/out/zero-cache/src/services/analyze.js +2 -5
  215. package/out/zero-cache/src/services/analyze.js.map +1 -1
  216. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  217. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  218. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  219. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  220. package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
  221. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  222. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  223. package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
  224. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  225. package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
  226. package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
  227. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  228. package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
  229. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  230. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  231. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  232. package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
  233. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  234. package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
  235. package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
  236. package/out/zero-cache/src/services/change-source/protocol/current/data.js +0 -2
  237. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  238. package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
  239. package/out/zero-cache/src/services/change-source/protocol/current/json.js.map +1 -1
  240. package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
  241. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  242. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
  243. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  244. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  245. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  246. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  247. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  248. package/out/zero-cache/src/services/change-streamer/replica-monitor.js.map +1 -1
  249. package/out/zero-cache/src/services/change-streamer/schema/init.js +25 -21
  250. package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
  251. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  252. package/out/zero-cache/src/services/change-streamer/snapshot.js +0 -15
  253. package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
  254. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  255. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  256. package/out/zero-cache/src/services/heapz.js.map +1 -1
  257. package/out/zero-cache/src/services/http-service.js.map +1 -1
  258. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  259. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
  260. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  261. package/out/zero-cache/src/services/mutagen/error.js.map +1 -1
  262. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  263. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  264. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  265. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  266. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  267. package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
  268. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  269. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  270. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
  271. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  272. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  273. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  274. package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
  275. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  276. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  277. package/out/zero-cache/src/services/run-ast.d.ts.map +1 -1
  278. package/out/zero-cache/src/services/run-ast.js +0 -1
  279. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  280. package/out/zero-cache/src/services/runner.js.map +1 -1
  281. package/out/zero-cache/src/services/running-state.js.map +1 -1
  282. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  283. package/out/zero-cache/src/services/statz.js.map +1 -1
  284. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
  285. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  286. package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
  287. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  288. package/out/zero-cache/src/services/view-syncer/cvr-purger.d.ts.map +1 -1
  289. package/out/zero-cache/src/services/view-syncer/cvr-purger.js +1 -2
  290. package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
  291. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  292. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  293. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  294. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +14 -0
  295. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  296. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +25 -2
  297. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  298. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  299. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  300. package/out/zero-cache/src/services/view-syncer/row-set-signature.js.map +1 -1
  301. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  302. package/out/zero-cache/src/services/view-syncer/schema/init.js +113 -97
  303. package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
  304. package/out/zero-cache/src/services/view-syncer/schema/types.js +1 -103
  305. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  306. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  307. package/out/zero-cache/src/services/view-syncer/tracer.js.map +1 -1
  308. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
  309. package/out/zero-cache/src/services/view-syncer/view-syncer.js +1 -4
  310. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  311. package/out/zero-cache/src/types/configuration-error.js.map +1 -1
  312. package/out/zero-cache/src/types/error-with-level.js.map +1 -1
  313. package/out/zero-cache/src/types/http.js.map +1 -1
  314. package/out/zero-cache/src/types/lexi-version.js.map +1 -1
  315. package/out/zero-cache/src/types/lite.js.map +1 -1
  316. package/out/zero-cache/src/types/names.js.map +1 -1
  317. package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
  318. package/out/zero-cache/src/types/pg.js.map +1 -1
  319. package/out/zero-cache/src/types/processes.js.map +1 -1
  320. package/out/zero-cache/src/types/profiler.js.map +1 -1
  321. package/out/zero-cache/src/types/row-key.js.map +1 -1
  322. package/out/zero-cache/src/types/shards.js.map +1 -1
  323. package/out/zero-cache/src/types/sql.js.map +1 -1
  324. package/out/zero-cache/src/types/state-version.js.map +1 -1
  325. package/out/zero-cache/src/types/streams.js.map +1 -1
  326. package/out/zero-cache/src/types/strings.js.map +1 -1
  327. package/out/zero-cache/src/types/subscription.js.map +1 -1
  328. package/out/zero-cache/src/types/timeout.js.map +1 -1
  329. package/out/zero-cache/src/types/url-params.js.map +1 -1
  330. package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
  331. package/out/zero-cache/src/types/ws.js.map +1 -1
  332. package/out/zero-cache/src/workers/connect-params.js.map +1 -1
  333. package/out/zero-cache/src/workers/connection.js.map +1 -1
  334. package/out/zero-cache/src/workers/mutator.js.map +1 -1
  335. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  336. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  337. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  338. package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
  339. package/out/zero-client/src/client/connection-manager.js +1 -2
  340. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  341. package/out/zero-client/src/client/connection.js.map +1 -1
  342. package/out/zero-client/src/client/context.js.map +1 -1
  343. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  344. package/out/zero-client/src/client/crud.js.map +1 -1
  345. package/out/zero-client/src/client/custom.js +1 -2
  346. package/out/zero-client/src/client/custom.js.map +1 -1
  347. package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
  348. package/out/zero-client/src/client/enable-analytics.js.map +1 -1
  349. package/out/zero-client/src/client/error.js.map +1 -1
  350. package/out/zero-client/src/client/http-string.js.map +1 -1
  351. package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
  352. package/out/zero-client/src/client/inspector/client.js.map +1 -1
  353. package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
  354. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  355. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  356. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  357. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  358. package/out/zero-client/src/client/keys.js.map +1 -1
  359. package/out/zero-client/src/client/log-options.js.map +1 -1
  360. package/out/zero-client/src/client/make-mutate-property.js.map +1 -1
  361. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  362. package/out/zero-client/src/client/metrics.js.map +1 -1
  363. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  364. package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
  365. package/out/zero-client/src/client/options.js.map +1 -1
  366. package/out/zero-client/src/client/query-manager.js.map +1 -1
  367. package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
  368. package/out/zero-client/src/client/server-option.js.map +1 -1
  369. package/out/zero-client/src/client/version.js +1 -1
  370. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  371. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  372. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  373. package/out/zero-client/src/client/zero.js +32 -58
  374. package/out/zero-client/src/client/zero.js.map +1 -1
  375. package/out/zero-client/src/util/nanoid.js.map +1 -1
  376. package/out/zero-client/src/util/socket.d.ts +3 -0
  377. package/out/zero-client/src/util/socket.d.ts.map +1 -0
  378. package/out/zero-client/src/util/socket.js +8 -0
  379. package/out/zero-client/src/util/socket.js.map +1 -0
  380. package/out/zero-protocol/src/analyze-query-result.js +0 -3
  381. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  382. package/out/zero-protocol/src/application-error.js.map +1 -1
  383. package/out/zero-protocol/src/ast.js.map +1 -1
  384. package/out/zero-protocol/src/change-desired-queries.js +0 -1
  385. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  386. package/out/zero-protocol/src/client-schema.js.map +1 -1
  387. package/out/zero-protocol/src/close-connection.js.map +1 -1
  388. package/out/zero-protocol/src/connect.js +0 -7
  389. package/out/zero-protocol/src/connect.js.map +1 -1
  390. package/out/zero-protocol/src/custom-queries.js.map +1 -1
  391. package/out/zero-protocol/src/data.js.map +1 -1
  392. package/out/zero-protocol/src/delete-clients.js.map +1 -1
  393. package/out/zero-protocol/src/down.js.map +1 -1
  394. package/out/zero-protocol/src/error.js +0 -7
  395. package/out/zero-protocol/src/error.js.map +1 -1
  396. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  397. package/out/zero-protocol/src/inspect-up.js +0 -1
  398. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  399. package/out/zero-protocol/src/mutate-server.js.map +1 -1
  400. package/out/zero-protocol/src/mutation-id.js.map +1 -1
  401. package/out/zero-protocol/src/mutation.js.map +1 -1
  402. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  403. package/out/zero-protocol/src/ping.js.map +1 -1
  404. package/out/zero-protocol/src/poke.js +0 -4
  405. package/out/zero-protocol/src/poke.js.map +1 -1
  406. package/out/zero-protocol/src/pong.js.map +1 -1
  407. package/out/zero-protocol/src/primary-key.js.map +1 -1
  408. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  409. package/out/zero-protocol/src/pull.js.map +1 -1
  410. package/out/zero-protocol/src/push.js +0 -16
  411. package/out/zero-protocol/src/push.js.map +1 -1
  412. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  413. package/out/zero-protocol/src/query-hash.js.map +1 -1
  414. package/out/zero-protocol/src/query-server.js.map +1 -1
  415. package/out/zero-protocol/src/row-patch.js.map +1 -1
  416. package/out/zero-protocol/src/up.js.map +1 -1
  417. package/out/zero-protocol/src/update-auth.js.map +1 -1
  418. package/out/zero-protocol/src/version.js.map +1 -1
  419. package/out/zero-react/src/use-connection-state.js +2 -4
  420. package/out/zero-react/src/use-connection-state.js.map +1 -1
  421. package/out/zero-react/src/use-query.js +4 -6
  422. package/out/zero-react/src/use-query.js.map +1 -1
  423. package/out/zero-react/src/use-zero-online.js +2 -4
  424. package/out/zero-react/src/use-zero-online.js.map +1 -1
  425. package/out/zero-react/src/zero-provider.js +12 -15
  426. package/out/zero-react/src/zero-provider.js.map +1 -1
  427. package/out/zero-schema/src/builder/relationship-builder.js.map +1 -1
  428. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  429. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  430. package/out/zero-schema/src/compiled-permissions.js.map +1 -1
  431. package/out/zero-schema/src/name-mapper.js.map +1 -1
  432. package/out/zero-schema/src/permissions.js.map +1 -1
  433. package/out/zero-schema/src/schema-config.js.map +1 -1
  434. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  435. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  436. package/out/zero-server/src/adapters/pg.js +1 -1
  437. package/out/zero-server/src/adapters/pg.js.map +1 -1
  438. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  439. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  440. package/out/zero-server/src/custom.js +1 -2
  441. package/out/zero-server/src/custom.js.map +1 -1
  442. package/out/zero-server/src/logging.js.map +1 -1
  443. package/out/zero-server/src/pg-query-executor.js.map +1 -1
  444. package/out/zero-server/src/process-mutations.js.map +1 -1
  445. package/out/zero-server/src/push-processor.js.map +1 -1
  446. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  447. package/out/zero-server/src/schema.js.map +1 -1
  448. package/out/zero-server/src/zql-database.js.map +1 -1
  449. package/out/zero-solid/src/solid-view.js +1 -1
  450. package/out/zero-solid/src/solid-view.js.map +1 -1
  451. package/out/zero-solid/src/use-connection-state.js +1 -1
  452. package/out/zero-solid/src/use-connection-state.js.map +1 -1
  453. package/out/zero-solid/src/use-query.js +2 -2
  454. package/out/zero-solid/src/use-query.js.map +1 -1
  455. package/out/zero-solid/src/use-zero-online.js +1 -1
  456. package/out/zero-solid/src/use-zero-online.js.map +1 -1
  457. package/out/zero-solid/src/use-zero.js +1 -1
  458. package/out/zero-solid/src/use-zero.js.map +1 -1
  459. package/out/zero-types/src/format.js.map +1 -1
  460. package/out/zero-types/src/name-mapper.js.map +1 -1
  461. package/out/zql/src/builder/builder.js.map +1 -1
  462. package/out/zql/src/builder/debug-delegate.d.ts +0 -5
  463. package/out/zql/src/builder/debug-delegate.d.ts.map +1 -1
  464. package/out/zql/src/builder/debug-delegate.js +1 -10
  465. package/out/zql/src/builder/debug-delegate.js.map +1 -1
  466. package/out/zql/src/builder/filter.js.map +1 -1
  467. package/out/zql/src/builder/like.js.map +1 -1
  468. package/out/zql/src/error.js.map +1 -1
  469. package/out/zql/src/ivm/array-view.js.map +1 -1
  470. package/out/zql/src/ivm/cap.js.map +1 -1
  471. package/out/zql/src/ivm/change.js.map +1 -1
  472. package/out/zql/src/ivm/constraint.js +1 -1
  473. package/out/zql/src/ivm/constraint.js.map +1 -1
  474. package/out/zql/src/ivm/data.js.map +1 -1
  475. package/out/zql/src/ivm/exists.js.map +1 -1
  476. package/out/zql/src/ivm/fan-in.js.map +1 -1
  477. package/out/zql/src/ivm/fan-out.js.map +1 -1
  478. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  479. package/out/zql/src/ivm/filter-push.js.map +1 -1
  480. package/out/zql/src/ivm/filter.js.map +1 -1
  481. package/out/zql/src/ivm/flipped-join.d.ts +8 -4
  482. package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
  483. package/out/zql/src/ivm/flipped-join.js +63 -59
  484. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  485. package/out/zql/src/ivm/join-utils.js.map +1 -1
  486. package/out/zql/src/ivm/join.js.map +1 -1
  487. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
  488. package/out/zql/src/ivm/memory-source.js.map +1 -1
  489. package/out/zql/src/ivm/memory-storage.js.map +1 -1
  490. package/out/zql/src/ivm/operator.d.ts +1 -1
  491. package/out/zql/src/ivm/operator.js.map +1 -1
  492. package/out/zql/src/ivm/push-accumulated.js.map +1 -1
  493. package/out/zql/src/ivm/schema.d.ts +8 -0
  494. package/out/zql/src/ivm/schema.d.ts.map +1 -1
  495. package/out/zql/src/ivm/skip-yields.js.map +1 -1
  496. package/out/zql/src/ivm/skip.js.map +1 -1
  497. package/out/zql/src/ivm/source.js.map +1 -1
  498. package/out/zql/src/ivm/stream.js.map +1 -1
  499. package/out/zql/src/ivm/take.js.map +1 -1
  500. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  501. package/out/zql/src/ivm/union-fan-out.js.map +1 -1
  502. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  503. package/out/zql/src/mutate/crud.js.map +1 -1
  504. package/out/zql/src/mutate/custom.js.map +1 -1
  505. package/out/zql/src/mutate/mutator-registry.js.map +1 -1
  506. package/out/zql/src/mutate/mutator.js.map +1 -1
  507. package/out/zql/src/planner/planner-builder.js.map +1 -1
  508. package/out/zql/src/planner/planner-connection.js.map +1 -1
  509. package/out/zql/src/planner/planner-constraint.js.map +1 -1
  510. package/out/zql/src/planner/planner-debug.js.map +1 -1
  511. package/out/zql/src/planner/planner-fan-in.js.map +1 -1
  512. package/out/zql/src/planner/planner-fan-out.js.map +1 -1
  513. package/out/zql/src/planner/planner-graph.js.map +1 -1
  514. package/out/zql/src/planner/planner-join.d.ts.map +1 -1
  515. package/out/zql/src/planner/planner-join.js +1 -2
  516. package/out/zql/src/planner/planner-join.js.map +1 -1
  517. package/out/zql/src/planner/planner-node.js.map +1 -1
  518. package/out/zql/src/planner/planner-source.js.map +1 -1
  519. package/out/zql/src/planner/planner-terminus.js.map +1 -1
  520. package/out/zql/src/query/complete-ordering.js.map +1 -1
  521. package/out/zql/src/query/create-builder.js.map +1 -1
  522. package/out/zql/src/query/error.js.map +1 -1
  523. package/out/zql/src/query/escape-like.js.map +1 -1
  524. package/out/zql/src/query/expression.js.map +1 -1
  525. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  526. package/out/zql/src/query/metrics-delegate.js.map +1 -1
  527. package/out/zql/src/query/named.js.map +1 -1
  528. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  529. package/out/zql/src/query/query-impl.js +1 -1
  530. package/out/zql/src/query/query-impl.js.map +1 -1
  531. package/out/zql/src/query/query-internals.js.map +1 -1
  532. package/out/zql/src/query/query-registry.js.map +1 -1
  533. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  534. package/out/zql/src/query/static-query.js.map +1 -1
  535. package/out/zql/src/query/ttl.js.map +1 -1
  536. package/out/zql/src/query/validate-input.js.map +1 -1
  537. package/out/zqlite/src/database-storage.js.map +1 -1
  538. package/out/zqlite/src/db.js.map +1 -1
  539. package/out/zqlite/src/explain-queries.js.map +1 -1
  540. package/out/zqlite/src/internal/sql-inline.js.map +1 -1
  541. package/out/zqlite/src/internal/sql.js.map +1 -1
  542. package/out/zqlite/src/internal/statement-cache.js.map +1 -1
  543. package/out/zqlite/src/query-builder.js.map +1 -1
  544. package/out/zqlite/src/query-delegate.js.map +1 -1
  545. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  546. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  547. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  548. package/out/zqlite/src/table-source.d.ts.map +1 -1
  549. package/out/zqlite/src/table-source.js +6 -6
  550. package/out/zqlite/src/table-source.js.map +1 -1
  551. package/package.json +23 -23
  552. package/out/_virtual/__vite-optional-peer-dep_pg-native_pg.js +0 -13
  553. package/out/_virtual/__vite-optional-peer-dep_pg-native_pg.js.map +0 -1
  554. package/out/node_modules/.pnpm/@opentelemetry_semantic-conventions@1.41.1/node_modules/@opentelemetry/semantic-conventions/build/esm/stable_attributes.js +0 -12
  555. package/out/node_modules/.pnpm/@opentelemetry_semantic-conventions@1.41.1/node_modules/@opentelemetry/semantic-conventions/build/esm/stable_attributes.js.map +0 -1
  556. package/out/node_modules/.pnpm/pg-cloudflare@1.3.0/node_modules/pg-cloudflare/dist/empty.js +0 -11
  557. package/out/node_modules/.pnpm/pg-cloudflare@1.3.0/node_modules/pg-cloudflare/dist/empty.js.map +0 -1
  558. package/out/node_modules/.pnpm/pg-connection-string@2.12.0/node_modules/pg-connection-string/index.js +0 -130
  559. package/out/node_modules/.pnpm/pg-connection-string@2.12.0/node_modules/pg-connection-string/index.js.map +0 -1
  560. package/out/node_modules/.pnpm/pg-int8@1.0.1/node_modules/pg-int8/index.js +0 -62
  561. package/out/node_modules/.pnpm/pg-int8@1.0.1/node_modules/pg-int8/index.js.map +0 -1
  562. package/out/node_modules/.pnpm/pg-pool@3.13.0_pg@8.20.0/node_modules/pg-pool/index.js +0 -353
  563. package/out/node_modules/.pnpm/pg-pool@3.13.0_pg@8.20.0/node_modules/pg-pool/index.js.map +0 -1
  564. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/buffer-reader.js +0 -60
  565. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/buffer-reader.js.map +0 -1
  566. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/buffer-writer.js +0 -81
  567. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/buffer-writer.js.map +0 -1
  568. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/index.js +0 -35
  569. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/index.js.map +0 -1
  570. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/messages.js +0 -167
  571. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/messages.js.map +0 -1
  572. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/parser.js +0 -288
  573. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/parser.js.map +0 -1
  574. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/serializer.js +0 -177
  575. package/out/node_modules/.pnpm/pg-protocol@1.13.0/node_modules/pg-protocol/dist/serializer.js.map +0 -1
  576. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/index.js +0 -46
  577. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/index.js.map +0 -1
  578. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/arrayParser.js +0 -16
  579. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/arrayParser.js.map +0 -1
  580. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/binaryParsers.js +0 -165
  581. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/binaryParsers.js.map +0 -1
  582. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/builtins.js +0 -81
  583. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/builtins.js.map +0 -1
  584. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/textParsers.js +0 -167
  585. package/out/node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/textParsers.js.map +0 -1
  586. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/esm/index.js +0 -19
  587. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/esm/index.js.map +0 -1
  588. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/client.js +0 -508
  589. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/client.js.map +0 -1
  590. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/connection-parameters.js +0 -104
  591. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/connection-parameters.js.map +0 -1
  592. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/connection.js +0 -160
  593. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/connection.js.map +0 -1
  594. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/cert-signatures.js +0 -97
  595. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/cert-signatures.js.map +0 -1
  596. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/sasl.js +0 -131
  597. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/sasl.js.map +0 -1
  598. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/utils-legacy.js +0 -39
  599. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/utils-legacy.js.map +0 -1
  600. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/utils-webcrypto.js +0 -89
  601. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/utils-webcrypto.js.map +0 -1
  602. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/utils.js +0 -13
  603. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/crypto/utils.js.map +0 -1
  604. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/defaults.js +0 -46
  605. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/defaults.js.map +0 -1
  606. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/index.js +0 -71
  607. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/index.js.map +0 -1
  608. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/native/client.js +0 -226
  609. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/native/client.js.map +0 -1
  610. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/native/index.js +0 -11
  611. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/native/index.js.map +0 -1
  612. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/native/query.js +0 -117
  613. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/native/query.js.map +0 -1
  614. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/query.js +0 -151
  615. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/query.js.map +0 -1
  616. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/result.js +0 -76
  617. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/result.js.map +0 -1
  618. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/stream.js +0 -73
  619. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/stream.js.map +0 -1
  620. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/type-overrides.js +0 -35
  621. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/type-overrides.js.map +0 -1
  622. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/utils.js +0 -118
  623. package/out/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/utils.js.map +0 -1
  624. package/out/node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/helper.js +0 -147
  625. package/out/node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/helper.js.map +0 -1
  626. package/out/node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/index.js +0 -21
  627. package/out/node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/index.js.map +0 -1
  628. package/out/node_modules/.pnpm/postgres-array@2.0.0/node_modules/postgres-array/index.js +0 -84
  629. package/out/node_modules/.pnpm/postgres-array@2.0.0/node_modules/postgres-array/index.js.map +0 -1
  630. package/out/node_modules/.pnpm/postgres-bytea@1.0.1/node_modules/postgres-bytea/index.js +0 -28
  631. package/out/node_modules/.pnpm/postgres-bytea@1.0.1/node_modules/postgres-bytea/index.js.map +0 -1
  632. package/out/node_modules/.pnpm/postgres-date@1.0.7/node_modules/postgres-date/index.js +0 -65
  633. package/out/node_modules/.pnpm/postgres-date@1.0.7/node_modules/postgres-date/index.js.map +0 -1
  634. package/out/node_modules/.pnpm/postgres-interval@1.2.0/node_modules/postgres-interval/index.js +0 -107
  635. package/out/node_modules/.pnpm/postgres-interval@1.2.0/node_modules/postgres-interval/index.js.map +0 -1
  636. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.js +0 -696
  637. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
  638. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.js +0 -44
  639. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.js.map +0 -1
  640. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.development.js +0 -1585
  641. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.development.js.map +0 -1
  642. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js +0 -329
  643. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js.map +0 -1
  644. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/index.js +0 -13
  645. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/index.js.map +0 -1
  646. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js +0 -13
  647. package/out/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js.map +0 -1
  648. package/out/node_modules/.pnpm/solid-js@1.9.13/node_modules/solid-js/dist/server.js +0 -131
  649. package/out/node_modules/.pnpm/solid-js@1.9.13/node_modules/solid-js/dist/server.js.map +0 -1
  650. package/out/node_modules/.pnpm/solid-js@1.9.13/node_modules/solid-js/store/dist/server.js +0 -96
  651. package/out/node_modules/.pnpm/solid-js@1.9.13/node_modules/solid-js/store/dist/server.js.map +0 -1
  652. package/out/node_modules/.pnpm/split2@4.2.0/node_modules/split2/index.js +0 -95
  653. package/out/node_modules/.pnpm/split2@4.2.0/node_modules/split2/index.js.map +0 -1
  654. package/out/node_modules/.pnpm/xtend@4.0.2/node_modules/xtend/mutable.js +0 -18
  655. package/out/node_modules/.pnpm/xtend@4.0.2/node_modules/xtend/mutable.js.map +0 -1
  656. package/out/shared/src/ring-buffer.d.ts +0 -32
  657. package/out/shared/src/ring-buffer.d.ts.map +0 -1
  658. package/out/shared/src/ring-buffer.js +0 -109
  659. package/out/shared/src/ring-buffer.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"tables.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-streamer/schema/tables.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {ident} from 'pg-format';\nimport type postgres from 'postgres';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../../shared/src/abort-error.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport {type PostgresDB} from '../../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../../types/shards.ts';\nimport type {\n BackfillID,\n Change,\n TableMetadata,\n} from '../../change-source/protocol/current/data.ts';\nimport type {SubscriptionState} from '../../replicator/schema/replication-state.ts';\n\n// For readability in the sql statements.\nfunction schema(shard: ShardID) {\n return ident(cdcSchema(shard));\n}\n\nexport const PG_SCHEMA = 'cdc';\n\nfunction createSchema(shard: ShardID) {\n return /*sql*/ `CREATE SCHEMA IF NOT EXISTS ${schema(shard)};`;\n}\n\nexport type ChangeLogEntry = {\n // A strictly monotonically increasing, lexicographically sortable\n // value that uniquely identifies a position in the change stream.\n watermark: string;\n change: Change;\n};\n\ntype FullChangeLogEntry = ChangeLogEntry & {pos: number};\n\nfunction createChangeLogTable(shard: ShardID) {\n // Note: The \"change\" column used to be JSONB, but that was problematic in that\n // it does not handle the NULL unicode character.\n // https://vladimir.varank.in/notes/2021/01/you-dont-insert-unicode-null-character-as-postgres-jsonb/\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"changeLog\" (\n watermark TEXT,\n pos INT8,\n change JSON NOT NULL,\n precommit TEXT, -- Only exists on commit entries. Purely for debugging.\n PRIMARY KEY (watermark, pos)\n );\n`;\n}\n\n/**\n * Tracks the watermark from which to resume the change stream and the\n * current owner (task ID) acting as the single writer to the changeLog.\n */\nexport type ReplicationState = {\n lastWatermark: string;\n owner: string | null;\n ownerAddress: string | null;\n};\n\nexport function createReplicationStateTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationState\" (\n \"lastWatermark\" TEXT NOT NULL,\n \"owner\" TEXT,\n \"ownerAddress\" TEXT,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport async function discoverChangeStreamerAddress(\n shard: ShardID,\n sql: PostgresDB,\n): Promise<string | null> {\n const result = await sql<{ownerAddress: string | null}[]> /*sql*/ `\n SELECT \"ownerAddress\" FROM ${sql(cdcSchema(shard))}.\"replicationState\"`;\n return result[0].ownerAddress;\n}\n\n/**\n * This mirrors the analogously named table in the SQLite replica\n * (`services/replicator/schema/replication-state.ts`), and is used\n * to detect when the replica has been reset and is no longer compatible\n * with the current ChangeLog.\n */\nexport type ReplicationConfig = {\n replicaVersion: string;\n publications: readonly string[];\n};\n\nfunction createReplicationConfigTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationConfig\" (\n \"replicaVersion\" TEXT NOT NULL,\n \"publications\" TEXT[] NOT NULL,\n \"resetRequired\" BOOL,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport function createBackfillTables(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"tableMetadata\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"metadata\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\")\n );\n\n CREATE TABLE ${schema(shard)}.\"backfilling\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"column\" TEXT NOT NULL,\n \"backfill\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\", \"column\")\n );\n `;\n}\n\nexport type TableMetadataRow = {\n schema: string;\n table: string;\n metadata: TableMetadata;\n};\n\nexport type BackfillingColumn = {\n schema: string;\n table: string;\n column: string;\n backfill: BackfillID;\n};\n\nfunction createTables(shard: ShardID) {\n return (\n createSchema(shard) +\n createChangeLogTable(shard) +\n createReplicationStateTable(shard) +\n createReplicationConfigTable(shard) +\n createBackfillTables(shard)\n );\n}\n\nexport async function setupCDCTables(\n lc: LogContext,\n db: postgres.TransactionSql,\n shard: ShardID,\n) {\n lc.info?.(`Setting up CDC tables`);\n await db.unsafe(createTables(shard));\n}\n\nexport async function markResetRequired(sql: PostgresDB, shard: ShardID) {\n const schema = cdcSchema(shard);\n await sql`\n UPDATE ${sql(schema)}.\"replicationConfig\"\n SET \"resetRequired\" = true`;\n}\n\nexport async function ensureReplicationConfig(\n lc: LogContext,\n db: PostgresDB,\n subscriptionState: Pick<\n SubscriptionState,\n 'publications' | 'replicaVersion' | 'watermark'\n >,\n shard: ShardID,\n autoReset: boolean,\n setTimeoutFn: typeof setTimeout = setTimeout,\n) {\n const {publications, replicaVersion, watermark} = subscriptionState;\n const replicaConfig = {publications, replicaVersion};\n const replicationState: ReplicationState = {\n lastWatermark: replicaVersion,\n owner: null,\n ownerAddress: null,\n };\n const schema = cdcSchema(shard);\n\n await runTx(db, async sql => {\n const stmts: PendingQuery<Row[]>[] = [];\n let needsTruncate = false;\n const results = await sql<\n {\n replicaVersion: string;\n publications: string[];\n resetRequired: boolean | null;\n }[]\n > /*sql*/ `\n SELECT \"replicaVersion\", \"publications\", \"resetRequired\" \n FROM ${sql(schema)}.\"replicationConfig\"`;\n\n if (results.length) {\n const {replicaVersion, publications} = results[0];\n if (\n replicaVersion !== replicaConfig.replicaVersion ||\n !equals(new Set(publications), new Set(replicaConfig.publications))\n ) {\n if (replicaConfig.replicaVersion !== watermark) {\n throw new AutoResetSignal(\n `Cannot reset change db@${replicaVersion} to ` +\n `service replica@${replicaConfig.replicaVersion} ` +\n `from watermark ${watermark}`,\n );\n }\n lc.info?.(\n `Data in cdc tables @${replicaVersion} is incompatible ` +\n `with replica @${replicaConfig.replicaVersion}. Clearing tables.`,\n );\n // Note: The order of TRUNCATE matters. The replicationState table is\n // truncated before the changeLog table, in order to acquire\n // (exclusive) locks on the tables in the same order that the\n // storer.ts acquires locks when writing the changeLog, namely:\n //\n // 1. SELECT ... FROM replicationState FOR UPDATE;\n // 2. INSERT INTO changeLog ...;\n // ...\n // n. UPDATE replicationState ...;\n needsTruncate = true;\n stmts.push(\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationState\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"changeLog\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationConfig\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"tableMetadata\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"backfilling\"`,\n );\n }\n }\n // Initialize (or re-initialize TRUNCATED) tables\n if (results.length === 0 || needsTruncate) {\n // The storer uses the earliest changeLog entry as the safe watermark\n // from which subscribers can be resumed. These initial entries ensure\n // that subscribers can start from a freshly synced replica, even if\n // new changes have been replicated and not purged from the changeLog.\n //\n // TODO: Replace this with an explicit `firstWatermark` column in the\n // change db.\n const watermark = replicaConfig.replicaVersion;\n const initialTx: FullChangeLogEntry[] = [\n {watermark, pos: 0, change: {tag: 'begin'}},\n {watermark, pos: 1, change: {tag: 'commit'}},\n ];\n\n stmts.push(\n sql`INSERT INTO ${sql(schema)}.\"replicationConfig\" ${sql(replicaConfig)}`,\n sql`INSERT INTO ${sql(schema)}.\"replicationState\" ${sql(replicationState)} \n ON CONFLICT (lock) DO UPDATE SET ${sql(replicationState)}`,\n ...initialTx.map(\n change => sql`INSERT INTO ${sql(schema)}.\"changeLog\" ${sql(change)}`,\n ),\n );\n\n if (needsTruncate) {\n // The TRUNCATE statements require ACCESS EXCLUSIVE locks, which may\n // be blocked by old storer catchup reads. Race against a timeout\n // that terminates the blocking backends if the TRUNCATE takes too\n // long.\n const timer = setTimeoutFn(async () => {\n lc.info?.(\n 'ensureReplicationConfig blocked, terminating lock holders',\n );\n await terminateChangeDBLockHolders(lc, db, shard);\n }, LOCK_HOLDER_TERMINATE_TIMEOUT_MS);\n try {\n return await Promise.all(stmts);\n } finally {\n clearTimeout(timer);\n }\n }\n return Promise.all(stmts);\n }\n\n const {resetRequired} = results[0];\n if (resetRequired) {\n if (autoReset) {\n throw new AutoResetSignal('reset required by replication stream');\n }\n lc.error?.(\n '\\n\\n\\n' +\n 'Reset required but --auto-reset is not enabled.\\n' +\n 'This can happen for upstream databases that do not support event triggers.\\n' +\n 'To correct this, see https://zero.rocicorp.dev/docs/connecting-to-postgres#schema-changes' +\n '\\n\\n\\n',\n );\n }\n\n return [];\n });\n}\n\n// The time to wait for a TRUNCATE in ensureReplicationConfig before\n// terminating blocking backends via terminateChangeDBLockHolders.\nconst LOCK_HOLDER_TERMINATE_TIMEOUT_MS = 5_000;\n\nexport const CHANGE_STREAMER_APP_NAME = 'zero-change-streamer';\n\nexport class AutoResetSignal extends AbortError {\n readonly name = 'AutoResetSignal';\n}\n\n/**\n * Terminates zero-cache backends that are blocking the current backend\n * from acquiring locks on CDC tables (e.g., during TRUNCATE).\n *\n * This is used during change-DB takeover when the new replication-manager's\n * `ensureReplicationConfig` needs to TRUNCATE tables, but the old\n * replication-manager's storer is still reading from them (e.g., large\n * catchup cursors).\n *\n * The function:\n * 1. Finds backends waiting for a lock on a TRUNCATE in {schema}\n * 2. Uses `pg_blocking_pids()` to identify which backends are blocking them\n * 3. Terminates blocking backends that have `application_name = 'zero-change-streamer'`\n *\n * Must be called on a **separate connection** from the one that is blocked,\n * since the blocked connection is inside a pending transaction.\n */\nexport async function terminateChangeDBLockHolders(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n) {\n const schema = cdcSchema(shard);\n\n // Step 1: Find backends that are blocked waiting for a lock,\n // whose query involves a TRUNCATE on this shard's CDC schema.\n const blocked = await db<{pid: number}[]>`\n SELECT pid FROM pg_stat_activity\n WHERE wait_event_type = 'Lock'\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND query LIKE ${'%TRUNCATE%' + schema + '%'}`;\n\n if (blocked.length === 0) {\n lc.info?.('no blocked TRUNCATE backends found');\n return;\n }\n\n const blockedPids = blocked.map(r => r.pid);\n lc.info?.(`found blocked TRUNCATE backends: ${JSON.stringify(blockedPids)}`);\n\n // Step 2: For each blocked backend, find and terminate its blockers\n // that are zero-change-streamer connections.\n const terminated = await db<\n {pid: number; applicationName: string; query: string; terminated: boolean}[]\n >`\n SELECT pid, application_name as \"applicationName\", query,\n pg_terminate_backend(pid) as terminated\n FROM pg_stat_activity\n WHERE pid = ANY(\n SELECT unnest(pg_blocking_pids(blocked.pid))\n FROM unnest(${blockedPids}::int[]) AS blocked(pid)\n )\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND pid != ALL(${blockedPids}::int[])`;\n\n if (terminated.length === 0) {\n lc.info?.(`no ${CHANGE_STREAMER_APP_NAME} blockers found to terminate`);\n } else {\n for (const {pid, applicationName, query, terminated: ok} of terminated) {\n lc.info?.(\n `terminated blocking backend pid=${pid} app=${applicationName} ok=${ok} query=${query.slice(0, 200)}`,\n );\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tables.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-streamer/schema/tables.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {ident} from 'pg-format';\nimport type postgres from 'postgres';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../../shared/src/abort-error.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport {type PostgresDB} from '../../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../../types/shards.ts';\nimport type {\n BackfillID,\n Change,\n TableMetadata,\n} from '../../change-source/protocol/current/data.ts';\nimport type {SubscriptionState} from '../../replicator/schema/replication-state.ts';\n\n// For readability in the sql statements.\nfunction schema(shard: ShardID) {\n return ident(cdcSchema(shard));\n}\n\nexport const PG_SCHEMA = 'cdc';\n\nfunction createSchema(shard: ShardID) {\n return /*sql*/ `CREATE SCHEMA IF NOT EXISTS ${schema(shard)};`;\n}\n\nexport type ChangeLogEntry = {\n // A strictly monotonically increasing, lexicographically sortable\n // value that uniquely identifies a position in the change stream.\n watermark: string;\n change: Change;\n};\n\ntype FullChangeLogEntry = ChangeLogEntry & {pos: number};\n\nfunction createChangeLogTable(shard: ShardID) {\n // Note: The \"change\" column used to be JSONB, but that was problematic in that\n // it does not handle the NULL unicode character.\n // https://vladimir.varank.in/notes/2021/01/you-dont-insert-unicode-null-character-as-postgres-jsonb/\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"changeLog\" (\n watermark TEXT,\n pos INT8,\n change JSON NOT NULL,\n precommit TEXT, -- Only exists on commit entries. Purely for debugging.\n PRIMARY KEY (watermark, pos)\n );\n`;\n}\n\n/**\n * Tracks the watermark from which to resume the change stream and the\n * current owner (task ID) acting as the single writer to the changeLog.\n */\nexport type ReplicationState = {\n lastWatermark: string;\n owner: string | null;\n ownerAddress: string | null;\n};\n\nexport function createReplicationStateTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationState\" (\n \"lastWatermark\" TEXT NOT NULL,\n \"owner\" TEXT,\n \"ownerAddress\" TEXT,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport async function discoverChangeStreamerAddress(\n shard: ShardID,\n sql: PostgresDB,\n): Promise<string | null> {\n const result = await sql<{ownerAddress: string | null}[]> /*sql*/ `\n SELECT \"ownerAddress\" FROM ${sql(cdcSchema(shard))}.\"replicationState\"`;\n return result[0].ownerAddress;\n}\n\n/**\n * This mirrors the analogously named table in the SQLite replica\n * (`services/replicator/schema/replication-state.ts`), and is used\n * to detect when the replica has been reset and is no longer compatible\n * with the current ChangeLog.\n */\nexport type ReplicationConfig = {\n replicaVersion: string;\n publications: readonly string[];\n};\n\nfunction createReplicationConfigTable(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"replicationConfig\" (\n \"replicaVersion\" TEXT NOT NULL,\n \"publications\" TEXT[] NOT NULL,\n \"resetRequired\" BOOL,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)\n );\n`;\n}\n\nexport function createBackfillTables(shard: ShardID) {\n return /*sql*/ `\n CREATE TABLE ${schema(shard)}.\"tableMetadata\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"metadata\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\")\n );\n\n CREATE TABLE ${schema(shard)}.\"backfilling\" (\n \"schema\" TEXT NOT NULL,\n \"table\" TEXT NOT NULL,\n \"column\" TEXT NOT NULL,\n \"backfill\" JSONB NOT NULL,\n PRIMARY KEY(\"schema\", \"table\", \"column\")\n );\n `;\n}\n\nexport type TableMetadataRow = {\n schema: string;\n table: string;\n metadata: TableMetadata;\n};\n\nexport type BackfillingColumn = {\n schema: string;\n table: string;\n column: string;\n backfill: BackfillID;\n};\n\nfunction createTables(shard: ShardID) {\n return (\n createSchema(shard) +\n createChangeLogTable(shard) +\n createReplicationStateTable(shard) +\n createReplicationConfigTable(shard) +\n createBackfillTables(shard)\n );\n}\n\nexport async function setupCDCTables(\n lc: LogContext,\n db: postgres.TransactionSql,\n shard: ShardID,\n) {\n lc.info?.(`Setting up CDC tables`);\n await db.unsafe(createTables(shard));\n}\n\nexport async function markResetRequired(sql: PostgresDB, shard: ShardID) {\n const schema = cdcSchema(shard);\n await sql`\n UPDATE ${sql(schema)}.\"replicationConfig\"\n SET \"resetRequired\" = true`;\n}\n\nexport async function ensureReplicationConfig(\n lc: LogContext,\n db: PostgresDB,\n subscriptionState: Pick<\n SubscriptionState,\n 'publications' | 'replicaVersion' | 'watermark'\n >,\n shard: ShardID,\n autoReset: boolean,\n setTimeoutFn: typeof setTimeout = setTimeout,\n) {\n const {publications, replicaVersion, watermark} = subscriptionState;\n const replicaConfig = {publications, replicaVersion};\n const replicationState: ReplicationState = {\n lastWatermark: replicaVersion,\n owner: null,\n ownerAddress: null,\n };\n const schema = cdcSchema(shard);\n\n await runTx(db, async sql => {\n const stmts: PendingQuery<Row[]>[] = [];\n let needsTruncate = false;\n const results = await sql<\n {\n replicaVersion: string;\n publications: string[];\n resetRequired: boolean | null;\n }[]\n > /*sql*/ `\n SELECT \"replicaVersion\", \"publications\", \"resetRequired\" \n FROM ${sql(schema)}.\"replicationConfig\"`;\n\n if (results.length) {\n const {replicaVersion, publications} = results[0];\n if (\n replicaVersion !== replicaConfig.replicaVersion ||\n !equals(new Set(publications), new Set(replicaConfig.publications))\n ) {\n if (replicaConfig.replicaVersion !== watermark) {\n throw new AutoResetSignal(\n `Cannot reset change db@${replicaVersion} to ` +\n `service replica@${replicaConfig.replicaVersion} ` +\n `from watermark ${watermark}`,\n );\n }\n lc.info?.(\n `Data in cdc tables @${replicaVersion} is incompatible ` +\n `with replica @${replicaConfig.replicaVersion}. Clearing tables.`,\n );\n // Note: The order of TRUNCATE matters. The replicationState table is\n // truncated before the changeLog table, in order to acquire\n // (exclusive) locks on the tables in the same order that the\n // storer.ts acquires locks when writing the changeLog, namely:\n //\n // 1. SELECT ... FROM replicationState FOR UPDATE;\n // 2. INSERT INTO changeLog ...;\n // ...\n // n. UPDATE replicationState ...;\n needsTruncate = true;\n stmts.push(\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationState\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"changeLog\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"replicationConfig\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"tableMetadata\"`,\n sql`TRUNCATE TABLE ${sql(schema)}.\"backfilling\"`,\n );\n }\n }\n // Initialize (or re-initialize TRUNCATED) tables\n if (results.length === 0 || needsTruncate) {\n // The storer uses the earliest changeLog entry as the safe watermark\n // from which subscribers can be resumed. These initial entries ensure\n // that subscribers can start from a freshly synced replica, even if\n // new changes have been replicated and not purged from the changeLog.\n //\n // TODO: Replace this with an explicit `firstWatermark` column in the\n // change db.\n const watermark = replicaConfig.replicaVersion;\n const initialTx: FullChangeLogEntry[] = [\n {watermark, pos: 0, change: {tag: 'begin'}},\n {watermark, pos: 1, change: {tag: 'commit'}},\n ];\n\n stmts.push(\n sql`INSERT INTO ${sql(schema)}.\"replicationConfig\" ${sql(replicaConfig)}`,\n sql`INSERT INTO ${sql(schema)}.\"replicationState\" ${sql(replicationState)} \n ON CONFLICT (lock) DO UPDATE SET ${sql(replicationState)}`,\n ...initialTx.map(\n change => sql`INSERT INTO ${sql(schema)}.\"changeLog\" ${sql(change)}`,\n ),\n );\n\n if (needsTruncate) {\n // The TRUNCATE statements require ACCESS EXCLUSIVE locks, which may\n // be blocked by old storer catchup reads. Race against a timeout\n // that terminates the blocking backends if the TRUNCATE takes too\n // long.\n const timer = setTimeoutFn(async () => {\n lc.info?.(\n 'ensureReplicationConfig blocked, terminating lock holders',\n );\n await terminateChangeDBLockHolders(lc, db, shard);\n }, LOCK_HOLDER_TERMINATE_TIMEOUT_MS);\n try {\n return await Promise.all(stmts);\n } finally {\n clearTimeout(timer);\n }\n }\n return Promise.all(stmts);\n }\n\n const {resetRequired} = results[0];\n if (resetRequired) {\n if (autoReset) {\n throw new AutoResetSignal('reset required by replication stream');\n }\n lc.error?.(\n '\\n\\n\\n' +\n 'Reset required but --auto-reset is not enabled.\\n' +\n 'This can happen for upstream databases that do not support event triggers.\\n' +\n 'To correct this, see https://zero.rocicorp.dev/docs/connecting-to-postgres#schema-changes' +\n '\\n\\n\\n',\n );\n }\n\n return [];\n });\n}\n\n// The time to wait for a TRUNCATE in ensureReplicationConfig before\n// terminating blocking backends via terminateChangeDBLockHolders.\nconst LOCK_HOLDER_TERMINATE_TIMEOUT_MS = 5_000;\n\nexport const CHANGE_STREAMER_APP_NAME = 'zero-change-streamer';\n\nexport class AutoResetSignal extends AbortError {\n readonly name = 'AutoResetSignal';\n}\n\n/**\n * Terminates zero-cache backends that are blocking the current backend\n * from acquiring locks on CDC tables (e.g., during TRUNCATE).\n *\n * This is used during change-DB takeover when the new replication-manager's\n * `ensureReplicationConfig` needs to TRUNCATE tables, but the old\n * replication-manager's storer is still reading from them (e.g., large\n * catchup cursors).\n *\n * The function:\n * 1. Finds backends waiting for a lock on a TRUNCATE in {schema}\n * 2. Uses `pg_blocking_pids()` to identify which backends are blocking them\n * 3. Terminates blocking backends that have `application_name = 'zero-change-streamer'`\n *\n * Must be called on a **separate connection** from the one that is blocked,\n * since the blocked connection is inside a pending transaction.\n */\nexport async function terminateChangeDBLockHolders(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n) {\n const schema = cdcSchema(shard);\n\n // Step 1: Find backends that are blocked waiting for a lock,\n // whose query involves a TRUNCATE on this shard's CDC schema.\n const blocked = await db<{pid: number}[]>`\n SELECT pid FROM pg_stat_activity\n WHERE wait_event_type = 'Lock'\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND query LIKE ${'%TRUNCATE%' + schema + '%'}`;\n\n if (blocked.length === 0) {\n lc.info?.('no blocked TRUNCATE backends found');\n return;\n }\n\n const blockedPids = blocked.map(r => r.pid);\n lc.info?.(`found blocked TRUNCATE backends: ${JSON.stringify(blockedPids)}`);\n\n // Step 2: For each blocked backend, find and terminate its blockers\n // that are zero-change-streamer connections.\n const terminated = await db<\n {pid: number; applicationName: string; query: string; terminated: boolean}[]\n >`\n SELECT pid, application_name as \"applicationName\", query,\n pg_terminate_backend(pid) as terminated\n FROM pg_stat_activity\n WHERE pid = ANY(\n SELECT unnest(pg_blocking_pids(blocked.pid))\n FROM unnest(${blockedPids}::int[]) AS blocked(pid)\n )\n AND application_name = ${CHANGE_STREAMER_APP_NAME}\n AND pid != ALL(${blockedPids}::int[])`;\n\n if (terminated.length === 0) {\n lc.info?.(`no ${CHANGE_STREAMER_APP_NAME} blockers found to terminate`);\n } else {\n for (const {pid, applicationName, query, terminated: ok} of terminated) {\n lc.info?.(\n `terminated blocking backend pid=${pid} app=${applicationName} ok=${ok} query=${query.slice(0, 200)}`,\n );\n }\n }\n}\n"],"mappings":"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"}
@@ -15,23 +15,8 @@ import { valita_exports } from "../../../../shared/src/valita.js";
15
15
  */
16
16
  var statusSchema = valita_exports.object({
17
17
  tag: valita_exports.literal("status"),
18
- /**
19
- * The location from which litestream should perform the restore.
20
- */
21
18
  backupURL: valita_exports.string(),
22
- /**
23
- * The `replicaVersion` of the backup. If a subscriber's restored or
24
- * existing replica is of a different version, it should delete it and
25
- * retry the restore from litestream (i.e. equivalent to a
26
- * `WrongReplicaVersion` response from a `/changes` subscription).
27
- */
28
19
  replicaVersion: valita_exports.string(),
29
- /**
30
- * The earliest watermark from which catchup is possible. If the
31
- * subscriber's replica is older that this watermark, it should delete it
32
- * and (retry the) restore from litestream (i.e. equivalent to a
33
- * `WatermarkTooOld` response from a `/changes` subscription).
34
- */
35
20
  minWatermark: valita_exports.string()
36
21
  });
37
22
  var statusMessageSchema = valita_exports.tuple([valita_exports.literal("status"), statusSchema]);
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/snapshot.ts"],"sourcesContent":["/**\n * Definitions for the `snapshot` API, which serves the purpose of:\n * - informing subscribers (i.e. view-syncers) of the (litestream)\n * backup location from which to restore a replica snapshot\n * - checking whether a restored backup or existing replica is\n * compatible with the change-streamer\n * - preventing change-log cleanup while a snapshot restore is in\n * progress\n * - tracking the approximate time it takes from the beginning of\n * snapshot \"reservation\" to the subsequent subscription, which\n * serves as the minimum interval to wait before cleaning up\n * backed up changes.\n */\n\nimport * as v from '../../../../shared/src/valita.ts';\n\nconst statusSchema = v.object({\n tag: v.literal('status'),\n\n /**\n * The location from which litestream should perform the restore.\n */\n backupURL: v.string(),\n\n /**\n * The `replicaVersion` of the backup. If a subscriber's restored or\n * existing replica is of a different version, it should delete it and\n * retry the restore from litestream (i.e. equivalent to a\n * `WrongReplicaVersion` response from a `/changes` subscription).\n */\n replicaVersion: v.string(),\n\n /**\n * The earliest watermark from which catchup is possible. If the\n * subscriber's replica is older that this watermark, it should delete it\n * and (retry the) restore from litestream (i.e. equivalent to a\n * `WatermarkTooOld` response from a `/changes` subscription).\n */\n minWatermark: v.string(),\n});\n\nexport type SnapshotStatus = v.Infer<typeof statusSchema>;\n\nconst statusMessageSchema = v.tuple([v.literal('status'), statusSchema]);\n\nexport const snapshotMessageSchema = v.union(statusMessageSchema);\n\nexport type SnapshotMessage = v.Infer<typeof statusMessageSchema>;\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAM,eAAe,eAAE,OAAO;CAC5B,KAAK,eAAE,QAAQ,QAAQ;;;;CAKvB,WAAW,eAAE,OAAO;;;;;;;CAQpB,gBAAgB,eAAE,OAAO;;;;;;;CAQzB,cAAc,eAAE,OAAO;AACzB,CAAC;AAID,IAAM,sBAAsB,eAAE,MAAM,CAAC,eAAE,QAAQ,QAAQ,GAAG,YAAY,CAAC;AAEvE,IAAa,wBAAwB,eAAE,MAAM,mBAAmB"}
1
+ {"version":3,"file":"snapshot.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/snapshot.ts"],"sourcesContent":["/**\n * Definitions for the `snapshot` API, which serves the purpose of:\n * - informing subscribers (i.e. view-syncers) of the (litestream)\n * backup location from which to restore a replica snapshot\n * - checking whether a restored backup or existing replica is\n * compatible with the change-streamer\n * - preventing change-log cleanup while a snapshot restore is in\n * progress\n * - tracking the approximate time it takes from the beginning of\n * snapshot \"reservation\" to the subsequent subscription, which\n * serves as the minimum interval to wait before cleaning up\n * backed up changes.\n */\n\nimport * as v from '../../../../shared/src/valita.ts';\n\nconst statusSchema = v.object({\n tag: v.literal('status'),\n\n /**\n * The location from which litestream should perform the restore.\n */\n backupURL: v.string(),\n\n /**\n * The `replicaVersion` of the backup. If a subscriber's restored or\n * existing replica is of a different version, it should delete it and\n * retry the restore from litestream (i.e. equivalent to a\n * `WrongReplicaVersion` response from a `/changes` subscription).\n */\n replicaVersion: v.string(),\n\n /**\n * The earliest watermark from which catchup is possible. If the\n * subscriber's replica is older that this watermark, it should delete it\n * and (retry the) restore from litestream (i.e. equivalent to a\n * `WatermarkTooOld` response from a `/changes` subscription).\n */\n minWatermark: v.string(),\n});\n\nexport type SnapshotStatus = v.Infer<typeof statusSchema>;\n\nconst statusMessageSchema = v.tuple([v.literal('status'), statusSchema]);\n\nexport const snapshotMessageSchema = v.union(statusMessageSchema);\n\nexport type SnapshotMessage = v.Infer<typeof statusMessageSchema>;\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAM,eAAe,eAAE,OAAO;CAC5B,KAAK,eAAE,QAAQ,SAAS;CAKxB,WAAW,eAAE,QAAQ;CAQrB,gBAAgB,eAAE,QAAQ;CAQ1B,cAAc,eAAE,QAAQ;CACzB,CAAC;AAIF,IAAM,sBAAsB,eAAE,MAAM,CAAC,eAAE,QAAQ,SAAS,EAAE,aAAa,CAAC;AAExE,IAAa,wBAAwB,eAAE,MAAM,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"storer.js","names":["#lc","#shard","#taskID","#discoveryAddress","#discoveryProtocol","#db","#replicaVersion","#onConsumed","#onFatal","#queue","#backPressureThresholdBytes","#statementTimeoutMs","#cdc","#approximateQueuedBytes","#running","#readyForMore","#stopped","#processQueue","#cancelQueueEntries","#startCatchup","#trackBackfillMetadata","#maybeReleaseBackPressure","#catchup","#upsertTableMetadataStmt","#upsertColumnBackfillStmt","#tx","#released"],"sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"sourcesContent":["import {getHeapStatistics} from 'node:v8';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../types/shards.ts';\nimport {\n backfillRequestSchema,\n isDataChange,\n isSchemaChange,\n type BackfillID,\n type BackfillRequest,\n type Change,\n type DataChange,\n type Identifier,\n type SchemaChange,\n type TableMetadata,\n} from '../change-source/protocol/current.ts';\nimport {\n type ChangeStreamData,\n type Commit,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {\n DownstreamStatusMessage,\n UpstreamStatusMessage,\n} from '../change-source/protocol/current/status.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {Service} from '../service.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {\n AutoResetSignal,\n markResetRequired,\n type BackfillingColumn,\n type TableMetadataRow,\n} from './schema/tables.ts';\nimport type {Subscriber} from './subscriber.ts';\n\ntype SubscriberAndMode = {\n subscriber: Subscriber;\n mode: ReplicatorMode;\n};\n\ntype QueueEntry =\n | [\n 'change',\n watermark: string,\n json: string,\n orig: Exclude<Change, DataChange> | null, // null for DataChanges\n ]\n | ['ready', callback: () => void]\n | ['subscriber', SubscriberAndMode]\n | DownstreamStatusMessage\n | ['abort']\n | 'stop';\n\ntype PendingTransaction = {\n pool: TransactionPool;\n preCommitWatermark: string;\n pos: number;\n startingReplicationState: Promise<ReplicationOwner>;\n ack: boolean;\n};\n\ntype ReplicationOwner = {\n owner: string | null;\n};\n\nconst backfillRequestsSchema = v.array(backfillRequestSchema);\n\nexport type TuningOptions = {\n backPressureLimitHeapProportion: number;\n statementTimeoutMs: number;\n};\n\n/**\n * Handles the storage of changes and the catchup of subscribers\n * that are behind.\n *\n * In the context of catchup and cleanup, it is the responsibility of the\n * Storer to decide whether a client can be caught up, or whether the\n * changes needed to catch a client up have been purged.\n *\n * **Maintained invariant**: The Change DB is only empty for a\n * completely new replica (i.e. initial-sync with no changes from the\n * replication stream).\n * * In this case, all new subscribers are expected start from the\n * `replicaVersion`, which is the version at which initial sync\n * was performed, and any attempts to catchup from a different\n * point fail.\n *\n * Conversely, if non-initial changes have flowed through the system\n * (i.e. via the replication stream), the ChangeDB must *not* be empty,\n * and the earliest change in the `changeLog` represents the earliest\n * \"commit\" from (after) which a subscriber can be caught up.\n * * Any attempts to catchup from an earlier point must fail with\n * a `WatermarkTooOld` error.\n * * Failure to do so could result in streaming changes to the\n * subscriber such that there is a gap in its replication history.\n *\n * Note: Subscribers (i.e. `incremental-syncer`) consider an \"error\" signal\n * an unrecoverable error and shut down in response. This allows the\n * production system to replace it with a new task and fresh copy of the\n * replica backup.\n */\nexport class Storer implements Service {\n readonly id = 'storer';\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #taskID: string;\n readonly #discoveryAddress: string;\n readonly #discoveryProtocol: string;\n readonly #db: PostgresDB;\n readonly #replicaVersion: string;\n readonly #onConsumed: (c: Commit | UpstreamStatusMessage) => void;\n readonly #onFatal: (err: Error) => void;\n readonly #queue = new Queue<QueueEntry>();\n readonly #backPressureThresholdBytes: number;\n readonly #statementTimeoutMs: number;\n\n #approximateQueuedBytes = 0;\n #running = false;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n db: PostgresDB,\n replicaVersion: string,\n onConsumed: (c: Commit | UpstreamStatusMessage) => void,\n onFatal: (err: Error) => void,\n {backPressureLimitHeapProportion, statementTimeoutMs}: TuningOptions,\n ) {\n this.#lc = lc.withContext('component', 'change-log');\n this.#shard = shard;\n this.#taskID = taskID;\n this.#discoveryAddress = discoveryAddress;\n this.#discoveryProtocol = discoveryProtocol;\n this.#db = db;\n this.#replicaVersion = replicaVersion;\n this.#onConsumed = onConsumed;\n this.#onFatal = onFatal;\n this.#statementTimeoutMs = statementTimeoutMs;\n\n const heapStats = getHeapStatistics();\n this.#backPressureThresholdBytes =\n (heapStats.heap_size_limit - heapStats.used_heap_size) *\n backPressureLimitHeapProportion;\n\n this.#lc.info?.(\n `Using up to ${(this.#backPressureThresholdBytes / 1024 ** 2).toFixed(2)} MB of ` +\n `--max-old-space-size (~${(heapStats.heap_size_limit / 1024 ** 2).toFixed(2)} MB) ` +\n `to absorb upstream spikes`,\n {heapStats},\n );\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async assumeOwnership(purgeLock?: PurgeLock | null) {\n const db = this.#db;\n const owner = this.#taskID;\n const ownerAddress = this.#discoveryAddress;\n const ownerProtocol = this.#discoveryProtocol;\n // we omit `ws://` so that old view syncer versions that are not expecting the protocol continue to not get it\n const addressWithProtocol =\n ownerProtocol === 'ws'\n ? ownerAddress\n : `${ownerProtocol}://${ownerAddress}`;\n this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);\n const start = performance.now();\n await db`UPDATE ${this.#cdc('replicationState')} SET ${db({owner, ownerAddress: addressWithProtocol})}`;\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `assumed ownership at ${addressWithProtocol} (${elapsed} ms)`,\n );\n\n if (purgeLock) {\n // Once ownership has been assumed, any initial purge-lock preventing the\n // purging of change-log records can be released, as a change-streamer\n // that was attempting to purge records will correspondingly abort on the\n // ownership check.\n void purgeLock.release();\n }\n }\n\n async getStartStreamInitializationParameters(): Promise<{\n lastWatermark: string;\n backfillRequests: BackfillRequest[];\n }> {\n const [[{lastWatermark}], result] = await runTx(\n this.#db,\n sql => [\n sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}`,\n\n // Formats a BackfillRequest using json_object_agg() to construct the\n // `columns` object. It is LEFT JOIN'ed with the `tableMetadata` table\n // to make it optional and possibly `null`.\n sql`\n SELECT \n json_build_object(\n 'schema', b.\"schema\",\n 'name', b.\"table\",\n 'metadata', t.\"metadata\"\n ) as \"table\",\n json_object_agg(b.\"column\", b.\"backfill\") \n as \"columns\"\n FROM ${this.#cdc('backfilling')} as b\n LEFT JOIN ${this.#cdc('tableMetadata')} as t\n ON (b.\"schema\" = t.\"schema\" AND b.\"table\" = t.\"table\")\n GROUP BY b.\"schema\", b.\"table\", t.\"metadata\"\n `,\n ],\n {mode: Mode.READONLY},\n );\n\n return {\n lastWatermark,\n backfillRequests: v.parse(result, backfillRequestsSchema),\n };\n }\n\n async getMinWatermarkForCatchup(): Promise<string | null> {\n const [{minWatermark}] = await this.#db<{minWatermark: string | null}[]>\n /*sql*/ `\n SELECT min(watermark) as \"minWatermark\" FROM ${this.#cdc('changeLog')}`;\n return minWatermark;\n }\n\n purgeRecordsBefore(watermark: string): Promise<number> {\n return runTx(this.#db, async sql => {\n // This NOWAIT pre-check is an optimization to abort the transaction\n // (and release associated resources) early.\n await sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR UPDATE NOWAIT\n `;\n // If the row is purge-locked by an incoming replication-manager, it\n // will assume ownership of the change-log before releasing the lock.\n // This DELETE blocks until the lock is released, allowing the change\n // in ownership to be reliably detected (and the transaction aborted)\n // in the subsequent check.\n const [{deleted}] = await sql<{deleted: bigint}[]>`\n WITH purged AS (\n DELETE FROM ${this.#cdc('changeLog')} WHERE watermark < ${watermark} \n RETURNING watermark, pos\n ) SELECT COUNT(*) as deleted FROM purged;`;\n\n const [{owner}] = await sql<ReplicationOwner[]>`\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR SHARE`;\n if (owner !== this.#taskID) {\n throw new AbortError(\n `aborting changeLog purge to ${watermark} because ownership has been taken by ${owner}`,\n );\n }\n return Number(deleted);\n });\n }\n\n /**\n * @returns The JSON stringified stream message to be sent downstream.\n */\n store(watermark: string, data: ChangeStreamData) {\n // Eagerly stringify the JSON payload to:\n // - avoid redundant stringification when fanning out to subscribers\n // - efficiently estimate the amount of memory the payload consumes\n const json = BigIntJSON.stringify(data);\n this.#approximateQueuedBytes += json.length;\n\n const change = data[1];\n this.#queue.enqueue([\n 'change',\n watermark,\n json,\n isDataChange(change) ? null : change, // drop DataChanges to save memory\n ]);\n\n return json;\n }\n\n abort() {\n this.#queue.enqueue(['abort']);\n }\n\n status(s: DownstreamStatusMessage) {\n this.#queue.enqueue(s);\n }\n\n catchup(subscriber: Subscriber, mode: ReplicatorMode) {\n this.#queue.enqueue(['subscriber', {subscriber, mode}]);\n }\n\n #readyForMore: Resolver<void> | null = null;\n\n readyForMore(): Promise<void> | undefined {\n if (!this.#running) {\n return undefined;\n }\n if (\n this.#readyForMore === null &&\n this.#approximateQueuedBytes > this.#backPressureThresholdBytes\n ) {\n this.#lc.warn?.(\n `applying back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)\\n` +\n `\\n` +\n `To inspect changeLog backlog in your change DB:\\n` +\n ` SELECT\\n` +\n ` (change->'relation'->>'schema') || '.' || (change->'relation'->>'name') AS table_name,\\n` +\n ` change->>'tag' AS operation,\\n` +\n ` COUNT(*) AS count\\n` +\n ` FROM \"<app_id>/cdc\".\"changeLog\"\\n` +\n ` GROUP BY 1, 2\\n` +\n ` ORDER BY 3 DESC\\n` +\n ` LIMIT 20;`,\n );\n this.#readyForMore = resolver();\n }\n return this.#readyForMore?.promise;\n }\n\n #maybeReleaseBackPressure() {\n if (\n this.#readyForMore !== null &&\n // Wait for at least 20% of the threshold to free up.\n this.#approximateQueuedBytes < this.#backPressureThresholdBytes * 0.8\n ) {\n this.#lc.info?.(\n `releasing back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)`,\n );\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n }\n\n #stopped = promiseVoid;\n\n /**\n * Runs the storer loop until {@link stop()} is called, or an error is thrown.\n * Once {@link run()} completes, it can be called again.\n */\n async run() {\n assert(!this.#running, `storer is already running`);\n\n const {promise: stopped, resolve: signalStopped} = resolver();\n this.#running = true;\n this.#stopped = stopped;\n\n this.#lc.info?.('starting storer');\n let err: unknown;\n try {\n await this.#processQueue();\n } catch (e) {\n err = e; // used in finally\n throw e;\n } finally {\n // Release any pending backpressure so the upstream can proceed\n if (this.#readyForMore !== null) {\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n this.#cancelQueueEntries(\n this.#queue.drain().filter(entry => entry !== undefined),\n err,\n );\n this.#running = false;\n signalStopped();\n this.#lc.info?.('storer stopped');\n }\n }\n\n #cancelQueueEntries(queue: QueueEntry[], e: unknown) {\n if (queue.length === 0) {\n return;\n }\n this.#lc.info?.(\n `canceling ${queue.length} entries from the changeLog queue`,\n );\n const err = e instanceof Error ? e : new AbortError('server shutting down');\n for (const entry of queue) {\n if (entry === 'stop') {\n continue;\n }\n const type = entry[0];\n switch (type) {\n case 'subscriber': {\n // Disconnect subscribers waiting to be caught up so that they can\n // reconnect and try again.\n const {subscriber} = entry[1];\n this.#lc.info?.(`disconnecting ${subscriber.id}`);\n subscriber.fail(err);\n break;\n }\n }\n }\n }\n\n async #processQueue() {\n let tx: PendingTransaction | null = null;\n let msg: QueueEntry | false;\n\n const catchupQueue: SubscriberAndMode[] = [];\n try {\n while ((msg = await this.#queue.dequeue()) !== 'stop') {\n const [msgType] = msg;\n switch (msgType) {\n case 'ready': {\n const signalReady = msg[1];\n signalReady();\n continue;\n }\n case 'subscriber': {\n const subscriber = msg[1];\n if (tx) {\n catchupQueue.push(subscriber); // Wait for the current tx to complete.\n } else {\n await this.#startCatchup([subscriber]); // Catch up immediately.\n }\n continue;\n }\n case 'status':\n this.#onConsumed(msg);\n continue;\n case 'abort': {\n if (tx) {\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n }\n continue;\n }\n }\n // msgType === 'change'\n const [_, watermark, json, change] = msg;\n const tag = change?.tag;\n this.#approximateQueuedBytes -= json.length;\n\n if (tag === 'begin') {\n assert(!tx, 'received BEGIN in the middle of a transaction');\n const {promise, resolve, reject} = resolver<ReplicationOwner>();\n void promise.catch(() => {}); // handle rejections before the await\n tx = {\n pool: new TransactionPool(\n this.#lc.withContext('watermark', watermark),\n {\n mode: Mode.READ_COMMITTED,\n statementResponseTimeout: this.#statementTimeoutMs,\n },\n ),\n preCommitWatermark: watermark,\n pos: 0,\n startingReplicationState: promise,\n ack: !change.skipAck,\n };\n tx.pool.run(this.#db);\n // Acquire a lock on the replicationState row to detect and/or prevent\n // a concurrent ownership change.\n void tx.pool.process(tx => {\n tx<ReplicationOwner[]> /*sql*/ `\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR UPDATE`.then(\n ([result]) => resolve(result),\n reject,\n );\n return [];\n });\n } else {\n assert(tx, () => `received change outside of transaction: ${json}`);\n tx.pos++;\n }\n\n const entry = {\n watermark: tag === 'commit' ? watermark : tx.preCommitWatermark,\n precommit: tag === 'commit' ? tx.preCommitWatermark : null,\n pos: tx.pos,\n // For backwards compatibility, only the change message is stored\n // in the cdc changeLog.\n change: extractChangeSubstring(json, tag),\n };\n\n const processed = tx.pool.process(sql => [\n sql`INSERT INTO ${this.#cdc('changeLog')} ${sql(entry)}`,\n ...(change !== null && isSchemaChange(change)\n ? this.#trackBackfillMetadata(sql, change)\n : []),\n ]);\n\n if (tx.pos % 100 === 0) {\n // Backpressure is exerted on commit when awaiting tx.pool.done().\n // However, backpressure checks need to be regularly done for\n // very large transactions in order to avoid memory blowup.\n await processed;\n }\n this.#maybeReleaseBackPressure();\n\n if (tag === 'commit') {\n const {owner} = await tx.startingReplicationState;\n if (owner !== this.#taskID) {\n // Ownership change reflected in the replicationState read in 'begin'.\n tx.pool.fail(\n new AbortError(\n `changeLog ownership has been assumed by ${owner}`,\n ),\n );\n } else {\n // Update the replication state.\n const lastWatermark = watermark;\n void tx.pool.process(tx => [\n tx`\n UPDATE ${this.#cdc('replicationState')} SET ${tx({lastWatermark})}`,\n ]);\n tx.pool.setDone();\n }\n\n await tx.pool.done();\n\n // ACK the LSN to the upstream Postgres.\n if (tx.ack) {\n this.#onConsumed(['commit', change, {watermark}]);\n }\n tx = null;\n\n // Before beginning the next transaction, open a READONLY snapshot to\n // concurrently catchup any queued subscribers.\n await this.#startCatchup(catchupQueue.splice(0));\n } else if (tag === 'rollback') {\n // Aborted transactions are not stored in the changeLog. Abort the current tx\n // and process catchup of subscribers that were waiting for it to end.\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n\n await this.#startCatchup(catchupQueue.splice(0));\n }\n }\n } catch (e) {\n catchupQueue.forEach(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n }\n\n async #startCatchup(subs: SubscriberAndMode[]) {\n if (subs.length === 0) {\n return;\n }\n\n const reader = new TransactionPool(\n this.#lc.withContext('pool', 'catchup'),\n {mode: Mode.READONLY},\n );\n reader.run(this.#db);\n\n let lastWatermark: string | undefined;\n try {\n // Ensure that the transaction has started (and is thus holding a snapshot\n // of the database) before continuing on to commit more changes. This is\n // done by performing a single read on the db, which determines the\n // snapshot for the REPEATABLE_READ transaction.\n [{lastWatermark}] = await reader.processReadTask(\n sql => sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}\n `,\n );\n } catch (e) {\n subs.map(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n\n // Run the actual catchup queries in the background. Errors are handled in\n // #catchup() by disconnecting the associated subscriber.\n void Promise.all(\n subs.map(sub => this.#catchup(sub, lastWatermark, reader)),\n ).finally(() => reader.setDone());\n }\n\n async #catchup(\n {subscriber: sub, mode}: SubscriberAndMode,\n lastWatermark: string,\n reader: TransactionPool,\n ) {\n try {\n await reader.processReadTask(async tx => {\n const start = Date.now();\n\n // When starting from initial-sync, there won't be a change with a watermark\n // equal to the replica version. This is the empty changeLog scenario.\n let watermarkFound = sub.watermark === this.#replicaVersion;\n let count = 0;\n let lastBatchConsumed: Promise<unknown> | undefined;\n\n for await (const entries of tx<ChangeLogEntry[]> /*sql*/ `\n SELECT watermark, change->'tag' as tag, change::text FROM ${this.#cdc('changeLog')}\n WHERE watermark >= ${sub.watermark}\n AND watermark <= ${lastWatermark}\n ORDER BY watermark, pos`.cursor(2000)) {\n // Wait for the last batch of entries to be consumed by the\n // subscriber before sending down the current batch. This pipelining\n // allows one batch of changes to be received from the change-db\n // while the previous batch of changes are sent to the subscriber,\n // resulting in flow control that caps the number of changes\n // referenced in memory to 2 * batch-size.\n const start = performance.now();\n await lastBatchConsumed;\n const elapsed = performance.now() - start;\n if (lastBatchConsumed) {\n (elapsed > 100 ? this.#lc.info : this.#lc.debug)?.(\n `waited ${elapsed.toFixed(3)} ms for ${sub.id} to consume last batch of catchup entries`,\n );\n }\n\n for (const entry of entries) {\n if (entry.watermark === sub.watermark) {\n // This should be the first entry.\n // Catchup starts from *after* the watermark.\n watermarkFound = true;\n } else if (watermarkFound) {\n lastBatchConsumed = sub.catchup(toDownstream(entry));\n count++;\n } else if (mode === 'backup') {\n throw new AutoResetSignal(\n `backup replica at watermark ${sub.watermark} is behind change db: ${entry.watermark})`,\n );\n } else {\n this.#lc.warn?.(\n `rejecting subscriber at watermark ${sub.watermark} (earliest watermark: ${entry.watermark})`,\n );\n sub.close(\n ErrorType.WatermarkTooOld,\n `earliest supported watermark is ${entry.watermark} (requested ${sub.watermark})`,\n );\n return;\n }\n }\n }\n if (watermarkFound) {\n await lastBatchConsumed;\n this.#lc.info?.(\n `caught up ${sub.id} with ${count} changes (${\n Date.now() - start\n } ms)`,\n );\n } else {\n this.#lc.warn?.(\n `subscriber at watermark ${sub.watermark} is ahead of latest watermark`,\n );\n }\n // Flushes the backlog of messages buffered during catchup and\n // allows the subscription to forward subsequent messages immediately.\n sub.setCaughtUp();\n });\n } catch (err) {\n this.#lc.error?.(`error while catching up subscriber ${sub.id}`, err);\n if (err instanceof AutoResetSignal) {\n await markResetRequired(this.#db, this.#shard);\n this.#onFatal(err);\n }\n sub.fail(err);\n }\n }\n\n /**\n * Returns the db statements necessary to track backfill and table metadata\n * presented in the `change`, if any.\n */\n #trackBackfillMetadata(sql: PostgresTransaction, change: SchemaChange) {\n const stmts: PendingQuery<Row[]>[] = [];\n\n switch (change.tag) {\n case 'update-table-metadata': {\n const {table, new: metadata} = change;\n stmts.push(this.#upsertTableMetadataStmt(sql, table, metadata));\n break;\n }\n\n case 'create-table': {\n const {spec, metadata, backfill} = change;\n if (metadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, spec, metadata));\n }\n if (backfill) {\n Object.entries(backfill).forEach(([col, backfill]) => {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, spec, col, backfill),\n );\n });\n }\n break;\n }\n\n case 'rename-table': {\n const {old} = change;\n const row = {schema: change.new.schema, table: change.new.name};\n stmts.push(\n sql`UPDATE ${this.#cdc('tableMetadata')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n sql`UPDATE ${this.#cdc('backfilling')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n );\n break;\n }\n\n case 'drop-table': {\n const {\n id: {schema, name},\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('tableMetadata')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n );\n break;\n }\n\n case 'add-column': {\n const {table, tableMetadata, column, backfill} = change;\n if (tableMetadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, table, tableMetadata));\n }\n if (backfill) {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, table, column.name, backfill),\n );\n }\n break;\n }\n\n case 'update-column': {\n const {\n table: {schema, name: table},\n old: {name: oldName},\n new: {name: newName},\n } = change;\n if (oldName !== newName) {\n stmts.push(\n sql`UPDATE ${this.#cdc('backfilling')} SET \"column\" = ${newName}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" = ${oldName}`,\n );\n }\n break;\n }\n\n case 'drop-column': {\n const {\n table: {schema, name},\n column,\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name} AND \"column\" = ${column}`,\n );\n break;\n }\n\n case 'backfill-completed': {\n const {\n relation: {schema, name: table, rowKey},\n columns,\n } = change;\n const cols = [...rowKey.columns, ...columns];\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" IN ${sql(cols)}`,\n );\n }\n }\n return stmts;\n }\n\n #upsertTableMetadataStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n metadata: TableMetadata,\n ) {\n const row: TableMetadataRow = {schema, table, metadata};\n return sql`\n INSERT INTO ${this.#cdc('tableMetadata')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n #upsertColumnBackfillStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n column: string,\n backfill: BackfillID,\n ) {\n const row: BackfillingColumn = {schema, table, column, backfill};\n return sql`\n INSERT INTO ${this.#cdc('backfilling')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\", \"column\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n /**\n * Waits until all currently queued entries have been processed.\n * This is only used in tests.\n */\n async allProcessed() {\n if (this.#running) {\n const {promise, resolve} = resolver();\n this.#queue.enqueue(['ready', resolve]);\n await promise;\n }\n }\n\n stop() {\n if (this.#running) {\n this.#lc.info?.(`draining ${this.#queue.size()} changeLog entries`);\n this.#queue.enqueue('stop');\n }\n return this.#stopped;\n }\n}\n\n/**\n * Extracts the stringified change message from the stringified\n * stream message (e.g. the second tuple element). This optimization\n * facilitates stringifying (and sharing the result of) the stream\n * message exactly once, but storing only the change message substring\n * in the changeLog for backwards compatibility.\n */\nexport function extractChangeSubstring(\n streamMessageJSON: string,\n tag: Change['tag'] | undefined,\n) {\n switch (tag) {\n case 'begin':\n case 'commit':\n // e.g.\n // [\"begin\",<message-json>,{\"commitWatermark\":\"92fj2d0s\"}]\n // [\"commit\",<message-json>,{\"watermark\":\"92fj2d0s\"}]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(','),\n );\n default:\n // [\"data\",<message-json>]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(']'),\n );\n }\n}\n\ntype ChangeLogEntry = {\n watermark: string;\n tag: string;\n change: string;\n};\n\nfunction toDownstream(entry: ChangeLogEntry): WatermarkedChange {\n const {watermark, change} = entry;\n const tag = entry.tag as ChangeTag;\n switch (tag) {\n case 'begin':\n return [\n watermark,\n tag,\n `[\"begin\",${change},{\"commitWatermark\":\"${watermark}\"}]`,\n ];\n case 'commit':\n return [\n watermark,\n tag,\n `[\"commit\",${change},{\"watermark\":\"${watermark}\"}]`,\n ];\n case 'rollback':\n return [watermark, tag, `[\"rollback\",${change}]`];\n default:\n return [watermark, tag, `[\"data\",${change}]`];\n }\n}\n\nexport class PurgeLock {\n readonly #lc: LogContext;\n readonly #tx: TransactionPool;\n readonly replicaVersion: string;\n readonly minWatermark: string;\n\n constructor(\n lc: LogContext,\n tx: TransactionPool,\n replicaVersion: string,\n watermark: string,\n ) {\n this.#lc = lc;\n this.#tx = tx;\n this.replicaVersion = replicaVersion;\n this.minWatermark = watermark;\n }\n\n #released = false;\n\n async release() {\n if (this.#released) {\n return;\n }\n this.#released = true;\n this.#tx.setDone();\n await this.#tx\n .done()\n .catch(e => this.#lc.warn?.(`error from purge-lock release`, e));\n this.#lc.info?.(`released purge lock on ${this.minWatermark}`);\n }\n}\n\nexport class PurgeLocker {\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #db: PostgresDB;\n\n constructor(lc: LogContext, shard: ShardID, db: PostgresDB) {\n this.#lc = lc.withContext('component', 'purge-locker');\n this.#shard = shard;\n this.#db = db;\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async acquire() {\n const tx = new TransactionPool(this.#lc, {mode: Mode.READ_COMMITTED}).run(\n this.#db,\n );\n const row = await tx.processReadTask(\n sql => sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR SHARE \n `,\n );\n if (row.length === 0) {\n this.#lc.info?.(`changeLog is empty. No rows to purge-lock.`);\n tx.setDone();\n await tx.done();\n return null;\n }\n const [{watermark}] = row;\n const [{replicaVersion}] = await tx.processReadTask(\n sql => sql<{replicaVersion: string}[]>`\n SELECT \"replicaVersion\" FROM ${this.#cdc('replicationConfig')}\n `,\n );\n this.#lc.info?.(\n `locked watermark ${watermark} from being purged from replica@${replicaVersion}`,\n );\n return new PurgeLock(this.#lc, tx, replicaVersion, watermark);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6EA,IAAM,yBAAyB,eAAE,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqC5D,IAAa,SAAb,MAAuC;CACrC,KAAc;CACd;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,MAAkB;CACxC;CACA;CAEA,0BAA0B;CAC1B,WAAW;CAEX,YACE,IACA,OACA,QACA,kBACA,mBACA,IACA,gBACA,YACA,SACA,EAAC,iCAAiC,sBAClC;EACA,KAAKA,MAAM,GAAG,YAAY,aAAa,YAAY;EACnD,KAAKC,SAAS;EACd,KAAKC,UAAU;EACf,KAAKC,oBAAoB;EACzB,KAAKC,qBAAqB;EAC1B,KAAKC,MAAM;EACX,KAAKC,kBAAkB;EACvB,KAAKC,cAAc;EACnB,KAAKC,WAAW;EAChB,KAAKG,sBAAsB;EAE3B,MAAM,YAAY,kBAAkB;EACpC,KAAKD,+BACF,UAAU,kBAAkB,UAAU,kBACvC;EAEF,KAAKV,IAAI,OACP,gBAAgB,KAAKU,8BAA8B,QAAQ,GAAG,QAAQ,CAAC,EAAE,iCAC5C,UAAU,kBAAkB,QAAQ,GAAG,QAAQ,CAAC,EAAE,iCAE/E,EAAC,UAAS,CACZ;CACF;CAGA,KAAK,OAAe;EAClB,OAAO,KAAKL,IAAI,GAAG,UAAU,KAAKJ,MAAM,EAAE,GAAG,OAAO;CACtD;CAEA,MAAM,gBAAgB,WAA8B;EAClD,MAAM,KAAK,KAAKI;EAChB,MAAM,QAAQ,KAAKH;EACnB,MAAM,eAAe,KAAKC;EAC1B,MAAM,gBAAgB,KAAKC;EAE3B,MAAM,sBACJ,kBAAkB,OACd,eACA,GAAG,cAAc,KAAK;EAC5B,KAAKJ,IAAI,OAAO,yBAAyB,qBAAqB;EAC9D,MAAM,QAAQ,YAAY,IAAI;EAC9B,MAAM,EAAE,UAAU,KAAKY,KAAK,kBAAkB,EAAE,OAAO,GAAG;GAAC;GAAO,cAAc;EAAmB,CAAC;EACpG,MAAM,WAAW,YAAY,IAAI,IAAI,OAAO,QAAQ,CAAC;EACrD,KAAKZ,IAAI,OACP,wBAAwB,oBAAoB,IAAI,QAAQ,KAC1D;EAEA,IAAI,WAKF,UAAe,QAAQ;CAE3B;CAEA,MAAM,yCAGH;EACD,MAAM,CAAC,CAAC,EAAC,kBAAiB,UAAU,MAAM,MACxC,KAAKK,MACL,QAAO,CACL,GAA8B;sCACA,KAAKO,KAAK,kBAAkB,KAK1D,GAAG;;;;;;;;;iBASM,KAAKA,KAAK,aAAa,EAAE;sBACpB,KAAKA,KAAK,eAAe,EAAE;;;SAI3C,GACA,EAAC,MAAM,SAAa,CACtB;EAEA,OAAO;GACL;GACA,kBAAkB,MAAQ,QAAQ,sBAAsB;EAC1D;CACF;CAEA,MAAM,4BAAoD;EACxD,MAAM,CAAC,EAAC,kBAAiB,MAAM,KAAKP,GAC5B;qDACyC,KAAKO,KAAK,WAAW;EACtE,OAAO;CACT;CAEA,mBAAmB,WAAoC;EACrD,OAAO,MAAM,KAAKP,KAAK,OAAM,QAAO;GAGlC,MAAM,GAA0B;kCACJ,KAAKO,KAAK,WAAW,EAAE;;;;GASnD,MAAM,CAAC,EAAC,aAAY,MAAM,GAAwB;;wBAEhC,KAAKA,KAAK,WAAW,EAAE,qBAAqB,UAAU;;;GAIxE,MAAM,CAAC,EAAC,WAAU,MAAM,GAAuB;8BACvB,KAAKA,KAAK,kBAAkB,EAAE;GACtD,IAAI,UAAU,KAAKV,SACjB,MAAM,IAAI,WACR,+BAA+B,UAAU,uCAAuC,OAClF;GAEF,OAAO,OAAO,OAAO;EACvB,CAAC;CACH;;;;CAKA,MAAM,WAAmB,MAAwB;EAI/C,MAAM,OAAO,WAAW,UAAU,IAAI;EACtC,KAAKW,2BAA2B,KAAK;EAErC,MAAM,SAAS,KAAK;EACpB,KAAKJ,OAAO,QAAQ;GAClB;GACA;GACA;GACA,aAAa,MAAM,IAAI,OAAO;EAChC,CAAC;EAED,OAAO;CACT;CAEA,QAAQ;EACN,KAAKA,OAAO,QAAQ,CAAC,OAAO,CAAC;CAC/B;CAEA,OAAO,GAA4B;EACjC,KAAKA,OAAO,QAAQ,CAAC;CACvB;CAEA,QAAQ,YAAwB,MAAsB;EACpD,KAAKA,OAAO,QAAQ,CAAC,cAAc;GAAC;GAAY;EAAI,CAAC,CAAC;CACxD;CAEA,gBAAuC;CAEvC,eAA0C;EACxC,IAAI,CAAC,KAAKK,UACR;EAEF,IACE,KAAKC,kBAAkB,QACvB,KAAKF,0BAA0B,KAAKH,6BACpC;GACA,KAAKV,IAAI,OACP,+BAA+B,KAAKS,OAAO,KAAK,EAAE,qBAAqB,KAAKI,0BAA0B,QAAQ,GAAG,QAAQ,CAAC,EAAE,2SAW9H;GACA,KAAKE,gBAAgB,SAAS;EAChC;EACA,OAAO,KAAKA,eAAe;CAC7B;CAEA,4BAA4B;EAC1B,IACE,KAAKA,kBAAkB,QAEvB,KAAKF,0BAA0B,KAAKH,8BAA8B,IAClE;GACA,KAAKV,IAAI,OACP,gCAAgC,KAAKS,OAAO,KAAK,EAAE,qBAAqB,KAAKI,0BAA0B,QAAQ,GAAG,QAAQ,CAAC,EAAE,KAC/H;GACA,KAAKE,cAAc,QAAQ;GAC3B,KAAKA,gBAAgB;EACvB;CACF;CAEA,WAAW;;;;;CAMX,MAAM,MAAM;EACV,OAAO,CAAC,KAAKD,UAAU,2BAA2B;EAElD,MAAM,EAAC,SAAS,SAAS,SAAS,kBAAiB,SAAS;EAC5D,KAAKA,WAAW;EAChB,KAAKE,WAAW;EAEhB,KAAKhB,IAAI,OAAO,iBAAiB;EACjC,IAAI;EACJ,IAAI;GACF,MAAM,KAAKiB,cAAc;EAC3B,SAAS,GAAG;GACV,MAAM;GACN,MAAM;EACR,UAAU;GAER,IAAI,KAAKF,kBAAkB,MAAM;IAC/B,KAAKA,cAAc,QAAQ;IAC3B,KAAKA,gBAAgB;GACvB;GACA,KAAKG,oBACH,KAAKT,OAAO,MAAM,EAAE,QAAO,UAAS,UAAU,KAAA,CAAS,GACvD,GACF;GACA,KAAKK,WAAW;GAChB,cAAc;GACd,KAAKd,IAAI,OAAO,gBAAgB;EAClC;CACF;CAEA,oBAAoB,OAAqB,GAAY;EACnD,IAAI,MAAM,WAAW,GACnB;EAEF,KAAKA,IAAI,OACP,aAAa,MAAM,OAAO,kCAC5B;EACA,MAAM,MAAM,aAAa,QAAQ,IAAI,IAAI,WAAW,sBAAsB;EAC1E,KAAK,MAAM,SAAS,OAAO;GACzB,IAAI,UAAU,QACZ;GAGF,QADa,MAAM,IACnB;IACE,KAAK,cAAc;KAGjB,MAAM,EAAC,eAAc,MAAM;KAC3B,KAAKA,IAAI,OAAO,iBAAiB,WAAW,IAAI;KAChD,WAAW,KAAK,GAAG;KACnB;IACF;GACF;EACF;CACF;CAEA,MAAMiB,gBAAgB;EACpB,IAAI,KAAgC;EACpC,IAAI;EAEJ,MAAM,eAAoC,CAAC;EAC3C,IAAI;GACF,QAAQ,MAAM,MAAM,KAAKR,OAAO,QAAQ,OAAO,QAAQ;IACrD,MAAM,CAAC,WAAW;IAClB,QAAQ,SAAR;KACE,KAAK,SAAS;MACZ,MAAM,cAAc,IAAI;MACxB,YAAY;MACZ;KACF;KACA,KAAK,cAAc;MACjB,MAAM,aAAa,IAAI;MACvB,IAAI,IACF,aAAa,KAAK,UAAU;WAE5B,MAAM,KAAKU,cAAc,CAAC,UAAU,CAAC;MAEvC;KACF;KACA,KAAK;MACH,KAAKZ,YAAY,GAAG;MACpB;KACF,KAAK;MACH,IAAI,IAAI;OACN,GAAG,KAAK,MAAM;OACd,MAAM,GAAG,KAAK,KAAK;OACnB,KAAK;MACP;MACA;IAEJ;IAEA,MAAM,CAAC,GAAG,WAAW,MAAM,UAAU;IACrC,MAAM,MAAM,QAAQ;IACpB,KAAKM,2BAA2B,KAAK;IAErC,IAAI,QAAQ,SAAS;KACnB,OAAO,CAAC,IAAI,+CAA+C;KAC3D,MAAM,EAAC,SAAS,SAAS,WAAU,SAA2B;KAC9D,QAAa,YAAY,CAAC,CAAC;KAC3B,KAAK;MACH,MAAM,IAAI,gBACR,KAAKb,IAAI,YAAY,aAAa,SAAS,GAC3C;OACE,MAAM;OACN,0BAA0B,KAAKW;MACjC,CACF;MACA,oBAAoB;MACpB,KAAK;MACL,0BAA0B;MAC1B,KAAK,CAAC,OAAO;KACf;KACA,GAAG,KAAK,IAAI,KAAKN,GAAG;KAGpB,GAAQ,KAAK,SAAQ,OAAM;MACzB,EAA+B;gCACX,KAAKO,KAAK,kBAAkB,EAAE,aAAa,MAC5D,CAAC,YAAY,QAAQ,MAAM,GAC5B,MACF;MACA,OAAO,CAAC;KACV,CAAC;IACH,OAAO;KACL,OAAO,UAAU,2CAA2C,MAAM;KAClE,GAAG;IACL;IAEA,MAAM,QAAQ;KACZ,WAAW,QAAQ,WAAW,YAAY,GAAG;KAC7C,WAAW,QAAQ,WAAW,GAAG,qBAAqB;KACtD,KAAK,GAAG;KAGR,QAAQ,uBAAuB,MAAM,GAAG;IAC1C;IAEA,MAAM,YAAY,GAAG,KAAK,SAAQ,QAAO,CACvC,GAAG,eAAe,KAAKA,KAAK,WAAW,EAAE,GAAG,IAAI,KAAK,KACrD,GAAI,WAAW,QAAQ,eAAe,MAAM,IACxC,KAAKQ,uBAAuB,KAAK,MAAM,IACvC,CAAC,CACP,CAAC;IAED,IAAI,GAAG,MAAM,QAAQ,GAInB,MAAM;IAER,KAAKC,0BAA0B;IAE/B,IAAI,QAAQ,UAAU;KACpB,MAAM,EAAC,UAAS,MAAM,GAAG;KACzB,IAAI,UAAU,KAAKnB,SAEjB,GAAG,KAAK,KACN,IAAI,WACF,2CAA2C,OAC7C,CACF;UACK;MAEL,MAAM,gBAAgB;MACtB,GAAQ,KAAK,SAAQ,OAAM,CACzB,EAAE;qBACK,KAAKU,KAAK,kBAAkB,EAAE,OAAO,GAAG,EAAC,cAAa,CAAC,GAChE,CAAC;MACD,GAAG,KAAK,QAAQ;KAClB;KAEA,MAAM,GAAG,KAAK,KAAK;KAGnB,IAAI,GAAG,KACL,KAAKL,YAAY;MAAC;MAAU;MAAQ,EAAC,UAAS;KAAC,CAAC;KAElD,KAAK;KAIL,MAAM,KAAKY,cAAc,aAAa,OAAO,CAAC,CAAC;IACjD,OAAO,IAAI,QAAQ,YAAY;KAG7B,GAAG,KAAK,MAAM;KACd,MAAM,GAAG,KAAK,KAAK;KACnB,KAAK;KAEL,MAAM,KAAKA,cAAc,aAAa,OAAO,CAAC,CAAC;IACjD;GACF;EACF,SAAS,GAAG;GACV,aAAa,SAAS,EAAC,iBAAgB,WAAW,KAAK,CAAC,CAAC;GACzD,MAAM;EACR;CACF;CAEA,MAAMA,cAAc,MAA2B;EAC7C,IAAI,KAAK,WAAW,GAClB;EAGF,MAAM,SAAS,IAAI,gBACjB,KAAKnB,IAAI,YAAY,QAAQ,SAAS,GACtC,EAAC,MAAM,SAAa,CACtB;EACA,OAAO,IAAI,KAAKK,GAAG;EAEnB,IAAI;EACJ,IAAI;GAKF,CAAC,CAAC,kBAAkB,MAAM,OAAO,iBAC/B,QAAO,GAA8B;sCACP,KAAKO,KAAK,kBAAkB,EAAE;OAE9D;EACF,SAAS,GAAG;GACV,KAAK,KAAK,EAAC,iBAAgB,WAAW,KAAK,CAAC,CAAC;GAC7C,MAAM;EACR;EAIA,QAAa,IACX,KAAK,KAAI,QAAO,KAAKU,SAAS,KAAK,eAAe,MAAM,CAAC,CAC3D,EAAE,cAAc,OAAO,QAAQ,CAAC;CAClC;CAEA,MAAMA,SACJ,EAAC,YAAY,KAAK,QAClB,eACA,QACA;EACA,IAAI;GACF,MAAM,OAAO,gBAAgB,OAAM,OAAM;IACvC,MAAM,QAAQ,KAAK,IAAI;IAIvB,IAAI,iBAAiB,IAAI,cAAc,KAAKhB;IAC5C,IAAI,QAAQ;IACZ,IAAI;IAEJ,WAAW,MAAM,WAAW,EAA6B;sEACK,KAAKM,KAAK,WAAW,EAAE;gCAC7D,IAAI,UAAU;gCACd,cAAc;oCACV,OAAO,GAAI,GAAG;KAOxC,MAAM,QAAQ,YAAY,IAAI;KAC9B,MAAM;KACN,MAAM,UAAU,YAAY,IAAI,IAAI;KACpC,IAAI,mBACF,CAAC,UAAU,MAAM,KAAKZ,IAAI,OAAO,KAAKA,IAAI,SACxC,UAAU,QAAQ,QAAQ,CAAC,EAAE,UAAU,IAAI,GAAG,0CAChD;KAGF,KAAK,MAAM,SAAS,SAClB,IAAI,MAAM,cAAc,IAAI,WAG1B,iBAAiB;UACZ,IAAI,gBAAgB;MACzB,oBAAoB,IAAI,QAAQ,aAAa,KAAK,CAAC;MACnD;KACF,OAAO,IAAI,SAAS,UAClB,MAAM,IAAI,gBACR,+BAA+B,IAAI,UAAU,wBAAwB,MAAM,UAAU,EACvF;UACK;MACL,KAAKA,IAAI,OACP,qCAAqC,IAAI,UAAU,wBAAwB,MAAM,UAAU,EAC7F;MACA,IAAI,MACF,GACA,mCAAmC,MAAM,UAAU,cAAc,IAAI,UAAU,EACjF;MACA;KACF;IAEJ;IACA,IAAI,gBAAgB;KAClB,MAAM;KACN,KAAKA,IAAI,OACP,aAAa,IAAI,GAAG,QAAQ,MAAM,YAChC,KAAK,IAAI,IAAI,MACd,KACH;IACF,OACE,KAAKA,IAAI,OACP,2BAA2B,IAAI,UAAU,8BAC3C;IAIF,IAAI,YAAY;GAClB,CAAC;EACH,SAAS,KAAK;GACZ,KAAKA,IAAI,QAAQ,sCAAsC,IAAI,MAAM,GAAG;GACpE,IAAI,eAAe,iBAAiB;IAClC,MAAM,kBAAkB,KAAKK,KAAK,KAAKJ,MAAM;IAC7C,KAAKO,SAAS,GAAG;GACnB;GACA,IAAI,KAAK,GAAG;EACd;CACF;;;;;CAMA,uBAAuB,KAA0B,QAAsB;EACrE,MAAM,QAA+B,CAAC;EAEtC,QAAQ,OAAO,KAAf;GACE,KAAK,yBAAyB;IAC5B,MAAM,EAAC,OAAO,KAAK,aAAY;IAC/B,MAAM,KAAK,KAAKe,yBAAyB,KAAK,OAAO,QAAQ,CAAC;IAC9D;GACF;GAEA,KAAK,gBAAgB;IACnB,MAAM,EAAC,MAAM,UAAU,aAAY;IACnC,IAAI,UACF,MAAM,KAAK,KAAKA,yBAAyB,KAAK,MAAM,QAAQ,CAAC;IAE/D,IAAI,UACF,OAAO,QAAQ,QAAQ,EAAE,SAAS,CAAC,KAAK,cAAc;KACpD,MAAM,KACJ,KAAKC,0BAA0B,KAAK,MAAM,KAAK,QAAQ,CACzD;IACF,CAAC;IAEH;GACF;GAEA,KAAK,gBAAgB;IACnB,MAAM,EAAC,QAAO;IACd,MAAM,MAAM;KAAC,QAAQ,OAAO,IAAI;KAAQ,OAAO,OAAO,IAAI;IAAI;IAC9D,MAAM,KACJ,GAAG,UAAU,KAAKZ,KAAK,eAAe,EAAE,OAAO,IAAI,GAAG,EAAE;mCAC/B,IAAI,OAAO,iBAAiB,IAAI,QACzD,GAAG,UAAU,KAAKA,KAAK,aAAa,EAAE,OAAO,IAAI,GAAG,EAAE;mCAC7B,IAAI,OAAO,iBAAiB,IAAI,MAC3D;IACA;GACF;GAEA,KAAK,cAAc;IACjB,MAAM,EACJ,IAAI,EAAC,QAAQ,WACX;IACJ,MAAM,KACJ,GAAG,eAAe,KAAKA,KAAK,eAAe,EAAE;mCACpB,OAAO,iBAAiB,QACjD,GAAG,eAAe,KAAKA,KAAK,aAAa,EAAE;mCAClB,OAAO,iBAAiB,MACnD;IACA;GACF;GAEA,KAAK,cAAc;IACjB,MAAM,EAAC,OAAO,eAAe,QAAQ,aAAY;IACjD,IAAI,eACF,MAAM,KAAK,KAAKW,yBAAyB,KAAK,OAAO,aAAa,CAAC;IAErE,IAAI,UACF,MAAM,KACJ,KAAKC,0BAA0B,KAAK,OAAO,OAAO,MAAM,QAAQ,CAClE;IAEF;GACF;GAEA,KAAK,iBAAiB;IACpB,MAAM,EACJ,OAAO,EAAC,QAAQ,MAAM,SACtB,KAAK,EAAC,MAAM,WACZ,KAAK,EAAC,MAAM,cACV;IACJ,IAAI,YAAY,SACd,MAAM,KACJ,GAAG,UAAU,KAAKZ,KAAK,aAAa,EAAE,kBAAkB,QAAQ;mCACzC,OAAO,iBAAiB,MAAM,kBAAkB,SACzE;IAEF;GACF;GAEA,KAAK,eAAe;IAClB,MAAM,EACJ,OAAO,EAAC,QAAQ,QAChB,WACE;IACJ,MAAM,KACJ,GAAG,eAAe,KAAKA,KAAK,aAAa,EAAE;mCAClB,OAAO,iBAAiB,KAAK,kBAAkB,QAC1E;IACA;GACF;GAEA,KAAK,sBAAsB;IACzB,MAAM,EACJ,UAAU,EAAC,QAAQ,MAAM,OAAO,UAChC,YACE;IACJ,MAAM,OAAO,CAAC,GAAG,OAAO,SAAS,GAAG,OAAO;IAC3C,MAAM,KACJ,GAAG,eAAe,KAAKA,KAAK,aAAa,EAAE;mCAClB,OAAO,iBAAiB,MAAM,mBAAmB,IAAI,IAAI,GACpF;GACF;EACF;EACA,OAAO;CACT;CAEA,yBACE,KACA,EAAC,QAAQ,MAAM,SACf,UACA;EACA,MAAM,MAAwB;GAAC;GAAQ;GAAO;EAAQ;EACtD,OAAO,GAAG;sBACQ,KAAKA,KAAK,eAAe,EAAE,GAAG,IAAI,GAAG,EAAE;;0BAEnC,IAAI,GAAG,EAAE;;CAEjC;CAEA,0BACE,KACA,EAAC,QAAQ,MAAM,SACf,QACA,UACA;EACA,MAAM,MAAyB;GAAC;GAAQ;GAAO;GAAQ;EAAQ;EAC/D,OAAO,GAAG;sBACQ,KAAKA,KAAK,aAAa,EAAE,GAAG,IAAI,GAAG,EAAE;;0BAEjC,IAAI,GAAG,EAAE;;CAEjC;;;;;CAMA,MAAM,eAAe;EACnB,IAAI,KAAKE,UAAU;GACjB,MAAM,EAAC,SAAS,YAAW,SAAS;GACpC,KAAKL,OAAO,QAAQ,CAAC,SAAS,OAAO,CAAC;GACtC,MAAM;EACR;CACF;CAEA,OAAO;EACL,IAAI,KAAKK,UAAU;GACjB,KAAKd,IAAI,OAAO,YAAY,KAAKS,OAAO,KAAK,EAAE,mBAAmB;GAClE,KAAKA,OAAO,QAAQ,MAAM;EAC5B;EACA,OAAO,KAAKO;CACd;AACF;;;;;;;;AASA,SAAgB,uBACd,mBACA,KACA;CACA,QAAQ,KAAR;EACE,KAAK;EACL,KAAK,UAIH,OAAO,kBAAkB,UACvB,kBAAkB,QAAQ,GAAG,IAAI,GACjC,kBAAkB,YAAY,GAAG,CACnC;EACF,SAEE,OAAO,kBAAkB,UACvB,kBAAkB,QAAQ,GAAG,IAAI,GACjC,kBAAkB,YAAY,GAAG,CACnC;CACJ;AACF;AAQA,SAAS,aAAa,OAA0C;CAC9D,MAAM,EAAC,WAAW,WAAU;CAC5B,MAAM,MAAM,MAAM;CAClB,QAAQ,KAAR;EACE,KAAK,SACH,OAAO;GACL;GACA;GACA,YAAY,OAAO,uBAAuB,UAAU;EACtD;EACF,KAAK,UACH,OAAO;GACL;GACA;GACA,aAAa,OAAO,iBAAiB,UAAU;EACjD;EACF,KAAK,YACH,OAAO;GAAC;GAAW;GAAK,eAAe,OAAO;EAAE;EAClD,SACE,OAAO;GAAC;GAAW;GAAK,WAAW,OAAO;EAAE;CAChD;AACF;AAEA,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CACA;CAEA,YACE,IACA,IACA,gBACA,WACA;EACA,KAAKhB,MAAM;EACX,KAAKyB,MAAM;EACX,KAAK,iBAAiB;EACtB,KAAK,eAAe;CACtB;CAEA,YAAY;CAEZ,MAAM,UAAU;EACd,IAAI,KAAKC,WACP;EAEF,KAAKA,YAAY;EACjB,KAAKD,IAAI,QAAQ;EACjB,MAAM,KAAKA,IACR,KAAK,EACL,OAAM,MAAK,KAAKzB,IAAI,OAAO,iCAAiC,CAAC,CAAC;EACjE,KAAKA,IAAI,OAAO,0BAA0B,KAAK,cAAc;CAC/D;AACF;AAEA,IAAa,cAAb,MAAyB;CACvB;CACA;CACA;CAEA,YAAY,IAAgB,OAAgB,IAAgB;EAC1D,KAAKA,MAAM,GAAG,YAAY,aAAa,cAAc;EACrD,KAAKC,SAAS;EACd,KAAKI,MAAM;CACb;CAGA,KAAK,OAAe;EAClB,OAAO,KAAKA,IAAI,GAAG,UAAU,KAAKJ,MAAM,EAAE,GAAG,OAAO;CACtD;CAEA,MAAM,UAAU;EACd,MAAM,KAAK,IAAI,gBAAgB,KAAKD,KAAK,EAAC,MAAM,eAAmB,CAAC,EAAE,IACpE,KAAKK,GACP;EACA,MAAM,MAAM,MAAM,GAAG,iBACnB,QAAO,GAA0B;8BACT,KAAKO,KAAK,WAAW,EAAE;;;KAIjD;EACA,IAAI,IAAI,WAAW,GAAG;GACpB,KAAKZ,IAAI,OAAO,4CAA4C;GAC5D,GAAG,QAAQ;GACX,MAAM,GAAG,KAAK;GACd,OAAO;EACT;EACA,MAAM,CAAC,EAAC,eAAc;EACtB,MAAM,CAAC,EAAC,oBAAmB,MAAM,GAAG,iBAClC,QAAO,GAA+B;uCACL,KAAKY,KAAK,mBAAmB,EAAE;OAElE;EACA,KAAKZ,IAAI,OACP,oBAAoB,UAAU,kCAAkC,gBAClE;EACA,OAAO,IAAI,UAAU,KAAKA,KAAK,IAAI,gBAAgB,SAAS;CAC9D;AACF"}
1
+ {"version":3,"file":"storer.js","names":["#lc","#shard","#taskID","#discoveryAddress","#discoveryProtocol","#db","#replicaVersion","#onConsumed","#onFatal","#queue","#backPressureThresholdBytes","#statementTimeoutMs","#cdc","#approximateQueuedBytes","#running","#readyForMore","#stopped","#processQueue","#cancelQueueEntries","#startCatchup","#trackBackfillMetadata","#maybeReleaseBackPressure","#catchup","#upsertTableMetadataStmt","#upsertColumnBackfillStmt","#tx","#released"],"sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"sourcesContent":["import {getHeapStatistics} from 'node:v8';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../types/shards.ts';\nimport {\n backfillRequestSchema,\n isDataChange,\n isSchemaChange,\n type BackfillID,\n type BackfillRequest,\n type Change,\n type DataChange,\n type Identifier,\n type SchemaChange,\n type TableMetadata,\n} from '../change-source/protocol/current.ts';\nimport {\n type ChangeStreamData,\n type Commit,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {\n DownstreamStatusMessage,\n UpstreamStatusMessage,\n} from '../change-source/protocol/current/status.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {Service} from '../service.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {\n AutoResetSignal,\n markResetRequired,\n type BackfillingColumn,\n type TableMetadataRow,\n} from './schema/tables.ts';\nimport type {Subscriber} from './subscriber.ts';\n\ntype SubscriberAndMode = {\n subscriber: Subscriber;\n mode: ReplicatorMode;\n};\n\ntype QueueEntry =\n | [\n 'change',\n watermark: string,\n json: string,\n orig: Exclude<Change, DataChange> | null, // null for DataChanges\n ]\n | ['ready', callback: () => void]\n | ['subscriber', SubscriberAndMode]\n | DownstreamStatusMessage\n | ['abort']\n | 'stop';\n\ntype PendingTransaction = {\n pool: TransactionPool;\n preCommitWatermark: string;\n pos: number;\n startingReplicationState: Promise<ReplicationOwner>;\n ack: boolean;\n};\n\ntype ReplicationOwner = {\n owner: string | null;\n};\n\nconst backfillRequestsSchema = v.array(backfillRequestSchema);\n\nexport type TuningOptions = {\n backPressureLimitHeapProportion: number;\n statementTimeoutMs: number;\n};\n\n/**\n * Handles the storage of changes and the catchup of subscribers\n * that are behind.\n *\n * In the context of catchup and cleanup, it is the responsibility of the\n * Storer to decide whether a client can be caught up, or whether the\n * changes needed to catch a client up have been purged.\n *\n * **Maintained invariant**: The Change DB is only empty for a\n * completely new replica (i.e. initial-sync with no changes from the\n * replication stream).\n * * In this case, all new subscribers are expected start from the\n * `replicaVersion`, which is the version at which initial sync\n * was performed, and any attempts to catchup from a different\n * point fail.\n *\n * Conversely, if non-initial changes have flowed through the system\n * (i.e. via the replication stream), the ChangeDB must *not* be empty,\n * and the earliest change in the `changeLog` represents the earliest\n * \"commit\" from (after) which a subscriber can be caught up.\n * * Any attempts to catchup from an earlier point must fail with\n * a `WatermarkTooOld` error.\n * * Failure to do so could result in streaming changes to the\n * subscriber such that there is a gap in its replication history.\n *\n * Note: Subscribers (i.e. `incremental-syncer`) consider an \"error\" signal\n * an unrecoverable error and shut down in response. This allows the\n * production system to replace it with a new task and fresh copy of the\n * replica backup.\n */\nexport class Storer implements Service {\n readonly id = 'storer';\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #taskID: string;\n readonly #discoveryAddress: string;\n readonly #discoveryProtocol: string;\n readonly #db: PostgresDB;\n readonly #replicaVersion: string;\n readonly #onConsumed: (c: Commit | UpstreamStatusMessage) => void;\n readonly #onFatal: (err: Error) => void;\n readonly #queue = new Queue<QueueEntry>();\n readonly #backPressureThresholdBytes: number;\n readonly #statementTimeoutMs: number;\n\n #approximateQueuedBytes = 0;\n #running = false;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n db: PostgresDB,\n replicaVersion: string,\n onConsumed: (c: Commit | UpstreamStatusMessage) => void,\n onFatal: (err: Error) => void,\n {backPressureLimitHeapProportion, statementTimeoutMs}: TuningOptions,\n ) {\n this.#lc = lc.withContext('component', 'change-log');\n this.#shard = shard;\n this.#taskID = taskID;\n this.#discoveryAddress = discoveryAddress;\n this.#discoveryProtocol = discoveryProtocol;\n this.#db = db;\n this.#replicaVersion = replicaVersion;\n this.#onConsumed = onConsumed;\n this.#onFatal = onFatal;\n this.#statementTimeoutMs = statementTimeoutMs;\n\n const heapStats = getHeapStatistics();\n this.#backPressureThresholdBytes =\n (heapStats.heap_size_limit - heapStats.used_heap_size) *\n backPressureLimitHeapProportion;\n\n this.#lc.info?.(\n `Using up to ${(this.#backPressureThresholdBytes / 1024 ** 2).toFixed(2)} MB of ` +\n `--max-old-space-size (~${(heapStats.heap_size_limit / 1024 ** 2).toFixed(2)} MB) ` +\n `to absorb upstream spikes`,\n {heapStats},\n );\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async assumeOwnership(purgeLock?: PurgeLock | null) {\n const db = this.#db;\n const owner = this.#taskID;\n const ownerAddress = this.#discoveryAddress;\n const ownerProtocol = this.#discoveryProtocol;\n // we omit `ws://` so that old view syncer versions that are not expecting the protocol continue to not get it\n const addressWithProtocol =\n ownerProtocol === 'ws'\n ? ownerAddress\n : `${ownerProtocol}://${ownerAddress}`;\n this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);\n const start = performance.now();\n await db`UPDATE ${this.#cdc('replicationState')} SET ${db({owner, ownerAddress: addressWithProtocol})}`;\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `assumed ownership at ${addressWithProtocol} (${elapsed} ms)`,\n );\n\n if (purgeLock) {\n // Once ownership has been assumed, any initial purge-lock preventing the\n // purging of change-log records can be released, as a change-streamer\n // that was attempting to purge records will correspondingly abort on the\n // ownership check.\n void purgeLock.release();\n }\n }\n\n async getStartStreamInitializationParameters(): Promise<{\n lastWatermark: string;\n backfillRequests: BackfillRequest[];\n }> {\n const [[{lastWatermark}], result] = await runTx(\n this.#db,\n sql => [\n sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}`,\n\n // Formats a BackfillRequest using json_object_agg() to construct the\n // `columns` object. It is LEFT JOIN'ed with the `tableMetadata` table\n // to make it optional and possibly `null`.\n sql`\n SELECT \n json_build_object(\n 'schema', b.\"schema\",\n 'name', b.\"table\",\n 'metadata', t.\"metadata\"\n ) as \"table\",\n json_object_agg(b.\"column\", b.\"backfill\") \n as \"columns\"\n FROM ${this.#cdc('backfilling')} as b\n LEFT JOIN ${this.#cdc('tableMetadata')} as t\n ON (b.\"schema\" = t.\"schema\" AND b.\"table\" = t.\"table\")\n GROUP BY b.\"schema\", b.\"table\", t.\"metadata\"\n `,\n ],\n {mode: Mode.READONLY},\n );\n\n return {\n lastWatermark,\n backfillRequests: v.parse(result, backfillRequestsSchema),\n };\n }\n\n async getMinWatermarkForCatchup(): Promise<string | null> {\n const [{minWatermark}] = await this.#db<{minWatermark: string | null}[]>\n /*sql*/ `\n SELECT min(watermark) as \"minWatermark\" FROM ${this.#cdc('changeLog')}`;\n return minWatermark;\n }\n\n purgeRecordsBefore(watermark: string): Promise<number> {\n return runTx(this.#db, async sql => {\n // This NOWAIT pre-check is an optimization to abort the transaction\n // (and release associated resources) early.\n await sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR UPDATE NOWAIT\n `;\n // If the row is purge-locked by an incoming replication-manager, it\n // will assume ownership of the change-log before releasing the lock.\n // This DELETE blocks until the lock is released, allowing the change\n // in ownership to be reliably detected (and the transaction aborted)\n // in the subsequent check.\n const [{deleted}] = await sql<{deleted: bigint}[]>`\n WITH purged AS (\n DELETE FROM ${this.#cdc('changeLog')} WHERE watermark < ${watermark} \n RETURNING watermark, pos\n ) SELECT COUNT(*) as deleted FROM purged;`;\n\n const [{owner}] = await sql<ReplicationOwner[]>`\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR SHARE`;\n if (owner !== this.#taskID) {\n throw new AbortError(\n `aborting changeLog purge to ${watermark} because ownership has been taken by ${owner}`,\n );\n }\n return Number(deleted);\n });\n }\n\n /**\n * @returns The JSON stringified stream message to be sent downstream.\n */\n store(watermark: string, data: ChangeStreamData) {\n // Eagerly stringify the JSON payload to:\n // - avoid redundant stringification when fanning out to subscribers\n // - efficiently estimate the amount of memory the payload consumes\n const json = BigIntJSON.stringify(data);\n this.#approximateQueuedBytes += json.length;\n\n const change = data[1];\n this.#queue.enqueue([\n 'change',\n watermark,\n json,\n isDataChange(change) ? null : change, // drop DataChanges to save memory\n ]);\n\n return json;\n }\n\n abort() {\n this.#queue.enqueue(['abort']);\n }\n\n status(s: DownstreamStatusMessage) {\n this.#queue.enqueue(s);\n }\n\n catchup(subscriber: Subscriber, mode: ReplicatorMode) {\n this.#queue.enqueue(['subscriber', {subscriber, mode}]);\n }\n\n #readyForMore: Resolver<void> | null = null;\n\n readyForMore(): Promise<void> | undefined {\n if (!this.#running) {\n return undefined;\n }\n if (\n this.#readyForMore === null &&\n this.#approximateQueuedBytes > this.#backPressureThresholdBytes\n ) {\n this.#lc.warn?.(\n `applying back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)\\n` +\n `\\n` +\n `To inspect changeLog backlog in your change DB:\\n` +\n ` SELECT\\n` +\n ` (change->'relation'->>'schema') || '.' || (change->'relation'->>'name') AS table_name,\\n` +\n ` change->>'tag' AS operation,\\n` +\n ` COUNT(*) AS count\\n` +\n ` FROM \"<app_id>/cdc\".\"changeLog\"\\n` +\n ` GROUP BY 1, 2\\n` +\n ` ORDER BY 3 DESC\\n` +\n ` LIMIT 20;`,\n );\n this.#readyForMore = resolver();\n }\n return this.#readyForMore?.promise;\n }\n\n #maybeReleaseBackPressure() {\n if (\n this.#readyForMore !== null &&\n // Wait for at least 20% of the threshold to free up.\n this.#approximateQueuedBytes < this.#backPressureThresholdBytes * 0.8\n ) {\n this.#lc.info?.(\n `releasing back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)`,\n );\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n }\n\n #stopped = promiseVoid;\n\n /**\n * Runs the storer loop until {@link stop()} is called, or an error is thrown.\n * Once {@link run()} completes, it can be called again.\n */\n async run() {\n assert(!this.#running, `storer is already running`);\n\n const {promise: stopped, resolve: signalStopped} = resolver();\n this.#running = true;\n this.#stopped = stopped;\n\n this.#lc.info?.('starting storer');\n let err: unknown;\n try {\n await this.#processQueue();\n } catch (e) {\n err = e; // used in finally\n throw e;\n } finally {\n // Release any pending backpressure so the upstream can proceed\n if (this.#readyForMore !== null) {\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n this.#cancelQueueEntries(\n this.#queue.drain().filter(entry => entry !== undefined),\n err,\n );\n this.#running = false;\n signalStopped();\n this.#lc.info?.('storer stopped');\n }\n }\n\n #cancelQueueEntries(queue: QueueEntry[], e: unknown) {\n if (queue.length === 0) {\n return;\n }\n this.#lc.info?.(\n `canceling ${queue.length} entries from the changeLog queue`,\n );\n const err = e instanceof Error ? e : new AbortError('server shutting down');\n for (const entry of queue) {\n if (entry === 'stop') {\n continue;\n }\n const type = entry[0];\n switch (type) {\n case 'subscriber': {\n // Disconnect subscribers waiting to be caught up so that they can\n // reconnect and try again.\n const {subscriber} = entry[1];\n this.#lc.info?.(`disconnecting ${subscriber.id}`);\n subscriber.fail(err);\n break;\n }\n }\n }\n }\n\n async #processQueue() {\n let tx: PendingTransaction | null = null;\n let msg: QueueEntry | false;\n\n const catchupQueue: SubscriberAndMode[] = [];\n try {\n while ((msg = await this.#queue.dequeue()) !== 'stop') {\n const [msgType] = msg;\n switch (msgType) {\n case 'ready': {\n const signalReady = msg[1];\n signalReady();\n continue;\n }\n case 'subscriber': {\n const subscriber = msg[1];\n if (tx) {\n catchupQueue.push(subscriber); // Wait for the current tx to complete.\n } else {\n await this.#startCatchup([subscriber]); // Catch up immediately.\n }\n continue;\n }\n case 'status':\n this.#onConsumed(msg);\n continue;\n case 'abort': {\n if (tx) {\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n }\n continue;\n }\n }\n // msgType === 'change'\n const [_, watermark, json, change] = msg;\n const tag = change?.tag;\n this.#approximateQueuedBytes -= json.length;\n\n if (tag === 'begin') {\n assert(!tx, 'received BEGIN in the middle of a transaction');\n const {promise, resolve, reject} = resolver<ReplicationOwner>();\n void promise.catch(() => {}); // handle rejections before the await\n tx = {\n pool: new TransactionPool(\n this.#lc.withContext('watermark', watermark),\n {\n mode: Mode.READ_COMMITTED,\n statementResponseTimeout: this.#statementTimeoutMs,\n },\n ),\n preCommitWatermark: watermark,\n pos: 0,\n startingReplicationState: promise,\n ack: !change.skipAck,\n };\n tx.pool.run(this.#db);\n // Acquire a lock on the replicationState row to detect and/or prevent\n // a concurrent ownership change.\n void tx.pool.process(tx => {\n tx<ReplicationOwner[]> /*sql*/ `\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR UPDATE`.then(\n ([result]) => resolve(result),\n reject,\n );\n return [];\n });\n } else {\n assert(tx, () => `received change outside of transaction: ${json}`);\n tx.pos++;\n }\n\n const entry = {\n watermark: tag === 'commit' ? watermark : tx.preCommitWatermark,\n precommit: tag === 'commit' ? tx.preCommitWatermark : null,\n pos: tx.pos,\n // For backwards compatibility, only the change message is stored\n // in the cdc changeLog.\n change: extractChangeSubstring(json, tag),\n };\n\n const processed = tx.pool.process(sql => [\n sql`INSERT INTO ${this.#cdc('changeLog')} ${sql(entry)}`,\n ...(change !== null && isSchemaChange(change)\n ? this.#trackBackfillMetadata(sql, change)\n : []),\n ]);\n\n if (tx.pos % 100 === 0) {\n // Backpressure is exerted on commit when awaiting tx.pool.done().\n // However, backpressure checks need to be regularly done for\n // very large transactions in order to avoid memory blowup.\n await processed;\n }\n this.#maybeReleaseBackPressure();\n\n if (tag === 'commit') {\n const {owner} = await tx.startingReplicationState;\n if (owner !== this.#taskID) {\n // Ownership change reflected in the replicationState read in 'begin'.\n tx.pool.fail(\n new AbortError(\n `changeLog ownership has been assumed by ${owner}`,\n ),\n );\n } else {\n // Update the replication state.\n const lastWatermark = watermark;\n void tx.pool.process(tx => [\n tx`\n UPDATE ${this.#cdc('replicationState')} SET ${tx({lastWatermark})}`,\n ]);\n tx.pool.setDone();\n }\n\n await tx.pool.done();\n\n // ACK the LSN to the upstream Postgres.\n if (tx.ack) {\n this.#onConsumed(['commit', change, {watermark}]);\n }\n tx = null;\n\n // Before beginning the next transaction, open a READONLY snapshot to\n // concurrently catchup any queued subscribers.\n await this.#startCatchup(catchupQueue.splice(0));\n } else if (tag === 'rollback') {\n // Aborted transactions are not stored in the changeLog. Abort the current tx\n // and process catchup of subscribers that were waiting for it to end.\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n\n await this.#startCatchup(catchupQueue.splice(0));\n }\n }\n } catch (e) {\n catchupQueue.forEach(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n }\n\n async #startCatchup(subs: SubscriberAndMode[]) {\n if (subs.length === 0) {\n return;\n }\n\n const reader = new TransactionPool(\n this.#lc.withContext('pool', 'catchup'),\n {mode: Mode.READONLY},\n );\n reader.run(this.#db);\n\n let lastWatermark: string | undefined;\n try {\n // Ensure that the transaction has started (and is thus holding a snapshot\n // of the database) before continuing on to commit more changes. This is\n // done by performing a single read on the db, which determines the\n // snapshot for the REPEATABLE_READ transaction.\n [{lastWatermark}] = await reader.processReadTask(\n sql => sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}\n `,\n );\n } catch (e) {\n subs.map(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n\n // Run the actual catchup queries in the background. Errors are handled in\n // #catchup() by disconnecting the associated subscriber.\n void Promise.all(\n subs.map(sub => this.#catchup(sub, lastWatermark, reader)),\n ).finally(() => reader.setDone());\n }\n\n async #catchup(\n {subscriber: sub, mode}: SubscriberAndMode,\n lastWatermark: string,\n reader: TransactionPool,\n ) {\n try {\n await reader.processReadTask(async tx => {\n const start = Date.now();\n\n // When starting from initial-sync, there won't be a change with a watermark\n // equal to the replica version. This is the empty changeLog scenario.\n let watermarkFound = sub.watermark === this.#replicaVersion;\n let count = 0;\n let lastBatchConsumed: Promise<unknown> | undefined;\n\n for await (const entries of tx<ChangeLogEntry[]> /*sql*/ `\n SELECT watermark, change->'tag' as tag, change::text FROM ${this.#cdc('changeLog')}\n WHERE watermark >= ${sub.watermark}\n AND watermark <= ${lastWatermark}\n ORDER BY watermark, pos`.cursor(2000)) {\n // Wait for the last batch of entries to be consumed by the\n // subscriber before sending down the current batch. This pipelining\n // allows one batch of changes to be received from the change-db\n // while the previous batch of changes are sent to the subscriber,\n // resulting in flow control that caps the number of changes\n // referenced in memory to 2 * batch-size.\n const start = performance.now();\n await lastBatchConsumed;\n const elapsed = performance.now() - start;\n if (lastBatchConsumed) {\n (elapsed > 100 ? this.#lc.info : this.#lc.debug)?.(\n `waited ${elapsed.toFixed(3)} ms for ${sub.id} to consume last batch of catchup entries`,\n );\n }\n\n for (const entry of entries) {\n if (entry.watermark === sub.watermark) {\n // This should be the first entry.\n // Catchup starts from *after* the watermark.\n watermarkFound = true;\n } else if (watermarkFound) {\n lastBatchConsumed = sub.catchup(toDownstream(entry));\n count++;\n } else if (mode === 'backup') {\n throw new AutoResetSignal(\n `backup replica at watermark ${sub.watermark} is behind change db: ${entry.watermark})`,\n );\n } else {\n this.#lc.warn?.(\n `rejecting subscriber at watermark ${sub.watermark} (earliest watermark: ${entry.watermark})`,\n );\n sub.close(\n ErrorType.WatermarkTooOld,\n `earliest supported watermark is ${entry.watermark} (requested ${sub.watermark})`,\n );\n return;\n }\n }\n }\n if (watermarkFound) {\n await lastBatchConsumed;\n this.#lc.info?.(\n `caught up ${sub.id} with ${count} changes (${\n Date.now() - start\n } ms)`,\n );\n } else {\n this.#lc.warn?.(\n `subscriber at watermark ${sub.watermark} is ahead of latest watermark`,\n );\n }\n // Flushes the backlog of messages buffered during catchup and\n // allows the subscription to forward subsequent messages immediately.\n sub.setCaughtUp();\n });\n } catch (err) {\n this.#lc.error?.(`error while catching up subscriber ${sub.id}`, err);\n if (err instanceof AutoResetSignal) {\n await markResetRequired(this.#db, this.#shard);\n this.#onFatal(err);\n }\n sub.fail(err);\n }\n }\n\n /**\n * Returns the db statements necessary to track backfill and table metadata\n * presented in the `change`, if any.\n */\n #trackBackfillMetadata(sql: PostgresTransaction, change: SchemaChange) {\n const stmts: PendingQuery<Row[]>[] = [];\n\n switch (change.tag) {\n case 'update-table-metadata': {\n const {table, new: metadata} = change;\n stmts.push(this.#upsertTableMetadataStmt(sql, table, metadata));\n break;\n }\n\n case 'create-table': {\n const {spec, metadata, backfill} = change;\n if (metadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, spec, metadata));\n }\n if (backfill) {\n Object.entries(backfill).forEach(([col, backfill]) => {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, spec, col, backfill),\n );\n });\n }\n break;\n }\n\n case 'rename-table': {\n const {old} = change;\n const row = {schema: change.new.schema, table: change.new.name};\n stmts.push(\n sql`UPDATE ${this.#cdc('tableMetadata')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n sql`UPDATE ${this.#cdc('backfilling')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n );\n break;\n }\n\n case 'drop-table': {\n const {\n id: {schema, name},\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('tableMetadata')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n );\n break;\n }\n\n case 'add-column': {\n const {table, tableMetadata, column, backfill} = change;\n if (tableMetadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, table, tableMetadata));\n }\n if (backfill) {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, table, column.name, backfill),\n );\n }\n break;\n }\n\n case 'update-column': {\n const {\n table: {schema, name: table},\n old: {name: oldName},\n new: {name: newName},\n } = change;\n if (oldName !== newName) {\n stmts.push(\n sql`UPDATE ${this.#cdc('backfilling')} SET \"column\" = ${newName}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" = ${oldName}`,\n );\n }\n break;\n }\n\n case 'drop-column': {\n const {\n table: {schema, name},\n column,\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name} AND \"column\" = ${column}`,\n );\n break;\n }\n\n case 'backfill-completed': {\n const {\n relation: {schema, name: table, rowKey},\n columns,\n } = change;\n const cols = [...rowKey.columns, ...columns];\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" IN ${sql(cols)}`,\n );\n }\n }\n return stmts;\n }\n\n #upsertTableMetadataStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n metadata: TableMetadata,\n ) {\n const row: TableMetadataRow = {schema, table, metadata};\n return sql`\n INSERT INTO ${this.#cdc('tableMetadata')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n #upsertColumnBackfillStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n column: string,\n backfill: BackfillID,\n ) {\n const row: BackfillingColumn = {schema, table, column, backfill};\n return sql`\n INSERT INTO ${this.#cdc('backfilling')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\", \"column\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n /**\n * Waits until all currently queued entries have been processed.\n * This is only used in tests.\n */\n async allProcessed() {\n if (this.#running) {\n const {promise, resolve} = resolver();\n this.#queue.enqueue(['ready', resolve]);\n await promise;\n }\n }\n\n stop() {\n if (this.#running) {\n this.#lc.info?.(`draining ${this.#queue.size()} changeLog entries`);\n this.#queue.enqueue('stop');\n }\n return this.#stopped;\n }\n}\n\n/**\n * Extracts the stringified change message from the stringified\n * stream message (e.g. the second tuple element). This optimization\n * facilitates stringifying (and sharing the result of) the stream\n * message exactly once, but storing only the change message substring\n * in the changeLog for backwards compatibility.\n */\nexport function extractChangeSubstring(\n streamMessageJSON: string,\n tag: Change['tag'] | undefined,\n) {\n switch (tag) {\n case 'begin':\n case 'commit':\n // e.g.\n // [\"begin\",<message-json>,{\"commitWatermark\":\"92fj2d0s\"}]\n // [\"commit\",<message-json>,{\"watermark\":\"92fj2d0s\"}]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(','),\n );\n default:\n // [\"data\",<message-json>]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(']'),\n );\n }\n}\n\ntype ChangeLogEntry = {\n watermark: string;\n tag: string;\n change: string;\n};\n\nfunction toDownstream(entry: ChangeLogEntry): WatermarkedChange {\n const {watermark, change} = entry;\n const tag = entry.tag as ChangeTag;\n switch (tag) {\n case 'begin':\n return [\n watermark,\n tag,\n `[\"begin\",${change},{\"commitWatermark\":\"${watermark}\"}]`,\n ];\n case 'commit':\n return [\n watermark,\n tag,\n `[\"commit\",${change},{\"watermark\":\"${watermark}\"}]`,\n ];\n case 'rollback':\n return [watermark, tag, `[\"rollback\",${change}]`];\n default:\n return [watermark, tag, `[\"data\",${change}]`];\n }\n}\n\nexport class PurgeLock {\n readonly #lc: LogContext;\n readonly #tx: TransactionPool;\n readonly replicaVersion: string;\n readonly minWatermark: string;\n\n constructor(\n lc: LogContext,\n tx: TransactionPool,\n replicaVersion: string,\n watermark: string,\n ) {\n this.#lc = lc;\n this.#tx = tx;\n this.replicaVersion = replicaVersion;\n this.minWatermark = watermark;\n }\n\n #released = false;\n\n async release() {\n if (this.#released) {\n return;\n }\n this.#released = true;\n this.#tx.setDone();\n await this.#tx\n .done()\n .catch(e => this.#lc.warn?.(`error from purge-lock release`, e));\n this.#lc.info?.(`released purge lock on ${this.minWatermark}`);\n }\n}\n\nexport class PurgeLocker {\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #db: PostgresDB;\n\n constructor(lc: LogContext, shard: ShardID, db: PostgresDB) {\n this.#lc = lc.withContext('component', 'purge-locker');\n this.#shard = shard;\n this.#db = db;\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async acquire() {\n const tx = new TransactionPool(this.#lc, {mode: Mode.READ_COMMITTED}).run(\n this.#db,\n );\n const row = await tx.processReadTask(\n sql => sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR SHARE \n `,\n );\n if (row.length === 0) {\n this.#lc.info?.(`changeLog is empty. No rows to purge-lock.`);\n tx.setDone();\n await tx.done();\n return null;\n }\n const [{watermark}] = row;\n const [{replicaVersion}] = await tx.processReadTask(\n sql => sql<{replicaVersion: string}[]>`\n SELECT \"replicaVersion\" FROM ${this.#cdc('replicationConfig')}\n `,\n );\n this.#lc.info?.(\n `locked watermark ${watermark} from being purged from replica@${replicaVersion}`,\n );\n return new PurgeLock(this.#lc, tx, replicaVersion, watermark);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"subscriber.js","names":["#protocolVersion","#downstream","#latestStatus","#watermark","#acked","#backlog","#sendChange","#initialized","#sendDownstream","#initialize","#sendStringifiedDownstream","#pending","#processed","#samples"],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport {type Downstream, type Status} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly #downstream: Subscription<string>;\n readonly #latestStatus: () => Status;\n #watermark: string;\n #acked: string;\n #backlog: WatermarkedChange[] | null;\n\n constructor(\n protocolVersion: number,\n id: string,\n watermark: string,\n downstream: Subscription<string>,\n latestStatus: () => Status,\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.#downstream = downstream;\n this.#latestStatus = latestStatus;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = [];\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n async send(change: WatermarkedChange) {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n this.#backlog.push(change);\n } else {\n await this.#sendChange(change);\n }\n }\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n this.sendStatus(this.#latestStatus());\n }\n }\n\n sendStatus(status: Status) {\n if (this.#protocolVersion >= 2 && this.#initialized) {\n void this.#sendDownstream(['status', status]);\n }\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp() {\n this.#initialize();\n assert(\n this.#backlog,\n 'setCaughtUp() called but subscriber is not in catchup mode',\n );\n // Note that this method must be asynchronous in order for send() to\n // interpret the #backlog variable correctly. This is the only place\n // where I/O flow control is not heeded. However, it will be awaited\n // by the next caller to send().\n for (const change of this.#backlog) {\n void this.#sendChange(change);\n }\n this.#backlog = null;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n this.#acked = max(this.#acked, watermark);\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n this.#pending++;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#processed++;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): {processRate: number; pending: number} {\n const pending = this.#pending;\n if (this.#samples.length < 2) {\n return {processRate: 0, pending};\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {processRate, pending};\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n fail(err?: unknown) {\n this.close(ErrorType.Unknown, String(err));\n }\n\n close(error?: ErrorType, message?: string) {\n if (error) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream(['error', {type: error, message}]).finally(() =>\n this.#downstream.cancel(),\n );\n } else {\n this.#downstream.cancel();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subscriber.js","names":["#protocolVersion","#downstream","#latestStatus","#watermark","#acked","#backlog","#sendChange","#initialized","#sendDownstream","#initialize","#sendStringifiedDownstream","#pending","#processed","#samples"],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport {type Downstream, type Status} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly #downstream: Subscription<string>;\n readonly #latestStatus: () => Status;\n #watermark: string;\n #acked: string;\n #backlog: WatermarkedChange[] | null;\n\n constructor(\n protocolVersion: number,\n id: string,\n watermark: string,\n downstream: Subscription<string>,\n latestStatus: () => Status,\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.#downstream = downstream;\n this.#latestStatus = latestStatus;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = [];\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n async send(change: WatermarkedChange) {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n this.#backlog.push(change);\n } else {\n await this.#sendChange(change);\n }\n }\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n this.sendStatus(this.#latestStatus());\n }\n }\n\n sendStatus(status: Status) {\n if (this.#protocolVersion >= 2 && this.#initialized) {\n void this.#sendDownstream(['status', status]);\n }\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp() {\n this.#initialize();\n assert(\n this.#backlog,\n 'setCaughtUp() called but subscriber is not in catchup mode',\n );\n // Note that this method must be asynchronous in order for send() to\n // interpret the #backlog variable correctly. This is the only place\n // where I/O flow control is not heeded. However, it will be awaited\n // by the next caller to send().\n for (const change of this.#backlog) {\n void this.#sendChange(change);\n }\n this.#backlog = null;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n this.#acked = max(this.#acked, watermark);\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n this.#pending++;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#processed++;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): {processRate: number; pending: number} {\n const pending = this.#pending;\n if (this.#samples.length < 2) {\n return {processRate: 0, pending};\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {processRate, pending};\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n fail(err?: unknown) {\n this.close(ErrorType.Unknown, String(err));\n }\n\n close(error?: ErrorType, message?: string) {\n if (error) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream(['error', {type: error, message}]).finally(() =>\n this.#downstream.cancel(),\n );\n } else {\n this.#downstream.cancel();\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"heapz.js","names":[],"sources":["../../../../../zero-cache/src/services/heapz.ts"],"sourcesContent":["import fs from 'fs';\nimport v8 from 'v8';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {isAdminPasswordValid} from '../config/zero-config.ts';\n\nexport function handleHeapzRequest(\n lc: LogContext,\n config: NormalizedZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (!isAdminPasswordValid(lc, config, credentials?.pass)) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Heapz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const filename = v8.writeHeapSnapshot();\n const stream = fs.createReadStream(filename);\n void res\n .header('Content-Type', 'application/octet-stream')\n .header('Content-Disposition', `attachment; filename=${filename}`)\n .send(stream);\n\n // Clean up temp file after streaming\n stream.on('end', () => {\n fs.unlink(filename, err => {\n if (err) {\n lc.error?.('Error deleting heap snapshot:', err);\n }\n });\n });\n}\n"],"mappings":";;;;;AAQA,SAAgB,mBACd,IACA,QACA,KACA,KACA;CAEA,IAAI,CAAC,qBAAqB,IAAI,QADV,KAAK,GACa,GAAa,IAAI,GAAG;EACxD,IACG,KAAK,GAAG,EACR,OAAO,oBAAoB,sCAAoC,EAC/D,KAAK,cAAc;EACtB;CACF;CAEA,MAAM,WAAW,GAAG,kBAAkB;CACtC,MAAM,SAAS,GAAG,iBAAiB,QAAQ;CAC3C,IACG,OAAO,gBAAgB,0BAA0B,EACjD,OAAO,uBAAuB,wBAAwB,UAAU,EAChE,KAAK,MAAM;CAGd,OAAO,GAAG,aAAa;EACrB,GAAG,OAAO,WAAU,QAAO;GACzB,IAAI,KACF,GAAG,QAAQ,iCAAiC,GAAG;EAEnD,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"heapz.js","names":[],"sources":["../../../../../zero-cache/src/services/heapz.ts"],"sourcesContent":["import fs from 'fs';\nimport v8 from 'v8';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {isAdminPasswordValid} from '../config/zero-config.ts';\n\nexport function handleHeapzRequest(\n lc: LogContext,\n config: NormalizedZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (!isAdminPasswordValid(lc, config, credentials?.pass)) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Heapz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const filename = v8.writeHeapSnapshot();\n const stream = fs.createReadStream(filename);\n void res\n .header('Content-Type', 'application/octet-stream')\n .header('Content-Disposition', `attachment; filename=${filename}`)\n .send(stream);\n\n // Clean up temp file after streaming\n stream.on('end', () => {\n fs.unlink(filename, err => {\n if (err) {\n lc.error?.('Error deleting heap snapshot:', err);\n }\n });\n });\n}\n"],"mappings":";;;;;AAQA,SAAgB,mBACd,IACA,QACA,KACA,KACA;AAEA,KAAI,CAAC,qBAAqB,IAAI,QADV,KAAK,IAAI,EACsB,KAAK,EAAE;AACnD,MACF,KAAK,IAAI,CACT,OAAO,oBAAoB,uCAAqC,CAChE,KAAK,eAAe;AACvB;;CAGF,MAAM,WAAW,GAAG,mBAAmB;CACvC,MAAM,SAAS,GAAG,iBAAiB,SAAS;AACvC,KACF,OAAO,gBAAgB,2BAA2B,CAClD,OAAO,uBAAuB,wBAAwB,WAAW,CACjE,KAAK,OAAO;AAGf,QAAO,GAAG,aAAa;AACrB,KAAG,OAAO,WAAU,QAAO;AACzB,OAAI,IACF,IAAG,QAAQ,iCAAiC,IAAI;IAElD;GACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"http-service.js","names":["#fastify","#port","#heartbeatMonitor","#init"],"sources":["../../../../../zero-cache/src/services/http-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport Fastify, {type FastifyInstance} from 'fastify';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {HeartbeatMonitor} from './life-cycle.ts';\nimport {RunningState} from './running-state.ts';\nimport type {Service} from './service.ts';\n\nexport type Options = {\n port: number;\n keepaliveTimeoutMs: number | undefined;\n};\n\n/**\n * Common functionality for all HttpServices. These include:\n * * Responding to health checks at \"/\"\n * * Tracking optional heartbeats at \"/keepalive\" and draining when they stop.\n */\nexport class HttpService implements Service {\n readonly id: string;\n protected readonly _lc: LogContext;\n readonly #fastify: FastifyInstance;\n readonly #port: number;\n protected readonly _state: RunningState;\n readonly #heartbeatMonitor: HeartbeatMonitor | undefined;\n readonly #init: (fastify: FastifyInstance) => void | Promise<void>;\n\n constructor(\n id: string,\n lc: LogContext,\n opts: Options,\n init: (fastify: FastifyInstance) => void | Promise<void>,\n ) {\n const {port, keepaliveTimeoutMs} = opts;\n this.id = id;\n this._lc = lc.withContext('component', this.id);\n this.#fastify = Fastify();\n this.#port = port;\n this.#init = init;\n this._state = new RunningState(id);\n this.#heartbeatMonitor = keepaliveTimeoutMs\n ? new HeartbeatMonitor(this._lc, keepaliveTimeoutMs)\n : undefined;\n }\n\n // Life-cycle hooks for subclass implementations\n protected _onStart() {}\n protected _onStop(): Promise<void> {\n return promiseVoid;\n }\n // start() is used in unit tests.\n // run() is the lifecycle method called by the ServiceRunner.\n async start(): Promise<string> {\n this.#fastify.get('/', (_req, res) => res.send('OK'));\n this.#fastify.get('/keepalive', ({headers}, res) => {\n this.#heartbeatMonitor?.onHeartbeat(headers);\n return res.send('OK');\n });\n await this.#init(this.#fastify);\n const address = await this.#fastify.listen({\n host: '::',\n port: this.#port,\n });\n this._lc.info?.(`${this.id} listening at ${address}`);\n this._onStart();\n return address;\n }\n\n async run(): Promise<void> {\n await this.start();\n await this._state.stopped();\n }\n\n async stop(): Promise<void> {\n this._lc.info?.(`${this.id}: no longer accepting connections`);\n this.#heartbeatMonitor?.stop();\n this._state.stop(this._lc);\n await this.#fastify.close();\n await this._onStop();\n }\n}\n"],"mappings":";;;;;;;;;;AAiBA,IAAa,cAAb,MAA4C;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,IACA,IACA,MACA,MACA;EACA,MAAM,EAAC,MAAM,uBAAsB;EACnC,KAAK,KAAK;EACV,KAAK,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAKA,WAAW,QAAQ;EACxB,KAAKC,QAAQ;EACb,KAAKE,QAAQ;EACb,KAAK,SAAS,IAAI,aAAa,EAAE;EACjC,KAAKD,oBAAoB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,kBAAkB,IACjD,KAAA;CACN;CAGA,WAAqB,CAAC;CACtB,UAAmC;EACjC,OAAO;CACT;CAGA,MAAM,QAAyB;EAC7B,KAAKF,SAAS,IAAI,MAAM,MAAM,QAAQ,IAAI,KAAK,IAAI,CAAC;EACpD,KAAKA,SAAS,IAAI,eAAe,EAAC,WAAU,QAAQ;GAClD,KAAKE,mBAAmB,YAAY,OAAO;GAC3C,OAAO,IAAI,KAAK,IAAI;EACtB,CAAC;EACD,MAAM,KAAKC,MAAM,KAAKH,QAAQ;EAC9B,MAAM,UAAU,MAAM,KAAKA,SAAS,OAAO;GACzC,MAAM;GACN,MAAM,KAAKC;EACb,CAAC;EACD,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,gBAAgB,SAAS;EACpD,KAAK,SAAS;EACd,OAAO;CACT;CAEA,MAAM,MAAqB;EACzB,MAAM,KAAK,MAAM;EACjB,MAAM,KAAK,OAAO,QAAQ;CAC5B;CAEA,MAAM,OAAsB;EAC1B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,kCAAkC;EAC7D,KAAKC,mBAAmB,KAAK;EAC7B,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,MAAM,KAAKF,SAAS,MAAM;EAC1B,MAAM,KAAK,QAAQ;CACrB;AACF"}
1
+ {"version":3,"file":"http-service.js","names":["#fastify","#port","#heartbeatMonitor","#init"],"sources":["../../../../../zero-cache/src/services/http-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport Fastify, {type FastifyInstance} from 'fastify';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {HeartbeatMonitor} from './life-cycle.ts';\nimport {RunningState} from './running-state.ts';\nimport type {Service} from './service.ts';\n\nexport type Options = {\n port: number;\n keepaliveTimeoutMs: number | undefined;\n};\n\n/**\n * Common functionality for all HttpServices. These include:\n * * Responding to health checks at \"/\"\n * * Tracking optional heartbeats at \"/keepalive\" and draining when they stop.\n */\nexport class HttpService implements Service {\n readonly id: string;\n protected readonly _lc: LogContext;\n readonly #fastify: FastifyInstance;\n readonly #port: number;\n protected readonly _state: RunningState;\n readonly #heartbeatMonitor: HeartbeatMonitor | undefined;\n readonly #init: (fastify: FastifyInstance) => void | Promise<void>;\n\n constructor(\n id: string,\n lc: LogContext,\n opts: Options,\n init: (fastify: FastifyInstance) => void | Promise<void>,\n ) {\n const {port, keepaliveTimeoutMs} = opts;\n this.id = id;\n this._lc = lc.withContext('component', this.id);\n this.#fastify = Fastify();\n this.#port = port;\n this.#init = init;\n this._state = new RunningState(id);\n this.#heartbeatMonitor = keepaliveTimeoutMs\n ? new HeartbeatMonitor(this._lc, keepaliveTimeoutMs)\n : undefined;\n }\n\n // Life-cycle hooks for subclass implementations\n protected _onStart() {}\n protected _onStop(): Promise<void> {\n return promiseVoid;\n }\n // start() is used in unit tests.\n // run() is the lifecycle method called by the ServiceRunner.\n async start(): Promise<string> {\n this.#fastify.get('/', (_req, res) => res.send('OK'));\n this.#fastify.get('/keepalive', ({headers}, res) => {\n this.#heartbeatMonitor?.onHeartbeat(headers);\n return res.send('OK');\n });\n await this.#init(this.#fastify);\n const address = await this.#fastify.listen({\n host: '::',\n port: this.#port,\n });\n this._lc.info?.(`${this.id} listening at ${address}`);\n this._onStart();\n return address;\n }\n\n async run(): Promise<void> {\n await this.start();\n await this._state.stopped();\n }\n\n async stop(): Promise<void> {\n this._lc.info?.(`${this.id}: no longer accepting connections`);\n this.#heartbeatMonitor?.stop();\n this._state.stop(this._lc);\n await this.#fastify.close();\n await this._onStop();\n }\n}\n"],"mappings":";;;;;;;;;;AAiBA,IAAa,cAAb,MAA4C;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,IACA,IACA,MACA,MACA;EACA,MAAM,EAAC,MAAM,uBAAsB;AACnC,OAAK,KAAK;AACV,OAAK,MAAM,GAAG,YAAY,aAAa,KAAK,GAAG;AAC/C,QAAA,UAAgB,SAAS;AACzB,QAAA,OAAa;AACb,QAAA,OAAa;AACb,OAAK,SAAS,IAAI,aAAa,GAAG;AAClC,QAAA,mBAAyB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,mBAAmB,GAClD,KAAA;;CAIN,WAAqB;CACrB,UAAmC;AACjC,SAAO;;CAIT,MAAM,QAAyB;AAC7B,QAAA,QAAc,IAAI,MAAM,MAAM,QAAQ,IAAI,KAAK,KAAK,CAAC;AACrD,QAAA,QAAc,IAAI,eAAe,EAAC,WAAU,QAAQ;AAClD,SAAA,kBAAwB,YAAY,QAAQ;AAC5C,UAAO,IAAI,KAAK,KAAK;IACrB;AACF,QAAM,MAAA,KAAW,MAAA,QAAc;EAC/B,MAAM,UAAU,MAAM,MAAA,QAAc,OAAO;GACzC,MAAM;GACN,MAAM,MAAA;GACP,CAAC;AACF,OAAK,IAAI,OAAO,GAAG,KAAK,GAAG,gBAAgB,UAAU;AACrD,OAAK,UAAU;AACf,SAAO;;CAGT,MAAM,MAAqB;AACzB,QAAM,KAAK,OAAO;AAClB,QAAM,KAAK,OAAO,SAAS;;CAG7B,MAAM,OAAsB;AAC1B,OAAK,IAAI,OAAO,GAAG,KAAK,GAAG,mCAAmC;AAC9D,QAAA,kBAAwB,MAAM;AAC9B,OAAK,OAAO,KAAK,KAAK,IAAI;AAC1B,QAAM,MAAA,QAAc,OAAO;AAC3B,QAAM,KAAK,SAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"life-cycle.js","names":["#lc","#userFacing","#all","#exitImpl","#start","#ready","#startDrain","#kill","#exit","#runningState","#drainStart","#onExit","#initializing","#nextID","#stopInterval","#lastHeartbeat","#checkIntervalTimer","#checkStopInterval","#checkImmediateTimer"],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {ConfigurationError} from '../types/configuration-error.ts';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT` and `SIGABRT`, all workers\n * are stopped without draining. `SIGQUIT` is used to represent an\n * intentional shutdown (for which draining is not beneficial), whereas\n * `SIGABRT` is used for unexpected process exits.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT', 'SIGABRT'] as const;\n\ntype GracefulShutdownSignal = (typeof GRACEFUL_SHUTDOWN)[number];\n\n// An internal error code used to indicate that a message has already been\n// logged at level ERRROR. When a process exits with this error code, the\n// parent process logs the exit at level WARN instead of ERROR.\nexport const UNHANDLED_EXCEPTION_ERROR_CODE = 13;\n\n// An internal error code used to indicate that the server should exit\n// without draining (e.g. due to a supporting worker get a signal to shut\n// down), but the exit is otherwise intentional.\nexport const INTENTIONAL_SHUTDOWN_ERROR_CODE = 14;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0\n ? 'SIGTERM' // graceful, drained shutdown\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT' // intentional abort without drain\n : 'SIGABRT', // unintentional shutdown, alertable error\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` (intentional shutdown) or `SIGABRT` (unexpected\n // shutdown) to all workers. For these signals, workers are stopped\n // immediately without draining, since there is no merit to slowly draining\n // when supporting workers have stopped.\n //\n // The logic for handling these signals is in `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () =>\n this.#exit(signal === 'SIGQUIT' ? INTENTIONAL_SHUTDOWN_ERROR_CODE : -1),\n );\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n done() {\n return this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: GracefulShutdownSignal) {\n if (this.#all.size === 0) {\n // Shutdown if a signal is received before any subprocesses are added.\n this.#lc.info?.(`exiting on ${signal}`);\n this.#exit(0);\n }\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n let isOpen = true;\n proc.on('close', (code, signal) => {\n isOpen = false;\n this.#onExit(code, signal, null, type, name, proc);\n });\n\n // As per https://nodejs.org/api/child_process.html#event-error\n // 'error' events can happen when sending a message to a child process\n // fails. This is not really an error when the server is shutting down,\n // so log any post-close errors at 'warn'.\n proc.on('error', err =>\n this.#lc[!isOpen || this.#drainStart > 0 ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n if (type === 'supporting') {\n // Supporting workers like the replication-manager shut down without a\n // drain signal when receiving protocol-specific instructions (like auto\n // reset). In this case, a special error code is used to signal that the\n // server should be shut down without draining, but it is otherwise not\n // considered an unexpected/alertable error.\n if (code === 0 && (this.#drainStart === 0 || this.#userFacing.size > 0)) {\n code = INTENTIONAL_SHUTDOWN_ERROR_CODE;\n }\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(code);\n }\n\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : this.#drainStart > 0 || code === UNHANDLED_EXCEPTION_ERROR_CODE\n ? 'warn'\n : 'error';\n this.#lc[log]?.(\n sig\n ? `${name} (${pid}) killed with (${sig})`\n : `${name} (${pid}) exited with code (${code})`,\n err ?? '',\n );\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(0);\n }\n\n if (this.#drainStart === 0) {\n // If a user-facing worker exits without receiving a drain signal,\n // shutdown the server.\n return this.#exit(code || -1);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`.\n *\n * @returns a Promise that resolves/rejects when any of the services stops/throws.\n */\n\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.once(signal, () => {\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n\n services.forEach(async svc => {\n if (GRACEFUL_SIGNALS.includes(signal) && svc.drain) {\n lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.drain();\n }\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.stop();\n });\n });\n }\n\n try {\n // Run all services and resolve when any of them stops.\n const svc = await Promise.race(\n services.map(svc => svc.run().then(() => svc)),\n );\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n }\n}\n\nexport async function exitAfter(lc: LogContext, run: () => Promise<void>) {\n try {\n await run();\n lc.info?.(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n if (e instanceof ConfigurationError) {\n lc.error?.(`exiting with configuration error: ${String(e)}`, e);\n process.exit(0);\n }\n lc.error?.(`exiting with error: ${String(e)}`, e);\n process.exit(-1);\n }\n}\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval: number) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"life-cycle.js","names":["#lc","#userFacing","#all","#exitImpl","#start","#ready","#startDrain","#kill","#exit","#runningState","#drainStart","#onExit","#initializing","#nextID","#stopInterval","#lastHeartbeat","#checkIntervalTimer","#checkStopInterval","#checkImmediateTimer"],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {ConfigurationError} from '../types/configuration-error.ts';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT` and `SIGABRT`, all workers\n * are stopped without draining. `SIGQUIT` is used to represent an\n * intentional shutdown (for which draining is not beneficial), whereas\n * `SIGABRT` is used for unexpected process exits.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT', 'SIGABRT'] as const;\n\ntype GracefulShutdownSignal = (typeof GRACEFUL_SHUTDOWN)[number];\n\n// An internal error code used to indicate that a message has already been\n// logged at level ERRROR. When a process exits with this error code, the\n// parent process logs the exit at level WARN instead of ERROR.\nexport const UNHANDLED_EXCEPTION_ERROR_CODE = 13;\n\n// An internal error code used to indicate that the server should exit\n// without draining (e.g. due to a supporting worker get a signal to shut\n// down), but the exit is otherwise intentional.\nexport const INTENTIONAL_SHUTDOWN_ERROR_CODE = 14;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0\n ? 'SIGTERM' // graceful, drained shutdown\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT' // intentional abort without drain\n : 'SIGABRT', // unintentional shutdown, alertable error\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` (intentional shutdown) or `SIGABRT` (unexpected\n // shutdown) to all workers. For these signals, workers are stopped\n // immediately without draining, since there is no merit to slowly draining\n // when supporting workers have stopped.\n //\n // The logic for handling these signals is in `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () =>\n this.#exit(signal === 'SIGQUIT' ? INTENTIONAL_SHUTDOWN_ERROR_CODE : -1),\n );\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n done() {\n return this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: GracefulShutdownSignal) {\n if (this.#all.size === 0) {\n // Shutdown if a signal is received before any subprocesses are added.\n this.#lc.info?.(`exiting on ${signal}`);\n this.#exit(0);\n }\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n let isOpen = true;\n proc.on('close', (code, signal) => {\n isOpen = false;\n this.#onExit(code, signal, null, type, name, proc);\n });\n\n // As per https://nodejs.org/api/child_process.html#event-error\n // 'error' events can happen when sending a message to a child process\n // fails. This is not really an error when the server is shutting down,\n // so log any post-close errors at 'warn'.\n proc.on('error', err =>\n this.#lc[!isOpen || this.#drainStart > 0 ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n if (type === 'supporting') {\n // Supporting workers like the replication-manager shut down without a\n // drain signal when receiving protocol-specific instructions (like auto\n // reset). In this case, a special error code is used to signal that the\n // server should be shut down without draining, but it is otherwise not\n // considered an unexpected/alertable error.\n if (code === 0 && (this.#drainStart === 0 || this.#userFacing.size > 0)) {\n code = INTENTIONAL_SHUTDOWN_ERROR_CODE;\n }\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(code);\n }\n\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : this.#drainStart > 0 || code === UNHANDLED_EXCEPTION_ERROR_CODE\n ? 'warn'\n : 'error';\n this.#lc[log]?.(\n sig\n ? `${name} (${pid}) killed with (${sig})`\n : `${name} (${pid}) exited with code (${code})`,\n err ?? '',\n );\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(0);\n }\n\n if (this.#drainStart === 0) {\n // If a user-facing worker exits without receiving a drain signal,\n // shutdown the server.\n return this.#exit(code || -1);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`.\n *\n * @returns a Promise that resolves/rejects when any of the services stops/throws.\n */\n\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.once(signal, () => {\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n\n services.forEach(async svc => {\n if (GRACEFUL_SIGNALS.includes(signal) && svc.drain) {\n lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.drain();\n }\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.stop();\n });\n });\n }\n\n try {\n // Run all services and resolve when any of them stops.\n const svc = await Promise.race(\n services.map(svc => svc.run().then(() => svc)),\n );\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n }\n}\n\nexport async function exitAfter(lc: LogContext, run: () => Promise<void>) {\n try {\n await run();\n lc.info?.(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n if (e instanceof ConfigurationError) {\n lc.error?.(`exiting with configuration error: ${String(e)}`, e);\n process.exit(0);\n }\n lc.error?.(`exiting with error: ${String(e)}`, e);\n process.exit(-1);\n }\n}\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval: number) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sliding-window-limiter.js","names":["#windowSizeMs","#maxMutations","#priorWindow","#nextWindow","#newWindows","#rotateWindows"],"sources":["../../../../../../zero-cache/src/services/limiter/sliding-window-limiter.ts"],"sourcesContent":["import {assert} from '../../../../shared/src/asserts.ts';\n\ntype Window = {\n start: number;\n count: number;\n};\n\n/**\n * The limiter is a sliding window rate limiter that allows a maximum\n * number of mutations per window.\n *\n * The window is divided into two parts: the prev window and the next window.\n * The prev window is the part of the window that has already passed, and the\n * next window is the part of the window that is still in the future.\n *\n * The limiter keeps track of the number of mutations in each window. When the\n * current time moves completely past the next window, the limiter creates new\n * windows (this only happens if the limiter is not called for a long time).\n * When the current time moves completely into the next window, the\n * limiter rotates the windows.\n *\n * Case 1: need new windows\n * |----|----|\n * |----| (sliding window)\n *\n * Case 2: rotate windows\n * |----|----|\n * |----| (sliding window)\n *\n * or\n *\n * |----|----|\n * |----| (sliding window)\n *\n * The leading edge of the sliding window is current time.\n *\n * The limiter increments the count for the prev window if the current time is\n * in the prior window. Otherwise, it increments the count for the next\n * window.\n *\n * The limiter computes the total mutations by: taking the number of mutation in the next window and\n * adding the number of mutation from the prev window, weighted by\n * the fraction of the window that has passed. The total mutations must be less\n * than the maximum number of mutations allowed per window.\n *\n * |----|----|\n * |----| (sliding window)\n * ^-- the sliding window covers 25% of the prior window. Only 25% of that window's count counts.\n * The entirety of the next window's count counts.\n */\nexport class SlidingWindowLimiter {\n readonly #windowSizeMs: number;\n readonly #maxMutations: number;\n\n #priorWindow: Window;\n #nextWindow: Window;\n\n constructor(windowSizeMs: number, maxMutations: number) {\n this.#windowSizeMs = windowSizeMs;\n this.#maxMutations = maxMutations;\n\n [this.#priorWindow, this.#nextWindow] = this.#newWindows();\n }\n\n canDo(): boolean {\n const now = Date.now();\n\n // If the current sliding window is completely past the next window, we need new windows.\n if (\n now - this.#windowSizeMs >\n this.#nextWindow.start + this.#windowSizeMs\n ) {\n [this.#priorWindow, this.#nextWindow] = this.#newWindows();\n }\n\n // Has the current sliding window moved completely into the next window?\n // Then rotate the windows.\n if (now - this.#windowSizeMs >= this.#nextWindow.start) {\n this.#rotateWindows();\n }\n\n // Now compute the total mutations in the current window.\n // Weighted by the fraction of the window that has passed.\n const totalCalls = this.totalCallsForTime(now);\n const canDo = totalCalls < this.#maxMutations;\n\n // We don't increment counts if we throttled the user. This is so the user can\n // eventually do the action they are trying to do. If we incremented on every\n // attempt they can end up in a case where their excessive retries lock them out\n // continuously.\n if (canDo) {\n // If \"now\" in the prior or next window? Increment the count for the correct window.\n if (now < this.#nextWindow.start) {\n this.#priorWindow.count++;\n } else {\n this.#nextWindow.count++;\n }\n }\n\n return canDo;\n }\n\n totalCallsForTime(now: number): number {\n let fraction: number;\n if (now < this.#priorWindow.start + this.#windowSizeMs) {\n fraction = 1;\n } else {\n fraction =\n (this.#priorWindow.start +\n (this.#windowSizeMs - 1) -\n (now - this.#windowSizeMs)) /\n this.#windowSizeMs;\n }\n if (fraction < 0) {\n fraction = 0;\n }\n assert(\n fraction <= 1,\n 'The past cannot contribute more than the a full window.',\n );\n const totalCalls =\n this.#priorWindow.count * fraction + this.#nextWindow.count;\n return totalCalls;\n }\n\n get priorWindow(): Window {\n return this.#priorWindow;\n }\n\n get nextWindow(): Window {\n return this.#nextWindow;\n }\n\n /**\n * If there were no mutations in over `windowSizeMs` we need new windows.\n */\n #newWindows() {\n const now = Date.now();\n const start = now - (now % this.#windowSizeMs);\n return [\n {\n // clamp to a `windowSizeMs` boundary to start the prior window.\n start,\n count: 0,\n },\n {\n start: start + this.#windowSizeMs,\n count: 0,\n },\n ];\n }\n\n #rotateWindows() {\n this.#priorWindow = this.#nextWindow;\n this.#nextWindow = {\n start: this.#priorWindow.start + this.#windowSizeMs,\n count: 0,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,uBAAb,MAAkC;CAChC;CACA;CAEA;CACA;CAEA,YAAY,cAAsB,cAAsB;EACtD,KAAKA,gBAAgB;EACrB,KAAKC,gBAAgB;EAErB,CAAC,KAAKC,cAAc,KAAKC,eAAe,KAAKC,YAAY;CAC3D;CAEA,QAAiB;EACf,MAAM,MAAM,KAAK,IAAI;EAGrB,IACE,MAAM,KAAKJ,gBACX,KAAKG,YAAY,QAAQ,KAAKH,eAE9B,CAAC,KAAKE,cAAc,KAAKC,eAAe,KAAKC,YAAY;EAK3D,IAAI,MAAM,KAAKJ,iBAAiB,KAAKG,YAAY,OAC/C,KAAKE,eAAe;EAMtB,MAAM,QADa,KAAK,kBAAkB,GAC5B,IAAa,KAAKJ;EAMhC,IAAI,OAEF,IAAI,MAAM,KAAKE,YAAY,OACzB,KAAKD,aAAa;OAElB,KAAKC,YAAY;EAIrB,OAAO;CACT;CAEA,kBAAkB,KAAqB;EACrC,IAAI;EACJ,IAAI,MAAM,KAAKD,aAAa,QAAQ,KAAKF,eACvC,WAAW;OAEX,YACG,KAAKE,aAAa,SAChB,KAAKF,gBAAgB,MACrB,MAAM,KAAKA,kBACd,KAAKA;EAET,IAAI,WAAW,GACb,WAAW;EAEb,OACE,YAAY,GACZ,yDACF;EAGA,OADE,KAAKE,aAAa,QAAQ,WAAW,KAAKC,YAAY;CAE1D;CAEA,IAAI,cAAsB;EACxB,OAAO,KAAKD;CACd;CAEA,IAAI,aAAqB;EACvB,OAAO,KAAKC;CACd;;;;CAKA,cAAc;EACZ,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,QAAQ,MAAO,MAAM,KAAKH;EAChC,OAAO,CACL;GAEE;GACA,OAAO;EACT,GACA;GACE,OAAO,QAAQ,KAAKA;GACpB,OAAO;EACT,CACF;CACF;CAEA,iBAAiB;EACf,KAAKE,eAAe,KAAKC;EACzB,KAAKA,cAAc;GACjB,OAAO,KAAKD,aAAa,QAAQ,KAAKF;GACtC,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"sliding-window-limiter.js","names":["#windowSizeMs","#maxMutations","#priorWindow","#nextWindow","#newWindows","#rotateWindows"],"sources":["../../../../../../zero-cache/src/services/limiter/sliding-window-limiter.ts"],"sourcesContent":["import {assert} from '../../../../shared/src/asserts.ts';\n\ntype Window = {\n start: number;\n count: number;\n};\n\n/**\n * The limiter is a sliding window rate limiter that allows a maximum\n * number of mutations per window.\n *\n * The window is divided into two parts: the prev window and the next window.\n * The prev window is the part of the window that has already passed, and the\n * next window is the part of the window that is still in the future.\n *\n * The limiter keeps track of the number of mutations in each window. When the\n * current time moves completely past the next window, the limiter creates new\n * windows (this only happens if the limiter is not called for a long time).\n * When the current time moves completely into the next window, the\n * limiter rotates the windows.\n *\n * Case 1: need new windows\n * |----|----|\n * |----| (sliding window)\n *\n * Case 2: rotate windows\n * |----|----|\n * |----| (sliding window)\n *\n * or\n *\n * |----|----|\n * |----| (sliding window)\n *\n * The leading edge of the sliding window is current time.\n *\n * The limiter increments the count for the prev window if the current time is\n * in the prior window. Otherwise, it increments the count for the next\n * window.\n *\n * The limiter computes the total mutations by: taking the number of mutation in the next window and\n * adding the number of mutation from the prev window, weighted by\n * the fraction of the window that has passed. The total mutations must be less\n * than the maximum number of mutations allowed per window.\n *\n * |----|----|\n * |----| (sliding window)\n * ^-- the sliding window covers 25% of the prior window. Only 25% of that window's count counts.\n * The entirety of the next window's count counts.\n */\nexport class SlidingWindowLimiter {\n readonly #windowSizeMs: number;\n readonly #maxMutations: number;\n\n #priorWindow: Window;\n #nextWindow: Window;\n\n constructor(windowSizeMs: number, maxMutations: number) {\n this.#windowSizeMs = windowSizeMs;\n this.#maxMutations = maxMutations;\n\n [this.#priorWindow, this.#nextWindow] = this.#newWindows();\n }\n\n canDo(): boolean {\n const now = Date.now();\n\n // If the current sliding window is completely past the next window, we need new windows.\n if (\n now - this.#windowSizeMs >\n this.#nextWindow.start + this.#windowSizeMs\n ) {\n [this.#priorWindow, this.#nextWindow] = this.#newWindows();\n }\n\n // Has the current sliding window moved completely into the next window?\n // Then rotate the windows.\n if (now - this.#windowSizeMs >= this.#nextWindow.start) {\n this.#rotateWindows();\n }\n\n // Now compute the total mutations in the current window.\n // Weighted by the fraction of the window that has passed.\n const totalCalls = this.totalCallsForTime(now);\n const canDo = totalCalls < this.#maxMutations;\n\n // We don't increment counts if we throttled the user. This is so the user can\n // eventually do the action they are trying to do. If we incremented on every\n // attempt they can end up in a case where their excessive retries lock them out\n // continuously.\n if (canDo) {\n // If \"now\" in the prior or next window? Increment the count for the correct window.\n if (now < this.#nextWindow.start) {\n this.#priorWindow.count++;\n } else {\n this.#nextWindow.count++;\n }\n }\n\n return canDo;\n }\n\n totalCallsForTime(now: number): number {\n let fraction: number;\n if (now < this.#priorWindow.start + this.#windowSizeMs) {\n fraction = 1;\n } else {\n fraction =\n (this.#priorWindow.start +\n (this.#windowSizeMs - 1) -\n (now - this.#windowSizeMs)) /\n this.#windowSizeMs;\n }\n if (fraction < 0) {\n fraction = 0;\n }\n assert(\n fraction <= 1,\n 'The past cannot contribute more than the a full window.',\n );\n const totalCalls =\n this.#priorWindow.count * fraction + this.#nextWindow.count;\n return totalCalls;\n }\n\n get priorWindow(): Window {\n return this.#priorWindow;\n }\n\n get nextWindow(): Window {\n return this.#nextWindow;\n }\n\n /**\n * If there were no mutations in over `windowSizeMs` we need new windows.\n */\n #newWindows() {\n const now = Date.now();\n const start = now - (now % this.#windowSizeMs);\n return [\n {\n // clamp to a `windowSizeMs` boundary to start the prior window.\n start,\n count: 0,\n },\n {\n start: start + this.#windowSizeMs,\n count: 0,\n },\n ];\n }\n\n #rotateWindows() {\n this.#priorWindow = this.#nextWindow;\n this.#nextWindow = {\n start: this.#priorWindow.start + this.#windowSizeMs,\n count: 0,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,uBAAb,MAAkC;CAChC;CACA;CAEA;CACA;CAEA,YAAY,cAAsB,cAAsB;AACtD,QAAA,eAAqB;AACrB,QAAA,eAAqB;AAErB,GAAC,MAAA,aAAmB,MAAA,cAAoB,MAAA,YAAkB;;CAG5D,QAAiB;EACf,MAAM,MAAM,KAAK,KAAK;AAGtB,MACE,MAAM,MAAA,eACN,MAAA,WAAiB,QAAQ,MAAA,aAEzB,EAAC,MAAA,aAAmB,MAAA,cAAoB,MAAA,YAAkB;AAK5D,MAAI,MAAM,MAAA,gBAAsB,MAAA,WAAiB,MAC/C,OAAA,eAAqB;EAMvB,MAAM,QADa,KAAK,kBAAkB,IAAI,GACnB,MAAA;AAM3B,MAAI,MAEF,KAAI,MAAM,MAAA,WAAiB,MACzB,OAAA,YAAkB;MAElB,OAAA,WAAiB;AAIrB,SAAO;;CAGT,kBAAkB,KAAqB;EACrC,IAAI;AACJ,MAAI,MAAM,MAAA,YAAkB,QAAQ,MAAA,aAClC,YAAW;MAEX,aACG,MAAA,YAAkB,SAChB,MAAA,eAAqB,MACrB,MAAM,MAAA,iBACT,MAAA;AAEJ,MAAI,WAAW,EACb,YAAW;AAEb,SACE,YAAY,GACZ,0DACD;AAGD,SADE,MAAA,YAAkB,QAAQ,WAAW,MAAA,WAAiB;;CAI1D,IAAI,cAAsB;AACxB,SAAO,MAAA;;CAGT,IAAI,aAAqB;AACvB,SAAO,MAAA;;;;;CAMT,cAAc;EACZ,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,QAAQ,MAAO,MAAM,MAAA;AAC3B,SAAO,CACL;GAEE;GACA,OAAO;GACR,EACD;GACE,OAAO,QAAQ,MAAA;GACf,OAAO;GACR,CACF;;CAGH,iBAAiB;AACf,QAAA,cAAoB,MAAA;AACpB,QAAA,aAAmB;GACjB,OAAO,MAAA,YAAkB,QAAQ,MAAA;GACjC,OAAO;GACR"}