@rocicorp/zero 1.12.0-canary.3 → 1.12.0-canary.5

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 (819) hide show
  1. package/out/_virtual/{_@oxc-project_runtime@0.130.0/helpers → _@oxc-project_runtime@0.154.0/helpers/esm}/usingCtx.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.d.ts.map +1 -1
  3. package/out/analyze-query/src/analyze-cli.js +6 -4
  4. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  5. package/out/analyze-query/src/bin-transform.js +4 -3
  6. package/out/analyze-query/src/bin-transform.js.map +1 -1
  7. package/out/analyze-query/src/zql-chain.d.ts +23 -0
  8. package/out/analyze-query/src/zql-chain.d.ts.map +1 -0
  9. package/out/analyze-query/src/zql-chain.js +162 -0
  10. package/out/analyze-query/src/zql-chain.js.map +1 -0
  11. package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
  12. package/out/ast-to-zql/src/bin.js +4 -3
  13. package/out/ast-to-zql/src/bin.js.map +1 -1
  14. package/out/ast-to-zql/src/format.js.map +1 -1
  15. package/out/datadog/src/datadog-log-sink.js +5 -4
  16. package/out/datadog/src/datadog-log-sink.js.map +1 -1
  17. package/out/otel/src/log-options.js.map +1 -1
  18. package/out/replicache/src/btree/node.js +13 -11
  19. package/out/replicache/src/btree/node.js.map +1 -1
  20. package/out/replicache/src/btree/read.js +4 -2
  21. package/out/replicache/src/btree/read.js.map +1 -1
  22. package/out/replicache/src/btree/splice.js.map +1 -1
  23. package/out/replicache/src/btree/write.js +9 -5
  24. package/out/replicache/src/btree/write.js.map +1 -1
  25. package/out/replicache/src/config.js +1 -2
  26. package/out/replicache/src/connection-loop.js.map +1 -1
  27. package/out/replicache/src/dag/chunk.js +1 -1
  28. package/out/replicache/src/dag/gc.js.map +1 -1
  29. package/out/replicache/src/dag/lazy-store.js +8 -7
  30. package/out/replicache/src/dag/lazy-store.js.map +1 -1
  31. package/out/replicache/src/dag/store-impl.js +1 -1
  32. package/out/replicache/src/dag/store-impl.js.map +1 -1
  33. package/out/replicache/src/dag/visitor.js.map +1 -1
  34. package/out/replicache/src/db/commit.js +5 -3
  35. package/out/replicache/src/db/commit.js.map +1 -1
  36. package/out/replicache/src/db/read.js +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 +5 -2
  40. package/out/replicache/src/db/write.js.map +1 -1
  41. package/out/replicache/src/deleted-clients.js +3 -2
  42. package/out/replicache/src/deleted-clients.js.map +1 -1
  43. package/out/replicache/src/expo-sqlite.js +1 -2
  44. package/out/replicache/src/frozen-json.js +1 -1
  45. package/out/replicache/src/frozen-json.js.map +1 -1
  46. package/out/replicache/src/get-default-puller.js +1 -1
  47. package/out/replicache/src/hash.js +1 -1
  48. package/out/replicache/src/hash.js.map +1 -1
  49. package/out/replicache/src/impl.js +1 -2
  50. package/out/replicache/src/index-defs.js +5 -3
  51. package/out/replicache/src/index-defs.js.map +1 -1
  52. package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
  53. package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
  54. package/out/replicache/src/kv/idb-store.js +1 -1
  55. package/out/replicache/src/kv/idb-store.js.map +1 -1
  56. package/out/replicache/src/kv/mem-fallback-store.js +1 -1
  57. package/out/replicache/src/kv/mem-fallback-store.js.map +1 -1
  58. package/out/replicache/src/kv/mem-store.js.map +1 -1
  59. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  60. package/out/replicache/src/kv/op-sqlite/types.js.map +1 -1
  61. package/out/replicache/src/kv/read-impl.js.map +1 -1
  62. package/out/replicache/src/kv/sqlite-store.js +1 -1
  63. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  64. package/out/replicache/src/kv/write-impl-base.js +4 -1
  65. package/out/replicache/src/kv/write-impl-base.js.map +1 -1
  66. package/out/replicache/src/kv/write-impl.js.map +1 -1
  67. package/out/replicache/src/log-options.js +2 -1
  68. package/out/replicache/src/log-options.js.map +1 -1
  69. package/out/replicache/src/make-idb-name.js +1 -1
  70. package/out/replicache/src/new-client-channel.js +9 -7
  71. package/out/replicache/src/new-client-channel.js.map +1 -1
  72. package/out/replicache/src/on-persist-channel.js +1 -1
  73. package/out/replicache/src/on-persist-channel.js.map +1 -1
  74. package/out/replicache/src/op-sqlite.js +1 -2
  75. package/out/replicache/src/pending-mutations.js +2 -1
  76. package/out/replicache/src/pending-mutations.js.map +1 -1
  77. package/out/replicache/src/persist/client-gc.js +2 -2
  78. package/out/replicache/src/persist/client-gc.js.map +1 -1
  79. package/out/replicache/src/persist/client-group-gc.js +2 -2
  80. package/out/replicache/src/persist/client-group-gc.js.map +1 -1
  81. package/out/replicache/src/persist/client-groups.js +1 -1
  82. package/out/replicache/src/persist/client-groups.js.map +1 -1
  83. package/out/replicache/src/persist/clients.js +1 -1
  84. package/out/replicache/src/persist/clients.js.map +1 -1
  85. package/out/replicache/src/persist/collect-idb-databases.js +5 -4
  86. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  87. package/out/replicache/src/persist/gather-mem-only-visitor.js.map +1 -1
  88. package/out/replicache/src/persist/gather-not-cached-visitor.js.map +1 -1
  89. package/out/replicache/src/persist/heartbeat.js +2 -2
  90. package/out/replicache/src/persist/heartbeat.js.map +1 -1
  91. package/out/replicache/src/persist/idb-databases-store.js +1 -1
  92. package/out/replicache/src/persist/idb-databases-store.js.map +1 -1
  93. package/out/replicache/src/persist/make-client-id.js +1 -1
  94. package/out/replicache/src/persist/make-client-id.js.map +1 -1
  95. package/out/replicache/src/persist/persist.js +3 -2
  96. package/out/replicache/src/persist/persist.js.map +1 -1
  97. package/out/replicache/src/persist/refresh.js +8 -5
  98. package/out/replicache/src/persist/refresh.js.map +1 -1
  99. package/out/replicache/src/process-scheduler.js.map +1 -1
  100. package/out/replicache/src/replicache-impl.js +3 -3
  101. package/out/replicache/src/replicache-impl.js.map +1 -1
  102. package/out/replicache/src/scan-iterator.js.map +1 -1
  103. package/out/replicache/src/scan-options.js +7 -6
  104. package/out/replicache/src/scan-options.js.map +1 -1
  105. package/out/replicache/src/sqlite.js +1 -2
  106. package/out/replicache/src/subscriptions.js +1 -1
  107. package/out/replicache/src/subscriptions.js.map +1 -1
  108. package/out/replicache/src/sync/diff.js +3 -1
  109. package/out/replicache/src/sync/diff.js.map +1 -1
  110. package/out/replicache/src/sync/ids.js.map +1 -1
  111. package/out/replicache/src/sync/patch.js +2 -4
  112. package/out/replicache/src/sync/patch.js.map +1 -1
  113. package/out/replicache/src/sync/pull.js +3 -1
  114. package/out/replicache/src/sync/pull.js.map +1 -1
  115. package/out/replicache/src/sync/push.js.map +1 -1
  116. package/out/replicache/src/sync/request-id.js +1 -1
  117. package/out/replicache/src/sync/request-id.js.map +1 -1
  118. package/out/replicache/src/transactions.js +1 -1
  119. package/out/replicache/src/transactions.js.map +1 -1
  120. package/out/shared/src/arrays.js +4 -1
  121. package/out/shared/src/arrays.js.map +1 -1
  122. package/out/shared/src/asserts.js +1 -1
  123. package/out/shared/src/bigint-json.d.ts +9 -0
  124. package/out/shared/src/bigint-json.d.ts.map +1 -1
  125. package/out/shared/src/bigint-json.js +27 -2
  126. package/out/shared/src/bigint-json.js.map +1 -1
  127. package/out/shared/src/broadcast-channel.js +1 -1
  128. package/out/shared/src/btree-set.js +11 -8
  129. package/out/shared/src/btree-set.js.map +1 -1
  130. package/out/shared/src/cache.js.map +1 -1
  131. package/out/shared/src/config.js +1 -1
  132. package/out/shared/src/custom-key-map.js.map +1 -1
  133. package/out/shared/src/custom-key-set.js.map +1 -1
  134. package/out/shared/src/deep-clone.js +1 -1
  135. package/out/shared/src/document-visible.js.map +1 -1
  136. package/out/shared/src/dotenv.js.map +1 -1
  137. package/out/shared/src/error.js +1 -1
  138. package/out/shared/src/error.js.map +1 -1
  139. package/out/shared/src/hash.js +2 -2
  140. package/out/shared/src/hash.js.map +1 -1
  141. package/out/shared/src/iterables.js.map +1 -1
  142. package/out/shared/src/json-schema.js.map +1 -1
  143. package/out/shared/src/json.js.map +1 -1
  144. package/out/shared/src/logging-test-utils.js +1 -1
  145. package/out/shared/src/logging.js +1 -1
  146. package/out/shared/src/logging.js.map +1 -1
  147. package/out/shared/src/navigator.js +1 -1
  148. package/out/shared/src/options.js +9 -12
  149. package/out/shared/src/options.js.map +1 -1
  150. package/out/shared/src/promise-race.js.map +1 -1
  151. package/out/shared/src/queue.js.map +1 -1
  152. package/out/shared/src/record-proxy.js.map +1 -1
  153. package/out/shared/src/ring-buffer.js.map +1 -1
  154. package/out/shared/src/size-of-value.js +7 -10
  155. package/out/shared/src/size-of-value.js.map +1 -1
  156. package/out/shared/src/sorted-entries.js.map +1 -1
  157. package/out/shared/src/tdigest-schema.js.map +1 -1
  158. package/out/shared/src/tdigest.js.map +1 -1
  159. package/out/shared/src/valita.js +1 -1
  160. package/out/shared/src/valita.js.map +1 -1
  161. package/out/z2s/src/compiler.js +2 -4
  162. package/out/z2s/src/compiler.js.map +1 -1
  163. package/out/z2s/src/sql.js +1 -1
  164. package/out/z2s/src/sql.js.map +1 -1
  165. package/out/zero/package.js +211 -189
  166. package/out/zero/package.js.map +1 -1
  167. package/out/zero/src/adapters/drizzle.js +1 -2
  168. package/out/zero/src/adapters/kysely.js +1 -2
  169. package/out/zero/src/adapters/pg.js +1 -2
  170. package/out/zero/src/adapters/postgresjs.js +1 -2
  171. package/out/zero/src/adapters/prisma.js +1 -2
  172. package/out/zero/src/analyze.js +1 -2
  173. package/out/zero/src/change-protocol/v0.js +1 -2
  174. package/out/zero/src/expo-sqlite.js +1 -2
  175. package/out/zero/src/op-sqlite.js +1 -2
  176. package/out/zero/src/react-native.js.map +1 -1
  177. package/out/zero/src/sqlite.js +1 -2
  178. package/out/zero/src/zero-cache-dev.js +10 -9
  179. package/out/zero/src/zero-cache-dev.js.map +1 -1
  180. package/out/zero/src/zero-out.js +2 -1
  181. package/out/zero/src/zero-out.js.map +1 -1
  182. package/out/zero-cache/src/auth/auth.js +1 -1
  183. package/out/zero-cache/src/auth/auth.js.map +1 -1
  184. package/out/zero-cache/src/auth/jwt.js +4 -1
  185. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  186. package/out/zero-cache/src/auth/load-permissions.js +1 -1
  187. package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
  188. package/out/zero-cache/src/auth/read-authorizer.js +3 -2
  189. package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
  190. package/out/zero-cache/src/auth/write-authorizer.js +6 -17
  191. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  192. package/out/zero-cache/src/config/network.js +1 -1
  193. package/out/zero-cache/src/config/network.js.map +1 -1
  194. package/out/zero-cache/src/config/server-context.js.map +1 -1
  195. package/out/zero-cache/src/config/zero-config.d.ts +5 -0
  196. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  197. package/out/zero-cache/src/config/zero-config.js +13 -2
  198. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  199. package/out/zero-cache/src/custom/fetch.js +1 -1
  200. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  201. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  202. package/out/zero-cache/src/db/create.js.map +1 -1
  203. package/out/zero-cache/src/db/index-predicate.js.map +1 -1
  204. package/out/zero-cache/src/db/lite-tables.d.ts +2 -2
  205. package/out/zero-cache/src/db/lite-tables.d.ts.map +1 -1
  206. package/out/zero-cache/src/db/lite-tables.js.map +1 -1
  207. package/out/zero-cache/src/db/migration-lite.js +1 -1
  208. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  209. package/out/zero-cache/src/db/migration.js +5 -3
  210. package/out/zero-cache/src/db/migration.js.map +1 -1
  211. package/out/zero-cache/src/db/pg-copy-binary.js +4 -3
  212. package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
  213. package/out/zero-cache/src/db/pg-copy.js +0 -1
  214. package/out/zero-cache/src/db/pg-copy.js.map +1 -1
  215. package/out/zero-cache/src/db/pg-to-lite.js +1 -1
  216. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  217. package/out/zero-cache/src/db/run-transaction.js.map +1 -1
  218. package/out/zero-cache/src/db/specs.js +7 -5
  219. package/out/zero-cache/src/db/specs.js.map +1 -1
  220. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
  221. package/out/zero-cache/src/db/statements.d.ts.map +1 -1
  222. package/out/zero-cache/src/db/statements.js +9 -2
  223. package/out/zero-cache/src/db/statements.js.map +1 -1
  224. package/out/zero-cache/src/db/transaction-pool.js +5 -3
  225. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  226. package/out/zero-cache/src/db/warmup.js.map +1 -1
  227. package/out/zero-cache/src/observability/events.js.map +1 -1
  228. package/out/zero-cache/src/observability/log-throttle.js.map +1 -1
  229. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  230. package/out/zero-cache/src/scripts/decommission.js +1 -1
  231. package/out/zero-cache/src/scripts/deploy-permissions.js +5 -3
  232. package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
  233. package/out/zero-cache/src/scripts/permissions.js.map +1 -1
  234. package/out/zero-cache/src/server/anonymous-otel-start.js +2 -1
  235. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  236. package/out/zero-cache/src/server/change-streamer.js +1 -1
  237. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  238. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  239. package/out/zero-cache/src/server/logging.js +1 -1
  240. package/out/zero-cache/src/server/logging.js.map +1 -1
  241. package/out/zero-cache/src/server/main.js.map +1 -1
  242. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  243. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  244. package/out/zero-cache/src/server/reaper.js +2 -2
  245. package/out/zero-cache/src/server/reaper.js.map +1 -1
  246. package/out/zero-cache/src/server/replicator.js +1 -1
  247. package/out/zero-cache/src/server/replicator.js.map +1 -1
  248. package/out/zero-cache/src/server/runner/run-worker.js +2 -1
  249. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  250. package/out/zero-cache/src/server/runner/runtime.js +2 -1
  251. package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
  252. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  253. package/out/zero-cache/src/server/shadow-syncer.js +1 -1
  254. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  255. package/out/zero-cache/src/server/slot-keeper.js.map +1 -1
  256. package/out/zero-cache/src/server/syncer-assigner.js.map +1 -1
  257. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  258. package/out/zero-cache/src/server/syncer.js +9 -4
  259. package/out/zero-cache/src/server/syncer.js.map +1 -1
  260. package/out/zero-cache/src/server/worker-dispatcher.js +1 -1
  261. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  262. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  263. package/out/zero-cache/src/services/analyze.js +2 -2
  264. package/out/zero-cache/src/services/analyze.js.map +1 -1
  265. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +2 -2
  266. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  267. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  268. package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -1
  269. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
  270. package/out/zero-cache/src/services/change-source/common/replica-schema.js +4 -4
  271. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  272. package/out/zero-cache/src/services/change-source/custom/change-source.js +4 -4
  273. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  274. package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
  275. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +4 -4
  276. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  277. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +3 -3
  278. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
  279. package/out/zero-cache/src/services/change-source/pg/change-source.js +6 -8
  280. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  281. package/out/zero-cache/src/services/change-source/pg/decommission.js +2 -1
  282. package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
  283. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +9 -7
  284. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  285. package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
  286. package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
  287. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +5 -4
  288. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  289. package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
  290. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js +2 -2
  291. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js.map +1 -1
  292. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js +1 -1
  293. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js.map +1 -1
  294. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +3 -3
  295. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  296. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +2 -2
  297. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  298. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +13 -12
  299. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
  300. package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -1
  301. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  302. package/out/zero-cache/src/services/change-source/pg/schema/published.js +2 -2
  303. package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
  304. package/out/zero-cache/src/services/change-source/pg/schema/replica-stage-enum.js.map +1 -1
  305. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -7
  306. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  307. package/out/zero-cache/src/services/change-source/pg/schema/validation.js +1 -1
  308. package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
  309. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +1 -1
  310. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -1
  311. package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
  312. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  313. package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
  314. package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
  315. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  316. package/out/zero-cache/src/services/change-source/protocol/current.js +1 -1
  317. package/out/zero-cache/src/services/change-source/protocol/mod.js +1 -2
  318. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
  319. package/out/zero-cache/src/services/change-streamer/backfill-state.js +1 -1
  320. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -1
  321. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  322. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
  323. package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
  324. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  325. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +1 -1
  326. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -1
  327. package/out/zero-cache/src/services/change-streamer/change-log-codec.js +2 -3
  328. package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -1
  329. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +3 -3
  330. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
  331. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -1
  332. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +5 -3
  333. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  334. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +21 -21
  335. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  336. package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
  337. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  338. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
  339. package/out/zero-cache/src/services/change-streamer/forwarder.js +11 -10
  340. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  341. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js +1 -1
  342. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js.map +1 -1
  343. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +3 -3
  344. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
  345. package/out/zero-cache/src/services/change-streamer/replica-poller.js +2 -2
  346. package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -1
  347. package/out/zero-cache/src/services/change-streamer/restore-admission.js +12 -1
  348. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
  349. package/out/zero-cache/src/services/change-streamer/schema/init.js +1 -1
  350. package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
  351. package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
  352. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  353. package/out/zero-cache/src/services/change-streamer/snapshot-message.js +1 -1
  354. package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
  355. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  356. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +1 -1
  357. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
  358. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js +2 -2
  359. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js.map +1 -1
  360. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -1
  361. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
  362. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts.map +1 -1
  363. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +4 -6
  364. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -1
  365. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +3 -5
  366. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
  367. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  368. package/out/zero-cache/src/services/change-streamer/storer.js +3 -3
  369. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  370. package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
  371. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  372. package/out/zero-cache/src/services/change-streamer/subscriber.js +2 -2
  373. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  374. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +0 -1
  375. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
  376. package/out/zero-cache/src/services/heapz.js +2 -1
  377. package/out/zero-cache/src/services/heapz.js.map +1 -1
  378. package/out/zero-cache/src/services/http-service.js.map +1 -1
  379. package/out/zero-cache/src/services/life-cycle.js +2 -2
  380. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  381. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js +4 -2
  382. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
  383. package/out/zero-cache/src/services/litestream/commands.js +3 -3
  384. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  385. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +2 -2
  386. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
  387. package/out/zero-cache/src/services/litestream/litestream-controller.js.map +1 -1
  388. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  389. package/out/zero-cache/src/services/litestream/restore-progress.js.map +1 -1
  390. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  391. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  392. package/out/zero-cache/src/services/mutagen/pusher.js +3 -2
  393. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  394. package/out/zero-cache/src/services/plannerz.js +6 -5
  395. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  396. package/out/zero-cache/src/services/profz.js +5 -3
  397. package/out/zero-cache/src/services/profz.js.map +1 -1
  398. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  399. package/out/zero-cache/src/services/replicator/change-log-db.js +4 -3
  400. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
  401. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
  402. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
  403. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  404. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
  405. package/out/zero-cache/src/services/replicator/incremental-sync.js +4 -4
  406. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  407. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  408. package/out/zero-cache/src/services/replicator/replication-status.js +4 -3
  409. package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
  410. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  411. package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
  412. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  413. package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
  414. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
  415. package/out/zero-cache/src/services/replicator/schema/backfilling.js +2 -2
  416. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  417. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +19 -7
  418. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts.map +1 -1
  419. package/out/zero-cache/src/services/replicator/schema/change-log.js +31 -15
  420. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  421. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  422. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  423. package/out/zero-cache/src/services/replicator/schema/replication-state.js +10 -7
  424. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  425. package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
  426. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +1 -1
  427. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
  428. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +1 -1
  429. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -1
  430. package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
  431. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  432. package/out/zero-cache/src/services/replicator/write-worker.js +3 -3
  433. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  434. package/out/zero-cache/src/services/run-ast.js +1 -1
  435. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  436. package/out/zero-cache/src/services/runner.js.map +1 -1
  437. package/out/zero-cache/src/services/running-state.js.map +1 -1
  438. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  439. package/out/zero-cache/src/services/statz.js +4 -3
  440. package/out/zero-cache/src/services/statz.js.map +1 -1
  441. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +2 -2
  442. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
  443. package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -1
  444. package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -1
  445. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  446. package/out/zero-cache/src/services/view-syncer/client-handler.js +24 -3
  447. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  448. package/out/zero-cache/src/services/view-syncer/client-schema.d.ts +15 -2
  449. package/out/zero-cache/src/services/view-syncer/client-schema.d.ts.map +1 -1
  450. package/out/zero-cache/src/services/view-syncer/client-schema.js +46 -6
  451. package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
  452. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  453. package/out/zero-cache/src/services/view-syncer/cvr-purger.js +2 -2
  454. package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
  455. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  456. package/out/zero-cache/src/services/view-syncer/cvr-store.js +11 -6
  457. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  458. package/out/zero-cache/src/services/view-syncer/cvr.js +12 -4
  459. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  460. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +1 -1
  461. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -1
  462. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  463. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -1
  464. package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -1
  465. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +2 -2
  466. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -1
  467. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +6 -4
  468. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  469. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  470. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +13 -10
  471. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  472. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -1
  473. package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -1
  474. package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -1
  475. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
  476. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -1
  477. package/out/zero-cache/src/services/view-syncer/query-stats.js +1 -1
  478. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
  479. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +1 -1
  480. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  481. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
  482. package/out/zero-cache/src/services/view-syncer/schema/types.js +1 -1
  483. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  484. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js +7 -1
  485. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js.map +1 -1
  486. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +8 -1
  487. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  488. package/out/zero-cache/src/services/view-syncer/snapshotter.js +7 -6
  489. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  490. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
  491. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  492. package/out/zero-cache/src/services/view-syncer/view-syncer.js +19 -15
  493. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  494. package/out/zero-cache/src/types/error-with-level.js.map +1 -1
  495. package/out/zero-cache/src/types/lexi-version.js.map +1 -1
  496. package/out/zero-cache/src/types/lite.js +1 -1
  497. package/out/zero-cache/src/types/lite.js.map +1 -1
  498. package/out/zero-cache/src/types/pg-data-type.js +1 -1
  499. package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
  500. package/out/zero-cache/src/types/pg.js +6 -4
  501. package/out/zero-cache/src/types/pg.js.map +1 -1
  502. package/out/zero-cache/src/types/poke-chunk.js +2 -2
  503. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  504. package/out/zero-cache/src/types/processes.js +1 -1
  505. package/out/zero-cache/src/types/processes.js.map +1 -1
  506. package/out/zero-cache/src/types/profiler.js +1 -1
  507. package/out/zero-cache/src/types/profiler.js.map +1 -1
  508. package/out/zero-cache/src/types/row-key.js.map +1 -1
  509. package/out/zero-cache/src/types/sql.js.map +1 -1
  510. package/out/zero-cache/src/types/streams.js +10 -9
  511. package/out/zero-cache/src/types/streams.js.map +1 -1
  512. package/out/zero-cache/src/types/strings.js.map +1 -1
  513. package/out/zero-cache/src/types/subscription.js.map +1 -1
  514. package/out/zero-cache/src/types/timeout.js +1 -1
  515. package/out/zero-cache/src/types/websocket-handoff.js +1 -1
  516. package/out/zero-cache/src/types/ws.js +1 -1
  517. package/out/zero-cache/src/workers/connection.js +5 -6
  518. package/out/zero-cache/src/workers/connection.js.map +1 -1
  519. package/out/zero-cache/src/workers/mutator.js.map +1 -1
  520. package/out/zero-cache/src/workers/replicator.js +5 -4
  521. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  522. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  523. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +9 -1
  524. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  525. package/out/zero-cache/src/workers/syncer.js +4 -4
  526. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  527. package/out/zero-client/src/client/active-clients-manager.js +1 -1
  528. package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
  529. package/out/zero-client/src/client/bindings.js +11 -11
  530. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  531. package/out/zero-client/src/client/connection.js +1 -1
  532. package/out/zero-client/src/client/connection.js.map +1 -1
  533. package/out/zero-client/src/client/context.js.map +1 -1
  534. package/out/zero-client/src/client/crud-impl.js +1 -1
  535. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  536. package/out/zero-client/src/client/crud.js +1 -1
  537. package/out/zero-client/src/client/crud.js.map +1 -1
  538. package/out/zero-client/src/client/custom.js +1 -1
  539. package/out/zero-client/src/client/custom.js.map +1 -1
  540. package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
  541. package/out/zero-client/src/client/enable-analytics.js +1 -1
  542. package/out/zero-client/src/client/enable-analytics.js.map +1 -1
  543. package/out/zero-client/src/client/error.js.map +1 -1
  544. package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
  545. package/out/zero-client/src/client/inspector/client.js.map +1 -1
  546. package/out/zero-client/src/client/inspector/html-dialog-prompt.js +1 -1
  547. package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
  548. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  549. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  550. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  551. package/out/zero-client/src/client/ivm-branch.js +1 -3
  552. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  553. package/out/zero-client/src/client/keys.js +2 -1
  554. package/out/zero-client/src/client/keys.js.map +1 -1
  555. package/out/zero-client/src/client/log-options.js.map +1 -1
  556. package/out/zero-client/src/client/make-replicache-mutators.js +5 -3
  557. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  558. package/out/zero-client/src/client/metrics.js +3 -5
  559. package/out/zero-client/src/client/metrics.js.map +1 -1
  560. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  561. package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
  562. package/out/zero-client/src/client/options.js.map +1 -1
  563. package/out/zero-client/src/client/query-manager.js +2 -2
  564. package/out/zero-client/src/client/query-manager.js.map +1 -1
  565. package/out/zero-client/src/client/reload-error-handler.js +4 -3
  566. package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
  567. package/out/zero-client/src/client/version.js +1 -1
  568. package/out/zero-client/src/client/zero-poke-handler.js +1 -1
  569. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  570. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  571. package/out/zero-client/src/client/zero.js +19 -11
  572. package/out/zero-client/src/client/zero.js.map +1 -1
  573. package/out/zero-client/src/mod.js +23 -23
  574. package/out/zero-client/src/util/nanoid.js.map +1 -1
  575. package/out/zero-permissions/src/permissions.js +3 -2
  576. package/out/zero-permissions/src/permissions.js.map +1 -1
  577. package/out/zero-pg/src/mod.js +9 -9
  578. package/out/zero-protocol/src/analyze-query-result.js +1 -1
  579. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  580. package/out/zero-protocol/src/application-error.js.map +1 -1
  581. package/out/zero-protocol/src/ast.js +19 -9
  582. package/out/zero-protocol/src/ast.js.map +1 -1
  583. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  584. package/out/zero-protocol/src/client-schema.js +1 -1
  585. package/out/zero-protocol/src/client-schema.js.map +1 -1
  586. package/out/zero-protocol/src/close-connection.js +1 -1
  587. package/out/zero-protocol/src/close-connection.js.map +1 -1
  588. package/out/zero-protocol/src/connect.js +1 -1
  589. package/out/zero-protocol/src/connect.js.map +1 -1
  590. package/out/zero-protocol/src/custom-queries.js +1 -1
  591. package/out/zero-protocol/src/custom-queries.js.map +1 -1
  592. package/out/zero-protocol/src/data.js +4 -2
  593. package/out/zero-protocol/src/data.js.map +1 -1
  594. package/out/zero-protocol/src/delete-clients.js.map +1 -1
  595. package/out/zero-protocol/src/down.d.ts +27 -27
  596. package/out/zero-protocol/src/down.js.map +1 -1
  597. package/out/zero-protocol/src/error.js.map +1 -1
  598. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  599. package/out/zero-protocol/src/inspect-down.js +1 -1
  600. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  601. package/out/zero-protocol/src/inspect-up.js +1 -1
  602. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  603. package/out/zero-protocol/src/mutate-server.js +1 -1
  604. package/out/zero-protocol/src/mutate-server.js.map +1 -1
  605. package/out/zero-protocol/src/mutation-id.js.map +1 -1
  606. package/out/zero-protocol/src/mutation.js +1 -1
  607. package/out/zero-protocol/src/mutation.js.map +1 -1
  608. package/out/zero-protocol/src/mutations-patch.js +1 -1
  609. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  610. package/out/zero-protocol/src/ping.js +1 -1
  611. package/out/zero-protocol/src/ping.js.map +1 -1
  612. package/out/zero-protocol/src/poke.d.ts +6 -6
  613. package/out/zero-protocol/src/poke.js +1 -1
  614. package/out/zero-protocol/src/poke.js.map +1 -1
  615. package/out/zero-protocol/src/pong.js +1 -1
  616. package/out/zero-protocol/src/pong.js.map +1 -1
  617. package/out/zero-protocol/src/primary-key.js.map +1 -1
  618. package/out/zero-protocol/src/protocol-version.js +1 -0
  619. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  620. package/out/zero-protocol/src/pull.js +1 -1
  621. package/out/zero-protocol/src/pull.js.map +1 -1
  622. package/out/zero-protocol/src/push.js +1 -1
  623. package/out/zero-protocol/src/push.js.map +1 -1
  624. package/out/zero-protocol/src/queries-patch.js +1 -1
  625. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  626. package/out/zero-protocol/src/query-hash-visitor.js.map +1 -1
  627. package/out/zero-protocol/src/query-hash.js +2 -2
  628. package/out/zero-protocol/src/query-server.js +1 -1
  629. package/out/zero-protocol/src/query-server.js.map +1 -1
  630. package/out/zero-protocol/src/row-patch.js.map +1 -1
  631. package/out/zero-protocol/src/up.d.ts +60 -60
  632. package/out/zero-protocol/src/up.js.map +1 -1
  633. package/out/zero-protocol/src/update-auth.js.map +1 -1
  634. package/out/zero-protocol/src/version.js.map +1 -1
  635. package/out/zero-react/src/bindings.js +11 -11
  636. package/out/zero-react/src/mod.js +5 -5
  637. package/out/zero-react/src/use-query.js +1 -1
  638. package/out/zero-react/src/use-query.js.map +1 -1
  639. package/out/zero-react/src/zero-provider.d.ts +1 -1
  640. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  641. package/out/zero-react/src/zero-provider.js +2 -1
  642. package/out/zero-react/src/zero-provider.js.map +1 -1
  643. package/out/zero-react/src/zero.js +23 -23
  644. package/out/zero-schema/src/builder/schema-builder.js +3 -2
  645. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  646. package/out/zero-schema/src/builder/table-builder.js +1 -1
  647. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  648. package/out/zero-schema/src/compiled-permissions.js.map +1 -1
  649. package/out/zero-schema/src/name-mapper.js +7 -5
  650. package/out/zero-schema/src/name-mapper.js.map +1 -1
  651. package/out/zero-schema/src/schema-config.js +1 -1
  652. package/out/zero-schema/src/schema-config.js.map +1 -1
  653. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  654. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  655. package/out/zero-server/src/adapters/pg.js.map +1 -1
  656. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  657. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  658. package/out/zero-server/src/closable-db-transaction.js.map +1 -1
  659. package/out/zero-server/src/custom.js +1 -1
  660. package/out/zero-server/src/custom.js.map +1 -1
  661. package/out/zero-server/src/mod.js +8 -8
  662. package/out/zero-server/src/process-mutations.js.map +1 -1
  663. package/out/zero-server/src/push-processor.js.map +1 -1
  664. package/out/zero-server/src/queries/process-queries.js +2 -1
  665. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  666. package/out/zero-server/src/schema.js +7 -7
  667. package/out/zero-server/src/schema.js.map +1 -1
  668. package/out/zero-server/src/zql-database.js.map +1 -1
  669. package/out/zero-solid/src/bindings.js +11 -11
  670. package/out/zero-solid/src/mod.js +5 -5
  671. package/out/zero-solid/src/solid-view.js +1 -1
  672. package/out/zero-solid/src/solid-view.js.map +1 -1
  673. package/out/zero-solid/src/use-connection-state.js +2 -1
  674. package/out/zero-solid/src/use-connection-state.js.map +1 -1
  675. package/out/zero-solid/src/use-query.js.map +1 -1
  676. package/out/zero-solid/src/use-zero-online.js +2 -1
  677. package/out/zero-solid/src/use-zero-online.js.map +1 -1
  678. package/out/zero-solid/src/use-zero.js +3 -2
  679. package/out/zero-solid/src/use-zero.js.map +1 -1
  680. package/out/zero-solid/src/zero.js +23 -23
  681. package/out/zero-types/src/name-mapper.js.map +1 -1
  682. package/out/zql/src/builder/builder.js +15 -13
  683. package/out/zql/src/builder/builder.js.map +1 -1
  684. package/out/zql/src/builder/correlated-predicate-pushdown.js.map +1 -1
  685. package/out/zql/src/builder/debug-delegate.js.map +1 -1
  686. package/out/zql/src/builder/like.js +0 -1
  687. package/out/zql/src/builder/like.js.map +1 -1
  688. package/out/zql/src/ivm/array-view.js.map +1 -1
  689. package/out/zql/src/ivm/cap.d.ts +1 -1
  690. package/out/zql/src/ivm/cap.d.ts.map +1 -1
  691. package/out/zql/src/ivm/cap.js +10 -0
  692. package/out/zql/src/ivm/cap.js.map +1 -1
  693. package/out/zql/src/ivm/constraint.js +1 -1
  694. package/out/zql/src/ivm/constraint.js.map +1 -1
  695. package/out/zql/src/ivm/data.d.ts +15 -1
  696. package/out/zql/src/ivm/data.d.ts.map +1 -1
  697. package/out/zql/src/ivm/data.js +15 -1
  698. package/out/zql/src/ivm/data.js.map +1 -1
  699. package/out/zql/src/ivm/deferred-input.js.map +1 -1
  700. package/out/zql/src/ivm/exists.d.ts.map +1 -1
  701. package/out/zql/src/ivm/exists.js +32 -22
  702. package/out/zql/src/ivm/exists.js.map +1 -1
  703. package/out/zql/src/ivm/fan-in.js.map +1 -1
  704. package/out/zql/src/ivm/fan-out.js.map +1 -1
  705. package/out/zql/src/ivm/filter-operators.d.ts +5 -0
  706. package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
  707. package/out/zql/src/ivm/filter-operators.js +1 -1
  708. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  709. package/out/zql/src/ivm/filter-push.js.map +1 -1
  710. package/out/zql/src/ivm/filter.js.map +1 -1
  711. package/out/zql/src/ivm/flipped-join.js +1 -3
  712. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  713. package/out/zql/src/ivm/join-index-tap.js.map +1 -1
  714. package/out/zql/src/ivm/join-index.js.map +1 -1
  715. package/out/zql/src/ivm/join-utils.js +11 -13
  716. package/out/zql/src/ivm/join-utils.js.map +1 -1
  717. package/out/zql/src/ivm/join.d.ts.map +1 -1
  718. package/out/zql/src/ivm/join.js +6 -3
  719. package/out/zql/src/ivm/join.js.map +1 -1
  720. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
  721. package/out/zql/src/ivm/memory-source.js +27 -23
  722. package/out/zql/src/ivm/memory-source.js.map +1 -1
  723. package/out/zql/src/ivm/memory-storage.js.map +1 -1
  724. package/out/zql/src/ivm/operator.d.ts +6 -0
  725. package/out/zql/src/ivm/operator.d.ts.map +1 -1
  726. package/out/zql/src/ivm/operator.js +2 -2
  727. package/out/zql/src/ivm/operator.js.map +1 -1
  728. package/out/zql/src/ivm/push-accumulated.js +0 -1
  729. package/out/zql/src/ivm/push-accumulated.js.map +1 -1
  730. package/out/zql/src/ivm/skip-yields.js.map +1 -1
  731. package/out/zql/src/ivm/skip.js.map +1 -1
  732. package/out/zql/src/ivm/take-gate.js.map +1 -1
  733. package/out/zql/src/ivm/take.d.ts.map +1 -1
  734. package/out/zql/src/ivm/take.js +5 -4
  735. package/out/zql/src/ivm/take.js.map +1 -1
  736. package/out/zql/src/ivm/union-fan-in.js +1 -1
  737. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  738. package/out/zql/src/ivm/union-fan-out.js.map +1 -1
  739. package/out/zql/src/ivm/view-apply-change.js +1 -1
  740. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  741. package/out/zql/src/mutate/crud.js +3 -2
  742. package/out/zql/src/mutate/crud.js.map +1 -1
  743. package/out/zql/src/planner/planner-builder.js +1 -1
  744. package/out/zql/src/planner/planner-builder.js.map +1 -1
  745. package/out/zql/src/planner/planner-connection.js.map +1 -1
  746. package/out/zql/src/planner/planner-debug.js.map +1 -1
  747. package/out/zql/src/planner/planner-fan-in.js.map +1 -1
  748. package/out/zql/src/planner/planner-fan-out.js.map +1 -1
  749. package/out/zql/src/planner/planner-filter.js.map +1 -1
  750. package/out/zql/src/planner/planner-graph.js +1 -5
  751. package/out/zql/src/planner/planner-graph.js.map +1 -1
  752. package/out/zql/src/planner/planner-join.js +1 -1
  753. package/out/zql/src/planner/planner-join.js.map +1 -1
  754. package/out/zql/src/planner/planner-source.js.map +1 -1
  755. package/out/zql/src/planner/planner-terminus.js.map +1 -1
  756. package/out/zql/src/query/create-builder.js +2 -1
  757. package/out/zql/src/query/create-builder.js.map +1 -1
  758. package/out/zql/src/query/expression.js +1 -1
  759. package/out/zql/src/query/expression.js.map +1 -1
  760. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  761. package/out/zql/src/query/named.js +2 -1
  762. package/out/zql/src/query/named.js.map +1 -1
  763. package/out/zql/src/query/query-delegate-base.js +1 -1
  764. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  765. package/out/zql/src/query/query-impl.js +3 -2
  766. package/out/zql/src/query/query-impl.js.map +1 -1
  767. package/out/zql/src/query/query-registry.js +1 -1
  768. package/out/zql/src/query/query-registry.js.map +1 -1
  769. package/out/zql/src/query/query-transitions.js +1 -1
  770. package/out/zql/src/query/query-transitions.js.map +1 -1
  771. package/out/zql/src/query/runnable-query-impl.js +3 -2
  772. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  773. package/out/zql/src/query/static-query.js.map +1 -1
  774. package/out/zql/src/query/ttl.js +8 -8
  775. package/out/zql/src/query/ttl.js.map +1 -1
  776. package/out/zql/src/query/validate-input.js.map +1 -1
  777. package/out/zqlite/src/database-storage.d.ts +27 -8
  778. package/out/zqlite/src/database-storage.d.ts.map +1 -1
  779. package/out/zqlite/src/database-storage.js +190 -53
  780. package/out/zqlite/src/database-storage.js.map +1 -1
  781. package/out/zqlite/src/db.d.ts +47 -1
  782. package/out/zqlite/src/db.d.ts.map +1 -1
  783. package/out/zqlite/src/db.js +121 -21
  784. package/out/zqlite/src/db.js.map +1 -1
  785. package/out/zqlite/src/explain-queries.js.map +1 -1
  786. package/out/zqlite/src/fetch-sql.js.map +1 -1
  787. package/out/zqlite/src/index-usage.js.map +1 -1
  788. package/out/zqlite/src/internal/sql-inline.js.map +1 -1
  789. package/out/zqlite/src/internal/sql.js.map +1 -1
  790. package/out/zqlite/src/internal/statement-cache.d.ts +7 -0
  791. package/out/zqlite/src/internal/statement-cache.d.ts.map +1 -1
  792. package/out/zqlite/src/internal/statement-cache.js +11 -4
  793. package/out/zqlite/src/internal/statement-cache.js.map +1 -1
  794. package/out/zqlite/src/mod.js +5 -5
  795. package/out/zqlite/src/pending-delta.js.map +1 -1
  796. package/out/zqlite/src/query-builder.js +1 -1
  797. package/out/zqlite/src/query-builder.js.map +1 -1
  798. package/out/zqlite/src/query-delegate.js.map +1 -1
  799. package/out/zqlite/src/resolve-scalar-subqueries.js +2 -5
  800. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  801. package/out/zqlite/src/sqlite-cost-model.js +3 -2
  802. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  803. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  804. package/out/zqlite/src/table-source.d.ts +11 -0
  805. package/out/zqlite/src/table-source.d.ts.map +1 -1
  806. package/out/zqlite/src/table-source.js +63 -26
  807. package/out/zqlite/src/table-source.js.map +1 -1
  808. package/package.json +37 -36
  809. package/out/analyze-query/src/bin-analyze.d.ts +0 -2
  810. package/out/analyze-query/src/bin-analyze.d.ts.map +0 -1
  811. package/out/analyze-query/src/bin-analyze.js +0 -283
  812. package/out/analyze-query/src/bin-analyze.js.map +0 -1
  813. package/out/otel/src/maybe-time.d.ts +0 -3
  814. package/out/otel/src/maybe-time.d.ts.map +0 -1
  815. package/out/otel/src/maybe-time.js +0 -15
  816. package/out/otel/src/maybe-time.js.map +0 -1
  817. package/out/zero/src/analyze-query.d.ts +0 -3
  818. package/out/zero/src/analyze-query.d.ts.map +0 -1
  819. package/out/zero/src/analyze-query.js +0 -3
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-change-log-observability.js","names":["#lc","#messageProcessing","#logCommitDuration","#transactionRows","#rollbackCounter","#invariantFailureCounter","#hashComparisonCounter","#hashUnpairedCounter","#cookieMutationCounter","#receivedHead","#sqliteHead","#rows","#estimatedBytes","#cookieRows","#sourceHasher","#invariantFailure","#sourcePos","#sourceCommitHash","#resetSourceHash","#transactionWatermark","#recordRollback","#recordUnpaired","#compareHashes","#cookieMutations","#rollbacks","#invariantFailures","#hashMatches","#hashMismatches","#hashUnpaired"],"sources":["../../../../../../zero-cache/src/services/replicator/sqlite-change-log-observability.ts"],"sourcesContent":["/**\n * Observability for the SQLite change log.\n *\n * Everything here now runs in the **change-streamer** process, which is where\n * the writer lives. The module stays beside the rest of the change-log code\n * under `services/replicator/` for the same reason `change-log-db.ts` and\n * `change-log-stream-writer.ts` do: moving files would obscure the topology\n * diff, and the writer's home is a property of who calls it rather than of\n * where it sits. Metric names are unchanged from 7H — only their emitting\n * process, and therefore their resource attributes, moved.\n */\n\nimport {stat} from 'node:fs/promises';\nimport type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../../shared/src/asserts.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateLatencyHistogram,\n getOrCreateValueHistogram,\n} from '../../observability/metrics.ts';\nimport {versionFromLexi} from '../../types/lexi-version.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {extractChangeSubstring} from '../change-streamer/change-log-codec.ts';\nimport {ChangeLogTransactionHasher} from '../change-streamer/change-log-transaction-hash.ts';\nimport {\n readCookieRowCounts,\n type CookieOpTag,\n type CookieRowCounts,\n} from './change-log-cookies.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n readChangeLogBounds,\n readChangeLogMeta,\n type ChangeLogMeta,\n type ReconcileResult,\n} from './change-log-db.ts';\nimport type {ChangeLogStreamTransactionStats} from './change-log-stream-writer.ts';\n\n/**\n * Reports the reconciliation of the change-log database against the watermark\n * the stream connection resumes from. This runs once per stream connection, so\n * a reconnect storm is visible here as well as a restart.\n *\n * A wipe is not an error — it is how every abnormal change-log state is\n * collapsed into a correct one — but each wipe costs the retention window, so\n * every reconnecting subscriber below the head gets `too-old`. The `gap` reason\n * in particular means the log neither held the resume watermark nor could be\n * truncated to it, so a nonzero rate in steady state is an alert, not a\n * statistic.\n */\nexport function recordSQLiteChangeLogReconcile(\n lc: LogContext,\n result: ReconcileResult,\n): void {\n switch (result.action) {\n case 'truncated':\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.reconcile_truncated_rows',\n 'Phantom change-log rows removed at reconciliation, i.e. rows above ' +\n 'the watermark the change stream resumed from.',\n ).add(result.rows);\n break;\n case 'reseeded':\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.reconcile_wipes',\n 'Change-log wipes at reconciliation, by reason. Each one resets the ' +\n 'retention window, so subscribers below the head get `too-old`.',\n ).add(1, {reason: result.reason});\n break;\n case 'none':\n break;\n default:\n unreachable(result);\n }\n if (result.cookiesStale) {\n // The invariant-17 counter: after any reconcile that moved the head, the\n // log's cookie set has to be replaced with the one that belongs to the\n // resume watermark. A nonzero `truncated` series in steady state means\n // phantom truncation is happening on a shard where it should not.\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.cookie_reseed',\n 'Change-log cookie sets invalidated at reconciliation, by the ' +\n 'reconcile action that invalidated them.',\n ).add(1, {action: result.action});\n }\n lc.debug?.('SQLite change-log reconciliation', {\n sqliteChangeLogReconcile: result,\n });\n}\n\n/**\n * What the log says about itself. Nothing here reads the replica: after the\n * writer moved to the change-streamer, the replica's state version is a\n * consequence of this log rather than a fact about it.\n */\nexport type SQLiteChangeLogStartupInfo = ChangeLogMeta & {\n readonly minWatermark: string;\n readonly headWatermark: string;\n};\n\nexport type SQLiteChangeLogInfo = SQLiteChangeLogStartupInfo & {\n readonly rows: number;\n readonly estimatedBytes: number;\n readonly cookieRows: CookieRowCounts;\n};\n\n/**\n * Reads the meta row and covered range. Unlike {@link getSQLiteChangeLogInfo}\n * this does not scan the log; see {@link readChangeLogBounds}.\n *\n * Call this after reconciliation, which is the only point at which the head is\n * known to be the resume watermark.\n */\nexport function getSQLiteChangeLogStartupInfo(\n changeLog: Database,\n): SQLiteChangeLogStartupInfo {\n const meta = readChangeLogMeta(changeLog);\n const {minWatermark, headWatermark} = readChangeLogBounds(changeLog);\n if (minWatermark === null || headWatermark === null) {\n throw new Error('the SQLite change log must contain its seed transaction');\n }\n return {...meta, minWatermark, headWatermark};\n}\n\n/**\n * Extends {@link getSQLiteChangeLogStartupInfo} with the retained row and\n * estimated byte totals. Computing these scans the entire change-log table, so\n * only call it when the totals are consumed, i.e. when the writer (and thus the\n * observer) is enabled.\n */\nexport function getSQLiteChangeLogInfo(\n changeLog: Database,\n): SQLiteChangeLogInfo {\n const startup = getSQLiteChangeLogStartupInfo(changeLog);\n const aggregate = changeLog\n .prepare(/*sql*/ `\n SELECT count(*) AS \"rows\",\n coalesce(sum(\"estimatedBytes\"), 0) AS \"estimatedBytes\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n `)\n .get<{rows: number; estimatedBytes: number}>();\n\n return {\n ...startup,\n rows: aggregate.rows,\n estimatedBytes: aggregate.estimatedBytes,\n cookieRows: readCookieRowCounts(changeLog),\n };\n}\n\nexport function logSQLiteChangeLogStartup(\n lc: LogContext,\n file: string,\n info: SQLiteChangeLogStartupInfo,\n): void {\n lc.info?.('SQLite change-log startup', {\n sqliteChangeLog: {\n file,\n epoch: info.epoch,\n generation: info.generation,\n replicaID: info.replicaID,\n schemaVersion: info.schemaVersion,\n seedWatermark: info.seedWatermark,\n seededAtMs: info.seededAtMs,\n minWatermark: info.minWatermark,\n headWatermark: info.headWatermark,\n },\n });\n}\n\n/**\n * The on-disk footprint of a SQLite database: its main file plus its `-wal` and\n * `-wal2` sidecars, whichever exist. `-shm` is excluded — it is a\n * shared-memory index rather than retained bytes.\n *\n * A missing file counts as zero rather than warning. Which sidecars exist\n * depends on the journal mode, and the change-log database is absent entirely\n * until the writer creates it.\n */\nexport async function sqliteFileBytes(\n lc: LogContext,\n file: string,\n): Promise<number> {\n const sizes = await Promise.all(\n [file, `${file}-wal`, `${file}-wal2`].map(async path => {\n try {\n return (await stat(path)).size;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n lc.warn?.(`unable to stat ${path} for size metrics`, e);\n }\n return 0;\n }\n }),\n );\n return sizes.reduce((total, size) => total + size, 0);\n}\n\n/** What the writer reports back for a committed transaction. */\nexport type ChangeLogCommit = {\n readonly watermark: string;\n readonly stats: ChangeLogStreamTransactionStats;\n /**\n * Appending the commit row and committing the log's transaction. This is the\n * cost that sits on the forward path, once per upstream transaction.\n */\n readonly logCommitMs: number;\n};\n\nexport type SQLiteChangeLogObservabilityState = {\n readonly receivedHead: string;\n readonly sqliteHead: string;\n readonly headLag: number | undefined;\n readonly rows: number;\n readonly estimatedBytes: number;\n readonly cookieRows: CookieRowCounts;\n readonly cookieMutations: number;\n readonly rollbacks: number;\n readonly invariantFailures: number;\n readonly hashMatches: number;\n readonly hashMismatches: number;\n readonly hashUnpaired: number;\n};\n\nconst TRANSACTION_ROW_BUCKETS = [2, 3, 5, 10, 25, 50, 100, 250, 1000, 5000];\n\n/**\n * Records writer metrics in the change-streamer process. State changes only\n * after the corresponding write succeeds, except receivedHead, which advances\n * when a commit message arrives so that transient lag behind the log is\n * observable.\n */\nexport class SQLiteChangeLogObserver {\n readonly #lc: LogContext;\n readonly #messageProcessing = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.message_processing_duration',\n 'Sampled time to write a replication message to the SQLite change log.',\n );\n // The commit that sits on the forward path. There is no second commit to\n // order against any more: the replica's belongs to a subscriber downstream of\n // this loop, so `sqlite_change_log.replica_commit_duration` and the phantom\n // window it measured are gone. Sampled failed commits are still timed by\n // `message_processing_duration` keyed `tag=\"commit\"`.\n readonly #logCommitDuration = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.log_commit_duration',\n 'Time to append the commit row to the SQLite change log and commit its ' +\n 'transaction. This is on the forward path: it precedes the forward of ' +\n \"that transaction's commit message.\",\n );\n readonly #transactionRows = getOrCreateValueHistogram(\n 'replica',\n 'sqlite_change_log.transaction_rows',\n {\n description: 'Rows stored per committed SQLite change-log transaction.',\n unit: '{row}',\n bucketBoundaries: TRANSACTION_ROW_BUCKETS,\n },\n );\n readonly #rollbackCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.rollbacks',\n 'SQLite change-log transactions rolled back before commit.',\n );\n readonly #invariantFailureCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.invariant_failures',\n 'Detected SQLite change-log writer invariant failures.',\n );\n readonly #hashComparisonCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.hash_comparisons',\n 'Sampled ephemeral transaction hash comparisons between the received change stream and SQLite change-log writes.',\n );\n readonly #hashUnpairedCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.hash_unpaired',\n 'Committed change-log transactions without both ephemeral hashes available for comparison.',\n );\n // Near zero at steady state: cookie state only moves on schema changes.\n readonly #cookieMutationCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.cookie_mutations',\n 'Change-log cookie mutations folded from schema changes, by operation.',\n );\n\n #receivedHead: string;\n #sqliteHead: string;\n #rows: number;\n #estimatedBytes: number;\n #cookieRows: CookieRowCounts;\n #cookieMutations = 0;\n #transactionWatermark: string | undefined;\n #sourceHasher: ChangeLogTransactionHasher | undefined;\n #sourcePos = 0;\n #sourceCommitHash: string | undefined;\n #rollbacks = 0;\n #invariantFailures = 0;\n #hashMatches = 0;\n #hashMismatches = 0;\n #hashUnpaired = 0;\n\n constructor(lc: LogContext, info: SQLiteChangeLogInfo) {\n this.#lc = lc.withContext('component', 'sqlite-change-log-observer');\n // Reconciliation has just put the head at the resume watermark, which is\n // also the point the stream is about to be received from.\n this.#receivedHead = info.headWatermark;\n this.#sqliteHead = info.headWatermark;\n this.#rows = info.rows;\n this.#estimatedBytes = info.estimatedBytes;\n this.#cookieRows = info.cookieRows;\n\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.rows',\n 'Rows retained in the SQLite change log.',\n ).addCallback(result => result.observe(this.#rows));\n // Nonzero with no backfill in flight is a leaked cookie, i.e. a backfill\n // that will be re-requested forever.\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.cookie_rows',\n 'Cookie rows retained in the SQLite change log, by cookie table.',\n ).addCallback(result => {\n result.observe(this.#cookieRows.tableMetadata, {table: 'tableMetadata'});\n result.observe(this.#cookieRows.backfilling, {table: 'backfilling'});\n });\n getOrCreateGauge('replica', 'sqlite_change_log.retained_bytes', {\n description:\n 'Estimated UTF-8 payload bytes retained in the SQLite change log.',\n unit: 'By',\n }).addCallback(result => result.observe(this.#estimatedBytes));\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.head',\n 'SQLite change-log head converted from its lexicographic watermark.',\n ).addCallback(result => {\n const head = watermarkValue(this.#sqliteHead);\n if (head !== undefined) {\n result.observe(head);\n }\n });\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.head_lag',\n 'Distance from the latest received commit to the SQLite change-log ' +\n 'head. The log commits before the forward, so this is zero except ' +\n 'within a transaction.',\n ).addCallback(result => {\n const lag = watermarkDistance(this.#receivedHead, this.#sqliteHead);\n if (lag !== undefined) {\n result.observe(lag);\n }\n });\n }\n\n /**\n * Called as each message arrives, before it is written. For a sampled\n * transaction, hashes the *received* stream and compares it with the hash the\n * writer computes over what it *stored*.\n */\n messageReceived(\n data: ChangeStreamData,\n json: string,\n validateHash = true,\n storedChange = extractChangeSubstring(json, data[1].tag),\n ): void {\n const tag = data[1].tag;\n if (data[0] === 'begin') {\n if (this.#sourceHasher !== undefined) {\n this.#invariantFailure(\n 'Received change-log begin while a source hash is already open',\n {receivedWatermark: data[2].commitWatermark},\n );\n }\n const watermark = data[2].commitWatermark;\n this.#sourceHasher = validateHash\n ? new ChangeLogTransactionHasher()\n : undefined;\n this.#sourcePos = 0;\n this.#sourceCommitHash = undefined;\n this.#sourceHasher?.add({\n watermark,\n pos: this.#sourcePos,\n tag,\n change: storedChange,\n precommit: null,\n });\n return;\n }\n\n if (tag === 'rollback') {\n this.#resetSourceHash();\n return;\n }\n\n if (data[0] === 'commit') {\n this.#receivedHead = data[2].watermark;\n }\n\n if (!validateHash) {\n return;\n }\n\n const watermark = this.#transactionWatermark;\n if (this.#sourceHasher === undefined || watermark === undefined) {\n this.#invariantFailure(\n 'Received change-log message without an observed transaction',\n {tag},\n );\n return;\n }\n\n const commitWatermark =\n data[0] === 'commit' ? data[2].watermark : watermark;\n this.#sourceHasher.add({\n watermark: commitWatermark,\n pos: ++this.#sourcePos,\n tag,\n change: storedChange,\n precommit: data[0] === 'commit' ? watermark : null,\n });\n if (data[0] === 'commit') {\n this.#sourceCommitHash = this.#sourceHasher.digest();\n this.#sourceHasher = undefined;\n }\n }\n\n /** Called after the message has been written to the log. */\n messageProcessed(\n data: ChangeStreamData,\n commit: ChangeLogCommit | null,\n durationMs: number | undefined,\n ): void {\n const tag = data[1].tag;\n if (durationMs !== undefined) {\n this.#messageProcessing.recordMs(durationMs, {tag, outcome: 'success'});\n }\n\n if (data[0] === 'begin') {\n if (this.#transactionWatermark !== undefined) {\n this.#invariantFailure('SQLite change-log transaction already open', {\n openWatermark: this.#transactionWatermark,\n receivedWatermark: data[2].commitWatermark,\n });\n }\n this.#transactionWatermark = data[2].commitWatermark;\n return;\n }\n\n if (tag === 'rollback') {\n this.#recordRollback('upstream');\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n return;\n }\n\n if (data[0] !== 'commit') {\n return;\n }\n\n const watermark = data[2].watermark;\n if (commit !== null) {\n this.#logCommitDuration.recordMs(commit.logCommitMs);\n }\n if (this.#transactionWatermark !== watermark) {\n this.#invariantFailure(\n 'SQLite change-log commit does not match the observed begin',\n {openWatermark: this.#transactionWatermark, commitWatermark: watermark},\n );\n }\n if (commit === null) {\n this.#invariantFailure(\n 'SQLite change-log commit result is missing writer statistics',\n {commitWatermark: watermark},\n );\n this.#recordUnpaired(watermark, 'missing-sqlite-hash');\n } else {\n if (commit.watermark !== watermark) {\n this.#invariantFailure(\n 'SQLite change-log commit result has an unexpected watermark',\n {commitWatermark: watermark, resultWatermark: commit.watermark},\n );\n }\n this.#rows += commit.stats.rows;\n this.#estimatedBytes += commit.stats.estimatedBytes;\n this.#transactionRows.record(commit.stats.rows);\n if (\n this.#sourceCommitHash !== undefined ||\n commit.stats.hash !== undefined\n ) {\n this.#compareHashes(\n watermark,\n this.#sourceCommitHash,\n commit.stats.hash,\n );\n }\n }\n if (watermark < this.#sqliteHead) {\n this.#invariantFailure('SQLite change-log head regressed', {\n previousHead: this.#sqliteHead,\n commitWatermark: watermark,\n });\n }\n this.#sqliteHead = watermark;\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n messageFailed(\n data: ChangeStreamData,\n error: unknown,\n durationMs: number | undefined,\n ): void {\n const tag = data[1].tag;\n if (durationMs !== undefined) {\n this.#messageProcessing.recordMs(durationMs, {tag, outcome: 'error'});\n }\n if (data[0] === 'commit' && this.#sourceCommitHash !== undefined) {\n // No commit duration here: a failed commit produces no statistics, so\n // there is nothing to attribute. The attempt is timed by\n // `message_processing_duration{tag=\"commit\",outcome=\"error\"}` above.\n this.#recordUnpaired(data[2].watermark, 'sqlite-commit-failed');\n }\n if (\n error instanceof Error &&\n error.name === 'ChangeLogStreamInvariantError'\n ) {\n this.#invariantFailure(error.message);\n }\n if (this.#transactionWatermark !== undefined || data[0] === 'begin') {\n this.#recordRollback('processing-error');\n }\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n /**\n * A committed transaction folded schema changes into the cookie jar. Reported\n * separately from {@link messageProcessed} because it costs a read of the\n * cookie tables, which is only worth taking when they actually moved.\n */\n cookiesMutated(ops: readonly CookieOpTag[], rows: CookieRowCounts): void {\n for (const op of ops) {\n this.#cookieMutations++;\n this.#cookieMutationCounter.add(1, {op});\n }\n this.#cookieRows = rows;\n }\n\n /**\n * A missed truncate-above surfaces as a constraint violation on the writer's\n * plain `INSERT`, which the fail-soft policy would otherwise absorb quietly.\n * Classifying it as an invariant failure is what keeps a reconciliation bug\n * from looking like a lost cache.\n */\n invariantFailure(message: string, details?: Record<string, unknown>): void {\n this.#invariantFailure(message, details);\n }\n\n /** The source interrupted the stream mid-transaction. */\n abort(): void {\n if (this.#transactionWatermark !== undefined) {\n this.#recordRollback('source-interruption');\n this.#transactionWatermark = undefined;\n }\n this.#resetSourceHash();\n }\n\n /** Reconciliation moved the head, so the observer's totals are restated. */\n reconciled(info: SQLiteChangeLogInfo): void {\n this.#receivedHead = info.headWatermark;\n this.#sqliteHead = info.headWatermark;\n this.#rows = info.rows;\n this.#estimatedBytes = info.estimatedBytes;\n this.#cookieRows = info.cookieRows;\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n state(): SQLiteChangeLogObservabilityState {\n return {\n receivedHead: this.#receivedHead,\n sqliteHead: this.#sqliteHead,\n headLag: watermarkDistance(this.#receivedHead, this.#sqliteHead),\n rows: this.#rows,\n estimatedBytes: this.#estimatedBytes,\n cookieRows: this.#cookieRows,\n cookieMutations: this.#cookieMutations,\n rollbacks: this.#rollbacks,\n invariantFailures: this.#invariantFailures,\n hashMatches: this.#hashMatches,\n hashMismatches: this.#hashMismatches,\n hashUnpaired: this.#hashUnpaired,\n };\n }\n\n #recordRollback(\n reason: 'upstream' | 'processing-error' | 'source-interruption',\n ) {\n this.#rollbacks++;\n this.#rollbackCounter.add(1, {reason});\n }\n\n #invariantFailure(message: string, details?: Record<string, unknown>) {\n this.#invariantFailures++;\n this.#invariantFailureCounter.add(1);\n this.#lc.error?.(message, details);\n }\n\n #compareHashes(\n watermark: string,\n sourceHash: string | undefined,\n sqliteHash: string | undefined,\n ): void {\n if (sourceHash === undefined) {\n this.#recordUnpaired(watermark, 'missing-source-hash');\n return;\n }\n if (sqliteHash === undefined) {\n this.#recordUnpaired(watermark, 'missing-sqlite-hash');\n return;\n }\n if (sourceHash === sqliteHash) {\n this.#hashMatches++;\n this.#hashComparisonCounter.add(1, {outcome: 'match'});\n return;\n }\n this.#hashMismatches++;\n this.#hashComparisonCounter.add(1, {outcome: 'mismatch'});\n this.#lc.error?.('SQLite change-log transaction hash mismatch', {\n sqliteChangeLogHashComparison: {\n watermark,\n sourceHash: sourceHash.slice(0, 16),\n sqliteHash: sqliteHash.slice(0, 16),\n },\n });\n }\n\n #recordUnpaired(\n watermark: string,\n reason:\n | 'missing-source-hash'\n | 'missing-sqlite-hash'\n | 'sqlite-commit-failed',\n ): void {\n this.#hashUnpaired++;\n this.#hashUnpairedCounter.add(1, {reason});\n this.#lc.warn?.('SQLite change-log transaction hash was not compared', {\n sqliteChangeLogHashComparison: {watermark, reason},\n });\n }\n\n #resetSourceHash(): void {\n this.#sourceHasher = undefined;\n this.#sourcePos = 0;\n this.#sourceCommitHash = undefined;\n }\n}\n\nfunction watermarkValue(watermark: string): number | undefined {\n try {\n const value = Number(versionFromLexi(watermark));\n return Number.isFinite(value) ? value : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction watermarkDistance(\n receivedHead: string,\n sqliteHead: string,\n): number | undefined {\n if (receivedHead <= sqliteHead) {\n return 0;\n }\n try {\n const distance =\n versionFromLexi(receivedHead) - versionFromLexi(sqliteHead);\n const value = Number(distance);\n return Number.isFinite(value) ? value : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqlite-change-log-observability.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/sqlite-change-log-observability.ts"],"sourcesContent":["/**\n * Observability for the SQLite change log.\n *\n * Everything here now runs in the **change-streamer** process, which is where\n * the writer lives. The module stays beside the rest of the change-log code\n * under `services/replicator/` for the same reason `change-log-db.ts` and\n * `change-log-stream-writer.ts` do: moving files would obscure the topology\n * diff, and the writer's home is a property of who calls it rather than of\n * where it sits. Metric names are unchanged from 7H — only their emitting\n * process, and therefore their resource attributes, moved.\n */\n\nimport {stat} from 'node:fs/promises';\nimport type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../../shared/src/asserts.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateLatencyHistogram,\n getOrCreateValueHistogram,\n} from '../../observability/metrics.ts';\nimport {versionFromLexi} from '../../types/lexi-version.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {extractChangeSubstring} from '../change-streamer/change-log-codec.ts';\nimport {ChangeLogTransactionHasher} from '../change-streamer/change-log-transaction-hash.ts';\nimport {\n readCookieRowCounts,\n type CookieOpTag,\n type CookieRowCounts,\n} from './change-log-cookies.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n readChangeLogBounds,\n readChangeLogMeta,\n type ChangeLogMeta,\n type ReconcileResult,\n} from './change-log-db.ts';\nimport type {ChangeLogStreamTransactionStats} from './change-log-stream-writer.ts';\n\n/**\n * Reports the reconciliation of the change-log database against the watermark\n * the stream connection resumes from. This runs once per stream connection, so\n * a reconnect storm is visible here as well as a restart.\n *\n * A wipe is not an error — it is how every abnormal change-log state is\n * collapsed into a correct one — but each wipe costs the retention window, so\n * every reconnecting subscriber below the head gets `too-old`. The `gap` reason\n * in particular means the log neither held the resume watermark nor could be\n * truncated to it, so a nonzero rate in steady state is an alert, not a\n * statistic.\n */\nexport function recordSQLiteChangeLogReconcile(\n lc: LogContext,\n result: ReconcileResult,\n): void {\n switch (result.action) {\n case 'truncated':\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.reconcile_truncated_rows',\n 'Phantom change-log rows removed at reconciliation, i.e. rows above ' +\n 'the watermark the change stream resumed from.',\n ).add(result.rows);\n break;\n case 'reseeded':\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.reconcile_wipes',\n 'Change-log wipes at reconciliation, by reason. Each one resets the ' +\n 'retention window, so subscribers below the head get `too-old`.',\n ).add(1, {reason: result.reason});\n break;\n case 'none':\n break;\n default:\n unreachable(result);\n }\n if (result.cookiesStale) {\n // The invariant-17 counter: after any reconcile that moved the head, the\n // log's cookie set has to be replaced with the one that belongs to the\n // resume watermark. A nonzero `truncated` series in steady state means\n // phantom truncation is happening on a shard where it should not.\n getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.cookie_reseed',\n 'Change-log cookie sets invalidated at reconciliation, by the ' +\n 'reconcile action that invalidated them.',\n ).add(1, {action: result.action});\n }\n lc.debug?.('SQLite change-log reconciliation', {\n sqliteChangeLogReconcile: result,\n });\n}\n\n/**\n * What the log says about itself. Nothing here reads the replica: after the\n * writer moved to the change-streamer, the replica's state version is a\n * consequence of this log rather than a fact about it.\n */\nexport type SQLiteChangeLogStartupInfo = ChangeLogMeta & {\n readonly minWatermark: string;\n readonly headWatermark: string;\n};\n\nexport type SQLiteChangeLogInfo = SQLiteChangeLogStartupInfo & {\n readonly rows: number;\n readonly estimatedBytes: number;\n readonly cookieRows: CookieRowCounts;\n};\n\n/**\n * Reads the meta row and covered range. Unlike {@link getSQLiteChangeLogInfo}\n * this does not scan the log; see {@link readChangeLogBounds}.\n *\n * Call this after reconciliation, which is the only point at which the head is\n * known to be the resume watermark.\n */\nexport function getSQLiteChangeLogStartupInfo(\n changeLog: Database,\n): SQLiteChangeLogStartupInfo {\n const meta = readChangeLogMeta(changeLog);\n const {minWatermark, headWatermark} = readChangeLogBounds(changeLog);\n if (minWatermark === null || headWatermark === null) {\n throw new Error('the SQLite change log must contain its seed transaction');\n }\n return {...meta, minWatermark, headWatermark};\n}\n\n/**\n * Extends {@link getSQLiteChangeLogStartupInfo} with the retained row and\n * estimated byte totals. Computing these scans the entire change-log table, so\n * only call it when the totals are consumed, i.e. when the writer (and thus the\n * observer) is enabled.\n */\nexport function getSQLiteChangeLogInfo(\n changeLog: Database,\n): SQLiteChangeLogInfo {\n const startup = getSQLiteChangeLogStartupInfo(changeLog);\n const aggregate = changeLog\n .prepare(/*sql*/ `\n SELECT count(*) AS \"rows\",\n coalesce(sum(\"estimatedBytes\"), 0) AS \"estimatedBytes\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n `)\n .get<{rows: number; estimatedBytes: number}>();\n\n return {\n ...startup,\n rows: aggregate.rows,\n estimatedBytes: aggregate.estimatedBytes,\n cookieRows: readCookieRowCounts(changeLog),\n };\n}\n\nexport function logSQLiteChangeLogStartup(\n lc: LogContext,\n file: string,\n info: SQLiteChangeLogStartupInfo,\n): void {\n lc.info?.('SQLite change-log startup', {\n sqliteChangeLog: {\n file,\n epoch: info.epoch,\n generation: info.generation,\n replicaID: info.replicaID,\n schemaVersion: info.schemaVersion,\n seedWatermark: info.seedWatermark,\n seededAtMs: info.seededAtMs,\n minWatermark: info.minWatermark,\n headWatermark: info.headWatermark,\n },\n });\n}\n\n/**\n * The on-disk footprint of a SQLite database: its main file plus its `-wal` and\n * `-wal2` sidecars, whichever exist. `-shm` is excluded — it is a\n * shared-memory index rather than retained bytes.\n *\n * A missing file counts as zero rather than warning. Which sidecars exist\n * depends on the journal mode, and the change-log database is absent entirely\n * until the writer creates it.\n */\nexport async function sqliteFileBytes(\n lc: LogContext,\n file: string,\n): Promise<number> {\n const sizes = await Promise.all(\n [file, `${file}-wal`, `${file}-wal2`].map(async path => {\n try {\n return (await stat(path)).size;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n lc.warn?.(`unable to stat ${path} for size metrics`, e);\n }\n return 0;\n }\n }),\n );\n return sizes.reduce((total, size) => total + size, 0);\n}\n\n/** What the writer reports back for a committed transaction. */\nexport type ChangeLogCommit = {\n readonly watermark: string;\n readonly stats: ChangeLogStreamTransactionStats;\n /**\n * Appending the commit row and committing the log's transaction. This is the\n * cost that sits on the forward path, once per upstream transaction.\n */\n readonly logCommitMs: number;\n};\n\nexport type SQLiteChangeLogObservabilityState = {\n readonly receivedHead: string;\n readonly sqliteHead: string;\n readonly headLag: number | undefined;\n readonly rows: number;\n readonly estimatedBytes: number;\n readonly cookieRows: CookieRowCounts;\n readonly cookieMutations: number;\n readonly rollbacks: number;\n readonly invariantFailures: number;\n readonly hashMatches: number;\n readonly hashMismatches: number;\n readonly hashUnpaired: number;\n};\n\nconst TRANSACTION_ROW_BUCKETS = [2, 3, 5, 10, 25, 50, 100, 250, 1000, 5000];\n\n/**\n * Records writer metrics in the change-streamer process. State changes only\n * after the corresponding write succeeds, except receivedHead, which advances\n * when a commit message arrives so that transient lag behind the log is\n * observable.\n */\nexport class SQLiteChangeLogObserver {\n readonly #lc: LogContext;\n readonly #messageProcessing = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.message_processing_duration',\n 'Sampled time to write a replication message to the SQLite change log.',\n );\n // The commit that sits on the forward path. There is no second commit to\n // order against any more: the replica's belongs to a subscriber downstream of\n // this loop, so `sqlite_change_log.replica_commit_duration` and the phantom\n // window it measured are gone. Sampled failed commits are still timed by\n // `message_processing_duration` keyed `tag=\"commit\"`.\n readonly #logCommitDuration = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.log_commit_duration',\n 'Time to append the commit row to the SQLite change log and commit its ' +\n 'transaction. This is on the forward path: it precedes the forward of ' +\n \"that transaction's commit message.\",\n );\n readonly #transactionRows = getOrCreateValueHistogram(\n 'replica',\n 'sqlite_change_log.transaction_rows',\n {\n description: 'Rows stored per committed SQLite change-log transaction.',\n unit: '{row}',\n bucketBoundaries: TRANSACTION_ROW_BUCKETS,\n },\n );\n readonly #rollbackCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.rollbacks',\n 'SQLite change-log transactions rolled back before commit.',\n );\n readonly #invariantFailureCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.invariant_failures',\n 'Detected SQLite change-log writer invariant failures.',\n );\n readonly #hashComparisonCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.hash_comparisons',\n 'Sampled ephemeral transaction hash comparisons between the received change stream and SQLite change-log writes.',\n );\n readonly #hashUnpairedCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.hash_unpaired',\n 'Committed change-log transactions without both ephemeral hashes available for comparison.',\n );\n // Near zero at steady state: cookie state only moves on schema changes.\n readonly #cookieMutationCounter = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.cookie_mutations',\n 'Change-log cookie mutations folded from schema changes, by operation.',\n );\n\n #receivedHead: string;\n #sqliteHead: string;\n #rows: number;\n #estimatedBytes: number;\n #cookieRows: CookieRowCounts;\n #cookieMutations = 0;\n #transactionWatermark: string | undefined;\n #sourceHasher: ChangeLogTransactionHasher | undefined;\n #sourcePos = 0;\n #sourceCommitHash: string | undefined;\n #rollbacks = 0;\n #invariantFailures = 0;\n #hashMatches = 0;\n #hashMismatches = 0;\n #hashUnpaired = 0;\n\n constructor(lc: LogContext, info: SQLiteChangeLogInfo) {\n this.#lc = lc.withContext('component', 'sqlite-change-log-observer');\n // Reconciliation has just put the head at the resume watermark, which is\n // also the point the stream is about to be received from.\n this.#receivedHead = info.headWatermark;\n this.#sqliteHead = info.headWatermark;\n this.#rows = info.rows;\n this.#estimatedBytes = info.estimatedBytes;\n this.#cookieRows = info.cookieRows;\n\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.rows',\n 'Rows retained in the SQLite change log.',\n ).addCallback(result => result.observe(this.#rows));\n // Nonzero with no backfill in flight is a leaked cookie, i.e. a backfill\n // that will be re-requested forever.\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.cookie_rows',\n 'Cookie rows retained in the SQLite change log, by cookie table.',\n ).addCallback(result => {\n result.observe(this.#cookieRows.tableMetadata, {table: 'tableMetadata'});\n result.observe(this.#cookieRows.backfilling, {table: 'backfilling'});\n });\n getOrCreateGauge('replica', 'sqlite_change_log.retained_bytes', {\n description:\n 'Estimated UTF-8 payload bytes retained in the SQLite change log.',\n unit: 'By',\n }).addCallback(result => result.observe(this.#estimatedBytes));\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.head',\n 'SQLite change-log head converted from its lexicographic watermark.',\n ).addCallback(result => {\n const head = watermarkValue(this.#sqliteHead);\n if (head !== undefined) {\n result.observe(head);\n }\n });\n getOrCreateGauge(\n 'replica',\n 'sqlite_change_log.head_lag',\n 'Distance from the latest received commit to the SQLite change-log ' +\n 'head. The log commits before the forward, so this is zero except ' +\n 'within a transaction.',\n ).addCallback(result => {\n const lag = watermarkDistance(this.#receivedHead, this.#sqliteHead);\n if (lag !== undefined) {\n result.observe(lag);\n }\n });\n }\n\n /**\n * Called as each message arrives, before it is written. For a sampled\n * transaction, hashes the *received* stream and compares it with the hash the\n * writer computes over what it *stored*.\n */\n messageReceived(\n data: ChangeStreamData,\n json: string,\n validateHash = true,\n storedChange = extractChangeSubstring(json, data[1].tag),\n ): void {\n const tag = data[1].tag;\n if (data[0] === 'begin') {\n if (this.#sourceHasher !== undefined) {\n this.#invariantFailure(\n 'Received change-log begin while a source hash is already open',\n {receivedWatermark: data[2].commitWatermark},\n );\n }\n const watermark = data[2].commitWatermark;\n this.#sourceHasher = validateHash\n ? new ChangeLogTransactionHasher()\n : undefined;\n this.#sourcePos = 0;\n this.#sourceCommitHash = undefined;\n this.#sourceHasher?.add({\n watermark,\n pos: this.#sourcePos,\n tag,\n change: storedChange,\n precommit: null,\n });\n return;\n }\n\n if (tag === 'rollback') {\n this.#resetSourceHash();\n return;\n }\n\n if (data[0] === 'commit') {\n this.#receivedHead = data[2].watermark;\n }\n\n if (!validateHash) {\n return;\n }\n\n const watermark = this.#transactionWatermark;\n if (this.#sourceHasher === undefined || watermark === undefined) {\n this.#invariantFailure(\n 'Received change-log message without an observed transaction',\n {tag},\n );\n return;\n }\n\n const commitWatermark =\n data[0] === 'commit' ? data[2].watermark : watermark;\n this.#sourceHasher.add({\n watermark: commitWatermark,\n pos: ++this.#sourcePos,\n tag,\n change: storedChange,\n precommit: data[0] === 'commit' ? watermark : null,\n });\n if (data[0] === 'commit') {\n this.#sourceCommitHash = this.#sourceHasher.digest();\n this.#sourceHasher = undefined;\n }\n }\n\n /** Called after the message has been written to the log. */\n messageProcessed(\n data: ChangeStreamData,\n commit: ChangeLogCommit | null,\n durationMs: number | undefined,\n ): void {\n const tag = data[1].tag;\n if (durationMs !== undefined) {\n this.#messageProcessing.recordMs(durationMs, {tag, outcome: 'success'});\n }\n\n if (data[0] === 'begin') {\n if (this.#transactionWatermark !== undefined) {\n this.#invariantFailure('SQLite change-log transaction already open', {\n openWatermark: this.#transactionWatermark,\n receivedWatermark: data[2].commitWatermark,\n });\n }\n this.#transactionWatermark = data[2].commitWatermark;\n return;\n }\n\n if (tag === 'rollback') {\n this.#recordRollback('upstream');\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n return;\n }\n\n if (data[0] !== 'commit') {\n return;\n }\n\n const watermark = data[2].watermark;\n if (commit !== null) {\n this.#logCommitDuration.recordMs(commit.logCommitMs);\n }\n if (this.#transactionWatermark !== watermark) {\n this.#invariantFailure(\n 'SQLite change-log commit does not match the observed begin',\n {openWatermark: this.#transactionWatermark, commitWatermark: watermark},\n );\n }\n if (commit === null) {\n this.#invariantFailure(\n 'SQLite change-log commit result is missing writer statistics',\n {commitWatermark: watermark},\n );\n this.#recordUnpaired(watermark, 'missing-sqlite-hash');\n } else {\n if (commit.watermark !== watermark) {\n this.#invariantFailure(\n 'SQLite change-log commit result has an unexpected watermark',\n {commitWatermark: watermark, resultWatermark: commit.watermark},\n );\n }\n this.#rows += commit.stats.rows;\n this.#estimatedBytes += commit.stats.estimatedBytes;\n this.#transactionRows.record(commit.stats.rows);\n if (\n this.#sourceCommitHash !== undefined ||\n commit.stats.hash !== undefined\n ) {\n this.#compareHashes(\n watermark,\n this.#sourceCommitHash,\n commit.stats.hash,\n );\n }\n }\n if (watermark < this.#sqliteHead) {\n this.#invariantFailure('SQLite change-log head regressed', {\n previousHead: this.#sqliteHead,\n commitWatermark: watermark,\n });\n }\n this.#sqliteHead = watermark;\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n messageFailed(\n data: ChangeStreamData,\n error: unknown,\n durationMs: number | undefined,\n ): void {\n const tag = data[1].tag;\n if (durationMs !== undefined) {\n this.#messageProcessing.recordMs(durationMs, {tag, outcome: 'error'});\n }\n if (data[0] === 'commit' && this.#sourceCommitHash !== undefined) {\n // No commit duration here: a failed commit produces no statistics, so\n // there is nothing to attribute. The attempt is timed by\n // `message_processing_duration{tag=\"commit\",outcome=\"error\"}` above.\n this.#recordUnpaired(data[2].watermark, 'sqlite-commit-failed');\n }\n if (\n error instanceof Error &&\n error.name === 'ChangeLogStreamInvariantError'\n ) {\n this.#invariantFailure(error.message);\n }\n if (this.#transactionWatermark !== undefined || data[0] === 'begin') {\n this.#recordRollback('processing-error');\n }\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n /**\n * A committed transaction folded schema changes into the cookie jar. Reported\n * separately from {@link messageProcessed} because it costs a read of the\n * cookie tables, which is only worth taking when they actually moved.\n */\n cookiesMutated(ops: readonly CookieOpTag[], rows: CookieRowCounts): void {\n for (const op of ops) {\n this.#cookieMutations++;\n this.#cookieMutationCounter.add(1, {op});\n }\n this.#cookieRows = rows;\n }\n\n /**\n * A missed truncate-above surfaces as a constraint violation on the writer's\n * plain `INSERT`, which the fail-soft policy would otherwise absorb quietly.\n * Classifying it as an invariant failure is what keeps a reconciliation bug\n * from looking like a lost cache.\n */\n invariantFailure(message: string, details?: Record<string, unknown>): void {\n this.#invariantFailure(message, details);\n }\n\n /** The source interrupted the stream mid-transaction. */\n abort(): void {\n if (this.#transactionWatermark !== undefined) {\n this.#recordRollback('source-interruption');\n this.#transactionWatermark = undefined;\n }\n this.#resetSourceHash();\n }\n\n /** Reconciliation moved the head, so the observer's totals are restated. */\n reconciled(info: SQLiteChangeLogInfo): void {\n this.#receivedHead = info.headWatermark;\n this.#sqliteHead = info.headWatermark;\n this.#rows = info.rows;\n this.#estimatedBytes = info.estimatedBytes;\n this.#cookieRows = info.cookieRows;\n this.#transactionWatermark = undefined;\n this.#resetSourceHash();\n }\n\n state(): SQLiteChangeLogObservabilityState {\n return {\n receivedHead: this.#receivedHead,\n sqliteHead: this.#sqliteHead,\n headLag: watermarkDistance(this.#receivedHead, this.#sqliteHead),\n rows: this.#rows,\n estimatedBytes: this.#estimatedBytes,\n cookieRows: this.#cookieRows,\n cookieMutations: this.#cookieMutations,\n rollbacks: this.#rollbacks,\n invariantFailures: this.#invariantFailures,\n hashMatches: this.#hashMatches,\n hashMismatches: this.#hashMismatches,\n hashUnpaired: this.#hashUnpaired,\n };\n }\n\n #recordRollback(\n reason: 'upstream' | 'processing-error' | 'source-interruption',\n ) {\n this.#rollbacks++;\n this.#rollbackCounter.add(1, {reason});\n }\n\n #invariantFailure(message: string, details?: Record<string, unknown>) {\n this.#invariantFailures++;\n this.#invariantFailureCounter.add(1);\n this.#lc.error?.(message, details);\n }\n\n #compareHashes(\n watermark: string,\n sourceHash: string | undefined,\n sqliteHash: string | undefined,\n ): void {\n if (sourceHash === undefined) {\n this.#recordUnpaired(watermark, 'missing-source-hash');\n return;\n }\n if (sqliteHash === undefined) {\n this.#recordUnpaired(watermark, 'missing-sqlite-hash');\n return;\n }\n if (sourceHash === sqliteHash) {\n this.#hashMatches++;\n this.#hashComparisonCounter.add(1, {outcome: 'match'});\n return;\n }\n this.#hashMismatches++;\n this.#hashComparisonCounter.add(1, {outcome: 'mismatch'});\n this.#lc.error?.('SQLite change-log transaction hash mismatch', {\n sqliteChangeLogHashComparison: {\n watermark,\n sourceHash: sourceHash.slice(0, 16),\n sqliteHash: sqliteHash.slice(0, 16),\n },\n });\n }\n\n #recordUnpaired(\n watermark: string,\n reason:\n | 'missing-source-hash'\n | 'missing-sqlite-hash'\n | 'sqlite-commit-failed',\n ): void {\n this.#hashUnpaired++;\n this.#hashUnpairedCounter.add(1, {reason});\n this.#lc.warn?.('SQLite change-log transaction hash was not compared', {\n sqliteChangeLogHashComparison: {watermark, reason},\n });\n }\n\n #resetSourceHash(): void {\n this.#sourceHasher = undefined;\n this.#sourcePos = 0;\n this.#sourceCommitHash = undefined;\n }\n}\n\nfunction watermarkValue(watermark: string): number | undefined {\n try {\n const value = Number(versionFromLexi(watermark));\n return Number.isFinite(value) ? value : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction watermarkDistance(\n receivedHead: string,\n sqliteHead: string,\n): number | undefined {\n if (receivedHead <= sqliteHead) {\n return 0;\n }\n try {\n const distance =\n versionFromLexi(receivedHead) - versionFromLexi(sqliteHead);\n const value = Number(distance);\n return Number.isFinite(value) ? value : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
@@ -278,6 +278,6 @@ var SQLiteChangeLogPurger = class {
278
278
  }
279
279
  };
280
280
  //#endregion
281
- export { SQLiteChangeLogPurger };
281
+ export { SQLITE_CHANGE_LOG_COUNT_TRANSACTION_SQL, SQLITE_CHANGE_LOG_ELIGIBLE_COMMITS_SQL, SQLiteChangeLogPurger };
282
282
 
283
283
  //# sourceMappingURL=sqlite-change-log-purger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-change-log-purger.js","names":["#db","#now","#stmts","#budgetRows","#purge","#budget","#tear","#timed","#anyEligible"],"sources":["../../../../../../zero-cache/src/services/replicator/sqlite-change-log-purger.ts"],"sourcesContent":["/**\n * Deletes aged-out transactions from the change-log database, one bounded write\n * transaction at a time.\n *\n * The purger is a pure function of one database: it takes the floors its caller\n * has already gated on and narrows them, never widens them, and it reads no\n * replication state. That is what makes it testable without a scheduler and\n * what lets slice 9 run it from the change-streamer's stream loop, between the\n * writer's commits, on the writer's own connection.\n */\n\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {Database, Statement} from '../../../../zqlite/src/db.ts';\nimport {CHANGE_LOG_STREAM_TABLE} from './change-log-db.ts';\n\n/**\n * The eligible set, as three ANDed bounds. Only the `externalFloor` is a safety\n * property; the other two make the log serviceable and make purge conservative.\n *\n * `\"watermark\" < @externalFloor` is deliberately strict. A follower that lost\n * its replica restores it from S3 at watermark `W` and then asks this log for\n * everything after `W`, which requires the `commit` row *at* `W` to still be\n * here (`SQLITE_CHANGE_LOG_BOUNDARY_SQL`). Deleting it returns `too-old` to the\n * very subscriber the floor exists to protect.\n *\n * The head cap reads `max(\"watermark\")` from the log's own database, which\n * keeps the purger a pure function of one file. The cap is inert in normal\n * operation: the floor is derived from a backup of a replica that consumes this\n * loop's output, and the log commits before that output is forwarded, so\n * `externalFloor <= head` holds by construction. It engages only on a *stale*\n * log — writer stopped, mode flipped to `off`, another task took over — where\n * deleting to a floor that has run past a frozen log would empty the file. Note\n * that head can be a phantom — committed here but not yet re-deliverable by a\n * resumed stream — for as long as it takes the watermark the stream would\n * resume from to advance past it. That is harmless precisely because head is\n * never a purge candidate, and it is why the purger asserts nothing about\n * anything outside its own file.\n *\n * Ordered by `(\"writeTimeMs\", \"watermark\")` so it rides the partial index,\n * which carries one entry per transaction rather than one per change. The\n * `LIMIT` keeps a drain from re-scanning the remaining history on every batch,\n * which would be quadratic in the number of eligible transactions and would do\n * it inside the `BEGIN IMMEDIATE`, holding the write lock alongside the delete.\n *\n * Exported so the focused query-plan test covers the production query.\n */\nexport const SQLITE_CHANGE_LOG_ELIGIBLE_COMMITS_SQL = /*sql*/ `\n SELECT \"watermark\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"writeTimeMs\" IS NOT NULL\n AND \"writeTimeMs\" < @retentionCutoffMs\n AND \"watermark\" < @externalFloor\n AND \"watermark\" < (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n ORDER BY \"writeTimeMs\", \"watermark\"\n LIMIT @limit\n`;\n\n/**\n * Counts the rows at `@watermark`, capped at `@limit`. The cap is what keeps\n * batch sizing O(budget): this runs per candidate inside the\n * `BEGIN IMMEDIATE`, and the transaction being sized can be arbitrarily\n * large — an uncapped count of a multi-million-row backfill would rescan\n * every remaining row on every tear batch, holding the write lock (and the\n * appending path behind it) for the duration, quadratically over the drain.\n * The caller only needs to know whether the transaction fits its remaining\n * budget, so it passes `remaining + 1` and reads a count above `remaining`\n * as \"does not fit\".\n *\n * Exported so the focused tests cover the production query.\n */\nexport const SQLITE_CHANGE_LOG_COUNT_TRANSACTION_SQL = /*sql*/ `\n SELECT count(*) AS \"rows\" FROM (\n SELECT 1\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @watermark\n LIMIT @limit\n )\n`;\n\n/**\n * A prefix delete, so no transaction between the minimum and the head is ever\n * split. The `<=` is correct and is not the floor comparison: `deletedThrough`\n * is a watermark this batch already established as wholly eligible.\n */\nconst DELETE_THROUGH_SQL = /*sql*/ `\n DELETE FROM \"${CHANGE_LOG_STREAM_TABLE}\" WHERE \"watermark\" <= ?\n`;\n\nconst MIN_WATERMARK_SQL = /*sql*/ `\n SELECT min(\"watermark\") AS \"minWatermark\" FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n`;\n\n/**\n * The `pos` at `@offset` rows into the torn transaction, ascending. `undefined`\n * when fewer than `@offset` rows remain, which is how the tear knows it is on\n * its final chunk.\n */\nconst TEAR_BOUNDARY_SQL = /*sql*/ `\n SELECT \"pos\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @watermark\n ORDER BY \"pos\"\n LIMIT 1 OFFSET @offset\n`;\n\n/**\n * A range scan on the primary key. Expressed as `\"pos\" < @pos` rather than\n * `DELETE ... LIMIT n`, which is both non-portable and the wrong bound: it\n * carries no guarantee about *which* rows it removes, and the commit row must\n * survive until the chunk that finishes the tear.\n */\nconst DELETE_BELOW_POS_SQL = /*sql*/ `\n DELETE FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @watermark AND \"pos\" < @pos\n`;\n\nexport type PurgeBatchResult = {\n readonly deletedRows: number;\n /**\n * The watermark through which whole transactions were removed. `undefined`\n * when the batch only tore rows off the minimum transaction without\n * completing it — in that case `deletedRows > 0` and `moreEligible` is true.\n */\n readonly deletedThrough: string | undefined;\n /** True while a tear is in progress, as well as while whole transactions remain eligible. */\n readonly moreEligible: boolean;\n};\n\nexport type PurgeBatchOptions = {\n /**\n * The floor the caller has already gated on subscriber ACKs and snapshot\n * reservations — normally the confirmed S3 backup watermark. Transactions at\n * or above it are NOT durable anywhere else: the change log is excluded from\n * the litestream backup, so this is the boundary a follower restores to and\n * then catches up from. Compared strictly (`<`), so the transaction *at* the\n * floor survives as that follower's catchup boundary. The purger narrows this\n * input and never widens it.\n */\n readonly externalFloor: string;\n /**\n * A *minimum* retention, ANDed on top of `externalFloor` rather than an\n * alternative to it. Which of the two binds depends on the backup interval.\n */\n readonly retentionCutoffMs: number;\n /** Starting chunk size. Not the bound — `maxDurationMs` is. */\n readonly maxRows: number;\n /**\n * The statement duration target, enforced by feedback rather than by\n * interruption: SQLite cannot yield mid-statement, so the purger measures\n * each delete and shrinks the next batch's row budget when one overruns\n * (see {@link SQLiteChangeLogPurger.budgetRows}) — which means the first\n * delete after a restart, sized by `maxRows` alone, can still overrun it.\n * The target is this consequential because purge shares the writer's\n * connection: there is no lock to arbitrate and no `busy_timeout` to bound\n * the wait — a long statement blocks the appending path, and the forward\n * behind it, directly and for its full duration. That is why this is a\n * precondition of purging from the stream loop rather than a tuning knob.\n */\n readonly maxDurationMs: number;\n};\n\ntype Statements = {\n readonly eligibleCommits: Statement;\n readonly countTransaction: Statement;\n readonly deleteThrough: Statement;\n readonly minWatermark: Statement;\n readonly tearBoundary: Statement;\n readonly deleteBelowPos: Statement;\n};\n\nexport class SQLiteChangeLogPurger {\n readonly #db: Database;\n readonly #now: () => number;\n readonly #stmts: Statements;\n\n /**\n * The measured row budget, carried across batches. Row cost varies with\n * payload size, so `maxRows` alone cannot bound a statement's duration; this\n * shrinks when a delete overruns `maxDurationMs` and grows back when the\n * budget — rather than the eligible set — was what limited the batch.\n */\n #budgetRows: number | undefined;\n\n /**\n * @param db a writable connection to a change log the writer has already\n * created and reconciled — in slice 9, the writer's own connection.\n * Statements are prepared eagerly, so a caller that may run before the\n * log exists opens the connection lazily and treats construction failure\n * as a skipped cycle.\n */\n constructor(\n db: Database,\n now: () => number = performance.now.bind(performance),\n ) {\n this.#db = db;\n this.#now = now;\n this.#stmts = {\n eligibleCommits: db.prepare(SQLITE_CHANGE_LOG_ELIGIBLE_COMMITS_SQL),\n countTransaction: db.prepare(SQLITE_CHANGE_LOG_COUNT_TRANSACTION_SQL),\n deleteThrough: db.prepare(DELETE_THROUGH_SQL),\n minWatermark: db.prepare(MIN_WATERMARK_SQL),\n tearBoundary: db.prepare(TEAR_BOUNDARY_SQL),\n deleteBelowPos: db.prepare(DELETE_BELOW_POS_SQL),\n };\n }\n\n /** The row budget the next batch would use. Exposed for tests and metrics. */\n get budgetRows(): number | undefined {\n return this.#budgetRows;\n }\n\n /**\n * Removes one bounded batch of aged-out history, in one `BEGIN IMMEDIATE`\n * transaction. Callers loop while `moreEligible`, releasing whatever guards\n * they hold between batches.\n */\n purgeBatch(opts: PurgeBatchOptions): PurgeBatchResult {\n const {maxRows, maxDurationMs} = opts;\n assert(\n Number.isSafeInteger(maxRows) && maxRows > 0,\n 'change-log purge maxRows must be a positive safe integer',\n );\n assert(\n maxDurationMs > 0,\n 'change-log purge maxDurationMs must be positive',\n );\n\n this.#db.exec('BEGIN IMMEDIATE');\n try {\n const result = this.#purge(opts);\n this.#db.exec('COMMIT');\n return result;\n } catch (e) {\n if (this.#db.inTransaction) {\n this.#db.exec('ROLLBACK');\n }\n throw e;\n }\n }\n\n #purge(opts: PurgeBatchOptions): PurgeBatchResult {\n const {externalFloor, retentionCutoffMs} = opts;\n const budget = this.#budget(opts.maxRows);\n // Each transaction is at least a `begin` and a `commit`, so this is the\n // most candidates that can fit in the budget.\n const limit = Math.max(1, Math.ceil(budget / 2));\n const candidates = this.#stmts.eligibleCommits.all<{watermark: string}>({\n retentionCutoffMs,\n externalFloor,\n limit,\n });\n if (candidates.length === 0) {\n return {deletedRows: 0, deletedThrough: undefined, moreEligible: false};\n }\n\n // Accounted per candidate rather than per prefix, which is exact as long as\n // the candidates arrive in watermark order — they do, because the single\n // writer stamps `writeTimeMs` at commit time. A clock that went backwards\n // can put a non-candidate below `deletedThrough`, and the prefix delete\n // then removes more rows than were counted. Not a safety problem: both\n // watermark bounds are prefix-closed, only the minimum-retention courtesy\n // is crossed, and `maxRows` is the starting chunk size rather than the\n // bound — `maxDurationMs` is, and the governor measures what actually ran.\n let rows = 0;\n let deletedThrough: string | undefined;\n for (const {watermark} of candidates) {\n // The count is capped at one past the remaining budget: `n > remaining`\n // is exactly `rows + n > budget` for a fitting transaction, and a\n // too-large one stops scanning as soon as it is known not to fit.\n const remaining = budget - rows;\n const {rows: n} = this.#stmts.countTransaction.get<{rows: number}>({\n watermark,\n limit: remaining + 1,\n });\n if (n > remaining) {\n break;\n }\n rows += n;\n deletedThrough = watermark;\n }\n\n if (deletedThrough === undefined) {\n return this.#tear(opts, budget);\n }\n const deletedRows = this.#timed(\n opts,\n budget,\n () => this.#stmts.deleteThrough.run(deletedThrough).changes,\n );\n return {\n deletedRows,\n deletedThrough,\n moreEligible: this.#anyEligible(opts),\n };\n }\n\n /**\n * Removes one bounded chunk of the minimum transaction, which is the only\n * transaction that is ever torn.\n *\n * A single upstream transaction can be arbitrarily large — a backfill, a\n * schema migration — and SQLite cannot yield mid-statement, so the only way\n * to bound the lock hold is to delete it across several statements. That is\n * unobservable given three constraints, all of which live here:\n *\n * 1. Only `min(\"watermark\")` is torn, so at most one transaction is ever\n * partial. A reader never reads *into* a transaction — `read()` starts\n * strictly after `(fromWatermark, MAX_SAFE_INTEGER)` — so the only\n * transaction a tear can touch is one a subscriber reads *from*, and\n * reading from it does not read its rows.\n * 2. Rows go ascending by `pos`, commit row last. `plan()`'s boundary check\n * reads the *highest* `pos` at the watermark and requires it to be the\n * `commit`; removing that row first would give a subscriber sitting\n * exactly there a spurious `too-old`.\n * 3. The commit row is removed by a prefix delete, which advances\n * `min(\"watermark\")` in the same SQLite transaction, so no state exposes\n * the boundary as gone while the minimum still points at it. Normally that\n * is {@link SQLiteChangeLogPurger.purgeBatch}'s ordinary whole-transaction\n * delete, taken on the batch where what is left of the torn transaction\n * finally fits the budget; the branch below is the same delete for the\n * case where the minimum is not itself in the eligible set.\n *\n * A crash mid-tear needs no recovery state: the torn transaction is still the\n * minimum and still eligible, so the next batch continues it. Reconciliation\n * is unaffected — it tests the head, and a tear is at the tail.\n *\n * Tearing the minimum is safe even in the unusual case where it is not itself\n * in the eligible set (a writer whose clock went backwards can put a later\n * watermark at an earlier `writeTimeMs`). Both watermark bounds are\n * prefix-closed — a non-empty eligible set implies `min < externalFloor` and\n * `min < head` — and the prefix delete this batch would otherwise run removes\n * the minimum anyway. Only the time floor, which is a minimum-retention\n * courtesy rather than a safety property, can be crossed that way.\n */\n #tear(opts: PurgeBatchOptions, budget: number): PurgeBatchResult {\n const {minWatermark} = this.#stmts.minWatermark.get<{\n minWatermark: string | null;\n }>();\n assert(\n minWatermark !== null,\n 'change-log purge found eligible transactions in an empty log',\n );\n\n const boundary = this.#stmts.tearBoundary.get<{pos: number} | undefined>({\n watermark: minWatermark,\n offset: budget,\n });\n if (boundary === undefined) {\n // At most `budget` rows are left at the minimum, which is reachable only\n // when the minimum is not the transaction that failed to fit — i.e. when\n // it is not itself eligible. The prefix delete removes it whole and\n // advances `min(\"watermark\")`.\n const deletedRows = this.#timed(\n opts,\n budget,\n () => this.#stmts.deleteThrough.run(minWatermark).changes,\n );\n return {\n deletedRows,\n deletedThrough: minWatermark,\n moreEligible: this.#anyEligible(opts),\n };\n }\n // `boundary.pos` is the `budget`-th row by `pos`, so this removes exactly\n // `budget` rows and always makes progress. The commit row is the highest\n // `pos`, so it can only be reached by the branch above.\n const deletedRows = this.#timed(\n opts,\n budget,\n () =>\n this.#stmts.deleteBelowPos.run({\n watermark: minWatermark,\n pos: boundary.pos,\n }).changes,\n );\n return {deletedRows, deletedThrough: undefined, moreEligible: true};\n }\n\n #anyEligible({externalFloor, retentionCutoffMs}: PurgeBatchOptions): boolean {\n return (\n this.#stmts.eligibleCommits.get<{watermark: string} | undefined>({\n retentionCutoffMs,\n externalFloor,\n limit: 1,\n }) !== undefined\n );\n }\n\n #budget(maxRows: number): number {\n return Math.max(1, Math.min(maxRows, this.#budgetRows ?? maxRows));\n }\n\n /**\n * Runs a delete and feeds its measured duration back into the row budget.\n * Growth is gated on the budget having been the binding constraint: a batch\n * that finished quickly because only four rows were eligible is no evidence\n * that a larger statement would stay under the target.\n */\n #timed(\n {maxRows, maxDurationMs}: PurgeBatchOptions,\n budget: number,\n run: () => number,\n ): number {\n const start = this.#now();\n const deletedRows = run();\n const elapsedMs = this.#now() - start;\n\n if (elapsedMs > maxDurationMs) {\n this.#budgetRows = Math.max(1, Math.floor(budget / 2));\n } else if (deletedRows >= budget && elapsedMs * 2 <= maxDurationMs) {\n this.#budgetRows = Math.min(maxRows, budget * 2);\n } else {\n this.#budgetRows = budget;\n }\n return deletedRows;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqlite-change-log-purger.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/sqlite-change-log-purger.ts"],"sourcesContent":["/**\n * Deletes aged-out transactions from the change-log database, one bounded write\n * transaction at a time.\n *\n * The purger is a pure function of one database: it takes the floors its caller\n * has already gated on and narrows them, never widens them, and it reads no\n * replication state. That is what makes it testable without a scheduler and\n * what lets slice 9 run it from the change-streamer's stream loop, between the\n * writer's commits, on the writer's own connection.\n */\n\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {Database, Statement} from '../../../../zqlite/src/db.ts';\nimport {CHANGE_LOG_STREAM_TABLE} from './change-log-db.ts';\n\n/**\n * The eligible set, as three ANDed bounds. Only the `externalFloor` is a safety\n * property; the other two make the log serviceable and make purge conservative.\n *\n * `\"watermark\" < @externalFloor` is deliberately strict. A follower that lost\n * its replica restores it from S3 at watermark `W` and then asks this log for\n * everything after `W`, which requires the `commit` row *at* `W` to still be\n * here (`SQLITE_CHANGE_LOG_BOUNDARY_SQL`). Deleting it returns `too-old` to the\n * very subscriber the floor exists to protect.\n *\n * The head cap reads `max(\"watermark\")` from the log's own database, which\n * keeps the purger a pure function of one file. The cap is inert in normal\n * operation: the floor is derived from a backup of a replica that consumes this\n * loop's output, and the log commits before that output is forwarded, so\n * `externalFloor <= head` holds by construction. It engages only on a *stale*\n * log — writer stopped, mode flipped to `off`, another task took over — where\n * deleting to a floor that has run past a frozen log would empty the file. Note\n * that head can be a phantom — committed here but not yet re-deliverable by a\n * resumed stream — for as long as it takes the watermark the stream would\n * resume from to advance past it. That is harmless precisely because head is\n * never a purge candidate, and it is why the purger asserts nothing about\n * anything outside its own file.\n *\n * Ordered by `(\"writeTimeMs\", \"watermark\")` so it rides the partial index,\n * which carries one entry per transaction rather than one per change. The\n * `LIMIT` keeps a drain from re-scanning the remaining history on every batch,\n * which would be quadratic in the number of eligible transactions and would do\n * it inside the `BEGIN IMMEDIATE`, holding the write lock alongside the delete.\n *\n * Exported so the focused query-plan test covers the production query.\n */\nexport const SQLITE_CHANGE_LOG_ELIGIBLE_COMMITS_SQL = /*sql*/ `\n SELECT \"watermark\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"writeTimeMs\" IS NOT NULL\n AND \"writeTimeMs\" < @retentionCutoffMs\n AND \"watermark\" < @externalFloor\n AND \"watermark\" < (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n ORDER BY \"writeTimeMs\", \"watermark\"\n LIMIT @limit\n`;\n\n/**\n * Counts the rows at `@watermark`, capped at `@limit`. The cap is what keeps\n * batch sizing O(budget): this runs per candidate inside the\n * `BEGIN IMMEDIATE`, and the transaction being sized can be arbitrarily\n * large — an uncapped count of a multi-million-row backfill would rescan\n * every remaining row on every tear batch, holding the write lock (and the\n * appending path behind it) for the duration, quadratically over the drain.\n * The caller only needs to know whether the transaction fits its remaining\n * budget, so it passes `remaining + 1` and reads a count above `remaining`\n * as \"does not fit\".\n *\n * Exported so the focused tests cover the production query.\n */\nexport const SQLITE_CHANGE_LOG_COUNT_TRANSACTION_SQL = /*sql*/ `\n SELECT count(*) AS \"rows\" FROM (\n SELECT 1\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @watermark\n LIMIT @limit\n )\n`;\n\n/**\n * A prefix delete, so no transaction between the minimum and the head is ever\n * split. The `<=` is correct and is not the floor comparison: `deletedThrough`\n * is a watermark this batch already established as wholly eligible.\n */\nconst DELETE_THROUGH_SQL = /*sql*/ `\n DELETE FROM \"${CHANGE_LOG_STREAM_TABLE}\" WHERE \"watermark\" <= ?\n`;\n\nconst MIN_WATERMARK_SQL = /*sql*/ `\n SELECT min(\"watermark\") AS \"minWatermark\" FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n`;\n\n/**\n * The `pos` at `@offset` rows into the torn transaction, ascending. `undefined`\n * when fewer than `@offset` rows remain, which is how the tear knows it is on\n * its final chunk.\n */\nconst TEAR_BOUNDARY_SQL = /*sql*/ `\n SELECT \"pos\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @watermark\n ORDER BY \"pos\"\n LIMIT 1 OFFSET @offset\n`;\n\n/**\n * A range scan on the primary key. Expressed as `\"pos\" < @pos` rather than\n * `DELETE ... LIMIT n`, which is both non-portable and the wrong bound: it\n * carries no guarantee about *which* rows it removes, and the commit row must\n * survive until the chunk that finishes the tear.\n */\nconst DELETE_BELOW_POS_SQL = /*sql*/ `\n DELETE FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @watermark AND \"pos\" < @pos\n`;\n\nexport type PurgeBatchResult = {\n readonly deletedRows: number;\n /**\n * The watermark through which whole transactions were removed. `undefined`\n * when the batch only tore rows off the minimum transaction without\n * completing it — in that case `deletedRows > 0` and `moreEligible` is true.\n */\n readonly deletedThrough: string | undefined;\n /** True while a tear is in progress, as well as while whole transactions remain eligible. */\n readonly moreEligible: boolean;\n};\n\nexport type PurgeBatchOptions = {\n /**\n * The floor the caller has already gated on subscriber ACKs and snapshot\n * reservations — normally the confirmed S3 backup watermark. Transactions at\n * or above it are NOT durable anywhere else: the change log is excluded from\n * the litestream backup, so this is the boundary a follower restores to and\n * then catches up from. Compared strictly (`<`), so the transaction *at* the\n * floor survives as that follower's catchup boundary. The purger narrows this\n * input and never widens it.\n */\n readonly externalFloor: string;\n /**\n * A *minimum* retention, ANDed on top of `externalFloor` rather than an\n * alternative to it. Which of the two binds depends on the backup interval.\n */\n readonly retentionCutoffMs: number;\n /** Starting chunk size. Not the bound — `maxDurationMs` is. */\n readonly maxRows: number;\n /**\n * The statement duration target, enforced by feedback rather than by\n * interruption: SQLite cannot yield mid-statement, so the purger measures\n * each delete and shrinks the next batch's row budget when one overruns\n * (see {@link SQLiteChangeLogPurger.budgetRows}) — which means the first\n * delete after a restart, sized by `maxRows` alone, can still overrun it.\n * The target is this consequential because purge shares the writer's\n * connection: there is no lock to arbitrate and no `busy_timeout` to bound\n * the wait — a long statement blocks the appending path, and the forward\n * behind it, directly and for its full duration. That is why this is a\n * precondition of purging from the stream loop rather than a tuning knob.\n */\n readonly maxDurationMs: number;\n};\n\ntype Statements = {\n readonly eligibleCommits: Statement;\n readonly countTransaction: Statement;\n readonly deleteThrough: Statement;\n readonly minWatermark: Statement;\n readonly tearBoundary: Statement;\n readonly deleteBelowPos: Statement;\n};\n\nexport class SQLiteChangeLogPurger {\n readonly #db: Database;\n readonly #now: () => number;\n readonly #stmts: Statements;\n\n /**\n * The measured row budget, carried across batches. Row cost varies with\n * payload size, so `maxRows` alone cannot bound a statement's duration; this\n * shrinks when a delete overruns `maxDurationMs` and grows back when the\n * budget — rather than the eligible set — was what limited the batch.\n */\n #budgetRows: number | undefined;\n\n /**\n * @param db a writable connection to a change log the writer has already\n * created and reconciled — in slice 9, the writer's own connection.\n * Statements are prepared eagerly, so a caller that may run before the\n * log exists opens the connection lazily and treats construction failure\n * as a skipped cycle.\n */\n constructor(\n db: Database,\n now: () => number = performance.now.bind(performance),\n ) {\n this.#db = db;\n this.#now = now;\n this.#stmts = {\n eligibleCommits: db.prepare(SQLITE_CHANGE_LOG_ELIGIBLE_COMMITS_SQL),\n countTransaction: db.prepare(SQLITE_CHANGE_LOG_COUNT_TRANSACTION_SQL),\n deleteThrough: db.prepare(DELETE_THROUGH_SQL),\n minWatermark: db.prepare(MIN_WATERMARK_SQL),\n tearBoundary: db.prepare(TEAR_BOUNDARY_SQL),\n deleteBelowPos: db.prepare(DELETE_BELOW_POS_SQL),\n };\n }\n\n /** The row budget the next batch would use. Exposed for tests and metrics. */\n get budgetRows(): number | undefined {\n return this.#budgetRows;\n }\n\n /**\n * Removes one bounded batch of aged-out history, in one `BEGIN IMMEDIATE`\n * transaction. Callers loop while `moreEligible`, releasing whatever guards\n * they hold between batches.\n */\n purgeBatch(opts: PurgeBatchOptions): PurgeBatchResult {\n const {maxRows, maxDurationMs} = opts;\n assert(\n Number.isSafeInteger(maxRows) && maxRows > 0,\n 'change-log purge maxRows must be a positive safe integer',\n );\n assert(\n maxDurationMs > 0,\n 'change-log purge maxDurationMs must be positive',\n );\n\n this.#db.exec('BEGIN IMMEDIATE');\n try {\n const result = this.#purge(opts);\n this.#db.exec('COMMIT');\n return result;\n } catch (e) {\n if (this.#db.inTransaction) {\n this.#db.exec('ROLLBACK');\n }\n throw e;\n }\n }\n\n #purge(opts: PurgeBatchOptions): PurgeBatchResult {\n const {externalFloor, retentionCutoffMs} = opts;\n const budget = this.#budget(opts.maxRows);\n // Each transaction is at least a `begin` and a `commit`, so this is the\n // most candidates that can fit in the budget.\n const limit = Math.max(1, Math.ceil(budget / 2));\n const candidates = this.#stmts.eligibleCommits.all<{watermark: string}>({\n retentionCutoffMs,\n externalFloor,\n limit,\n });\n if (candidates.length === 0) {\n return {deletedRows: 0, deletedThrough: undefined, moreEligible: false};\n }\n\n // Accounted per candidate rather than per prefix, which is exact as long as\n // the candidates arrive in watermark order — they do, because the single\n // writer stamps `writeTimeMs` at commit time. A clock that went backwards\n // can put a non-candidate below `deletedThrough`, and the prefix delete\n // then removes more rows than were counted. Not a safety problem: both\n // watermark bounds are prefix-closed, only the minimum-retention courtesy\n // is crossed, and `maxRows` is the starting chunk size rather than the\n // bound — `maxDurationMs` is, and the governor measures what actually ran.\n let rows = 0;\n let deletedThrough: string | undefined;\n for (const {watermark} of candidates) {\n // The count is capped at one past the remaining budget: `n > remaining`\n // is exactly `rows + n > budget` for a fitting transaction, and a\n // too-large one stops scanning as soon as it is known not to fit.\n const remaining = budget - rows;\n const {rows: n} = this.#stmts.countTransaction.get<{rows: number}>({\n watermark,\n limit: remaining + 1,\n });\n if (n > remaining) {\n break;\n }\n rows += n;\n deletedThrough = watermark;\n }\n\n if (deletedThrough === undefined) {\n return this.#tear(opts, budget);\n }\n const deletedRows = this.#timed(\n opts,\n budget,\n () => this.#stmts.deleteThrough.run(deletedThrough).changes,\n );\n return {\n deletedRows,\n deletedThrough,\n moreEligible: this.#anyEligible(opts),\n };\n }\n\n /**\n * Removes one bounded chunk of the minimum transaction, which is the only\n * transaction that is ever torn.\n *\n * A single upstream transaction can be arbitrarily large — a backfill, a\n * schema migration — and SQLite cannot yield mid-statement, so the only way\n * to bound the lock hold is to delete it across several statements. That is\n * unobservable given three constraints, all of which live here:\n *\n * 1. Only `min(\"watermark\")` is torn, so at most one transaction is ever\n * partial. A reader never reads *into* a transaction — `read()` starts\n * strictly after `(fromWatermark, MAX_SAFE_INTEGER)` — so the only\n * transaction a tear can touch is one a subscriber reads *from*, and\n * reading from it does not read its rows.\n * 2. Rows go ascending by `pos`, commit row last. `plan()`'s boundary check\n * reads the *highest* `pos` at the watermark and requires it to be the\n * `commit`; removing that row first would give a subscriber sitting\n * exactly there a spurious `too-old`.\n * 3. The commit row is removed by a prefix delete, which advances\n * `min(\"watermark\")` in the same SQLite transaction, so no state exposes\n * the boundary as gone while the minimum still points at it. Normally that\n * is {@link SQLiteChangeLogPurger.purgeBatch}'s ordinary whole-transaction\n * delete, taken on the batch where what is left of the torn transaction\n * finally fits the budget; the branch below is the same delete for the\n * case where the minimum is not itself in the eligible set.\n *\n * A crash mid-tear needs no recovery state: the torn transaction is still the\n * minimum and still eligible, so the next batch continues it. Reconciliation\n * is unaffected — it tests the head, and a tear is at the tail.\n *\n * Tearing the minimum is safe even in the unusual case where it is not itself\n * in the eligible set (a writer whose clock went backwards can put a later\n * watermark at an earlier `writeTimeMs`). Both watermark bounds are\n * prefix-closed — a non-empty eligible set implies `min < externalFloor` and\n * `min < head` — and the prefix delete this batch would otherwise run removes\n * the minimum anyway. Only the time floor, which is a minimum-retention\n * courtesy rather than a safety property, can be crossed that way.\n */\n #tear(opts: PurgeBatchOptions, budget: number): PurgeBatchResult {\n const {minWatermark} = this.#stmts.minWatermark.get<{\n minWatermark: string | null;\n }>();\n assert(\n minWatermark !== null,\n 'change-log purge found eligible transactions in an empty log',\n );\n\n const boundary = this.#stmts.tearBoundary.get<{pos: number} | undefined>({\n watermark: minWatermark,\n offset: budget,\n });\n if (boundary === undefined) {\n // At most `budget` rows are left at the minimum, which is reachable only\n // when the minimum is not the transaction that failed to fit — i.e. when\n // it is not itself eligible. The prefix delete removes it whole and\n // advances `min(\"watermark\")`.\n const deletedRows = this.#timed(\n opts,\n budget,\n () => this.#stmts.deleteThrough.run(minWatermark).changes,\n );\n return {\n deletedRows,\n deletedThrough: minWatermark,\n moreEligible: this.#anyEligible(opts),\n };\n }\n // `boundary.pos` is the `budget`-th row by `pos`, so this removes exactly\n // `budget` rows and always makes progress. The commit row is the highest\n // `pos`, so it can only be reached by the branch above.\n const deletedRows = this.#timed(\n opts,\n budget,\n () =>\n this.#stmts.deleteBelowPos.run({\n watermark: minWatermark,\n pos: boundary.pos,\n }).changes,\n );\n return {deletedRows, deletedThrough: undefined, moreEligible: true};\n }\n\n #anyEligible({externalFloor, retentionCutoffMs}: PurgeBatchOptions): boolean {\n return (\n this.#stmts.eligibleCommits.get<{watermark: string} | undefined>({\n retentionCutoffMs,\n externalFloor,\n limit: 1,\n }) !== undefined\n );\n }\n\n #budget(maxRows: number): number {\n return Math.max(1, Math.min(maxRows, this.#budgetRows ?? maxRows));\n }\n\n /**\n * Runs a delete and feeds its measured duration back into the row budget.\n * Growth is gated on the budget having been the binding constraint: a batch\n * that finished quickly because only four rows were eligible is no evidence\n * that a larger statement would stay under the target.\n */\n #timed(\n {maxRows, maxDurationMs}: PurgeBatchOptions,\n budget: number,\n run: () => number,\n ): number {\n const start = this.#now();\n const deletedRows = run();\n const elapsedMs = this.#now() - start;\n\n if (elapsedMs > maxDurationMs) {\n this.#budgetRows = Math.max(1, Math.floor(budget / 2));\n } else if (deletedRows >= budget && elapsedMs * 2 <= maxDurationMs) {\n this.#budgetRows = Math.min(maxRows, budget * 2);\n } else {\n this.#budgetRows = budget;\n }\n return deletedRows;\n }\n}\n"],"mappings":"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"}
@@ -121,6 +121,6 @@ var ThreadWriteWorkerClient = class {
121
121
  }
122
122
  };
123
123
  //#endregion
124
- export { ThreadWriteWorkerClient, applyPragmas, serializeError };
124
+ export { ThreadWriteWorkerClient, applyPragmas, deserializeError, serializeError };
125
125
 
126
126
  //# sourceMappingURL=write-worker-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"write-worker-client.js","names":["#worker","#rejectAll","#errorHandler","#pending","#terminated","#call"],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker-client.ts"],"sourcesContent":["import {Worker} from 'node:worker_threads';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {WRITE_WORKER_URL} from '../../server/worker-urls.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {BackfillRequest} from '../change-source/protocol/current/upstream.ts';\nimport type {ChangeProcessorMode, CommitResult} from './change-processor.ts';\nimport type {SubscriptionState} from './schema/replication-state.ts';\n\nexport type PragmaConfig = {\n busyTimeout: number;\n analysisLimit: number;\n walAutocheckpoint?: number | undefined;\n};\n\n/**\n * Enables write-path checkpoint backpressure in the write worker. At each\n * commit boundary, if the WAL has grown to at least `checkpointThresholdPages`,\n * the worker forces an immediate litestream sync (sealing the WAL to LTX and\n * running litestream's checkpoint).\n *\n * The optional `walMaxPages` is an emergency break that pauses until the WAL drains,\n * rather than letting it exceed available disk space. This replaces litestream's\n * analogous `truncate-page-n` emergency break, which is more disruptive because of\n * https://github.com/benbjohnson/litestream/issues/1332. Note, however, this should\n * never been needed if non-initial snapshots are disabled (which is the default).\n *\n * Set only for the backup replicator when replicating with litestream v5; `null`\n * disables the feature entirely (no per-commit overhead).\n */\nexport type ForceCheckpointConfig = {\n checkpointThresholdPages: number;\n maxWalPages?: number | undefined;\n};\n\ntype ErrorHandler = (err: Error) => void;\n\n/**\n * Interface for a write worker that processes replication messages.\n */\n/**\n * The replica's {@link SubscriptionState}, along with its pending backfills,\n * read in the same snapshot.\n */\nexport type SubscriptionStateWithBackfills = SubscriptionState & {\n backfills: BackfillRequest[];\n};\n\nexport interface WriteWorkerClient {\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills>;\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null>;\n abort(): void;\n stop(): Promise<void>;\n onError(handler: ErrorHandler): void;\n}\n\nexport type SerializedError = {\n name: string;\n message: string;\n stack?: string | undefined;\n cause?: SerializedError | string | undefined;\n details?: Record<string, unknown> | undefined;\n};\n\nexport function serializeError(err: unknown): SerializedError {\n if (!(err instanceof Error)) {\n return {\n name: 'Error',\n message: String(err),\n details: err && typeof err === 'object' ? {...err} : undefined,\n };\n }\n\n // Error fields such as message, stack, and some native error details are\n // non-enumerable, so JSON.stringify(err) would usually return \"{}\".\n const details = Object.fromEntries(\n Object.getOwnPropertyNames(err)\n .filter(key => !['name', 'message', 'stack', 'cause'].includes(key))\n .map(key => [key, (err as unknown as Record<string, unknown>)[key]]),\n );\n const cause =\n err.cause instanceof Error\n ? serializeError(err.cause)\n : err.cause === undefined\n ? undefined\n : String(err.cause);\n\n return {\n name: err.name,\n message: err.message,\n stack: err.stack,\n cause,\n details: Object.keys(details).length ? details : undefined,\n };\n}\n\nexport function deserializeError(serialized: SerializedError): Error {\n const err = new Error(serialized.message);\n err.name = serialized.name;\n if (serialized.stack !== undefined) {\n err.stack = serialized.stack;\n }\n if (serialized.cause !== undefined) {\n err.cause =\n typeof serialized.cause === 'string'\n ? serialized.cause\n : deserializeError(serialized.cause);\n }\n if (serialized.details) {\n Object.assign(err, serialized.details);\n }\n return err;\n}\n\n// Wire protocol types.\nexport type ArgsMap = {\n init: [\n string,\n ChangeProcessorMode,\n PragmaConfig,\n LogConfig,\n ForceCheckpointConfig | null,\n ];\n getSubscriptionState: [];\n processMessages: [readonly ChangeStreamData[]];\n abort: [];\n stop: [];\n};\n\nexport type Method = keyof ArgsMap;\n\nexport type Request<M extends Method = Method> = {method: M; args: ArgsMap[M]};\n\nexport type ResultMap = {\n init: void;\n getSubscriptionState: SubscriptionStateWithBackfills;\n processMessages: CommitResult | null;\n abort: void;\n stop: void;\n};\n\nexport type Response<M extends Method = Method> =\n | {method: M; result: ResultMap[M]; error?: undefined}\n | {method: M; error: SerializedError; result?: undefined};\n\nexport type WriteError = {writeError: SerializedError};\n\nexport function applyPragmas(db: Database, pragmas: PragmaConfig) {\n db.pragma(`busy_timeout = ${pragmas.busyTimeout}`);\n db.pragma(`analysis_limit = ${pragmas.analysisLimit}`);\n if (pragmas.walAutocheckpoint !== undefined) {\n db.pragma(`wal_autocheckpoint = ${pragmas.walAutocheckpoint}`);\n }\n}\n\n/**\n * Delegates SQLite writes to a worker_thread,\n * keeping the main event loop free for WebSocket heartbeats and IPC.\n */\nexport class ThreadWriteWorkerClient implements WriteWorkerClient {\n readonly #worker: Worker;\n #pending: Resolver<unknown, Error> | null = null;\n #errorHandler: ErrorHandler = () => {};\n #terminated = false;\n\n constructor() {\n this.#worker = new Worker(WRITE_WORKER_URL);\n\n this.#worker.on('message', (msg: Response | WriteError) => {\n if ('writeError' in msg) {\n const error = deserializeError(msg.writeError);\n this.#rejectAll(error);\n this.#errorHandler(error);\n return;\n }\n const r = this.#pending;\n if (!r) return; // stale abort response\n this.#pending = null;\n if (msg.error !== undefined) {\n r.reject(deserializeError(msg.error));\n } else {\n r.resolve(msg.result);\n }\n });\n\n this.#worker.on('error', (err: Error) => {\n this.#rejectAll(err);\n this.#errorHandler(err);\n });\n\n this.#worker.on('exit', (code: number) => {\n this.#terminated = true;\n if (code !== 0) {\n const err = new Error(`Worker exited with code ${code}`);\n this.#rejectAll(err);\n this.#errorHandler(err);\n }\n });\n }\n\n #rejectAll(err: Error) {\n const r = this.#pending;\n if (r) {\n this.#pending = null;\n r.reject(err);\n }\n }\n\n #call<M extends Method>(method: M, args: ArgsMap[M]): Promise<ResultMap[M]> {\n assert(this.#pending === null, `concurrent call: ${method}`);\n const r = resolver<ResultMap[M]>();\n this.#pending = r as Resolver<unknown, Error>;\n this.#worker.postMessage({method, args} satisfies Request);\n return r.promise;\n }\n\n init(\n dbPath: string,\n mode: ChangeProcessorMode,\n pragmas: PragmaConfig,\n logConfig: LogConfig,\n checkpoint: ForceCheckpointConfig | null = null,\n ): Promise<void> {\n return this.#call('init', [dbPath, mode, pragmas, logConfig, checkpoint]);\n }\n\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills> {\n return this.#call('getSubscriptionState', []);\n }\n\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null> {\n return this.#call('processMessages', [downstream]);\n }\n\n abort(): void {\n if (!this.#terminated) {\n this.#worker.postMessage({method: 'abort', args: []} satisfies Request);\n }\n }\n\n async stop(): Promise<void> {\n await this.#call('stop', []);\n if (!this.#terminated) {\n await this.#worker.terminate();\n }\n }\n\n onError(handler: ErrorHandler): void {\n this.#errorHandler = handler;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"write-worker-client.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker-client.ts"],"sourcesContent":["import {Worker} from 'node:worker_threads';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport type {Database} from '../../../../zqlite/src/db.ts';\nimport {WRITE_WORKER_URL} from '../../server/worker-urls.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {BackfillRequest} from '../change-source/protocol/current/upstream.ts';\nimport type {ChangeProcessorMode, CommitResult} from './change-processor.ts';\nimport type {SubscriptionState} from './schema/replication-state.ts';\n\nexport type PragmaConfig = {\n busyTimeout: number;\n analysisLimit: number;\n walAutocheckpoint?: number | undefined;\n};\n\n/**\n * Enables write-path checkpoint backpressure in the write worker. At each\n * commit boundary, if the WAL has grown to at least `checkpointThresholdPages`,\n * the worker forces an immediate litestream sync (sealing the WAL to LTX and\n * running litestream's checkpoint).\n *\n * The optional `walMaxPages` is an emergency break that pauses until the WAL drains,\n * rather than letting it exceed available disk space. This replaces litestream's\n * analogous `truncate-page-n` emergency break, which is more disruptive because of\n * https://github.com/benbjohnson/litestream/issues/1332. Note, however, this should\n * never been needed if non-initial snapshots are disabled (which is the default).\n *\n * Set only for the backup replicator when replicating with litestream v5; `null`\n * disables the feature entirely (no per-commit overhead).\n */\nexport type ForceCheckpointConfig = {\n checkpointThresholdPages: number;\n maxWalPages?: number | undefined;\n};\n\ntype ErrorHandler = (err: Error) => void;\n\n/**\n * Interface for a write worker that processes replication messages.\n */\n/**\n * The replica's {@link SubscriptionState}, along with its pending backfills,\n * read in the same snapshot.\n */\nexport type SubscriptionStateWithBackfills = SubscriptionState & {\n backfills: BackfillRequest[];\n};\n\nexport interface WriteWorkerClient {\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills>;\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null>;\n abort(): void;\n stop(): Promise<void>;\n onError(handler: ErrorHandler): void;\n}\n\nexport type SerializedError = {\n name: string;\n message: string;\n stack?: string | undefined;\n cause?: SerializedError | string | undefined;\n details?: Record<string, unknown> | undefined;\n};\n\nexport function serializeError(err: unknown): SerializedError {\n if (!(err instanceof Error)) {\n return {\n name: 'Error',\n message: String(err),\n details: err && typeof err === 'object' ? {...err} : undefined,\n };\n }\n\n // Error fields such as message, stack, and some native error details are\n // non-enumerable, so JSON.stringify(err) would usually return \"{}\".\n const details = Object.fromEntries(\n Object.getOwnPropertyNames(err)\n .filter(key => !['name', 'message', 'stack', 'cause'].includes(key))\n .map(key => [key, (err as unknown as Record<string, unknown>)[key]]),\n );\n const cause =\n err.cause instanceof Error\n ? serializeError(err.cause)\n : err.cause === undefined\n ? undefined\n : String(err.cause);\n\n return {\n name: err.name,\n message: err.message,\n stack: err.stack,\n cause,\n details: Object.keys(details).length ? details : undefined,\n };\n}\n\nexport function deserializeError(serialized: SerializedError): Error {\n const err = new Error(serialized.message);\n err.name = serialized.name;\n if (serialized.stack !== undefined) {\n err.stack = serialized.stack;\n }\n if (serialized.cause !== undefined) {\n err.cause =\n typeof serialized.cause === 'string'\n ? serialized.cause\n : deserializeError(serialized.cause);\n }\n if (serialized.details) {\n Object.assign(err, serialized.details);\n }\n return err;\n}\n\n// Wire protocol types.\nexport type ArgsMap = {\n init: [\n string,\n ChangeProcessorMode,\n PragmaConfig,\n LogConfig,\n ForceCheckpointConfig | null,\n ];\n getSubscriptionState: [];\n processMessages: [readonly ChangeStreamData[]];\n abort: [];\n stop: [];\n};\n\nexport type Method = keyof ArgsMap;\n\nexport type Request<M extends Method = Method> = {method: M; args: ArgsMap[M]};\n\nexport type ResultMap = {\n init: void;\n getSubscriptionState: SubscriptionStateWithBackfills;\n processMessages: CommitResult | null;\n abort: void;\n stop: void;\n};\n\nexport type Response<M extends Method = Method> =\n | {method: M; result: ResultMap[M]; error?: undefined}\n | {method: M; error: SerializedError; result?: undefined};\n\nexport type WriteError = {writeError: SerializedError};\n\nexport function applyPragmas(db: Database, pragmas: PragmaConfig) {\n db.pragma(`busy_timeout = ${pragmas.busyTimeout}`);\n db.pragma(`analysis_limit = ${pragmas.analysisLimit}`);\n if (pragmas.walAutocheckpoint !== undefined) {\n db.pragma(`wal_autocheckpoint = ${pragmas.walAutocheckpoint}`);\n }\n}\n\n/**\n * Delegates SQLite writes to a worker_thread,\n * keeping the main event loop free for WebSocket heartbeats and IPC.\n */\nexport class ThreadWriteWorkerClient implements WriteWorkerClient {\n readonly #worker: Worker;\n #pending: Resolver<unknown, Error> | null = null;\n #errorHandler: ErrorHandler = () => {};\n #terminated = false;\n\n constructor() {\n this.#worker = new Worker(WRITE_WORKER_URL);\n\n this.#worker.on('message', (msg: Response | WriteError) => {\n if ('writeError' in msg) {\n const error = deserializeError(msg.writeError);\n this.#rejectAll(error);\n this.#errorHandler(error);\n return;\n }\n const r = this.#pending;\n if (!r) return; // stale abort response\n this.#pending = null;\n if (msg.error !== undefined) {\n r.reject(deserializeError(msg.error));\n } else {\n r.resolve(msg.result);\n }\n });\n\n this.#worker.on('error', (err: Error) => {\n this.#rejectAll(err);\n this.#errorHandler(err);\n });\n\n this.#worker.on('exit', (code: number) => {\n this.#terminated = true;\n if (code !== 0) {\n const err = new Error(`Worker exited with code ${code}`);\n this.#rejectAll(err);\n this.#errorHandler(err);\n }\n });\n }\n\n #rejectAll(err: Error) {\n const r = this.#pending;\n if (r) {\n this.#pending = null;\n r.reject(err);\n }\n }\n\n #call<M extends Method>(method: M, args: ArgsMap[M]): Promise<ResultMap[M]> {\n assert(this.#pending === null, `concurrent call: ${method}`);\n const r = resolver<ResultMap[M]>();\n this.#pending = r as Resolver<unknown, Error>;\n this.#worker.postMessage({method, args} satisfies Request);\n return r.promise;\n }\n\n init(\n dbPath: string,\n mode: ChangeProcessorMode,\n pragmas: PragmaConfig,\n logConfig: LogConfig,\n checkpoint: ForceCheckpointConfig | null = null,\n ): Promise<void> {\n return this.#call('init', [dbPath, mode, pragmas, logConfig, checkpoint]);\n }\n\n getSubscriptionState(): Promise<SubscriptionStateWithBackfills> {\n return this.#call('getSubscriptionState', []);\n }\n\n processMessages(\n downstream: readonly ChangeStreamData[],\n ): Promise<CommitResult | null> {\n return this.#call('processMessages', [downstream]);\n }\n\n abort(): void {\n if (!this.#terminated) {\n this.#worker.postMessage({method: 'abort', args: []} satisfies Request);\n }\n }\n\n async stop(): Promise<void> {\n await this.#call('stop', []);\n if (!this.#terminated) {\n await this.#worker.terminate();\n }\n }\n\n onError(handler: ErrorHandler): void {\n this.#errorHandler = handler;\n }\n}\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { assert } from "../../../../shared/src/asserts.js";
2
2
  import { must } from "../../../../shared/src/must.js";
3
3
  import { Database } from "../../../../zqlite/src/db.js";
4
- import { readReplicaBackfills } from "./schema/backfilling.js";
5
- import { getSubscriptionState } from "./schema/replication-state.js";
6
- import { StatementRunner } from "../../db/statements.js";
7
4
  import { isSQLiteCorruption, logSQLiteCorruptionDiagnostics, registerSQLiteCorruptionDiagnosticTarget } from "../../db/sqlite-corruption.js";
8
5
  import { createLogContext } from "../../server/logging.js";
6
+ import { StatementRunner } from "../../db/statements.js";
7
+ import { readReplicaBackfills } from "./schema/backfilling.js";
8
+ import { getSubscriptionState } from "./schema/replication-state.js";
9
9
  import { deleteLiteDB } from "../../db/delete-lite-db.js";
10
10
  import { ChangeProcessor } from "./change-processor.js";
11
11
  import { LitestreamController } from "../litestream/litestream-controller.js";
@@ -1 +1 @@
1
- {"version":3,"file":"write-worker.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker.ts"],"sourcesContent":["import {parentPort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n registerSQLiteCorruptionDiagnosticTarget,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {createLogContext} from '../../server/logging.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {LitestreamCheckpointer} from '../litestream/litestream-checkpointer.ts';\nimport {LitestreamController} from '../litestream/litestream-controller.ts';\nimport {ChangeProcessor, type ChangeProcessorMode} from './change-processor.ts';\nimport {readReplicaBackfills} from './schema/backfilling.ts';\nimport {getSubscriptionState} from './schema/replication-state.ts';\nimport {\n applyPragmas,\n serializeError,\n type ArgsMap,\n type ForceCheckpointConfig,\n type Method,\n type PragmaConfig,\n type Request,\n type Response,\n type ResultMap,\n type WriteError,\n} from './write-worker-client.ts';\n\nif (!parentPort) {\n throw new Error('write-worker must be run as a worker thread');\n}\n\nconst port = parentPort;\n\ntype API = {\n [M in Method]: (...args: ArgsMap[M]) => ResultMap[M] | Promise<ResultMap[M]>;\n};\n\nfunction createAPI(): API {\n let db: Database | undefined;\n let runner: StatementRunner | undefined;\n let processor: ChangeProcessor | undefined;\n let mode: ChangeProcessorMode | undefined;\n let lc: LogContext | undefined;\n let replicaDbPath: string | undefined;\n let unregisterCorruptionDiagnosticTargets: (() => void)[] = [];\n\n // Set when write-path checkpoint backpressure is enabled (backup replicator\n // on litestream v5). `undefined` disables the feature.\n let checkpointerConfig: ForceCheckpointConfig | undefined;\n let checkpointer: LitestreamCheckpointer | undefined;\n\n function unregisterCorruptionDiagnostics() {\n unregisterCorruptionDiagnosticTargets.forEach(unregister => unregister());\n unregisterCorruptionDiagnosticTargets = [];\n }\n\n function handleCorruptedDb(err: unknown) {\n if (!lc || !replicaDbPath || !isSQLiteCorruption(err)) {\n return;\n }\n logSQLiteCorruptionDiagnostics(lc, 'write-worker', replicaDbPath, err);\n try {\n lc.warn?.(`deleting corrupted db at ${replicaDbPath}`);\n deleteLiteDB(replicaDbPath);\n } catch (e) {\n lc.warn?.(`error deleting corrupted db at ${replicaDbPath}`, e);\n }\n }\n\n function createProcessor() {\n processor = new ChangeProcessor(must(runner), must(mode), (_lc, err) => {\n handleCorruptedDb(err);\n port.postMessage({\n writeError: serializeError(err),\n } satisfies WriteError);\n });\n }\n\n function createCheckpointer() {\n if (checkpointerConfig) {\n assert(lc && db && replicaDbPath, `not initialized`);\n checkpointer = new LitestreamCheckpointer(\n lc,\n db,\n new LitestreamController(lc, replicaDbPath),\n checkpointerConfig,\n );\n }\n }\n\n return {\n init(\n dbPath: string,\n cpMode: ChangeProcessorMode,\n pragmas: PragmaConfig,\n logConfig: LogConfig,\n checkpointConfig: ForceCheckpointConfig | null,\n ): void {\n replicaDbPath = dbPath;\n lc = createLogContext({log: logConfig}, 'write-worker');\n unregisterCorruptionDiagnostics();\n unregisterCorruptionDiagnosticTargets.push(\n registerSQLiteCorruptionDiagnosticTarget({\n debugName: 'write-worker',\n dbPath,\n }),\n );\n try {\n db = new Database(lc, dbPath);\n applyPragmas(db, pragmas);\n runner = new StatementRunner(db);\n mode = cpMode;\n checkpointerConfig = checkpointConfig ?? undefined;\n createProcessor();\n createCheckpointer();\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n getSubscriptionState() {\n try {\n // The write worker is the replica's only writer, so these reads are\n // of the same snapshot.\n return {\n ...getSubscriptionState(must(runner)),\n backfills: readReplicaBackfills(must(runner).db),\n };\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n async processMessages(downstream: readonly ChangeStreamData[]) {\n try {\n let commitResult = null;\n for (const message of downstream) {\n const committed = must(processor).processMessage(must(lc), message);\n if (committed) {\n commitResult = committed;\n if (checkpointer) {\n await checkpointer.maybeCheckpoint();\n }\n }\n }\n return commitResult;\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n abort() {\n must(processor).abort(must(lc));\n checkpointer?.close();\n createProcessor();\n createCheckpointer();\n },\n\n stop() {\n checkpointer?.close();\n checkpointer = undefined;\n db?.close();\n db = undefined;\n runner = undefined;\n processor = undefined;\n replicaDbPath = undefined;\n unregisterCorruptionDiagnostics();\n },\n };\n}\n\nconst api = createAPI();\n\nport.on('message', async (msg: Request) => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TS can't narrow msg.method + msg.args together\n const result = await (api[msg.method] as (...args: any[]) => unknown)(\n ...msg.args,\n );\n // abort is fire-and-forget — no pending slot on the client side.\n if (msg.method !== 'abort') {\n port.postMessage({method: msg.method, result} as Response);\n }\n } catch (e) {\n if (msg.method !== 'abort') {\n port.postMessage({\n method: msg.method,\n error: serializeError(e),\n } as Response);\n }\n }\n});\n"],"mappings":";;;;;;;;;;;;;;;AAiCA,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,6CAA6C;AAG/D,IAAM,OAAO;AAMb,SAAS,YAAiB;CACxB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI,wCAAwD,CAAC;CAI7D,IAAI;CACJ,IAAI;CAEJ,SAAS,kCAAkC;EACzC,sCAAsC,SAAQ,eAAc,WAAW,CAAC;EACxE,wCAAwC,CAAC;CAC3C;CAEA,SAAS,kBAAkB,KAAc;EACvC,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,mBAAmB,GAAG,GAClD;EAEF,+BAA+B,IAAI,gBAAgB,eAAe,GAAG;EACrE,IAAI;GACF,GAAG,OAAO,4BAA4B,eAAe;GACrD,aAAa,aAAa;EAC5B,SAAS,GAAG;GACV,GAAG,OAAO,kCAAkC,iBAAiB,CAAC;EAChE;CACF;CAEA,SAAS,kBAAkB;EACzB,YAAY,IAAI,gBAAgB,KAAK,MAAM,GAAG,KAAK,IAAI,IAAI,KAAK,QAAQ;GACtE,kBAAkB,GAAG;GACrB,KAAK,YAAY,EACf,YAAY,eAAe,GAAG,EAChC,CAAsB;EACxB,CAAC;CACH;CAEA,SAAS,qBAAqB;EAC5B,IAAI,oBAAoB;GACtB,OAAO,MAAM,MAAM,eAAe,iBAAiB;GACnD,eAAe,IAAI,uBACjB,IACA,IACA,IAAI,qBAAqB,IAAI,aAAa,GAC1C,kBACF;EACF;CACF;CAEA,OAAO;EACL,KACE,QACA,QACA,SACA,WACA,kBACM;GACN,gBAAgB;GAChB,KAAK,iBAAiB,EAAC,KAAK,UAAS,GAAG,cAAc;GACtD,gCAAgC;GAChC,sCAAsC,KACpC,yCAAyC;IACvC,WAAW;IACX;GACF,CAAC,CACH;GACA,IAAI;IACF,KAAK,IAAI,SAAS,IAAI,MAAM;IAC5B,aAAa,IAAI,OAAO;IACxB,SAAS,IAAI,gBAAgB,EAAE;IAC/B,OAAO;IACP,qBAAqB,oBAAoB,KAAA;IACzC,gBAAgB;IAChB,mBAAmB;GACrB,SAAS,GAAG;IACV,kBAAkB,CAAC;IACnB,MAAM;GACR;EACF;EAEA,uBAAuB;GACrB,IAAI;IAGF,OAAO;KACL,GAAG,qBAAqB,KAAK,MAAM,CAAC;KACpC,WAAW,qBAAqB,KAAK,MAAM,EAAE,EAAE;IACjD;GACF,SAAS,GAAG;IACV,kBAAkB,CAAC;IACnB,MAAM;GACR;EACF;EAEA,MAAM,gBAAgB,YAAyC;GAC7D,IAAI;IACF,IAAI,eAAe;IACnB,KAAK,MAAM,WAAW,YAAY;KAChC,MAAM,YAAY,KAAK,SAAS,EAAE,eAAe,KAAK,EAAE,GAAG,OAAO;KAClE,IAAI,WAAW;MACb,eAAe;MACf,IAAI,cACF,MAAM,aAAa,gBAAgB;KAEvC;IACF;IACA,OAAO;GACT,SAAS,GAAG;IACV,kBAAkB,CAAC;IACnB,MAAM;GACR;EACF;EAEA,QAAQ;GACN,KAAK,SAAS,EAAE,MAAM,KAAK,EAAE,CAAC;GAC9B,cAAc,MAAM;GACpB,gBAAgB;GAChB,mBAAmB;EACrB;EAEA,OAAO;GACL,cAAc,MAAM;GACpB,eAAe,KAAA;GACf,IAAI,MAAM;GACV,KAAK,KAAA;GACL,SAAS,KAAA;GACT,YAAY,KAAA;GACZ,gBAAgB,KAAA;GAChB,gCAAgC;EAClC;CACF;AACF;AAEA,IAAM,MAAM,UAAU;AAEtB,KAAK,GAAG,WAAW,OAAO,QAAiB;CACzC,IAAI;EAEF,MAAM,SAAS,MAAO,IAAI,IAAI,QAC5B,GAAG,IAAI,IACT;EAEA,IAAI,IAAI,WAAW,SACjB,KAAK,YAAY;GAAC,QAAQ,IAAI;GAAQ;EAAM,CAAa;CAE7D,SAAS,GAAG;EACV,IAAI,IAAI,WAAW,SACjB,KAAK,YAAY;GACf,QAAQ,IAAI;GACZ,OAAO,eAAe,CAAC;EACzB,CAAa;CAEjB;AACF,CAAC"}
1
+ {"version":3,"file":"write-worker.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker.ts"],"sourcesContent":["import {parentPort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n registerSQLiteCorruptionDiagnosticTarget,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {createLogContext} from '../../server/logging.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {LitestreamCheckpointer} from '../litestream/litestream-checkpointer.ts';\nimport {LitestreamController} from '../litestream/litestream-controller.ts';\nimport {ChangeProcessor, type ChangeProcessorMode} from './change-processor.ts';\nimport {readReplicaBackfills} from './schema/backfilling.ts';\nimport {getSubscriptionState} from './schema/replication-state.ts';\nimport {\n applyPragmas,\n serializeError,\n type ArgsMap,\n type ForceCheckpointConfig,\n type Method,\n type PragmaConfig,\n type Request,\n type Response,\n type ResultMap,\n type WriteError,\n} from './write-worker-client.ts';\n\nif (!parentPort) {\n throw new Error('write-worker must be run as a worker thread');\n}\n\nconst port = parentPort;\n\ntype API = {\n [M in Method]: (...args: ArgsMap[M]) => ResultMap[M] | Promise<ResultMap[M]>;\n};\n\nfunction createAPI(): API {\n let db: Database | undefined;\n let runner: StatementRunner | undefined;\n let processor: ChangeProcessor | undefined;\n let mode: ChangeProcessorMode | undefined;\n let lc: LogContext | undefined;\n let replicaDbPath: string | undefined;\n let unregisterCorruptionDiagnosticTargets: (() => void)[] = [];\n\n // Set when write-path checkpoint backpressure is enabled (backup replicator\n // on litestream v5). `undefined` disables the feature.\n let checkpointerConfig: ForceCheckpointConfig | undefined;\n let checkpointer: LitestreamCheckpointer | undefined;\n\n function unregisterCorruptionDiagnostics() {\n unregisterCorruptionDiagnosticTargets.forEach(unregister => unregister());\n unregisterCorruptionDiagnosticTargets = [];\n }\n\n function handleCorruptedDb(err: unknown) {\n if (!lc || !replicaDbPath || !isSQLiteCorruption(err)) {\n return;\n }\n logSQLiteCorruptionDiagnostics(lc, 'write-worker', replicaDbPath, err);\n try {\n lc.warn?.(`deleting corrupted db at ${replicaDbPath}`);\n deleteLiteDB(replicaDbPath);\n } catch (e) {\n lc.warn?.(`error deleting corrupted db at ${replicaDbPath}`, e);\n }\n }\n\n function createProcessor() {\n processor = new ChangeProcessor(must(runner), must(mode), (_lc, err) => {\n handleCorruptedDb(err);\n port.postMessage({\n writeError: serializeError(err),\n } satisfies WriteError);\n });\n }\n\n function createCheckpointer() {\n if (checkpointerConfig) {\n assert(lc && db && replicaDbPath, `not initialized`);\n checkpointer = new LitestreamCheckpointer(\n lc,\n db,\n new LitestreamController(lc, replicaDbPath),\n checkpointerConfig,\n );\n }\n }\n\n return {\n init(\n dbPath: string,\n cpMode: ChangeProcessorMode,\n pragmas: PragmaConfig,\n logConfig: LogConfig,\n checkpointConfig: ForceCheckpointConfig | null,\n ): void {\n replicaDbPath = dbPath;\n lc = createLogContext({log: logConfig}, 'write-worker');\n unregisterCorruptionDiagnostics();\n unregisterCorruptionDiagnosticTargets.push(\n registerSQLiteCorruptionDiagnosticTarget({\n debugName: 'write-worker',\n dbPath,\n }),\n );\n try {\n db = new Database(lc, dbPath);\n applyPragmas(db, pragmas);\n runner = new StatementRunner(db);\n mode = cpMode;\n checkpointerConfig = checkpointConfig ?? undefined;\n createProcessor();\n createCheckpointer();\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n getSubscriptionState() {\n try {\n // The write worker is the replica's only writer, so these reads are\n // of the same snapshot.\n return {\n ...getSubscriptionState(must(runner)),\n backfills: readReplicaBackfills(must(runner).db),\n };\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n async processMessages(downstream: readonly ChangeStreamData[]) {\n try {\n let commitResult = null;\n for (const message of downstream) {\n const committed = must(processor).processMessage(must(lc), message);\n if (committed) {\n commitResult = committed;\n if (checkpointer) {\n await checkpointer.maybeCheckpoint();\n }\n }\n }\n return commitResult;\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n abort() {\n must(processor).abort(must(lc));\n checkpointer?.close();\n createProcessor();\n createCheckpointer();\n },\n\n stop() {\n checkpointer?.close();\n checkpointer = undefined;\n db?.close();\n db = undefined;\n runner = undefined;\n processor = undefined;\n replicaDbPath = undefined;\n unregisterCorruptionDiagnostics();\n },\n };\n}\n\nconst api = createAPI();\n\nport.on('message', async (msg: Request) => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TS can't narrow msg.method + msg.args together\n const result = await (api[msg.method] as (...args: any[]) => unknown)(\n ...msg.args,\n );\n // abort is fire-and-forget — no pending slot on the client side.\n if (msg.method !== 'abort') {\n port.postMessage({method: msg.method, result} as Response);\n }\n } catch (e) {\n if (msg.method !== 'abort') {\n port.postMessage({\n method: msg.method,\n error: serializeError(e),\n } as Response);\n }\n }\n});\n"],"mappings":"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"}
@@ -6,9 +6,9 @@ import { hashOfAST } from "../../../zero-protocol/src/query-hash.js";
6
6
  import { buildPipeline } from "../../../zql/src/builder/builder.js";
7
7
  import { astToZQL } from "../../../ast-to-zql/src/ast-to-zql.js";
8
8
  import { formatOutput } from "../../../ast-to-zql/src/format.js";
9
+ import { transformAndHashQuery } from "../auth/read-authorizer.js";
9
10
  import { computeZqlSpecs } from "../db/lite-tables.js";
10
11
  import { resolveSimpleScalarSubqueries } from "../../../zqlite/src/resolve-scalar-subqueries.js";
11
- import { transformAndHashQuery } from "../auth/read-authorizer.js";
12
12
  import { fetchScalarValue, hydrate } from "./view-syncer/pipeline-driver.js";
13
13
  //#region ../zero-cache/src/services/run-ast.ts
14
14
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"run-ast.js","names":[],"sources":["../../../../../zero-cache/src/services/run-ast.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\n// @circular-dep-ignore\nimport {astToZQL} from '../../../ast-to-zql/src/ast-to-zql.ts';\n// @circular-dep-ignore\nimport {formatOutput} from '../../../ast-to-zql/src/format.ts';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport type {AnalyzeQueryResult} from '../../../zero-protocol/src/analyze-query-result.ts';\nimport type {AST, LiteralValue} from '../../../zero-protocol/src/ast.ts';\nimport {mapAST} from '../../../zero-protocol/src/ast.ts';\nimport type {ClientSchema} from '../../../zero-protocol/src/client-schema.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport type {PermissionsConfig} from '../../../zero-schema/src/compiled-permissions.ts';\nimport type {NameMapper} from '../../../zero-schema/src/name-mapper.ts';\nimport {\n buildPipeline,\n type BuilderDelegate,\n} from '../../../zql/src/builder/builder.ts';\nimport {ChangeType} from '../../../zql/src/ivm/change-type.ts';\nimport type {Input} from '../../../zql/src/ivm/operator.ts';\nimport type {ConnectionCostModel} from '../../../zql/src/planner/planner-connection.ts';\nimport type {PlanDebugger} from '../../../zql/src/planner/planner-debug.ts';\nimport type {Database} from '../../../zqlite/src/db.ts';\nimport {resolveSimpleScalarSubqueries} from '../../../zqlite/src/resolve-scalar-subqueries.ts';\nimport type {JWTAuth} from '../auth/auth.ts';\nimport {transformAndHashQuery} from '../auth/read-authorizer.ts';\nimport {computeZqlSpecs} from '../db/lite-tables.ts';\nimport type {LiteAndZqlSpec} from '../db/specs.ts';\nimport {\n fetchScalarValue,\n hydrate,\n type RowChange,\n} from './view-syncer/pipeline-driver.ts';\n\n/**\n * Maximum number of synced rows returned per table, and read rows returned\n * per table/query, when analyzing a query in zero-cache. Rows past this are\n * still counted. Keeping every row can exhaust the heap when analyzing queries\n * over large tables.\n */\nexport const MAX_ANALYZE_ROWS = 1000;\n\nexport type RunAstOptions = {\n applyPermissions?: boolean | undefined;\n auth?: JWTAuth | undefined;\n clientToServerMapper?: NameMapper | undefined;\n costModel?: ConnectionCostModel | undefined;\n db: Database;\n host: BuilderDelegate;\n /**\n * Maximum number of synced rows returned per table. Rows past this are still\n * counted. Defaults to no limit.\n */\n maxSyncedRowsPerTable?: number | undefined;\n permissions?: PermissionsConfig | undefined;\n planDebugger?: PlanDebugger | undefined;\n syncedRows?: boolean | undefined;\n tableSpecs: Map<string, LiteAndZqlSpec>;\n vendedRows?: boolean | undefined;\n};\n\nexport async function runAst(\n lc: LogContext,\n clientSchema: ClientSchema,\n ast: AST,\n isTransformed: boolean,\n options: RunAstOptions,\n yieldProcess: () => Promise<void>,\n): Promise<AnalyzeQueryResult> {\n const {\n clientToServerMapper,\n permissions,\n host,\n db,\n maxSyncedRowsPerTable = Infinity,\n } = options;\n const result: AnalyzeQueryResult = {\n warnings: [],\n syncedRows: undefined,\n syncedRowCount: 0,\n start: 0,\n end: 0,\n elapsed: 0,\n afterPermissions: undefined,\n readRows: undefined,\n readRowCountsByQuery: {},\n readRowCount: undefined,\n };\n\n if (!isTransformed) {\n // map the AST to server names if not already transformed\n ast = mapAST(ast, must(clientToServerMapper));\n }\n if (options.applyPermissions) {\n const auth = options.auth;\n if (!auth) {\n result.warnings.push(\n 'No auth data provided. Permission rules will compare to `NULL` wherever an auth data field is referenced.',\n );\n }\n ast = transformAndHashQuery(\n lc,\n 'clientGroupIDForAnalyze',\n ast,\n must(permissions),\n auth,\n false,\n ).transformedAst;\n result.afterPermissions = await formatOutput(ast.table + astToZQL(ast));\n }\n\n const hash = hashOfAST(ast);\n const zqlSpecs = computeZqlSpecs(lc, db, {includeBackfillingColumns: false});\n\n // Resolve scalar subqueries (e.g. whereExists with {scalar: true}) to\n // literal equality conditions so that SQLite can use indexes effectively.\n // Without this, correlated subqueries get stripped from SQL filters and\n // queries on large tables fall back to full table scans.\n // As in zero-cache, the rows the client reads for the gates are synced too,\n // so the gates' pipelines are kept to be hydrated after the query's.\n const companionInputs: Input[] = [];\n const executor = (\n subqueryAST: AST,\n childField: string,\n ): LiteralValue | null | undefined => {\n const input = buildPipeline(subqueryAST, host, 'scalar-subquery');\n companionInputs.push(input);\n return fetchScalarValue(input, childField);\n };\n\n const {\n ast: resolvedAst,\n companions,\n ignoredScalarHints,\n } = resolveSimpleScalarSubqueries(ast, options.tableSpecs, executor);\n for (const {table, uniqueKeys} of ignoredScalarHints) {\n lc.warn?.(\n `Ignoring {scalar: true} on the \"${table}\" subquery: it does not ` +\n `constrain every column of any unique key ` +\n `[${uniqueKeys.map(k => `(${k.join(', ')})`).join(', ')}] to a ` +\n `literal with \"=\", so it is not provably limited to one row. ` +\n `The gate runs as a plain EXISTS.`,\n );\n }\n\n const pipeline = buildPipeline(\n resolvedAst,\n host,\n 'query-id',\n options.costModel,\n lc,\n options.planDebugger,\n );\n\n // As in zero-cache, the rows of the gates follow the rows of the query,\n // except for gates from a permission rule: rows read for a rule are never\n // synced.\n const syncedCompanionInputs = companionInputs.filter(\n (_, i) => companions[i].system !== 'permissions',\n );\n function* rowChanges(): Iterable<RowChange | 'yield'> {\n try {\n for (const input of [pipeline, ...syncedCompanionInputs]) {\n yield* hydrate(input, hash, clientSchema, zqlSpecs);\n }\n } finally {\n for (const input of companionInputs) {\n input.destroy();\n }\n }\n }\n\n const start = performance.now();\n\n let syncedRowCount = 0;\n const rowsByTable: Record<string, Row[]> = {};\n // Dedupe on the row key rather than the whole row to bound memory.\n const seen: Set<string> = new Set();\n const truncatedTables: Set<string> = new Set();\n for (const rowChange of rowChanges()) {\n if (rowChange === 'yield') {\n await yieldProcess();\n continue;\n }\n assert(\n rowChange.type === ChangeType.ADD,\n 'Hydration only handles add row changes',\n );\n\n // yield to other tasks to avoid blocking for too long\n if (syncedRowCount % 10 === 0) {\n await Promise.resolve();\n }\n if (syncedRowCount % 100 === 0) {\n await sleep(1);\n }\n\n const key = JSON.stringify([rowChange.table, rowChange.rowKey]);\n if (seen.has(key)) {\n continue; // skip duplicates\n }\n syncedRowCount++;\n seen.add(key);\n if (options.syncedRows) {\n const rows = (rowsByTable[rowChange.table] ??= []);\n if (rows.length < maxSyncedRowsPerTable) {\n rows.push(rowChange.row);\n } else {\n truncatedTables.add(rowChange.table);\n }\n }\n }\n\n const end = performance.now();\n if (options.syncedRows) {\n result.syncedRows = rowsByTable;\n for (const table of truncatedTables) {\n result.warnings.push(\n `Only the first ${maxSyncedRowsPerTable} synced rows of \"${table}\" are included.`,\n );\n }\n }\n result.start = start;\n result.end = end;\n result.elapsed = end - start;\n\n // Always include the count of synced and vended rows.\n result.syncedRowCount = syncedRowCount;\n result.readRowCountsByQuery = host.debug?.getVendedRowCounts() ?? {};\n let readRowCount = 0;\n for (const c of Object.values(result.readRowCountsByQuery)) {\n for (const v of Object.values(c)) {\n readRowCount += v;\n }\n }\n result.readRowCount = readRowCount;\n result.dbScansByQuery = host.debug?.getNVisitCounts() ?? {};\n result.sqlitePlans = host.debug?.getSQLitePlans() ?? {};\n\n if (options.vendedRows) {\n const readRows = host.debug?.getVendedRows();\n result.readRows = readRows;\n for (const [table, byQuery] of Object.entries(readRows ?? {})) {\n for (const [query, rows] of Object.entries(byQuery)) {\n if ((result.readRowCountsByQuery[table]?.[query] ?? 0) > rows.length) {\n result.warnings.push(\n `Only the first ${rows.length} read rows of \"${table}\" are included for: ${query}`,\n );\n }\n }\n }\n }\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"run-ast.js","names":[],"sources":["../../../../../zero-cache/src/services/run-ast.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\n// @circular-dep-ignore\nimport {astToZQL} from '../../../ast-to-zql/src/ast-to-zql.ts';\n// @circular-dep-ignore\nimport {formatOutput} from '../../../ast-to-zql/src/format.ts';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport type {AnalyzeQueryResult} from '../../../zero-protocol/src/analyze-query-result.ts';\nimport type {AST, LiteralValue} from '../../../zero-protocol/src/ast.ts';\nimport {mapAST} from '../../../zero-protocol/src/ast.ts';\nimport type {ClientSchema} from '../../../zero-protocol/src/client-schema.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {hashOfAST} from '../../../zero-protocol/src/query-hash.ts';\nimport type {PermissionsConfig} from '../../../zero-schema/src/compiled-permissions.ts';\nimport type {NameMapper} from '../../../zero-schema/src/name-mapper.ts';\nimport {\n buildPipeline,\n type BuilderDelegate,\n} from '../../../zql/src/builder/builder.ts';\nimport {ChangeType} from '../../../zql/src/ivm/change-type.ts';\nimport type {Input} from '../../../zql/src/ivm/operator.ts';\nimport type {ConnectionCostModel} from '../../../zql/src/planner/planner-connection.ts';\nimport type {PlanDebugger} from '../../../zql/src/planner/planner-debug.ts';\nimport type {Database} from '../../../zqlite/src/db.ts';\nimport {resolveSimpleScalarSubqueries} from '../../../zqlite/src/resolve-scalar-subqueries.ts';\nimport type {JWTAuth} from '../auth/auth.ts';\nimport {transformAndHashQuery} from '../auth/read-authorizer.ts';\nimport {computeZqlSpecs} from '../db/lite-tables.ts';\nimport type {LiteAndZqlSpec} from '../db/specs.ts';\nimport {\n fetchScalarValue,\n hydrate,\n type RowChange,\n} from './view-syncer/pipeline-driver.ts';\n\n/**\n * Maximum number of synced rows returned per table, and read rows returned\n * per table/query, when analyzing a query in zero-cache. Rows past this are\n * still counted. Keeping every row can exhaust the heap when analyzing queries\n * over large tables.\n */\nexport const MAX_ANALYZE_ROWS = 1000;\n\nexport type RunAstOptions = {\n applyPermissions?: boolean | undefined;\n auth?: JWTAuth | undefined;\n clientToServerMapper?: NameMapper | undefined;\n costModel?: ConnectionCostModel | undefined;\n db: Database;\n host: BuilderDelegate;\n /**\n * Maximum number of synced rows returned per table. Rows past this are still\n * counted. Defaults to no limit.\n */\n maxSyncedRowsPerTable?: number | undefined;\n permissions?: PermissionsConfig | undefined;\n planDebugger?: PlanDebugger | undefined;\n syncedRows?: boolean | undefined;\n tableSpecs: Map<string, LiteAndZqlSpec>;\n vendedRows?: boolean | undefined;\n};\n\nexport async function runAst(\n lc: LogContext,\n clientSchema: ClientSchema,\n ast: AST,\n isTransformed: boolean,\n options: RunAstOptions,\n yieldProcess: () => Promise<void>,\n): Promise<AnalyzeQueryResult> {\n const {\n clientToServerMapper,\n permissions,\n host,\n db,\n maxSyncedRowsPerTable = Infinity,\n } = options;\n const result: AnalyzeQueryResult = {\n warnings: [],\n syncedRows: undefined,\n syncedRowCount: 0,\n start: 0,\n end: 0,\n elapsed: 0,\n afterPermissions: undefined,\n readRows: undefined,\n readRowCountsByQuery: {},\n readRowCount: undefined,\n };\n\n if (!isTransformed) {\n // map the AST to server names if not already transformed\n ast = mapAST(ast, must(clientToServerMapper));\n }\n if (options.applyPermissions) {\n const auth = options.auth;\n if (!auth) {\n result.warnings.push(\n 'No auth data provided. Permission rules will compare to `NULL` wherever an auth data field is referenced.',\n );\n }\n ast = transformAndHashQuery(\n lc,\n 'clientGroupIDForAnalyze',\n ast,\n must(permissions),\n auth,\n false,\n ).transformedAst;\n result.afterPermissions = await formatOutput(ast.table + astToZQL(ast));\n }\n\n const hash = hashOfAST(ast);\n const zqlSpecs = computeZqlSpecs(lc, db, {includeBackfillingColumns: false});\n\n // Resolve scalar subqueries (e.g. whereExists with {scalar: true}) to\n // literal equality conditions so that SQLite can use indexes effectively.\n // Without this, correlated subqueries get stripped from SQL filters and\n // queries on large tables fall back to full table scans.\n // As in zero-cache, the rows the client reads for the gates are synced too,\n // so the gates' pipelines are kept to be hydrated after the query's.\n const companionInputs: Input[] = [];\n const executor = (\n subqueryAST: AST,\n childField: string,\n ): LiteralValue | null | undefined => {\n const input = buildPipeline(subqueryAST, host, 'scalar-subquery');\n companionInputs.push(input);\n return fetchScalarValue(input, childField);\n };\n\n const {\n ast: resolvedAst,\n companions,\n ignoredScalarHints,\n } = resolveSimpleScalarSubqueries(ast, options.tableSpecs, executor);\n for (const {table, uniqueKeys} of ignoredScalarHints) {\n lc.warn?.(\n `Ignoring {scalar: true} on the \"${table}\" subquery: it does not ` +\n `constrain every column of any unique key ` +\n `[${uniqueKeys.map(k => `(${k.join(', ')})`).join(', ')}] to a ` +\n `literal with \"=\", so it is not provably limited to one row. ` +\n `The gate runs as a plain EXISTS.`,\n );\n }\n\n const pipeline = buildPipeline(\n resolvedAst,\n host,\n 'query-id',\n options.costModel,\n lc,\n options.planDebugger,\n );\n\n // As in zero-cache, the rows of the gates follow the rows of the query,\n // except for gates from a permission rule: rows read for a rule are never\n // synced.\n const syncedCompanionInputs = companionInputs.filter(\n (_, i) => companions[i].system !== 'permissions',\n );\n function* rowChanges(): Iterable<RowChange | 'yield'> {\n try {\n for (const input of [pipeline, ...syncedCompanionInputs]) {\n yield* hydrate(input, hash, clientSchema, zqlSpecs);\n }\n } finally {\n for (const input of companionInputs) {\n input.destroy();\n }\n }\n }\n\n const start = performance.now();\n\n let syncedRowCount = 0;\n const rowsByTable: Record<string, Row[]> = {};\n // Dedupe on the row key rather than the whole row to bound memory.\n const seen: Set<string> = new Set();\n const truncatedTables: Set<string> = new Set();\n for (const rowChange of rowChanges()) {\n if (rowChange === 'yield') {\n await yieldProcess();\n continue;\n }\n assert(\n rowChange.type === ChangeType.ADD,\n 'Hydration only handles add row changes',\n );\n\n // yield to other tasks to avoid blocking for too long\n if (syncedRowCount % 10 === 0) {\n await Promise.resolve();\n }\n if (syncedRowCount % 100 === 0) {\n await sleep(1);\n }\n\n const key = JSON.stringify([rowChange.table, rowChange.rowKey]);\n if (seen.has(key)) {\n continue; // skip duplicates\n }\n syncedRowCount++;\n seen.add(key);\n if (options.syncedRows) {\n const rows = (rowsByTable[rowChange.table] ??= []);\n if (rows.length < maxSyncedRowsPerTable) {\n rows.push(rowChange.row);\n } else {\n truncatedTables.add(rowChange.table);\n }\n }\n }\n\n const end = performance.now();\n if (options.syncedRows) {\n result.syncedRows = rowsByTable;\n for (const table of truncatedTables) {\n result.warnings.push(\n `Only the first ${maxSyncedRowsPerTable} synced rows of \"${table}\" are included.`,\n );\n }\n }\n result.start = start;\n result.end = end;\n result.elapsed = end - start;\n\n // Always include the count of synced and vended rows.\n result.syncedRowCount = syncedRowCount;\n result.readRowCountsByQuery = host.debug?.getVendedRowCounts() ?? {};\n let readRowCount = 0;\n for (const c of Object.values(result.readRowCountsByQuery)) {\n for (const v of Object.values(c)) {\n readRowCount += v;\n }\n }\n result.readRowCount = readRowCount;\n result.dbScansByQuery = host.debug?.getNVisitCounts() ?? {};\n result.sqlitePlans = host.debug?.getSQLitePlans() ?? {};\n\n if (options.vendedRows) {\n const readRows = host.debug?.getVendedRows();\n result.readRows = readRows;\n for (const [table, byQuery] of Object.entries(readRows ?? {})) {\n for (const [query, rows] of Object.entries(byQuery)) {\n if ((result.readRowCountsByQuery[table]?.[query] ?? 0) > rows.length) {\n result.warnings.push(\n `Only the first ${rows.length} read rows of \"${table}\" are included for: ${query}`,\n );\n }\n }\n }\n }\n return result;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","names":["#lc","#instances","#create","#isValid","#onStop"],"sources":["../../../../../zero-cache/src/services/runner.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {Service} from './service.ts';\n\n/**\n * Manages the creation and lifecycle of objects that implement\n * {@link Service}.\n */\nexport class ServiceRunner<S extends Service> {\n readonly #lc: LogContext;\n readonly #instances = new Map<string, S>();\n readonly #create: (id: string) => S;\n readonly #isValid: (existing: S) => boolean;\n readonly #onStop?: ((id: string) => void) | undefined;\n\n constructor(\n lc: LogContext,\n factory: (id: string) => S,\n isValid: (existing: S) => boolean = () => true,\n onStop?: ((id: string) => void) | undefined,\n ) {\n this.#lc = lc;\n this.#create = factory;\n this.#isValid = isValid;\n this.#onStop = onStop;\n }\n\n /**\n * Creates and runs the Service with the given `id`, returning\n * an existing one if it is still running a valid.\n */\n getService(id: string): S {\n const existing = this.#instances.get(id);\n if (existing && this.#isValid(existing)) {\n return existing;\n }\n const service = this.#create(id);\n this.#instances.set(id, service);\n void service\n .run()\n .catch(e =>\n this.#lc.error?.(\n `Error running ${service.constructor?.name} ${service.id}`,\n e,\n ),\n )\n .finally(() => {\n // Only remove the instance that just finished. If it was already\n // replaced (e.g. because it became invalid and a new instance was\n // created under the same id while it was still shutting down), the\n // replacement must stay tracked; otherwise the next getService()\n // would spawn a duplicate and the untracked one would never be\n // stopped by this runner.\n if (this.#instances.get(id) === service) {\n this.#instances.delete(id);\n this.#onStop?.(id);\n }\n });\n return service;\n }\n\n hasService(id: string): boolean {\n return this.#instances.has(id);\n }\n\n get size() {\n return this.#instances.size;\n }\n\n getServices(): Iterable<S> {\n return this.#instances.values();\n }\n}\n"],"mappings":";;;;;AAOA,IAAa,gBAAb,MAA8C;CAC5C;CACA,6BAAsB,IAAI,IAAe;CACzC;CACA;CACA;CAEA,YACE,IACA,SACA,gBAA0C,MAC1C,QACA;EACA,KAAKA,MAAM;EACX,KAAKE,UAAU;EACf,KAAKC,WAAW;EAChB,KAAKC,UAAU;CACjB;;;;;CAMA,WAAW,IAAe;EACxB,MAAM,WAAW,KAAKH,WAAW,IAAI,EAAE;EACvC,IAAI,YAAY,KAAKE,SAAS,QAAQ,GACpC,OAAO;EAET,MAAM,UAAU,KAAKD,QAAQ,EAAE;EAC/B,KAAKD,WAAW,IAAI,IAAI,OAAO;EAC/B,QACG,IAAI,EACJ,OAAM,MACL,KAAKD,IAAI,QACP,iBAAiB,QAAQ,aAAa,KAAK,GAAG,QAAQ,MACtD,CACF,CACF,EACC,cAAc;GAOb,IAAI,KAAKC,WAAW,IAAI,EAAE,MAAM,SAAS;IACvC,KAAKA,WAAW,OAAO,EAAE;IACzB,KAAKG,UAAU,EAAE;GACnB;EACF,CAAC;EACH,OAAO;CACT;CAEA,WAAW,IAAqB;EAC9B,OAAO,KAAKH,WAAW,IAAI,EAAE;CAC/B;CAEA,IAAI,OAAO;EACT,OAAO,KAAKA,WAAW;CACzB;CAEA,cAA2B;EACzB,OAAO,KAAKA,WAAW,OAAO;CAChC;AACF"}
1
+ {"version":3,"file":"runner.js","names":[],"sources":["../../../../../zero-cache/src/services/runner.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {Service} from './service.ts';\n\n/**\n * Manages the creation and lifecycle of objects that implement\n * {@link Service}.\n */\nexport class ServiceRunner<S extends Service> {\n readonly #lc: LogContext;\n readonly #instances = new Map<string, S>();\n readonly #create: (id: string) => S;\n readonly #isValid: (existing: S) => boolean;\n readonly #onStop?: ((id: string) => void) | undefined;\n\n constructor(\n lc: LogContext,\n factory: (id: string) => S,\n isValid: (existing: S) => boolean = () => true,\n onStop?: ((id: string) => void) | undefined,\n ) {\n this.#lc = lc;\n this.#create = factory;\n this.#isValid = isValid;\n this.#onStop = onStop;\n }\n\n /**\n * Creates and runs the Service with the given `id`, returning\n * an existing one if it is still running a valid.\n */\n getService(id: string): S {\n const existing = this.#instances.get(id);\n if (existing && this.#isValid(existing)) {\n return existing;\n }\n const service = this.#create(id);\n this.#instances.set(id, service);\n void service\n .run()\n .catch(e =>\n this.#lc.error?.(\n `Error running ${service.constructor?.name} ${service.id}`,\n e,\n ),\n )\n .finally(() => {\n // Only remove the instance that just finished. If it was already\n // replaced (e.g. because it became invalid and a new instance was\n // created under the same id while it was still shutting down), the\n // replacement must stay tracked; otherwise the next getService()\n // would spawn a duplicate and the untracked one would never be\n // stopped by this runner.\n if (this.#instances.get(id) === service) {\n this.#instances.delete(id);\n this.#onStop?.(id);\n }\n });\n return service;\n }\n\n hasService(id: string): boolean {\n return this.#instances.has(id);\n }\n\n get size() {\n return this.#instances.size;\n }\n\n getServices(): Iterable<S> {\n return this.#instances.values();\n }\n}\n"],"mappings":";;;;;AAOA,IAAa,gBAAb,MAA8C;CAC5C;CACA,6BAAsB,IAAI,IAAe;CACzC;CACA;CACA;CAEA,YACE,IACA,SACA,gBAA0C,MAC1C,QACA;EACA,KAAK,MAAM;EACX,KAAK,UAAU;EACf,KAAK,WAAW;EAChB,KAAK,UAAU;CACjB;;;;;CAMA,WAAW,IAAe;EACxB,MAAM,WAAW,KAAK,WAAW,IAAI,EAAE;EACvC,IAAI,YAAY,KAAK,SAAS,QAAQ,GACpC,OAAO;EAET,MAAM,UAAU,KAAK,QAAQ,EAAE;EAC/B,KAAK,WAAW,IAAI,IAAI,OAAO;EAC/B,QACG,IAAI,CAAC,CACL,OAAM,MACL,KAAK,IAAI,QACP,iBAAiB,QAAQ,aAAa,KAAK,GAAG,QAAQ,MACtD,CACF,CACF,CAAC,CACA,cAAc;GAOb,IAAI,KAAK,WAAW,IAAI,EAAE,MAAM,SAAS;IACvC,KAAK,WAAW,OAAO,EAAE;IACzB,KAAK,UAAU,EAAE;GACnB;EACF,CAAC;EACH,OAAO;CACT;CAEA,WAAW,IAAqB;EAC9B,OAAO,KAAK,WAAW,IAAI,EAAE;CAC/B;CAEA,IAAI,OAAO;EACT,OAAO,KAAK,WAAW;CACzB;CAEA,cAA2B;EACzB,OAAO,KAAK,WAAW,OAAO;CAChC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"running-state.js","names":["#serviceName","#controller","#sleep","#setTimeout","#stopped","#initialRetryDelay","#maxRetryDelay","#pendingTimeouts","#retryDelay"],"sources":["../../../../../zero-cache/src/services/running-state.ts"],"sourcesContent":["import {PG_UNDEFINED_TABLE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {sleepWithAbort} from '../../../shared/src/sleep.ts';\nimport {isPostgresError} from '../types/pg.ts';\n\nconst DEFAULT_INITIAL_RETRY_DELAY_MS = 25;\nexport const DEFAULT_MAX_RETRY_DELAY_MS = 10000;\n\nexport type RetryConfig = {\n initialRetryDelay?: number;\n maxRetryDelay?: number;\n};\n\nexport interface Cancelable {\n cancel(): void;\n}\n\nexport type UnregisterFn = () => void;\n\n/**\n * Facilitates lifecycle control with exponential backoff.\n */\nexport class RunningState {\n readonly #serviceName: string;\n readonly #controller: AbortController;\n readonly #sleep: typeof sleepWithAbort;\n readonly #setTimeout: typeof setTimeout;\n readonly #stopped: Promise<void>;\n\n readonly #initialRetryDelay: number;\n readonly #maxRetryDelay: number;\n readonly #pendingTimeouts = new Set<NodeJS.Timeout>();\n #retryDelay: number;\n\n constructor(\n serviceName: string,\n retryConfig?: RetryConfig,\n setTimeoutFn = setTimeout,\n sleeper = sleepWithAbort,\n ) {\n const {\n initialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY_MS,\n maxRetryDelay = DEFAULT_MAX_RETRY_DELAY_MS,\n } = retryConfig ?? {};\n\n this.#serviceName = serviceName;\n this.#initialRetryDelay = initialRetryDelay;\n this.#maxRetryDelay = maxRetryDelay;\n this.#retryDelay = initialRetryDelay;\n\n this.#controller = new AbortController();\n this.#sleep = sleeper;\n this.#setTimeout = setTimeoutFn;\n\n const {promise, resolve} = resolver();\n this.#stopped = promise;\n this.#controller.signal.addEventListener(\n 'abort',\n () => {\n resolve();\n for (const timeout of this.#pendingTimeouts) {\n clearTimeout(timeout);\n }\n this.#pendingTimeouts.clear();\n },\n {once: true},\n );\n }\n\n get signal(): AbortSignal {\n return this.#controller.signal;\n }\n\n get retryDelay() {\n return this.#retryDelay;\n }\n\n /**\n * Returns `true` until {@link stop()} has been called.\n *\n * This is usually called as part of the service's main loop\n * conditional to determine if the next iteration should execute.\n */\n shouldRun(): boolean {\n return !this.#controller.signal.aborted;\n }\n\n /**\n * Registers a Cancelable object to be invoked when {@link stop()} is called.\n * Returns a method to unregister the object.\n */\n cancelOnStop(c: Cancelable): UnregisterFn {\n const onStop = () => c.cancel();\n this.#controller.signal.addEventListener('abort', onStop, {once: true});\n return () => this.#controller.signal.removeEventListener('abort', onStop);\n }\n\n /**\n * Sets a Timeout that is automatically cancelled if the service is cancelled.\n */\n setTimeout<TArgs extends unknown[]>(\n fn: (...args: TArgs) => void,\n timeoutMs: number,\n ...args: TArgs\n ) {\n const timeout = this.#setTimeout(() => {\n clearTimeout(timeout);\n this.#pendingTimeouts.delete(timeout);\n return fn(...args);\n }, timeoutMs);\n\n this.#pendingTimeouts.add(timeout);\n return timeout;\n }\n\n /**\n * Returns a promise that resolves after `ms` milliseconds or when\n * the service is stopped.\n */\n async sleep(ms: number): Promise<void> {\n await Promise.race(this.#sleep(ms, this.#controller.signal));\n }\n\n /**\n * Called to stop the service. After this is called, {@link shouldRun()}\n * will return `false` and the {@link stopped()} Promise will be resolved.\n */\n stop(lc: LogContext, err?: unknown): void {\n if (this.shouldRun()) {\n const log = !err || err instanceof AbortError ? 'info' : 'error';\n lc[log]?.(`stopping ${this.#serviceName}`, err ?? '');\n this.#controller.abort();\n }\n }\n\n /**\n * Returns a Promise that resolves when {@link stop()} is called.\n */\n stopped(): Promise<void> {\n return this.#stopped;\n }\n\n /**\n * Call in response to an error or unexpected termination in the main\n * loop of the service. The returned Promise will resolve after an\n * exponential delay, or once {@link stop()} is called.\n *\n * If the supplied `err` is an `AbortError`, the service will shut down.\n */\n async backoff(lc: LogContext, err: unknown): Promise<void> {\n const delay = this.#retryDelay;\n this.#retryDelay = Math.min(delay * 2, this.#maxRetryDelay);\n\n // Common failure mode when the user deletes a pg schema out from under\n // us after init. Turn into an AbortError to force restart, which will\n // restart and re-init the schema.\n if (isPostgresError(err, PG_UNDEFINED_TABLE)) {\n err = new AbortError('undefined table', {cause: err});\n }\n\n if (err instanceof AbortError || err instanceof UnrecoverableError) {\n this.resetBackoff();\n this.stop(lc, err);\n } else if (this.shouldRun()) {\n // Use delay-based log level: higher delay means more retries\n const log: 'info' | 'warn' | 'error' =\n delay < 1000 ? 'info' : delay < 6500 ? 'warn' : 'error';\n\n lc[log]?.(`retrying ${this.#serviceName} in ${delay} ms`, err);\n await this.sleep(delay);\n }\n }\n\n /**\n * When using {@link backoff()}, this method should be called when the\n * implementation receives a healthy signal (e.g. a successful\n * response). This resets the delay used in {@link backoff()}.\n *\n * @returns The previous backoff delay\n */\n resetBackoff() {\n const prevDelay = this.#retryDelay;\n this.#retryDelay = this.#initialRetryDelay;\n return prevDelay;\n }\n}\n\n/**\n * Superclass for Errors that should bypass exponential backoff\n * and immediately shut down the server.\n */\nexport class UnrecoverableError extends Error {\n readonly name = 'UnrecoverableError';\n}\n"],"mappings":";;;;;;AAOA,IAAM,iCAAiC;AACvC,IAAa,6BAA6B;;;;AAgB1C,IAAa,eAAb,MAA0B;CACxB;CACA;CACA;CACA;CACA;CAEA;CACA;CACA,mCAA4B,IAAI,IAAoB;CACpD;CAEA,YACE,aACA,aACA,eAAe,YACf,UAAU,gBACV;EACA,MAAM,EACJ,oBAAoB,gCACpB,gBAAgB,+BACd,eAAe,CAAC;EAEpB,KAAKA,eAAe;EACpB,KAAKK,qBAAqB;EAC1B,KAAKC,iBAAiB;EACtB,KAAKE,cAAc;EAEnB,KAAKP,cAAc,IAAI,gBAAgB;EACvC,KAAKC,SAAS;EACd,KAAKC,cAAc;EAEnB,MAAM,EAAC,SAAS,YAAW,SAAS;EACpC,KAAKC,WAAW;EAChB,KAAKH,YAAY,OAAO,iBACtB,eACM;GACJ,QAAQ;GACR,KAAK,MAAM,WAAW,KAAKM,kBACzB,aAAa,OAAO;GAEtB,KAAKA,iBAAiB,MAAM;EAC9B,GACA,EAAC,MAAM,KAAI,CACb;CACF;CAEA,IAAI,SAAsB;EACxB,OAAO,KAAKN,YAAY;CAC1B;CAEA,IAAI,aAAa;EACf,OAAO,KAAKO;CACd;;;;;;;CAQA,YAAqB;EACnB,OAAO,CAAC,KAAKP,YAAY,OAAO;CAClC;;;;;CAMA,aAAa,GAA6B;EACxC,MAAM,eAAe,EAAE,OAAO;EAC9B,KAAKA,YAAY,OAAO,iBAAiB,SAAS,QAAQ,EAAC,MAAM,KAAI,CAAC;EACtE,aAAa,KAAKA,YAAY,OAAO,oBAAoB,SAAS,MAAM;CAC1E;;;;CAKA,WACE,IACA,WACA,GAAG,MACH;EACA,MAAM,UAAU,KAAKE,kBAAkB;GACrC,aAAa,OAAO;GACpB,KAAKI,iBAAiB,OAAO,OAAO;GACpC,OAAO,GAAG,GAAG,IAAI;EACnB,GAAG,SAAS;EAEZ,KAAKA,iBAAiB,IAAI,OAAO;EACjC,OAAO;CACT;;;;;CAMA,MAAM,MAAM,IAA2B;EACrC,MAAM,QAAQ,KAAK,KAAKL,OAAO,IAAI,KAAKD,YAAY,MAAM,CAAC;CAC7D;;;;;CAMA,KAAK,IAAgB,KAAqB;EACxC,IAAI,KAAK,UAAU,GAAG;GAEpB,GADY,CAAC,OAAO,eAAe,aAAa,SAAS,WAC/C,YAAY,KAAKD,gBAAgB,OAAO,EAAE;GACpD,KAAKC,YAAY,MAAM;EACzB;CACF;;;;CAKA,UAAyB;EACvB,OAAO,KAAKG;CACd;;;;;;;;CASA,MAAM,QAAQ,IAAgB,KAA6B;EACzD,MAAM,QAAQ,KAAKI;EACnB,KAAKA,cAAc,KAAK,IAAI,QAAQ,GAAG,KAAKF,cAAc;EAK1D,IAAI,gBAAgB,KAAK,kBAAkB,GACzC,MAAM,IAAI,WAAW,mBAAmB,EAAC,OAAO,IAAG,CAAC;EAGtD,IAAI,eAAe,cAAc,eAAe,oBAAoB;GAClE,KAAK,aAAa;GAClB,KAAK,KAAK,IAAI,GAAG;EACnB,OAAO,IAAI,KAAK,UAAU,GAAG;GAK3B,GAFE,QAAQ,MAAO,SAAS,QAAQ,OAAO,SAAS,WAExC,YAAY,KAAKN,aAAa,MAAM,MAAM,MAAM,GAAG;GAC7D,MAAM,KAAK,MAAM,KAAK;EACxB;CACF;;;;;;;;CASA,eAAe;EACb,MAAM,YAAY,KAAKQ;EACvB,KAAKA,cAAc,KAAKH;EACxB,OAAO;CACT;AACF;;;;;AAMA,IAAa,qBAAb,cAAwC,MAAM;CAC5C,OAAgB;AAClB"}
1
+ {"version":3,"file":"running-state.js","names":[],"sources":["../../../../../zero-cache/src/services/running-state.ts"],"sourcesContent":["import {PG_UNDEFINED_TABLE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {sleepWithAbort} from '../../../shared/src/sleep.ts';\nimport {isPostgresError} from '../types/pg.ts';\n\nconst DEFAULT_INITIAL_RETRY_DELAY_MS = 25;\nexport const DEFAULT_MAX_RETRY_DELAY_MS = 10000;\n\nexport type RetryConfig = {\n initialRetryDelay?: number;\n maxRetryDelay?: number;\n};\n\nexport interface Cancelable {\n cancel(): void;\n}\n\nexport type UnregisterFn = () => void;\n\n/**\n * Facilitates lifecycle control with exponential backoff.\n */\nexport class RunningState {\n readonly #serviceName: string;\n readonly #controller: AbortController;\n readonly #sleep: typeof sleepWithAbort;\n readonly #setTimeout: typeof setTimeout;\n readonly #stopped: Promise<void>;\n\n readonly #initialRetryDelay: number;\n readonly #maxRetryDelay: number;\n readonly #pendingTimeouts = new Set<NodeJS.Timeout>();\n #retryDelay: number;\n\n constructor(\n serviceName: string,\n retryConfig?: RetryConfig,\n setTimeoutFn = setTimeout,\n sleeper = sleepWithAbort,\n ) {\n const {\n initialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY_MS,\n maxRetryDelay = DEFAULT_MAX_RETRY_DELAY_MS,\n } = retryConfig ?? {};\n\n this.#serviceName = serviceName;\n this.#initialRetryDelay = initialRetryDelay;\n this.#maxRetryDelay = maxRetryDelay;\n this.#retryDelay = initialRetryDelay;\n\n this.#controller = new AbortController();\n this.#sleep = sleeper;\n this.#setTimeout = setTimeoutFn;\n\n const {promise, resolve} = resolver();\n this.#stopped = promise;\n this.#controller.signal.addEventListener(\n 'abort',\n () => {\n resolve();\n for (const timeout of this.#pendingTimeouts) {\n clearTimeout(timeout);\n }\n this.#pendingTimeouts.clear();\n },\n {once: true},\n );\n }\n\n get signal(): AbortSignal {\n return this.#controller.signal;\n }\n\n get retryDelay() {\n return this.#retryDelay;\n }\n\n /**\n * Returns `true` until {@link stop()} has been called.\n *\n * This is usually called as part of the service's main loop\n * conditional to determine if the next iteration should execute.\n */\n shouldRun(): boolean {\n return !this.#controller.signal.aborted;\n }\n\n /**\n * Registers a Cancelable object to be invoked when {@link stop()} is called.\n * Returns a method to unregister the object.\n */\n cancelOnStop(c: Cancelable): UnregisterFn {\n const onStop = () => c.cancel();\n this.#controller.signal.addEventListener('abort', onStop, {once: true});\n return () => this.#controller.signal.removeEventListener('abort', onStop);\n }\n\n /**\n * Sets a Timeout that is automatically cancelled if the service is cancelled.\n */\n setTimeout<TArgs extends unknown[]>(\n fn: (...args: TArgs) => void,\n timeoutMs: number,\n ...args: TArgs\n ) {\n const timeout = this.#setTimeout(() => {\n clearTimeout(timeout);\n this.#pendingTimeouts.delete(timeout);\n return fn(...args);\n }, timeoutMs);\n\n this.#pendingTimeouts.add(timeout);\n return timeout;\n }\n\n /**\n * Returns a promise that resolves after `ms` milliseconds or when\n * the service is stopped.\n */\n async sleep(ms: number): Promise<void> {\n await Promise.race(this.#sleep(ms, this.#controller.signal));\n }\n\n /**\n * Called to stop the service. After this is called, {@link shouldRun()}\n * will return `false` and the {@link stopped()} Promise will be resolved.\n */\n stop(lc: LogContext, err?: unknown): void {\n if (this.shouldRun()) {\n const log = !err || err instanceof AbortError ? 'info' : 'error';\n lc[log]?.(`stopping ${this.#serviceName}`, err ?? '');\n this.#controller.abort();\n }\n }\n\n /**\n * Returns a Promise that resolves when {@link stop()} is called.\n */\n stopped(): Promise<void> {\n return this.#stopped;\n }\n\n /**\n * Call in response to an error or unexpected termination in the main\n * loop of the service. The returned Promise will resolve after an\n * exponential delay, or once {@link stop()} is called.\n *\n * If the supplied `err` is an `AbortError`, the service will shut down.\n */\n async backoff(lc: LogContext, err: unknown): Promise<void> {\n const delay = this.#retryDelay;\n this.#retryDelay = Math.min(delay * 2, this.#maxRetryDelay);\n\n // Common failure mode when the user deletes a pg schema out from under\n // us after init. Turn into an AbortError to force restart, which will\n // restart and re-init the schema.\n if (isPostgresError(err, PG_UNDEFINED_TABLE)) {\n err = new AbortError('undefined table', {cause: err});\n }\n\n if (err instanceof AbortError || err instanceof UnrecoverableError) {\n this.resetBackoff();\n this.stop(lc, err);\n } else if (this.shouldRun()) {\n // Use delay-based log level: higher delay means more retries\n const log: 'info' | 'warn' | 'error' =\n delay < 1000 ? 'info' : delay < 6500 ? 'warn' : 'error';\n\n lc[log]?.(`retrying ${this.#serviceName} in ${delay} ms`, err);\n await this.sleep(delay);\n }\n }\n\n /**\n * When using {@link backoff()}, this method should be called when the\n * implementation receives a healthy signal (e.g. a successful\n * response). This resets the delay used in {@link backoff()}.\n *\n * @returns The previous backoff delay\n */\n resetBackoff() {\n const prevDelay = this.#retryDelay;\n this.#retryDelay = this.#initialRetryDelay;\n return prevDelay;\n }\n}\n\n/**\n * Superclass for Errors that should bypass exponential backoff\n * and immediately shut down the server.\n */\nexport class UnrecoverableError extends Error {\n readonly name = 'UnrecoverableError';\n}\n"],"mappings":";;;;;;AAOA,IAAM,iCAAiC;AACvC,IAAa,6BAA6B;;;;AAgB1C,IAAa,eAAb,MAA0B;CACxB;CACA;CACA;CACA;CACA;CAEA;CACA;CACA,mCAA4B,IAAI,IAAoB;CACpD;CAEA,YACE,aACA,aACA,eAAe,YACf,UAAU,gBACV;EACA,MAAM,EACJ,oBAAoB,gCACpB,gBAAgB,+BACd,eAAe,CAAC;EAEpB,KAAK,eAAe;EACpB,KAAK,qBAAqB;EAC1B,KAAK,iBAAiB;EACtB,KAAK,cAAc;EAEnB,KAAK,cAAc,IAAI,gBAAgB;EACvC,KAAK,SAAS;EACd,KAAK,cAAc;EAEnB,MAAM,EAAC,SAAS,YAAW,SAAS;EACpC,KAAK,WAAW;EAChB,KAAK,YAAY,OAAO,iBACtB,eACM;GACJ,QAAQ;GACR,KAAK,MAAM,WAAW,KAAK,kBACzB,aAAa,OAAO;GAEtB,KAAK,iBAAiB,MAAM;EAC9B,GACA,EAAC,MAAM,KAAI,CACb;CACF;CAEA,IAAI,SAAsB;EACxB,OAAO,KAAK,YAAY;CAC1B;CAEA,IAAI,aAAa;EACf,OAAO,KAAK;CACd;;;;;;;CAQA,YAAqB;EACnB,OAAO,CAAC,KAAK,YAAY,OAAO;CAClC;;;;;CAMA,aAAa,GAA6B;EACxC,MAAM,eAAe,EAAE,OAAO;EAC9B,KAAK,YAAY,OAAO,iBAAiB,SAAS,QAAQ,EAAC,MAAM,KAAI,CAAC;EACtE,aAAa,KAAK,YAAY,OAAO,oBAAoB,SAAS,MAAM;CAC1E;;;;CAKA,WACE,IACA,WACA,GAAG,MACH;EACA,MAAM,UAAU,KAAK,kBAAkB;GACrC,aAAa,OAAO;GACpB,KAAK,iBAAiB,OAAO,OAAO;GACpC,OAAO,GAAG,GAAG,IAAI;EACnB,GAAG,SAAS;EAEZ,KAAK,iBAAiB,IAAI,OAAO;EACjC,OAAO;CACT;;;;;CAMA,MAAM,MAAM,IAA2B;EACrC,MAAM,QAAQ,KAAK,KAAK,OAAO,IAAI,KAAK,YAAY,MAAM,CAAC;CAC7D;;;;;CAMA,KAAK,IAAgB,KAAqB;EACxC,IAAI,KAAK,UAAU,GAAG;GAEpB,GADY,CAAC,OAAO,eAAe,aAAa,SAAS,QAClD,GAAG,YAAY,KAAK,gBAAgB,OAAO,EAAE;GACpD,KAAK,YAAY,MAAM;EACzB;CACF;;;;CAKA,UAAyB;EACvB,OAAO,KAAK;CACd;;;;;;;;CASA,MAAM,QAAQ,IAAgB,KAA6B;EACzD,MAAM,QAAQ,KAAK;EACnB,KAAK,cAAc,KAAK,IAAI,QAAQ,GAAG,KAAK,cAAc;EAK1D,IAAI,gBAAgB,KAAK,kBAAkB,GACzC,MAAM,IAAI,WAAW,mBAAmB,EAAC,OAAO,IAAG,CAAC;EAGtD,IAAI,eAAe,cAAc,eAAe,oBAAoB;GAClE,KAAK,aAAa;GAClB,KAAK,KAAK,IAAI,GAAG;EACnB,OAAO,IAAI,KAAK,UAAU,GAAG;GAK3B,GAFE,QAAQ,MAAO,SAAS,QAAQ,OAAO,SAAS,QAE3C,GAAG,YAAY,KAAK,aAAa,MAAM,MAAM,MAAM,GAAG;GAC7D,MAAM,KAAK,MAAM,KAAK;EACxB;CACF;;;;;;;;CASA,eAAe;EACb,MAAM,YAAY,KAAK;EACvB,KAAK,cAAc,KAAK;EACxB,OAAO;CACT;AACF;;;;;AAMA,IAAa,qBAAb,cAAwC,MAAM;CAC5C,OAAgB;AAClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"shadow-sync-service.js","names":["#lc","#shard","#upstreamURI","#context","#options","#state"],"sources":["../../../../../../zero-cache/src/services/shadow-sync/shadow-sync-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport type {ShardConfig} from '../../types/shards.ts';\nimport type {ServerContext} from '../change-source/pg/initial-sync.ts';\nimport {shadowInitialSync} from '../change-source/pg/initial-sync.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nfunction shadowSyncRuns() {\n return getOrCreateCounter(\n 'replication',\n 'shadow-sync-runs',\n 'Number of shadow initial-sync runs, labeled by result.',\n );\n}\n\nfunction shadowSyncDuration() {\n return getOrCreateLatencyHistogram(\n 'replication',\n 'shadow-sync-duration',\n 'Wall-clock duration of a shadow initial-sync run, labeled by result.',\n );\n}\n\nexport type ShadowSyncOptions = {\n intervalMs: number;\n sampleRate: number;\n maxRowsPerTable: number;\n textCopy?: boolean | undefined;\n};\n\nexport class ShadowSyncService implements Service {\n readonly id = 'shadow-syncer';\n\n readonly #lc: LogContext;\n readonly #shard: ShardConfig;\n readonly #upstreamURI: string;\n readonly #context: ServerContext;\n readonly #options: ShadowSyncOptions;\n readonly #state = new RunningState('shadow-syncer');\n\n constructor(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n context: ServerContext,\n options: ShadowSyncOptions,\n ) {\n this.#lc = lc;\n this.#shard = shard;\n this.#upstreamURI = upstreamURI;\n this.#context = context;\n this.#options = options;\n }\n\n async run() {\n const {intervalMs, sampleRate, maxRowsPerTable, textCopy} = this.#options;\n\n // Why: first run fires in [intervalMs * 2/3, intervalMs) — late enough\n // that shadow sync never fires immediately on task startup, but always\n // before one full interval elapses so the canary completes at least once\n // per task lifetime (the replication manager is restarted every ~24h).\n // The last third is randomized so a fleet-wide restart does not cause\n // every task to canary simultaneously.\n const minFirstRunDelay = Math.floor((intervalMs * 2) / 3);\n const firstRunDelay =\n minFirstRunDelay +\n Math.floor(Math.random() * (intervalMs - minFirstRunDelay));\n this.#lc.info?.(\n `shadow-syncer started; first run in ${firstRunDelay} ms, then every ${intervalMs} ms`,\n );\n await this.#state.sleep(firstRunDelay);\n\n while (this.#state.shouldRun()) {\n const start = performance.now();\n try {\n await shadowInitialSync(\n this.#lc,\n this.#shard,\n this.#upstreamURI,\n {sampleRate, maxRowsPerTable},\n this.#context,\n textCopy !== undefined ? {textCopy} : undefined,\n );\n const elapsed = performance.now() - start;\n shadowSyncRuns().add(1, {result: 'success'});\n shadowSyncDuration().recordMs(elapsed, {result: 'success'});\n this.#lc.info?.(\n `shadow initial-sync completed (${elapsed.toFixed(0)} ms)`,\n );\n } catch (e) {\n const elapsed = performance.now() - start;\n shadowSyncRuns().add(1, {result: 'error'});\n shadowSyncDuration().recordMs(elapsed, {result: 'error'});\n this.#lc.error?.(\n `shadow initial-sync failed after ${elapsed.toFixed(0)} ms`,\n e,\n );\n }\n await this.#state.sleep(intervalMs);\n }\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n}\n"],"mappings":";;;;;AAYA,SAAS,iBAAiB;CACxB,OAAO,mBACL,eACA,oBACA,wDACF;AACF;AAEA,SAAS,qBAAqB;CAC5B,OAAO,4BACL,eACA,wBACA,sEACF;AACF;AASA,IAAa,oBAAb,MAAkD;CAChD,KAAc;CAEd;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,eAAe;CAElD,YACE,IACA,OACA,aACA,SACA,SACA;EACA,KAAKA,MAAM;EACX,KAAKC,SAAS;EACd,KAAKC,eAAe;EACpB,KAAKC,WAAW;EAChB,KAAKC,WAAW;CAClB;CAEA,MAAM,MAAM;EACV,MAAM,EAAC,YAAY,YAAY,iBAAiB,aAAY,KAAKA;EAQjE,MAAM,mBAAmB,KAAK,MAAO,aAAa,IAAK,CAAC;EACxD,MAAM,gBACJ,mBACA,KAAK,MAAM,KAAK,OAAO,KAAK,aAAa,iBAAiB;EAC5D,KAAKJ,IAAI,OACP,uCAAuC,cAAc,kBAAkB,WAAW,IACpF;EACA,MAAM,KAAKK,OAAO,MAAM,aAAa;EAErC,OAAO,KAAKA,OAAO,UAAU,GAAG;GAC9B,MAAM,QAAQ,YAAY,IAAI;GAC9B,IAAI;IACF,MAAM,kBACJ,KAAKL,KACL,KAAKC,QACL,KAAKC,cACL;KAAC;KAAY;IAAe,GAC5B,KAAKC,UACL,aAAa,KAAA,IAAY,EAAC,SAAQ,IAAI,KAAA,CACxC;IACA,MAAM,UAAU,YAAY,IAAI,IAAI;IACpC,eAAe,EAAE,IAAI,GAAG,EAAC,QAAQ,UAAS,CAAC;IAC3C,mBAAmB,EAAE,SAAS,SAAS,EAAC,QAAQ,UAAS,CAAC;IAC1D,KAAKH,IAAI,OACP,kCAAkC,QAAQ,QAAQ,CAAC,EAAE,KACvD;GACF,SAAS,GAAG;IACV,MAAM,UAAU,YAAY,IAAI,IAAI;IACpC,eAAe,EAAE,IAAI,GAAG,EAAC,QAAQ,QAAO,CAAC;IACzC,mBAAmB,EAAE,SAAS,SAAS,EAAC,QAAQ,QAAO,CAAC;IACxD,KAAKA,IAAI,QACP,oCAAoC,QAAQ,QAAQ,CAAC,EAAE,MACvD,CACF;GACF;GACA,MAAM,KAAKK,OAAO,MAAM,UAAU;EACpC;CACF;CAEA,OAAsB;EACpB,KAAKA,OAAO,KAAK,KAAKL,GAAG;EACzB,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"shadow-sync-service.js","names":[],"sources":["../../../../../../zero-cache/src/services/shadow-sync/shadow-sync-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport type {ShardConfig} from '../../types/shards.ts';\nimport type {ServerContext} from '../change-source/pg/initial-sync.ts';\nimport {shadowInitialSync} from '../change-source/pg/initial-sync.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nfunction shadowSyncRuns() {\n return getOrCreateCounter(\n 'replication',\n 'shadow-sync-runs',\n 'Number of shadow initial-sync runs, labeled by result.',\n );\n}\n\nfunction shadowSyncDuration() {\n return getOrCreateLatencyHistogram(\n 'replication',\n 'shadow-sync-duration',\n 'Wall-clock duration of a shadow initial-sync run, labeled by result.',\n );\n}\n\nexport type ShadowSyncOptions = {\n intervalMs: number;\n sampleRate: number;\n maxRowsPerTable: number;\n textCopy?: boolean | undefined;\n};\n\nexport class ShadowSyncService implements Service {\n readonly id = 'shadow-syncer';\n\n readonly #lc: LogContext;\n readonly #shard: ShardConfig;\n readonly #upstreamURI: string;\n readonly #context: ServerContext;\n readonly #options: ShadowSyncOptions;\n readonly #state = new RunningState('shadow-syncer');\n\n constructor(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n context: ServerContext,\n options: ShadowSyncOptions,\n ) {\n this.#lc = lc;\n this.#shard = shard;\n this.#upstreamURI = upstreamURI;\n this.#context = context;\n this.#options = options;\n }\n\n async run() {\n const {intervalMs, sampleRate, maxRowsPerTable, textCopy} = this.#options;\n\n // Why: first run fires in [intervalMs * 2/3, intervalMs) — late enough\n // that shadow sync never fires immediately on task startup, but always\n // before one full interval elapses so the canary completes at least once\n // per task lifetime (the replication manager is restarted every ~24h).\n // The last third is randomized so a fleet-wide restart does not cause\n // every task to canary simultaneously.\n const minFirstRunDelay = Math.floor((intervalMs * 2) / 3);\n const firstRunDelay =\n minFirstRunDelay +\n Math.floor(Math.random() * (intervalMs - minFirstRunDelay));\n this.#lc.info?.(\n `shadow-syncer started; first run in ${firstRunDelay} ms, then every ${intervalMs} ms`,\n );\n await this.#state.sleep(firstRunDelay);\n\n while (this.#state.shouldRun()) {\n const start = performance.now();\n try {\n await shadowInitialSync(\n this.#lc,\n this.#shard,\n this.#upstreamURI,\n {sampleRate, maxRowsPerTable},\n this.#context,\n textCopy !== undefined ? {textCopy} : undefined,\n );\n const elapsed = performance.now() - start;\n shadowSyncRuns().add(1, {result: 'success'});\n shadowSyncDuration().recordMs(elapsed, {result: 'success'});\n this.#lc.info?.(\n `shadow initial-sync completed (${elapsed.toFixed(0)} ms)`,\n );\n } catch (e) {\n const elapsed = performance.now() - start;\n shadowSyncRuns().add(1, {result: 'error'});\n shadowSyncDuration().recordMs(elapsed, {result: 'error'});\n this.#lc.error?.(\n `shadow initial-sync failed after ${elapsed.toFixed(0)} ms`,\n e,\n );\n }\n await this.#state.sleep(intervalMs);\n }\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n}\n"],"mappings":";;;;;AAYA,SAAS,iBAAiB;CACxB,OAAO,mBACL,eACA,oBACA,wDACF;AACF;AAEA,SAAS,qBAAqB;CAC5B,OAAO,4BACL,eACA,wBACA,sEACF;AACF;AASA,IAAa,oBAAb,MAAkD;CAChD,KAAc;CAEd;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,eAAe;CAElD,YACE,IACA,OACA,aACA,SACA,SACA;EACA,KAAK,MAAM;EACX,KAAK,SAAS;EACd,KAAK,eAAe;EACpB,KAAK,WAAW;EAChB,KAAK,WAAW;CAClB;CAEA,MAAM,MAAM;EACV,MAAM,EAAC,YAAY,YAAY,iBAAiB,aAAY,KAAK;EAQjE,MAAM,mBAAmB,KAAK,MAAO,aAAa,IAAK,CAAC;EACxD,MAAM,gBACJ,mBACA,KAAK,MAAM,KAAK,OAAO,KAAK,aAAa,iBAAiB;EAC5D,KAAK,IAAI,OACP,uCAAuC,cAAc,kBAAkB,WAAW,IACpF;EACA,MAAM,KAAK,OAAO,MAAM,aAAa;EAErC,OAAO,KAAK,OAAO,UAAU,GAAG;GAC9B,MAAM,QAAQ,YAAY,IAAI;GAC9B,IAAI;IACF,MAAM,kBACJ,KAAK,KACL,KAAK,QACL,KAAK,cACL;KAAC;KAAY;IAAe,GAC5B,KAAK,UACL,aAAa,KAAA,IAAY,EAAC,SAAQ,IAAI,KAAA,CACxC;IACA,MAAM,UAAU,YAAY,IAAI,IAAI;IACpC,eAAe,CAAC,CAAC,IAAI,GAAG,EAAC,QAAQ,UAAS,CAAC;IAC3C,mBAAmB,CAAC,CAAC,SAAS,SAAS,EAAC,QAAQ,UAAS,CAAC;IAC1D,KAAK,IAAI,OACP,kCAAkC,QAAQ,QAAQ,CAAC,EAAE,KACvD;GACF,SAAS,GAAG;IACV,MAAM,UAAU,YAAY,IAAI,IAAI;IACpC,eAAe,CAAC,CAAC,IAAI,GAAG,EAAC,QAAQ,QAAO,CAAC;IACzC,mBAAmB,CAAC,CAAC,SAAS,SAAS,EAAC,QAAQ,QAAO,CAAC;IACxD,KAAK,IAAI,QACP,oCAAoC,QAAQ,QAAQ,CAAC,EAAE,MACvD,CACF;GACF;GACA,MAAM,KAAK,OAAO,MAAM,UAAU;EACpC;CACF;CAEA,OAAsB;EACpB,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,OAAO;CACT;AACF"}
@@ -2,9 +2,9 @@ import { BigIntJSON } from "../../../shared/src/bigint-json.js";
2
2
  import { getShardID, upstreamSchema } from "../types/shards.js";
3
3
  import { getOperatorAccess } from "../config/zero-config.js";
4
4
  import { Database } from "../../../zqlite/src/db.js";
5
- import { getReplicationState } from "./replicator/schema/replication-state.js";
6
- import { StatementRunner } from "../db/statements.js";
7
5
  import { pgClient } from "../types/pg.js";
6
+ import { StatementRunner } from "../db/statements.js";
7
+ import { getReplicationState } from "./replicator/schema/replication-state.js";
8
8
  import { fromStateVersionString } from "./change-source/pg/lsn.js";
9
9
  import os from "os";
10
10
  import fs from "fs";
@@ -221,7 +221,8 @@ function printStats(group, statsObject) {
221
221
  * * `pretty`: formats the JSON object with indentation
222
222
  */
223
223
  async function handleStatzRequest(lc, config, req, res) {
224
- if (getOperatorAccess(lc, config, auth(req)?.pass) === void 0) {
224
+ const credentials = auth(req);
225
+ if (getOperatorAccess(lc, config, credentials?.pass) === void 0) {
225
226
  res.code(401).header("WWW-Authenticate", "Basic realm=\"Statz Protected Area\"").send("Unauthorized");
226
227
  return;
227
228
  }