@rocicorp/zero 1.11.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (737) hide show
  1. package/out/_virtual/{_@oxc-project_runtime@0.130.0/helpers → _@oxc-project_runtime@0.154.0/helpers/esm}/usingCtx.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js +2 -1
  3. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  4. package/out/analyze-query/src/bin-analyze.js +5 -3
  5. package/out/analyze-query/src/bin-analyze.js.map +1 -1
  6. package/out/analyze-query/src/bin-transform.js +3 -2
  7. package/out/analyze-query/src/bin-transform.js.map +1 -1
  8. package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
  9. package/out/ast-to-zql/src/bin.js +3 -2
  10. package/out/ast-to-zql/src/bin.js.map +1 -1
  11. package/out/ast-to-zql/src/format.js.map +1 -1
  12. package/out/datadog/src/datadog-log-sink.js +5 -4
  13. package/out/datadog/src/datadog-log-sink.js.map +1 -1
  14. package/out/otel/src/log-options.js.map +1 -1
  15. package/out/replicache/src/btree/node.js +13 -11
  16. package/out/replicache/src/btree/node.js.map +1 -1
  17. package/out/replicache/src/btree/read.js +4 -2
  18. package/out/replicache/src/btree/read.js.map +1 -1
  19. package/out/replicache/src/btree/splice.js.map +1 -1
  20. package/out/replicache/src/btree/write.js +9 -5
  21. package/out/replicache/src/btree/write.js.map +1 -1
  22. package/out/replicache/src/config.js +1 -2
  23. package/out/replicache/src/connection-loop.js.map +1 -1
  24. package/out/replicache/src/dag/chunk.js +1 -1
  25. package/out/replicache/src/dag/gc.js.map +1 -1
  26. package/out/replicache/src/dag/lazy-store.js +8 -7
  27. package/out/replicache/src/dag/lazy-store.js.map +1 -1
  28. package/out/replicache/src/dag/store-impl.js +1 -1
  29. package/out/replicache/src/dag/store-impl.js.map +1 -1
  30. package/out/replicache/src/dag/visitor.js.map +1 -1
  31. package/out/replicache/src/db/commit.js +5 -3
  32. package/out/replicache/src/db/commit.js.map +1 -1
  33. package/out/replicache/src/db/read.js +1 -1
  34. package/out/replicache/src/db/read.js.map +1 -1
  35. package/out/replicache/src/db/rebase.js.map +1 -1
  36. package/out/replicache/src/db/write.js +5 -2
  37. package/out/replicache/src/db/write.js.map +1 -1
  38. package/out/replicache/src/deleted-clients.js +3 -2
  39. package/out/replicache/src/deleted-clients.js.map +1 -1
  40. package/out/replicache/src/expo-sqlite.js +1 -2
  41. package/out/replicache/src/frozen-json.js +1 -1
  42. package/out/replicache/src/frozen-json.js.map +1 -1
  43. package/out/replicache/src/get-default-puller.js +1 -1
  44. package/out/replicache/src/hash.js +1 -1
  45. package/out/replicache/src/hash.js.map +1 -1
  46. package/out/replicache/src/impl.js +1 -2
  47. package/out/replicache/src/index-defs.js +5 -3
  48. package/out/replicache/src/index-defs.js.map +1 -1
  49. package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
  50. package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
  51. package/out/replicache/src/kv/idb-store.js +1 -1
  52. package/out/replicache/src/kv/idb-store.js.map +1 -1
  53. package/out/replicache/src/kv/mem-fallback-store.js +1 -1
  54. package/out/replicache/src/kv/mem-fallback-store.js.map +1 -1
  55. package/out/replicache/src/kv/mem-store.js.map +1 -1
  56. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  57. package/out/replicache/src/kv/op-sqlite/types.js.map +1 -1
  58. package/out/replicache/src/kv/read-impl.js.map +1 -1
  59. package/out/replicache/src/kv/sqlite-store.js +1 -1
  60. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  61. package/out/replicache/src/kv/write-impl-base.js +4 -1
  62. package/out/replicache/src/kv/write-impl-base.js.map +1 -1
  63. package/out/replicache/src/kv/write-impl.js.map +1 -1
  64. package/out/replicache/src/log-options.js +2 -1
  65. package/out/replicache/src/log-options.js.map +1 -1
  66. package/out/replicache/src/make-idb-name.js +1 -1
  67. package/out/replicache/src/new-client-channel.js +9 -7
  68. package/out/replicache/src/new-client-channel.js.map +1 -1
  69. package/out/replicache/src/on-persist-channel.js +1 -1
  70. package/out/replicache/src/on-persist-channel.js.map +1 -1
  71. package/out/replicache/src/op-sqlite.js +1 -2
  72. package/out/replicache/src/pending-mutations.js +2 -1
  73. package/out/replicache/src/pending-mutations.js.map +1 -1
  74. package/out/replicache/src/persist/client-gc.js +2 -2
  75. package/out/replicache/src/persist/client-gc.js.map +1 -1
  76. package/out/replicache/src/persist/client-group-gc.js +2 -2
  77. package/out/replicache/src/persist/client-group-gc.js.map +1 -1
  78. package/out/replicache/src/persist/client-groups.js +1 -1
  79. package/out/replicache/src/persist/client-groups.js.map +1 -1
  80. package/out/replicache/src/persist/clients.js +1 -1
  81. package/out/replicache/src/persist/clients.js.map +1 -1
  82. package/out/replicache/src/persist/collect-idb-databases.js +5 -4
  83. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  84. package/out/replicache/src/persist/gather-mem-only-visitor.js.map +1 -1
  85. package/out/replicache/src/persist/gather-not-cached-visitor.js.map +1 -1
  86. package/out/replicache/src/persist/heartbeat.js +2 -2
  87. package/out/replicache/src/persist/heartbeat.js.map +1 -1
  88. package/out/replicache/src/persist/idb-databases-store.js +1 -1
  89. package/out/replicache/src/persist/idb-databases-store.js.map +1 -1
  90. package/out/replicache/src/persist/make-client-id.js +1 -1
  91. package/out/replicache/src/persist/make-client-id.js.map +1 -1
  92. package/out/replicache/src/persist/persist.js +3 -2
  93. package/out/replicache/src/persist/persist.js.map +1 -1
  94. package/out/replicache/src/persist/refresh.js +8 -5
  95. package/out/replicache/src/persist/refresh.js.map +1 -1
  96. package/out/replicache/src/process-scheduler.js.map +1 -1
  97. package/out/replicache/src/replicache-impl.js +3 -3
  98. package/out/replicache/src/replicache-impl.js.map +1 -1
  99. package/out/replicache/src/scan-iterator.js.map +1 -1
  100. package/out/replicache/src/scan-options.js +7 -6
  101. package/out/replicache/src/scan-options.js.map +1 -1
  102. package/out/replicache/src/sqlite.js +1 -2
  103. package/out/replicache/src/subscriptions.js +1 -1
  104. package/out/replicache/src/subscriptions.js.map +1 -1
  105. package/out/replicache/src/sync/diff.js +3 -1
  106. package/out/replicache/src/sync/diff.js.map +1 -1
  107. package/out/replicache/src/sync/ids.js.map +1 -1
  108. package/out/replicache/src/sync/patch.js +2 -4
  109. package/out/replicache/src/sync/patch.js.map +1 -1
  110. package/out/replicache/src/sync/pull.js +3 -1
  111. package/out/replicache/src/sync/pull.js.map +1 -1
  112. package/out/replicache/src/sync/push.js.map +1 -1
  113. package/out/replicache/src/sync/request-id.js +1 -1
  114. package/out/replicache/src/sync/request-id.js.map +1 -1
  115. package/out/replicache/src/transactions.js +1 -1
  116. package/out/replicache/src/transactions.js.map +1 -1
  117. package/out/shared/src/arrays.js +4 -1
  118. package/out/shared/src/arrays.js.map +1 -1
  119. package/out/shared/src/asserts.js +1 -1
  120. package/out/shared/src/bigint-json.js.map +1 -1
  121. package/out/shared/src/broadcast-channel.js +1 -1
  122. package/out/shared/src/btree-set.js +11 -8
  123. package/out/shared/src/btree-set.js.map +1 -1
  124. package/out/shared/src/cache.js.map +1 -1
  125. package/out/shared/src/config.js +1 -1
  126. package/out/shared/src/custom-key-map.js.map +1 -1
  127. package/out/shared/src/custom-key-set.js.map +1 -1
  128. package/out/shared/src/deep-clone.js +1 -1
  129. package/out/shared/src/document-visible.js.map +1 -1
  130. package/out/shared/src/dotenv.js.map +1 -1
  131. package/out/shared/src/error.js +1 -1
  132. package/out/shared/src/error.js.map +1 -1
  133. package/out/shared/src/hash.js +2 -2
  134. package/out/shared/src/hash.js.map +1 -1
  135. package/out/shared/src/iterables.js.map +1 -1
  136. package/out/shared/src/json-schema.js.map +1 -1
  137. package/out/shared/src/json.js.map +1 -1
  138. package/out/shared/src/logging-test-utils.js +1 -1
  139. package/out/shared/src/logging.js +1 -1
  140. package/out/shared/src/logging.js.map +1 -1
  141. package/out/shared/src/navigator.js +1 -1
  142. package/out/shared/src/options.js +9 -12
  143. package/out/shared/src/options.js.map +1 -1
  144. package/out/shared/src/promise-race.js.map +1 -1
  145. package/out/shared/src/queue.js.map +1 -1
  146. package/out/shared/src/record-proxy.js.map +1 -1
  147. package/out/shared/src/ring-buffer.js.map +1 -1
  148. package/out/shared/src/size-of-value.js +7 -10
  149. package/out/shared/src/size-of-value.js.map +1 -1
  150. package/out/shared/src/sorted-entries.js.map +1 -1
  151. package/out/shared/src/tdigest-schema.js.map +1 -1
  152. package/out/shared/src/tdigest.js.map +1 -1
  153. package/out/shared/src/valita.js +1 -1
  154. package/out/shared/src/valita.js.map +1 -1
  155. package/out/z2s/src/compiler.js +2 -4
  156. package/out/z2s/src/compiler.js.map +1 -1
  157. package/out/z2s/src/sql.js +1 -1
  158. package/out/z2s/src/sql.js.map +1 -1
  159. package/out/zero/package.js +212 -189
  160. package/out/zero/package.js.map +1 -1
  161. package/out/zero/src/adapters/drizzle.js +1 -2
  162. package/out/zero/src/adapters/kysely.js +1 -2
  163. package/out/zero/src/adapters/pg.js +1 -2
  164. package/out/zero/src/adapters/postgresjs.js +1 -2
  165. package/out/zero/src/adapters/prisma.js +1 -2
  166. package/out/zero/src/analyze.js +1 -2
  167. package/out/zero/src/change-protocol/v0.js +1 -2
  168. package/out/zero/src/expo-sqlite.js +1 -2
  169. package/out/zero/src/op-sqlite.js +1 -2
  170. package/out/zero/src/react-native.js.map +1 -1
  171. package/out/zero/src/sqlite.js +1 -2
  172. package/out/zero/src/zero-cache-dev.js +10 -9
  173. package/out/zero/src/zero-cache-dev.js.map +1 -1
  174. package/out/zero/src/zero-out.js +2 -1
  175. package/out/zero/src/zero-out.js.map +1 -1
  176. package/out/zero-cache/src/auth/auth.js +1 -1
  177. package/out/zero-cache/src/auth/auth.js.map +1 -1
  178. package/out/zero-cache/src/auth/jwt.js +4 -1
  179. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  180. package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
  181. package/out/zero-cache/src/auth/read-authorizer.js +3 -2
  182. package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
  183. package/out/zero-cache/src/auth/write-authorizer.js +5 -16
  184. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  185. package/out/zero-cache/src/config/network.js +1 -1
  186. package/out/zero-cache/src/config/network.js.map +1 -1
  187. package/out/zero-cache/src/config/server-context.js.map +1 -1
  188. package/out/zero-cache/src/config/zero-config.js +2 -2
  189. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  190. package/out/zero-cache/src/custom/fetch.js +1 -1
  191. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  192. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  193. package/out/zero-cache/src/db/create.js.map +1 -1
  194. package/out/zero-cache/src/db/index-predicate.js.map +1 -1
  195. package/out/zero-cache/src/db/lite-tables.js.map +1 -1
  196. package/out/zero-cache/src/db/migration-lite.js +1 -1
  197. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  198. package/out/zero-cache/src/db/migration.js +5 -3
  199. package/out/zero-cache/src/db/migration.js.map +1 -1
  200. package/out/zero-cache/src/db/pg-copy-binary.js +4 -3
  201. package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
  202. package/out/zero-cache/src/db/pg-copy.js +0 -1
  203. package/out/zero-cache/src/db/pg-copy.js.map +1 -1
  204. package/out/zero-cache/src/db/pg-to-lite.js +1 -1
  205. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  206. package/out/zero-cache/src/db/run-transaction.js.map +1 -1
  207. package/out/zero-cache/src/db/specs.js +7 -5
  208. package/out/zero-cache/src/db/specs.js.map +1 -1
  209. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
  210. package/out/zero-cache/src/db/transaction-pool.js +5 -3
  211. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  212. package/out/zero-cache/src/db/warmup.js.map +1 -1
  213. package/out/zero-cache/src/observability/events.js.map +1 -1
  214. package/out/zero-cache/src/observability/log-throttle.js.map +1 -1
  215. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  216. package/out/zero-cache/src/scripts/deploy-permissions.js +4 -2
  217. package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
  218. package/out/zero-cache/src/scripts/permissions.js.map +1 -1
  219. package/out/zero-cache/src/server/anonymous-otel-start.js +2 -1
  220. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  221. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  222. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  223. package/out/zero-cache/src/server/logging.js +1 -1
  224. package/out/zero-cache/src/server/logging.js.map +1 -1
  225. package/out/zero-cache/src/server/main.js.map +1 -1
  226. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  227. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  228. package/out/zero-cache/src/server/reaper.js +2 -2
  229. package/out/zero-cache/src/server/reaper.js.map +1 -1
  230. package/out/zero-cache/src/server/replicator.js.map +1 -1
  231. package/out/zero-cache/src/server/runner/run-worker.js +2 -1
  232. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  233. package/out/zero-cache/src/server/runner/runtime.js +2 -1
  234. package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
  235. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  236. package/out/zero-cache/src/server/shadow-syncer.js +1 -1
  237. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  238. package/out/zero-cache/src/server/slot-keeper.js.map +1 -1
  239. package/out/zero-cache/src/server/syncer-assigner.js.map +1 -1
  240. package/out/zero-cache/src/server/syncer.js.map +1 -1
  241. package/out/zero-cache/src/server/worker-dispatcher.js +1 -1
  242. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  243. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  244. package/out/zero-cache/src/services/analyze.js +1 -1
  245. package/out/zero-cache/src/services/analyze.js.map +1 -1
  246. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +1 -1
  247. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  248. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  249. package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -1
  250. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
  251. package/out/zero-cache/src/services/change-source/common/replica-schema.js +3 -3
  252. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  253. package/out/zero-cache/src/services/change-source/custom/change-source.js +1 -1
  254. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  255. package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
  256. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +3 -3
  257. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  258. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +1 -1
  259. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
  260. package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -6
  261. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  262. package/out/zero-cache/src/services/change-source/pg/decommission.js +2 -1
  263. package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
  264. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +5 -3
  265. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  266. package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
  267. package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
  268. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +3 -2
  269. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  270. package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
  271. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js +2 -2
  272. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js.map +1 -1
  273. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js +1 -1
  274. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js.map +1 -1
  275. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +3 -3
  276. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  277. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +1 -1
  278. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  279. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +13 -12
  280. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
  281. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  282. package/out/zero-cache/src/services/change-source/pg/schema/published.js +1 -1
  283. package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
  284. package/out/zero-cache/src/services/change-source/pg/schema/replica-stage-enum.js.map +1 -1
  285. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +9 -6
  286. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  287. package/out/zero-cache/src/services/change-source/pg/schema/validation.js +1 -1
  288. package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
  289. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +1 -1
  290. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -1
  291. package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
  292. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  293. package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
  294. package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
  295. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  296. package/out/zero-cache/src/services/change-source/protocol/current.js +1 -1
  297. package/out/zero-cache/src/services/change-source/protocol/mod.js +1 -2
  298. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
  299. package/out/zero-cache/src/services/change-streamer/backfill-state.js +1 -1
  300. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -1
  301. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  302. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
  303. package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
  304. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  305. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +2 -2
  306. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
  307. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -1
  308. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +4 -2
  309. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  310. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +21 -21
  311. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  312. package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
  313. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  314. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
  315. package/out/zero-cache/src/services/change-streamer/forwarder.js +11 -10
  316. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  317. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js +1 -1
  318. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js.map +1 -1
  319. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +3 -3
  320. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
  321. package/out/zero-cache/src/services/change-streamer/replica-poller.js +1 -1
  322. package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -1
  323. package/out/zero-cache/src/services/change-streamer/restore-admission.js +12 -1
  324. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
  325. package/out/zero-cache/src/services/change-streamer/schema/init.js +1 -1
  326. package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
  327. package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
  328. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  329. package/out/zero-cache/src/services/change-streamer/snapshot-message.js +1 -1
  330. package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
  331. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  332. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +1 -1
  333. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
  334. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js +1 -1
  335. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js.map +1 -1
  336. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -1
  337. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
  338. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +1 -1
  339. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -1
  340. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +1 -3
  341. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
  342. package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
  343. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  344. package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
  345. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  346. package/out/zero-cache/src/services/change-streamer/subscriber.js +2 -2
  347. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  348. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +0 -1
  349. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
  350. package/out/zero-cache/src/services/heapz.js +2 -1
  351. package/out/zero-cache/src/services/heapz.js.map +1 -1
  352. package/out/zero-cache/src/services/http-service.js.map +1 -1
  353. package/out/zero-cache/src/services/life-cycle.js +1 -1
  354. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  355. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js +4 -2
  356. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
  357. package/out/zero-cache/src/services/litestream/commands.js +1 -1
  358. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  359. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +2 -2
  360. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
  361. package/out/zero-cache/src/services/litestream/litestream-controller.js.map +1 -1
  362. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  363. package/out/zero-cache/src/services/litestream/restore-progress.js.map +1 -1
  364. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  365. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  366. package/out/zero-cache/src/services/mutagen/pusher.js +3 -2
  367. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  368. package/out/zero-cache/src/services/plannerz.js +4 -3
  369. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  370. package/out/zero-cache/src/services/profz.js +5 -3
  371. package/out/zero-cache/src/services/profz.js.map +1 -1
  372. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  373. package/out/zero-cache/src/services/replicator/change-log-db.js +3 -2
  374. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
  375. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
  376. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
  377. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  378. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
  379. package/out/zero-cache/src/services/replicator/incremental-sync.js +1 -1
  380. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  381. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  382. package/out/zero-cache/src/services/replicator/replication-status.js +4 -3
  383. package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
  384. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  385. package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
  386. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  387. package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
  388. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
  389. package/out/zero-cache/src/services/replicator/schema/backfilling.js +1 -1
  390. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  391. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  392. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  393. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  394. package/out/zero-cache/src/services/replicator/schema/replication-state.js +10 -7
  395. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  396. package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
  397. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +1 -1
  398. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
  399. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +1 -1
  400. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -1
  401. package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
  402. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  403. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  404. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  405. package/out/zero-cache/src/services/runner.js.map +1 -1
  406. package/out/zero-cache/src/services/running-state.js.map +1 -1
  407. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  408. package/out/zero-cache/src/services/statz.js +2 -1
  409. package/out/zero-cache/src/services/statz.js.map +1 -1
  410. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +2 -2
  411. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
  412. package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -1
  413. package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -1
  414. package/out/zero-cache/src/services/view-syncer/client-handler.js +4 -2
  415. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  416. package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
  417. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  418. package/out/zero-cache/src/services/view-syncer/cvr-purger.js +2 -2
  419. package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
  420. package/out/zero-cache/src/services/view-syncer/cvr-store.js +9 -5
  421. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  422. package/out/zero-cache/src/services/view-syncer/cvr.js +3 -2
  423. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  424. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +1 -1
  425. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -1
  426. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  427. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -1
  428. package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -1
  429. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +2 -2
  430. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -1
  431. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +5 -3
  432. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  433. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +6 -4
  434. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  435. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -1
  436. package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -1
  437. package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -1
  438. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
  439. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -1
  440. package/out/zero-cache/src/services/view-syncer/query-stats.js +1 -1
  441. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
  442. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  443. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
  444. package/out/zero-cache/src/services/view-syncer/schema/types.js +1 -1
  445. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  446. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js +7 -1
  447. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js.map +1 -1
  448. package/out/zero-cache/src/services/view-syncer/snapshotter.js +3 -2
  449. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  450. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
  451. package/out/zero-cache/src/services/view-syncer/view-syncer.js +4 -6
  452. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  453. package/out/zero-cache/src/types/error-with-level.js.map +1 -1
  454. package/out/zero-cache/src/types/lexi-version.js.map +1 -1
  455. package/out/zero-cache/src/types/lite.js +1 -1
  456. package/out/zero-cache/src/types/lite.js.map +1 -1
  457. package/out/zero-cache/src/types/pg-data-type.js +1 -1
  458. package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
  459. package/out/zero-cache/src/types/pg.js +6 -4
  460. package/out/zero-cache/src/types/pg.js.map +1 -1
  461. package/out/zero-cache/src/types/poke-chunk.js +2 -2
  462. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  463. package/out/zero-cache/src/types/processes.js +1 -1
  464. package/out/zero-cache/src/types/processes.js.map +1 -1
  465. package/out/zero-cache/src/types/profiler.js.map +1 -1
  466. package/out/zero-cache/src/types/row-key.js.map +1 -1
  467. package/out/zero-cache/src/types/sql.js.map +1 -1
  468. package/out/zero-cache/src/types/streams.js +10 -9
  469. package/out/zero-cache/src/types/streams.js.map +1 -1
  470. package/out/zero-cache/src/types/strings.js.map +1 -1
  471. package/out/zero-cache/src/types/subscription.js.map +1 -1
  472. package/out/zero-cache/src/types/timeout.js +1 -1
  473. package/out/zero-cache/src/types/websocket-handoff.js +1 -1
  474. package/out/zero-cache/src/types/ws.js +1 -1
  475. package/out/zero-cache/src/workers/connection.js +2 -3
  476. package/out/zero-cache/src/workers/connection.js.map +1 -1
  477. package/out/zero-cache/src/workers/mutator.js.map +1 -1
  478. package/out/zero-cache/src/workers/replicator.js +5 -4
  479. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  480. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  481. package/out/zero-cache/src/workers/syncer.js +2 -2
  482. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  483. package/out/zero-client/src/client/active-clients-manager.js +1 -1
  484. package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
  485. package/out/zero-client/src/client/bindings.js +11 -11
  486. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  487. package/out/zero-client/src/client/connection.js +1 -1
  488. package/out/zero-client/src/client/connection.js.map +1 -1
  489. package/out/zero-client/src/client/context.js.map +1 -1
  490. package/out/zero-client/src/client/crud-impl.js +1 -1
  491. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  492. package/out/zero-client/src/client/crud.js +1 -1
  493. package/out/zero-client/src/client/crud.js.map +1 -1
  494. package/out/zero-client/src/client/custom.js +1 -1
  495. package/out/zero-client/src/client/custom.js.map +1 -1
  496. package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
  497. package/out/zero-client/src/client/enable-analytics.js +1 -1
  498. package/out/zero-client/src/client/enable-analytics.js.map +1 -1
  499. package/out/zero-client/src/client/error.js.map +1 -1
  500. package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
  501. package/out/zero-client/src/client/inspector/client.js.map +1 -1
  502. package/out/zero-client/src/client/inspector/html-dialog-prompt.js +1 -1
  503. package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
  504. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  505. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  506. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  507. package/out/zero-client/src/client/ivm-branch.js +1 -3
  508. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  509. package/out/zero-client/src/client/keys.js +2 -1
  510. package/out/zero-client/src/client/keys.js.map +1 -1
  511. package/out/zero-client/src/client/log-options.js.map +1 -1
  512. package/out/zero-client/src/client/make-replicache-mutators.js +5 -3
  513. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  514. package/out/zero-client/src/client/metrics.js +3 -5
  515. package/out/zero-client/src/client/metrics.js.map +1 -1
  516. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  517. package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
  518. package/out/zero-client/src/client/options.js.map +1 -1
  519. package/out/zero-client/src/client/query-manager.js +2 -2
  520. package/out/zero-client/src/client/query-manager.js.map +1 -1
  521. package/out/zero-client/src/client/reload-error-handler.js +4 -3
  522. package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
  523. package/out/zero-client/src/client/version.js +1 -1
  524. package/out/zero-client/src/client/zero-poke-handler.js +1 -1
  525. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  526. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  527. package/out/zero-client/src/client/zero.js +19 -11
  528. package/out/zero-client/src/client/zero.js.map +1 -1
  529. package/out/zero-client/src/mod.js +23 -23
  530. package/out/zero-client/src/util/nanoid.js.map +1 -1
  531. package/out/zero-permissions/src/permissions.js +3 -2
  532. package/out/zero-permissions/src/permissions.js.map +1 -1
  533. package/out/zero-pg/src/mod.js +9 -9
  534. package/out/zero-protocol/src/analyze-query-result.js +1 -1
  535. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  536. package/out/zero-protocol/src/application-error.js.map +1 -1
  537. package/out/zero-protocol/src/ast.js +19 -9
  538. package/out/zero-protocol/src/ast.js.map +1 -1
  539. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  540. package/out/zero-protocol/src/client-schema.js +1 -1
  541. package/out/zero-protocol/src/client-schema.js.map +1 -1
  542. package/out/zero-protocol/src/close-connection.js +1 -1
  543. package/out/zero-protocol/src/close-connection.js.map +1 -1
  544. package/out/zero-protocol/src/connect.js +1 -1
  545. package/out/zero-protocol/src/connect.js.map +1 -1
  546. package/out/zero-protocol/src/custom-queries.js +1 -1
  547. package/out/zero-protocol/src/custom-queries.js.map +1 -1
  548. package/out/zero-protocol/src/data.js +4 -2
  549. package/out/zero-protocol/src/data.js.map +1 -1
  550. package/out/zero-protocol/src/delete-clients.js.map +1 -1
  551. package/out/zero-protocol/src/down.js.map +1 -1
  552. package/out/zero-protocol/src/error.js.map +1 -1
  553. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  554. package/out/zero-protocol/src/inspect-down.js +1 -1
  555. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  556. package/out/zero-protocol/src/inspect-up.js +1 -1
  557. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  558. package/out/zero-protocol/src/mutate-server.js +1 -1
  559. package/out/zero-protocol/src/mutate-server.js.map +1 -1
  560. package/out/zero-protocol/src/mutation-id.js.map +1 -1
  561. package/out/zero-protocol/src/mutation.js +1 -1
  562. package/out/zero-protocol/src/mutation.js.map +1 -1
  563. package/out/zero-protocol/src/mutations-patch.js +1 -1
  564. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  565. package/out/zero-protocol/src/ping.js +1 -1
  566. package/out/zero-protocol/src/ping.js.map +1 -1
  567. package/out/zero-protocol/src/poke.js +1 -1
  568. package/out/zero-protocol/src/poke.js.map +1 -1
  569. package/out/zero-protocol/src/pong.js +1 -1
  570. package/out/zero-protocol/src/pong.js.map +1 -1
  571. package/out/zero-protocol/src/primary-key.js.map +1 -1
  572. package/out/zero-protocol/src/protocol-version.js +1 -0
  573. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  574. package/out/zero-protocol/src/pull.js +1 -1
  575. package/out/zero-protocol/src/pull.js.map +1 -1
  576. package/out/zero-protocol/src/push.js +1 -1
  577. package/out/zero-protocol/src/push.js.map +1 -1
  578. package/out/zero-protocol/src/queries-patch.js +1 -1
  579. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  580. package/out/zero-protocol/src/query-hash-visitor.js.map +1 -1
  581. package/out/zero-protocol/src/query-hash.js +2 -2
  582. package/out/zero-protocol/src/query-server.js +1 -1
  583. package/out/zero-protocol/src/query-server.js.map +1 -1
  584. package/out/zero-protocol/src/row-patch.js.map +1 -1
  585. package/out/zero-protocol/src/up.js.map +1 -1
  586. package/out/zero-protocol/src/update-auth.js.map +1 -1
  587. package/out/zero-protocol/src/version.js.map +1 -1
  588. package/out/zero-react/src/bindings.js +11 -11
  589. package/out/zero-react/src/mod.js +5 -5
  590. package/out/zero-react/src/use-query.js +1 -1
  591. package/out/zero-react/src/use-query.js.map +1 -1
  592. package/out/zero-react/src/zero-provider.d.ts +1 -1
  593. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  594. package/out/zero-react/src/zero-provider.js +2 -1
  595. package/out/zero-react/src/zero-provider.js.map +1 -1
  596. package/out/zero-react/src/zero.js +23 -23
  597. package/out/zero-schema/src/builder/schema-builder.js +3 -2
  598. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  599. package/out/zero-schema/src/builder/table-builder.js +1 -1
  600. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  601. package/out/zero-schema/src/compiled-permissions.js.map +1 -1
  602. package/out/zero-schema/src/name-mapper.js +7 -5
  603. package/out/zero-schema/src/name-mapper.js.map +1 -1
  604. package/out/zero-schema/src/schema-config.js +1 -1
  605. package/out/zero-schema/src/schema-config.js.map +1 -1
  606. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  607. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  608. package/out/zero-server/src/adapters/pg.js.map +1 -1
  609. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  610. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  611. package/out/zero-server/src/closable-db-transaction.js.map +1 -1
  612. package/out/zero-server/src/custom.js +1 -1
  613. package/out/zero-server/src/custom.js.map +1 -1
  614. package/out/zero-server/src/mod.js +8 -8
  615. package/out/zero-server/src/process-mutations.js.map +1 -1
  616. package/out/zero-server/src/push-processor.js.map +1 -1
  617. package/out/zero-server/src/queries/process-queries.js +2 -1
  618. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  619. package/out/zero-server/src/schema.js +7 -7
  620. package/out/zero-server/src/schema.js.map +1 -1
  621. package/out/zero-server/src/zql-database.js.map +1 -1
  622. package/out/zero-solid/src/bindings.js +11 -11
  623. package/out/zero-solid/src/mod.js +5 -5
  624. package/out/zero-solid/src/solid-view.js +1 -1
  625. package/out/zero-solid/src/solid-view.js.map +1 -1
  626. package/out/zero-solid/src/use-connection-state.js +2 -1
  627. package/out/zero-solid/src/use-connection-state.js.map +1 -1
  628. package/out/zero-solid/src/use-query.js.map +1 -1
  629. package/out/zero-solid/src/use-zero-online.js +2 -1
  630. package/out/zero-solid/src/use-zero-online.js.map +1 -1
  631. package/out/zero-solid/src/use-zero.js +3 -2
  632. package/out/zero-solid/src/use-zero.js.map +1 -1
  633. package/out/zero-solid/src/zero.js +23 -23
  634. package/out/zero-types/src/name-mapper.js.map +1 -1
  635. package/out/zql/src/builder/builder.js +15 -13
  636. package/out/zql/src/builder/builder.js.map +1 -1
  637. package/out/zql/src/builder/correlated-predicate-pushdown.js.map +1 -1
  638. package/out/zql/src/builder/debug-delegate.js.map +1 -1
  639. package/out/zql/src/builder/like.js +0 -1
  640. package/out/zql/src/builder/like.js.map +1 -1
  641. package/out/zql/src/ivm/array-view.js.map +1 -1
  642. package/out/zql/src/ivm/cap.js.map +1 -1
  643. package/out/zql/src/ivm/constraint.js +1 -1
  644. package/out/zql/src/ivm/constraint.js.map +1 -1
  645. package/out/zql/src/ivm/data.js.map +1 -1
  646. package/out/zql/src/ivm/deferred-input.js.map +1 -1
  647. package/out/zql/src/ivm/exists.js +20 -18
  648. package/out/zql/src/ivm/exists.js.map +1 -1
  649. package/out/zql/src/ivm/fan-in.js.map +1 -1
  650. package/out/zql/src/ivm/fan-out.js.map +1 -1
  651. package/out/zql/src/ivm/filter-operators.js +1 -1
  652. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  653. package/out/zql/src/ivm/filter-push.js.map +1 -1
  654. package/out/zql/src/ivm/filter.js.map +1 -1
  655. package/out/zql/src/ivm/flipped-join.js +1 -3
  656. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  657. package/out/zql/src/ivm/join-index-tap.js.map +1 -1
  658. package/out/zql/src/ivm/join-index.js.map +1 -1
  659. package/out/zql/src/ivm/join-utils.js +11 -13
  660. package/out/zql/src/ivm/join-utils.js.map +1 -1
  661. package/out/zql/src/ivm/join.js.map +1 -1
  662. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
  663. package/out/zql/src/ivm/memory-source.js +26 -22
  664. package/out/zql/src/ivm/memory-source.js.map +1 -1
  665. package/out/zql/src/ivm/memory-storage.js.map +1 -1
  666. package/out/zql/src/ivm/operator.js +2 -2
  667. package/out/zql/src/ivm/push-accumulated.js +0 -1
  668. package/out/zql/src/ivm/push-accumulated.js.map +1 -1
  669. package/out/zql/src/ivm/skip-yields.js.map +1 -1
  670. package/out/zql/src/ivm/skip.js.map +1 -1
  671. package/out/zql/src/ivm/take-gate.js.map +1 -1
  672. package/out/zql/src/ivm/take.js +1 -1
  673. package/out/zql/src/ivm/take.js.map +1 -1
  674. package/out/zql/src/ivm/union-fan-in.js +1 -1
  675. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  676. package/out/zql/src/ivm/union-fan-out.js.map +1 -1
  677. package/out/zql/src/ivm/view-apply-change.js +1 -1
  678. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  679. package/out/zql/src/mutate/crud.js +3 -2
  680. package/out/zql/src/mutate/crud.js.map +1 -1
  681. package/out/zql/src/planner/planner-builder.js +1 -1
  682. package/out/zql/src/planner/planner-builder.js.map +1 -1
  683. package/out/zql/src/planner/planner-connection.js.map +1 -1
  684. package/out/zql/src/planner/planner-debug.js.map +1 -1
  685. package/out/zql/src/planner/planner-fan-in.js.map +1 -1
  686. package/out/zql/src/planner/planner-fan-out.js.map +1 -1
  687. package/out/zql/src/planner/planner-filter.js.map +1 -1
  688. package/out/zql/src/planner/planner-graph.js +1 -5
  689. package/out/zql/src/planner/planner-graph.js.map +1 -1
  690. package/out/zql/src/planner/planner-join.js +1 -1
  691. package/out/zql/src/planner/planner-join.js.map +1 -1
  692. package/out/zql/src/planner/planner-source.js.map +1 -1
  693. package/out/zql/src/planner/planner-terminus.js.map +1 -1
  694. package/out/zql/src/query/create-builder.js +2 -1
  695. package/out/zql/src/query/create-builder.js.map +1 -1
  696. package/out/zql/src/query/expression.js +1 -1
  697. package/out/zql/src/query/expression.js.map +1 -1
  698. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  699. package/out/zql/src/query/named.js +2 -1
  700. package/out/zql/src/query/named.js.map +1 -1
  701. package/out/zql/src/query/query-delegate-base.js +1 -1
  702. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  703. package/out/zql/src/query/query-impl.js +3 -2
  704. package/out/zql/src/query/query-impl.js.map +1 -1
  705. package/out/zql/src/query/query-registry.js +1 -1
  706. package/out/zql/src/query/query-registry.js.map +1 -1
  707. package/out/zql/src/query/query-transitions.js +1 -1
  708. package/out/zql/src/query/query-transitions.js.map +1 -1
  709. package/out/zql/src/query/runnable-query-impl.js +3 -2
  710. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  711. package/out/zql/src/query/static-query.js.map +1 -1
  712. package/out/zql/src/query/ttl.js +8 -8
  713. package/out/zql/src/query/ttl.js.map +1 -1
  714. package/out/zql/src/query/validate-input.js.map +1 -1
  715. package/out/zqlite/src/database-storage.js +2 -2
  716. package/out/zqlite/src/database-storage.js.map +1 -1
  717. package/out/zqlite/src/db.js +2 -2
  718. package/out/zqlite/src/db.js.map +1 -1
  719. package/out/zqlite/src/explain-queries.js.map +1 -1
  720. package/out/zqlite/src/index-usage.js.map +1 -1
  721. package/out/zqlite/src/internal/sql-inline.js.map +1 -1
  722. package/out/zqlite/src/internal/sql.js.map +1 -1
  723. package/out/zqlite/src/internal/statement-cache.js +1 -1
  724. package/out/zqlite/src/internal/statement-cache.js.map +1 -1
  725. package/out/zqlite/src/mod.js +5 -5
  726. package/out/zqlite/src/pending-delta.js.map +1 -1
  727. package/out/zqlite/src/query-builder.js +1 -1
  728. package/out/zqlite/src/query-builder.js.map +1 -1
  729. package/out/zqlite/src/query-delegate.js.map +1 -1
  730. package/out/zqlite/src/resolve-scalar-subqueries.js +2 -5
  731. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  732. package/out/zqlite/src/sqlite-cost-model.js +3 -2
  733. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  734. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  735. package/out/zqlite/src/table-source.js +1 -1
  736. package/out/zqlite/src/table-source.js.map +1 -1
  737. package/package.json +16 -14
@@ -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 +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":"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"}
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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"}
@@ -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"}
@@ -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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"statz.js","names":[],"sources":["../../../../../zero-cache/src/services/statz.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {BigIntJSON} from '../../../shared/src/bigint-json.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {NormalizedZeroConfig as ZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {StatementRunner} from '../db/statements.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {getShardID, upstreamSchema} from '../types/shards.ts';\nimport {fromStateVersionString} from './change-source/pg/lsn.ts';\nimport {getReplicationState} from './replicator/schema/replication-state.ts';\n\nasync function upstreamStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config));\n const sql = pgClient(lc, config.upstream.db, 'statz-upstream');\n try {\n return await getPgStats([\n [\n 'numReplicas',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"replicas\"`,\n ],\n [\n 'numClientsWithMutations',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n [\n 'numMutationsProcessed',\n sql`SELECT SUM(\"lastMutationID\") as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\nasync function cvrStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cvr';\n const sql = pgClient(lc, config.cvr.db, 'statz-cvr');\n\n function numQueriesPerClientGroup(\n active: boolean,\n ): ReturnType<ReturnType<typeof pgClient>> {\n const filter = active\n ? sql`WHERE \"inactivatedAt\" IS NULL AND deleted = false`\n : sql`WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`;\n return sql`WITH\n group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(*) AS num_queries\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n ),\n -- Count distinct clientIDs per clientGroupID\n client_per_group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(DISTINCT \"clientID\") AS num_clients\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n )\n -- Combine all the information\n SELECT\n g.\"clientGroupID\",\n cpg.num_clients,\n g.num_queries\n FROM group_counts g\n JOIN client_per_group_counts cpg ON g.\"clientGroupID\" = cpg.\"clientGroupID\"\n ORDER BY g.num_queries DESC;`;\n }\n\n try {\n return await getPgStats([\n [\n 'totalNumQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\"`,\n ],\n [\n 'numUniqueQueryHashes',\n sql`SELECT COUNT(DISTINCT \"queryHash\") as \"c\" FROM ${sql(\n schema,\n )}.\"desires\"`,\n ],\n [\n 'numActiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NULL AND \"deleted\" = false`,\n ],\n [\n 'numInactiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`,\n ],\n [\n 'numDeletedQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"deleted\" = true`,\n ],\n [\n 'freshQueriesPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS fresh_count\n FROM ${sql(schema)}.\"desires\"\n WHERE\n (\"inactivatedAt\" IS NOT NULL\n AND (\"inactivatedAt\" + \"ttl\") > NOW()) OR (\"inactivatedAt\" IS NULL\n AND deleted = false)\n GROUP BY \"clientGroupID\"\n )\n\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS \"p99\",\n MIN(fresh_count) AS \"min\",\n MAX(fresh_count) AS \"max\",\n AVG(fresh_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n 'rowsPerClientGroupPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\"\n GROUP BY \"clientGroupID\"\n )\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS \"p99\",\n MIN(row_count) AS \"min\",\n MAX(row_count) AS \"max\",\n AVG(row_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n // check for AST blowup due to DNF conversion.\n 'astSizes',\n sql`SELECT\n percentile_cont(0.25) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"25th_percentile\",\n percentile_cont(0.5) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"50th_percentile\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"75th_percentile\",\n percentile_cont(0.9) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"90th_percentile\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"95th_percentile\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"99th_percentile\",\n MIN(length(\"clientAST\"::text)) AS \"minimum_length\",\n MAX(length(\"clientAST\"::text)) AS \"maximum_length\",\n AVG(length(\"clientAST\"::text))::integer AS \"average_length\",\n COUNT(*) AS \"total_records\"\n FROM ${sql(schema)}.\"queries\";`,\n ],\n [\n // output the hash of the largest AST\n 'biggestAstHash',\n sql`SELECT \"queryHash\", length(\"clientAST\"::text) AS \"ast_length\"\n FROM ${sql(schema)}.\"queries\"\n ORDER BY length(\"clientAST\"::text) DESC\n LIMIT 1;`,\n ],\n [\n 'totalActiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(true),\n ],\n [\n 'totalInactiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(false),\n ],\n [\n 'totalRowsPerClientGroup',\n sql`SELECT \"clientGroupID\", COUNT(*) as \"c\" FROM ${sql(\n schema,\n )}.\"rows\" GROUP BY \"clientGroupID\" ORDER BY \"c\" DESC`,\n ],\n [\n 'numRowsPerQuery',\n sql`SELECT\n k.key AS \"queryHash\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\" r,\n LATERAL jsonb_each(r.\"refCounts\") k\n GROUP BY k.key\n ORDER BY row_count DESC;`,\n ],\n ] satisfies [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][]);\n } finally {\n await sql.end();\n }\n}\n\nasync function changeLogStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cdc';\n const sql = pgClient(lc, config.change.db, 'statz-change');\n\n try {\n return await getPgStats([\n [\n 'changeLogSize',\n sql`SELECT COUNT(*) as \"change_log_size\" FROM ${sql(schema)}.\"changeLog\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\n// The replica is opened read-only: /statz is available to the operator\n// password, and must not change the replica. Among other things, a writable\n// connection would run `PRAGMA optimize` when closed.\nfunction replicaStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return Object.fromEntries([\n ['wal file sizes', walFileSizes(config.replica.file)],\n ['page count', pick(first(db.pragma('PAGE_COUNT')))],\n ['page size', pick(first(db.pragma('PAGE_SIZE')))],\n ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],\n ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],\n ['cache size', pick(first(db.pragma('CACHE_SIZE')))],\n ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],\n ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],\n ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],\n ['db file stats', fs.statSync(config.replica.file)],\n ] as const);\n } finally {\n db.close();\n }\n}\n\nfunction replicationStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return getReplicationStats(db);\n } finally {\n db.close();\n }\n}\n\n/**\n * The sizes of the replica's WAL files, by suffix, read from the filesystem.\n * `PRAGMA wal_checkpoint` would report the WAL's frame counts, but it runs a\n * checkpoint to do so, and fails on a read-only connection in WAL mode.\n */\nfunction walFileSizes(file: string): Record<string, number> {\n const sizes: Record<string, number> = {};\n // `-wal` is used in WAL mode, and both files in WAL2 mode.\n for (const suffix of ['-wal', '-wal2']) {\n const stats = fs.statSync(file + suffix, {throwIfNoEntry: false});\n if (stats) {\n sizes[suffix] = stats.size;\n }\n }\n return sizes;\n}\n\nfunction getReplicationStats(db: Database) {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n const lsn = fromStateVersionString(stateVersion);\n return {lsn};\n}\n\nfunction osStats() {\n return Object.fromEntries([\n ['load avg', os.loadavg()],\n ['uptime', os.uptime()],\n ['total mem', os.totalmem()],\n ['free mem', os.freemem()],\n ['cpus', os.cpus().length],\n ['available parallelism', os.availableParallelism()],\n ['platform', os.platform()],\n ['arch', os.arch()],\n ['release', os.release()],\n ] as const);\n}\n\nasync function getPgStats(\n pendingQueries: [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][],\n) {\n const results = await Promise.all(\n pendingQueries.map(async ([name, query]) => [name, await query] as const),\n );\n return Object.fromEntries(results);\n}\n\ntype StatsObject = Record<string, unknown>;\n\nfunction printStats(group: string, statsObject: StatsObject): string {\n const lines: string[] = ['\\n' + header(group)];\n for (const [name, result] of Object.entries(statsObject)) {\n lines.push('\\n' + name + ': ' + BigIntJSON.stringify(result, null, 2));\n }\n lines.push('\\n');\n return lines.join('');\n}\n\n/**\n * Serves counts and sizes. This is available to the operator password as well\n * as the admin password (see `--operator-password`), so it must never return\n * application data: no row values, query ASTs or query arguments.\n *\n * HTTP query parameters:\n * * `group`: restricts the groups for which stats are computed\n * * `format=json`: returns the stats as a JSON object\n * * `pretty`: formats the JSON object with indentation\n */\nexport async function handleStatzRequest(\n lc: LogContext,\n config: ZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Statz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const statsFns: Record<string, () => Promise<StatsObject> | StatsObject> = {\n upstream: () => upstreamStats(lc, config),\n cvr: () => cvrStats(lc, config),\n changeLog: () => changeLogStats(lc, config),\n replica: () => replicaStats(lc, config),\n replication: () => replicationStats(lc, config),\n os: () => osStats(),\n };\n\n async function computeStats(group: string): Promise<[string, StatsObject]> {\n try {\n return [group, await statsFns[group]()];\n } catch (e) {\n lc.error?.(`error computing ${group} stats`, e);\n return [group, {error: String(e)}];\n }\n }\n\n const query = req.query as Record<string, unknown>;\n const groups =\n typeof query.group === 'string'\n ? query.group.split(',')\n : Array.isArray(query.group)\n ? query.group\n : undefined;\n\n const stats = await Promise.all(\n groups\n ? groups.filter(g => g in statsFns).map(computeStats)\n : Object.keys(statsFns).map(computeStats),\n );\n\n if (query.format === 'json') {\n const indent = query.pretty !== undefined ? 2 : undefined;\n await res\n .header('Content-Type', 'application/json')\n .send(BigIntJSON.stringify(Object.fromEntries(stats), null, indent));\n return;\n } else {\n const body = stats\n .map(([group, statsObject]) => printStats(group, statsObject))\n .join('');\n await res.header('Content-Type', 'text/plain; charset=utf-8').send(body);\n }\n}\n\nfunction first(x: object[]): object {\n return x[0];\n}\n\nfunction pick(x: object): unknown {\n return Object.values(x)[0];\n}\n\nfunction header(name: string): string {\n return `=== ${name} ===\\n`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"statz.js","names":[],"sources":["../../../../../zero-cache/src/services/statz.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {BigIntJSON} from '../../../shared/src/bigint-json.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {NormalizedZeroConfig as ZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {StatementRunner} from '../db/statements.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {getShardID, upstreamSchema} from '../types/shards.ts';\nimport {fromStateVersionString} from './change-source/pg/lsn.ts';\nimport {getReplicationState} from './replicator/schema/replication-state.ts';\n\nasync function upstreamStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config));\n const sql = pgClient(lc, config.upstream.db, 'statz-upstream');\n try {\n return await getPgStats([\n [\n 'numReplicas',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"replicas\"`,\n ],\n [\n 'numClientsWithMutations',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n [\n 'numMutationsProcessed',\n sql`SELECT SUM(\"lastMutationID\") as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\nasync function cvrStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cvr';\n const sql = pgClient(lc, config.cvr.db, 'statz-cvr');\n\n function numQueriesPerClientGroup(\n active: boolean,\n ): ReturnType<ReturnType<typeof pgClient>> {\n const filter = active\n ? sql`WHERE \"inactivatedAt\" IS NULL AND deleted = false`\n : sql`WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`;\n return sql`WITH\n group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(*) AS num_queries\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n ),\n -- Count distinct clientIDs per clientGroupID\n client_per_group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(DISTINCT \"clientID\") AS num_clients\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n )\n -- Combine all the information\n SELECT\n g.\"clientGroupID\",\n cpg.num_clients,\n g.num_queries\n FROM group_counts g\n JOIN client_per_group_counts cpg ON g.\"clientGroupID\" = cpg.\"clientGroupID\"\n ORDER BY g.num_queries DESC;`;\n }\n\n try {\n return await getPgStats([\n [\n 'totalNumQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\"`,\n ],\n [\n 'numUniqueQueryHashes',\n sql`SELECT COUNT(DISTINCT \"queryHash\") as \"c\" FROM ${sql(\n schema,\n )}.\"desires\"`,\n ],\n [\n 'numActiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NULL AND \"deleted\" = false`,\n ],\n [\n 'numInactiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`,\n ],\n [\n 'numDeletedQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"deleted\" = true`,\n ],\n [\n 'freshQueriesPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS fresh_count\n FROM ${sql(schema)}.\"desires\"\n WHERE\n (\"inactivatedAt\" IS NOT NULL\n AND (\"inactivatedAt\" + \"ttl\") > NOW()) OR (\"inactivatedAt\" IS NULL\n AND deleted = false)\n GROUP BY \"clientGroupID\"\n )\n\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS \"p99\",\n MIN(fresh_count) AS \"min\",\n MAX(fresh_count) AS \"max\",\n AVG(fresh_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n 'rowsPerClientGroupPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\"\n GROUP BY \"clientGroupID\"\n )\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS \"p99\",\n MIN(row_count) AS \"min\",\n MAX(row_count) AS \"max\",\n AVG(row_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n // check for AST blowup due to DNF conversion.\n 'astSizes',\n sql`SELECT\n percentile_cont(0.25) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"25th_percentile\",\n percentile_cont(0.5) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"50th_percentile\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"75th_percentile\",\n percentile_cont(0.9) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"90th_percentile\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"95th_percentile\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"99th_percentile\",\n MIN(length(\"clientAST\"::text)) AS \"minimum_length\",\n MAX(length(\"clientAST\"::text)) AS \"maximum_length\",\n AVG(length(\"clientAST\"::text))::integer AS \"average_length\",\n COUNT(*) AS \"total_records\"\n FROM ${sql(schema)}.\"queries\";`,\n ],\n [\n // output the hash of the largest AST\n 'biggestAstHash',\n sql`SELECT \"queryHash\", length(\"clientAST\"::text) AS \"ast_length\"\n FROM ${sql(schema)}.\"queries\"\n ORDER BY length(\"clientAST\"::text) DESC\n LIMIT 1;`,\n ],\n [\n 'totalActiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(true),\n ],\n [\n 'totalInactiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(false),\n ],\n [\n 'totalRowsPerClientGroup',\n sql`SELECT \"clientGroupID\", COUNT(*) as \"c\" FROM ${sql(\n schema,\n )}.\"rows\" GROUP BY \"clientGroupID\" ORDER BY \"c\" DESC`,\n ],\n [\n 'numRowsPerQuery',\n sql`SELECT\n k.key AS \"queryHash\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\" r,\n LATERAL jsonb_each(r.\"refCounts\") k\n GROUP BY k.key\n ORDER BY row_count DESC;`,\n ],\n ] satisfies [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][]);\n } finally {\n await sql.end();\n }\n}\n\nasync function changeLogStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cdc';\n const sql = pgClient(lc, config.change.db, 'statz-change');\n\n try {\n return await getPgStats([\n [\n 'changeLogSize',\n sql`SELECT COUNT(*) as \"change_log_size\" FROM ${sql(schema)}.\"changeLog\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\n// The replica is opened read-only: /statz is available to the operator\n// password, and must not change the replica. Among other things, a writable\n// connection would run `PRAGMA optimize` when closed.\nfunction replicaStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return Object.fromEntries([\n ['wal file sizes', walFileSizes(config.replica.file)],\n ['page count', pick(first(db.pragma('PAGE_COUNT')))],\n ['page size', pick(first(db.pragma('PAGE_SIZE')))],\n ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],\n ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],\n ['cache size', pick(first(db.pragma('CACHE_SIZE')))],\n ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],\n ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],\n ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],\n ['db file stats', fs.statSync(config.replica.file)],\n ] as const);\n } finally {\n db.close();\n }\n}\n\nfunction replicationStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return getReplicationStats(db);\n } finally {\n db.close();\n }\n}\n\n/**\n * The sizes of the replica's WAL files, by suffix, read from the filesystem.\n * `PRAGMA wal_checkpoint` would report the WAL's frame counts, but it runs a\n * checkpoint to do so, and fails on a read-only connection in WAL mode.\n */\nfunction walFileSizes(file: string): Record<string, number> {\n const sizes: Record<string, number> = {};\n // `-wal` is used in WAL mode, and both files in WAL2 mode.\n for (const suffix of ['-wal', '-wal2']) {\n const stats = fs.statSync(file + suffix, {throwIfNoEntry: false});\n if (stats) {\n sizes[suffix] = stats.size;\n }\n }\n return sizes;\n}\n\nfunction getReplicationStats(db: Database) {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n const lsn = fromStateVersionString(stateVersion);\n return {lsn};\n}\n\nfunction osStats() {\n return Object.fromEntries([\n ['load avg', os.loadavg()],\n ['uptime', os.uptime()],\n ['total mem', os.totalmem()],\n ['free mem', os.freemem()],\n ['cpus', os.cpus().length],\n ['available parallelism', os.availableParallelism()],\n ['platform', os.platform()],\n ['arch', os.arch()],\n ['release', os.release()],\n ] as const);\n}\n\nasync function getPgStats(\n pendingQueries: [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][],\n) {\n const results = await Promise.all(\n pendingQueries.map(async ([name, query]) => [name, await query] as const),\n );\n return Object.fromEntries(results);\n}\n\ntype StatsObject = Record<string, unknown>;\n\nfunction printStats(group: string, statsObject: StatsObject): string {\n const lines: string[] = ['\\n' + header(group)];\n for (const [name, result] of Object.entries(statsObject)) {\n lines.push('\\n' + name + ': ' + BigIntJSON.stringify(result, null, 2));\n }\n lines.push('\\n');\n return lines.join('');\n}\n\n/**\n * Serves counts and sizes. This is available to the operator password as well\n * as the admin password (see `--operator-password`), so it must never return\n * application data: no row values, query ASTs or query arguments.\n *\n * HTTP query parameters:\n * * `group`: restricts the groups for which stats are computed\n * * `format=json`: returns the stats as a JSON object\n * * `pretty`: formats the JSON object with indentation\n */\nexport async function handleStatzRequest(\n lc: LogContext,\n config: ZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Statz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const statsFns: Record<string, () => Promise<StatsObject> | StatsObject> = {\n upstream: () => upstreamStats(lc, config),\n cvr: () => cvrStats(lc, config),\n changeLog: () => changeLogStats(lc, config),\n replica: () => replicaStats(lc, config),\n replication: () => replicationStats(lc, config),\n os: () => osStats(),\n };\n\n async function computeStats(group: string): Promise<[string, StatsObject]> {\n try {\n return [group, await statsFns[group]()];\n } catch (e) {\n lc.error?.(`error computing ${group} stats`, e);\n return [group, {error: String(e)}];\n }\n }\n\n const query = req.query as Record<string, unknown>;\n const groups =\n typeof query.group === 'string'\n ? query.group.split(',')\n : Array.isArray(query.group)\n ? query.group\n : undefined;\n\n const stats = await Promise.all(\n groups\n ? groups.filter(g => g in statsFns).map(computeStats)\n : Object.keys(statsFns).map(computeStats),\n );\n\n if (query.format === 'json') {\n const indent = query.pretty !== undefined ? 2 : undefined;\n await res\n .header('Content-Type', 'application/json')\n .send(BigIntJSON.stringify(Object.fromEntries(stats), null, indent));\n return;\n } else {\n const body = stats\n .map(([group, statsObject]) => printStats(group, statsObject))\n .join('');\n await res.header('Content-Type', 'text/plain; charset=utf-8').send(body);\n }\n}\n\nfunction first(x: object[]): object {\n return x[0];\n}\n\nfunction pick(x: object): unknown {\n return Object.values(x)[0];\n}\n\nfunction header(name: string): string {\n return `=== ${name} ===\\n`;\n}\n"],"mappings":";;;;;;;;;;;;AAeA,eAAe,cAAc,IAAgB,QAAoB;CAC/D,MAAM,SAAS,eAAe,WAAW,MAAM,CAAC;CAChD,MAAM,MAAM,SAAS,IAAI,OAAO,SAAS,IAAI,gBAAgB;CAC7D,IAAI;EACF,OAAO,MAAM,WAAW;GACtB,CACE,eACA,GAAG,+BAA+B,IAAI,MAAM,EAAE,YAChD;GACA,CACE,2BACA,GAAG,+BAA+B,IAAI,MAAM,EAAE,WAChD;GACA,CACE,yBACA,GAAG,4CAA4C,IAAI,MAAM,EAAE,WAC7D;EACF,CAAC;CACH,UAAU;EACR,MAAM,IAAI,IAAI;CAChB;AACF;AAEA,eAAe,SAAS,IAAgB,QAAoB;CAC1D,MAAM,SAAS,eAAe,WAAW,MAAM,CAAC,IAAI;CACpD,MAAM,MAAM,SAAS,IAAI,OAAO,IAAI,IAAI,WAAW;CAEnD,SAAS,yBACP,QACyC;EACzC,MAAM,SAAS,SACX,GAAG,sDACH,GAAG;EACP,OAAO,GAAG;;;;;aAKD,IAAI,MAAM,EAAE;QACjB,OAAO;;;;;;;;aAQF,IAAI,MAAM,EAAE;QACjB,OAAO;;;;;;;;;;;CAWb;CAEA,IAAI;EACF,OAAO,MAAM,WAAW;GACtB,CACE,mBACA,GAAG,+BAA+B,IAAI,MAAM,EAAE,WAChD;GACA,CACE,wBACA,GAAG,kDAAkD,IACnD,MACF,EAAE,WACJ;GACA,CACE,oBACA,GAAG,+BAA+B,IAAI,MAAM,EAAE,+DAChD;GACA,CACE,sBACA,GAAG,+BAA+B,IAAI,MAAM,EAAE,mFAChD;GACA,CACE,qBACA,GAAG,+BAA+B,IAAI,MAAM,EAAE,kCAChD;GACA,CACE,2BACA,GAAG;;;;;eAKI,IAAI,MAAM,EAAE;;;;;;;;;;;;;;;;;gCAkBrB;GACA,CACE,iCACA,GAAG;;;;;eAKI,IAAI,MAAM,EAAE;;;;;;;;;;;;gCAarB;GACA,CAEE,YACA,GAAG;;;;;;;;;;;aAWE,IAAI,MAAM,EAAE,YACnB;GACA,CAEE,kBACA,GAAG;aACE,IAAI,MAAM,EAAE;;eAGnB;GACA,CACE,6CACA,yBAAyB,IAAI,CAC/B;GACA,CACE,+CACA,yBAAyB,KAAK,CAChC;GACA,CACE,2BACA,GAAG,gDAAgD,IACjD,MACF,EAAE,mDACJ;GACA,CACE,mBACA,GAAG;;;aAGE,IAAI,MAAM,EAAE;;;+BAInB;EACF,CAGG;CACL,UAAU;EACR,MAAM,IAAI,IAAI;CAChB;AACF;AAEA,eAAe,eAAe,IAAgB,QAAoB;CAChE,MAAM,SAAS,eAAe,WAAW,MAAM,CAAC,IAAI;CACpD,MAAM,MAAM,SAAS,IAAI,OAAO,OAAO,IAAI,cAAc;CAEzD,IAAI;EACF,OAAO,MAAM,WAAW,CACtB,CACE,iBACA,GAAG,6CAA6C,IAAI,MAAM,EAAE,aAC9D,CACF,CAAC;CACH,UAAU;EACR,MAAM,IAAI,IAAI;CAChB;AACF;AAKA,SAAS,aAAa,IAAgB,QAAoB;CACxD,MAAM,KAAK,IAAI,SAAS,IAAI,OAAO,QAAQ,MAAM,EAAC,UAAU,KAAI,CAAC;CACjE,IAAI;EACF,OAAO,OAAO,YAAY;GACxB,CAAC,kBAAkB,aAAa,OAAO,QAAQ,IAAI,CAAC;GACpD,CAAC,cAAc,KAAK,MAAM,GAAG,OAAO,YAAY,CAAC,CAAC,CAAC;GACnD,CAAC,aAAa,KAAK,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,CAAC;GACjD,CAAC,gBAAgB,KAAK,MAAM,GAAG,OAAO,cAAc,CAAC,CAAC,CAAC;GACvD,CAAC,eAAe,KAAK,MAAM,GAAG,OAAO,aAAa,CAAC,CAAC,CAAC;GACrD,CAAC,cAAc,KAAK,MAAM,GAAG,OAAO,YAAY,CAAC,CAAC,CAAC;GACnD,CAAC,eAAe,KAAK,MAAM,GAAG,OAAO,aAAa,CAAC,CAAC,CAAC;GACrD,CAAC,kBAAkB,KAAK,MAAM,GAAG,OAAO,gBAAgB,CAAC,CAAC,CAAC;GAC3D,CAAC,sBAAsB,KAAK,MAAM,GAAG,OAAO,oBAAoB,CAAC,CAAC,CAAC;GACnE,CAAC,iBAAiB,GAAG,SAAS,OAAO,QAAQ,IAAI,CAAC;EACpD,CAAU;CACZ,UAAU;EACR,GAAG,MAAM;CACX;AACF;AAEA,SAAS,iBAAiB,IAAgB,QAAoB;CAC5D,MAAM,KAAK,IAAI,SAAS,IAAI,OAAO,QAAQ,MAAM,EAAC,UAAU,KAAI,CAAC;CACjE,IAAI;EACF,OAAO,oBAAoB,EAAE;CAC/B,UAAU;EACR,GAAG,MAAM;CACX;AACF;;;;;;AAOA,SAAS,aAAa,MAAsC;CAC1D,MAAM,QAAgC,CAAC;CAEvC,KAAK,MAAM,UAAU,CAAC,QAAQ,OAAO,GAAG;EACtC,MAAM,QAAQ,GAAG,SAAS,OAAO,QAAQ,EAAC,gBAAgB,MAAK,CAAC;EAChE,IAAI,OACF,MAAM,UAAU,MAAM;CAE1B;CACA,OAAO;AACT;AAEA,SAAS,oBAAoB,IAAc;CACzC,MAAM,EAAC,iBAAgB,oBAAoB,IAAI,gBAAgB,EAAE,CAAC;CAElE,OAAO,EAAC,KADI,uBAAuB,YAC3B,EAAG;AACb;AAEA,SAAS,UAAU;CACjB,OAAO,OAAO,YAAY;EACxB,CAAC,YAAY,GAAG,QAAQ,CAAC;EACzB,CAAC,UAAU,GAAG,OAAO,CAAC;EACtB,CAAC,aAAa,GAAG,SAAS,CAAC;EAC3B,CAAC,YAAY,GAAG,QAAQ,CAAC;EACzB,CAAC,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM;EACzB,CAAC,yBAAyB,GAAG,qBAAqB,CAAC;EACnD,CAAC,YAAY,GAAG,SAAS,CAAC;EAC1B,CAAC,QAAQ,GAAG,KAAK,CAAC;EAClB,CAAC,WAAW,GAAG,QAAQ,CAAC;CAC1B,CAAU;AACZ;AAEA,eAAe,WACb,gBAIA;CACA,MAAM,UAAU,MAAM,QAAQ,IAC5B,eAAe,IAAI,OAAO,CAAC,MAAM,WAAW,CAAC,MAAM,MAAM,KAAK,CAAU,CAC1E;CACA,OAAO,OAAO,YAAY,OAAO;AACnC;AAIA,SAAS,WAAW,OAAe,aAAkC;CACnE,MAAM,QAAkB,CAAC,OAAO,OAAO,KAAK,CAAC;CAC7C,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,WAAW,GACrD,MAAM,KAAK,OAAO,OAAO,OAAO,WAAW,UAAU,QAAQ,MAAM,CAAC,CAAC;CAEvE,MAAM,KAAK,IAAI;CACf,OAAO,MAAM,KAAK,EAAE;AACtB;;;;;;;;;;;AAYA,eAAsB,mBACpB,IACA,QACA,KACA,KACA;CACA,MAAM,cAAc,KAAK,GAAG;CAC5B,IAAI,kBAAkB,IAAI,QAAQ,aAAa,IAAI,MAAM,KAAA,GAAW;EAClE,IACG,KAAK,GAAG,CAAC,CACT,OAAO,oBAAoB,sCAAoC,CAAC,CAChE,KAAK,cAAc;EACtB;CACF;CAEA,MAAM,WAAqE;EACzE,gBAAgB,cAAc,IAAI,MAAM;EACxC,WAAW,SAAS,IAAI,MAAM;EAC9B,iBAAiB,eAAe,IAAI,MAAM;EAC1C,eAAe,aAAa,IAAI,MAAM;EACtC,mBAAmB,iBAAiB,IAAI,MAAM;EAC9C,UAAU,QAAQ;CACpB;CAEA,eAAe,aAAa,OAA+C;EACzE,IAAI;GACF,OAAO,CAAC,OAAO,MAAM,SAAS,MAAM,CAAC,CAAC;EACxC,SAAS,GAAG;GACV,GAAG,QAAQ,mBAAmB,MAAM,SAAS,CAAC;GAC9C,OAAO,CAAC,OAAO,EAAC,OAAO,OAAO,CAAC,EAAC,CAAC;EACnC;CACF;CAEA,MAAM,QAAQ,IAAI;CAClB,MAAM,SACJ,OAAO,MAAM,UAAU,WACnB,MAAM,MAAM,MAAM,GAAG,IACrB,MAAM,QAAQ,MAAM,KAAK,IACvB,MAAM,QACN,KAAA;CAER,MAAM,QAAQ,MAAM,QAAQ,IAC1B,SACI,OAAO,QAAO,MAAK,KAAK,QAAQ,CAAC,CAAC,IAAI,YAAY,IAClD,OAAO,KAAK,QAAQ,CAAC,CAAC,IAAI,YAAY,CAC5C;CAEA,IAAI,MAAM,WAAW,QAAQ;EAC3B,MAAM,SAAS,MAAM,WAAW,KAAA,IAAY,IAAI,KAAA;EAChD,MAAM,IACH,OAAO,gBAAgB,kBAAkB,CAAC,CAC1C,KAAK,WAAW,UAAU,OAAO,YAAY,KAAK,GAAG,MAAM,MAAM,CAAC;EACrE;CACF,OAAO;EACL,MAAM,OAAO,MACV,KAAK,CAAC,OAAO,iBAAiB,WAAW,OAAO,WAAW,CAAC,CAAC,CAC7D,KAAK,EAAE;EACV,MAAM,IAAI,OAAO,gBAAgB,2BAA2B,CAAC,CAAC,KAAK,IAAI;CACzE;AACF;AAEA,SAAS,MAAM,GAAqB;CAClC,OAAO,EAAE;AACX;AAEA,SAAS,KAAK,GAAoB;CAChC,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC;AAC1B;AAEA,SAAS,OAAO,MAAsB;CACpC,OAAO,OAAO,KAAK;AACrB"}
@@ -4,7 +4,7 @@ import { cvrSchema } from "../../types/shards.js";
4
4
  import { RunningState } from "../running-state.js";
5
5
  import { setActiveUsersGetter } from "../../server/anonymous-otel-start.js";
6
6
  //#region ../zero-cache/src/services/view-syncer/active-users-gauge.ts
7
- var DAY = 1440 * (60 * 1e3);
7
+ var DAY = 864e5;
8
8
  var ActiveUsersGauge = class {
9
9
  id = "active-users-gauge";
10
10
  #lc;
@@ -18,7 +18,7 @@ var ActiveUsersGauge = class {
18
18
  this.#lc = lc;
19
19
  this.#db = db;
20
20
  this.#schema = cvrSchema(shard);
21
- this.#updateIntervalMs = opts.updateIntervalMs ?? 60 * 1e3;
21
+ this.#updateIntervalMs = opts.updateIntervalMs ?? 6e4;
22
22
  this.#setActiveUsersGetter = setActiveUsersGetterFn;
23
23
  }
24
24
  async run() {
@@ -1 +1 @@
1
- {"version":3,"file":"active-users-gauge.js","names":["#lc","#db","#schema","#updateIntervalMs","#state","#setActiveUsersGetter","#lastActiveUsers"],"sources":["../../../../../../zero-cache/src/services/view-syncer/active-users-gauge.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {must} from '../../../../shared/src/must.ts';\nimport {mapValues} from '../../../../shared/src/objects.ts';\nimport {\n setActiveUsersGetter,\n type ActiveUsers,\n} from '../../server/anonymous-otel-start.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nconst MINUTE = 60 * 1000;\nconst DAY = 24 * 60 * MINUTE;\n\ntype Options = {\n updateIntervalMs?: number;\n};\n\nexport class ActiveUsersGauge implements Service {\n readonly id = 'active-users-gauge';\n\n readonly #lc: LogContext;\n readonly #db: PostgresDB;\n readonly #schema: string;\n readonly #updateIntervalMs: number;\n readonly #state = new RunningState('active-users-gauge');\n readonly #setActiveUsersGetter: typeof setActiveUsersGetter;\n\n // latest computed value exposed via the observable gauge callback\n #lastActiveUsers: ActiveUsers | undefined;\n\n constructor(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n opts: Options = {},\n setActiveUsersGetterFn = setActiveUsersGetter,\n ) {\n this.#lc = lc;\n this.#db = db;\n this.#schema = cvrSchema(shard);\n this.#updateIntervalMs = opts.updateIntervalMs ?? 60 * 1000; // default 1 minute\n this.#setActiveUsersGetter = setActiveUsersGetterFn;\n }\n\n async run(): Promise<void> {\n while (this.#state.shouldRun()) {\n try {\n const now = Date.now();\n const since30day = now - DAY * 30;\n const since7day = now - DAY * 7;\n const since1day = now - DAY;\n\n // This query performs a single scan over the `profile_ids_last_active`\n // index to compute aggregated results for all of our active user\n // metric variants.\n //\n // The eventually-correct metrics are `users_#da` which count distinct\n // profileIDs produced by the zero-client (i.e. starting with 'p').\n // The `users_#da_legacy` metrics include back-filled profileIDs (which\n // start with `cg`) and will over-count users on apps using memstore.\n const [actives] = await this.#db<[ActiveUsers]> /*sql*/ `\n SELECT \n COUNT(*) FILTER (WHERE \"lastActive\" >= ${since1day}) AS active_users_last_day,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since1day} AND starts_with(\"profileID\", 'p')) AS users_1da,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since7day} AND starts_with(\"profileID\", 'p')) AS users_7da,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE starts_with(\"profileID\", 'p')) AS users_30da,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since1day}) AS users_1da_legacy,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since7day}) AS users_7da_legacy,\n COUNT(DISTINCT(\"profileID\")) AS users_30da_legacy\n FROM ${this.#db(this.#schema)}.instances\n WHERE \"lastActive\" >= ${since30day}\n `;\n // Determine if the getter needs to be set (i.e. the first time).\n const setGetter = this.#lastActiveUsers === undefined;\n this.#lastActiveUsers = mapValues(actives, bigVal => Number(bigVal));\n\n // Set the getter after the first value is computed\n if (setGetter) {\n this.#setActiveUsersGetter(() => must(this.#lastActiveUsers));\n }\n\n this.#lc.debug?.(`updated active-users gauge`, this.#lastActiveUsers);\n } catch (e) {\n this.#lc.warn?.('error updating active-users gauge', e);\n }\n\n await this.#state.sleep(this.#updateIntervalMs);\n }\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return this.#state.stopped();\n }\n}\n"],"mappings":";;;;;;AAaA,IAAM,MAAM,QADG,KAAK;AAOpB,IAAa,mBAAb,MAAiD;CAC/C,KAAc;CAEd;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,oBAAoB;CACvD;CAGA;CAEA,YACE,IACA,IACA,OACA,OAAgB,CAAC,GACjB,yBAAyB,sBACzB;EACA,KAAKA,MAAM;EACX,KAAKC,MAAM;EACX,KAAKC,UAAU,UAAU,KAAK;EAC9B,KAAKC,oBAAoB,KAAK,oBAAoB,KAAK;EACvD,KAAKE,wBAAwB;CAC/B;CAEA,MAAM,MAAqB;EACzB,OAAO,KAAKD,OAAO,UAAU,GAAG;GAC9B,IAAI;IACF,MAAM,MAAM,KAAK,IAAI;IACrB,MAAM,aAAa,MAAM,MAAM;IAC/B,MAAM,YAAY,MAAM,MAAM;IAC9B,MAAM,YAAY,MAAM;IAUxB,MAAM,CAAC,WAAW,MAAM,KAAKH,GAA2B;;qDAEX,UAAU;yEACU,UAAU;yEACV,UAAU;;yEAEV,UAAU;yEACV,UAAU;;mBAEhE,KAAKA,IAAI,KAAKC,OAAO,EAAE;oCACN,WAAW;;IAGvC,MAAM,YAAY,KAAKI,qBAAqB,KAAA;IAC5C,KAAKA,mBAAmB,UAAU,UAAS,WAAU,OAAO,MAAM,CAAC;IAGnE,IAAI,WACF,KAAKD,4BAA4B,KAAK,KAAKC,gBAAgB,CAAC;IAG9D,KAAKN,IAAI,QAAQ,8BAA8B,KAAKM,gBAAgB;GACtE,SAAS,GAAG;IACV,KAAKN,IAAI,OAAO,qCAAqC,CAAC;GACxD;GAEA,MAAM,KAAKI,OAAO,MAAM,KAAKD,iBAAiB;EAChD;CACF;CAEA,OAAsB;EACpB,KAAKC,OAAO,KAAK,KAAKJ,GAAG;EACzB,OAAO,KAAKI,OAAO,QAAQ;CAC7B;AACF"}
1
+ {"version":3,"file":"active-users-gauge.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/active-users-gauge.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {must} from '../../../../shared/src/must.ts';\nimport {mapValues} from '../../../../shared/src/objects.ts';\nimport {\n setActiveUsersGetter,\n type ActiveUsers,\n} from '../../server/anonymous-otel-start.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nconst MINUTE = 60 * 1000;\nconst DAY = 24 * 60 * MINUTE;\n\ntype Options = {\n updateIntervalMs?: number;\n};\n\nexport class ActiveUsersGauge implements Service {\n readonly id = 'active-users-gauge';\n\n readonly #lc: LogContext;\n readonly #db: PostgresDB;\n readonly #schema: string;\n readonly #updateIntervalMs: number;\n readonly #state = new RunningState('active-users-gauge');\n readonly #setActiveUsersGetter: typeof setActiveUsersGetter;\n\n // latest computed value exposed via the observable gauge callback\n #lastActiveUsers: ActiveUsers | undefined;\n\n constructor(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n opts: Options = {},\n setActiveUsersGetterFn = setActiveUsersGetter,\n ) {\n this.#lc = lc;\n this.#db = db;\n this.#schema = cvrSchema(shard);\n this.#updateIntervalMs = opts.updateIntervalMs ?? 60 * 1000; // default 1 minute\n this.#setActiveUsersGetter = setActiveUsersGetterFn;\n }\n\n async run(): Promise<void> {\n while (this.#state.shouldRun()) {\n try {\n const now = Date.now();\n const since30day = now - DAY * 30;\n const since7day = now - DAY * 7;\n const since1day = now - DAY;\n\n // This query performs a single scan over the `profile_ids_last_active`\n // index to compute aggregated results for all of our active user\n // metric variants.\n //\n // The eventually-correct metrics are `users_#da` which count distinct\n // profileIDs produced by the zero-client (i.e. starting with 'p').\n // The `users_#da_legacy` metrics include back-filled profileIDs (which\n // start with `cg`) and will over-count users on apps using memstore.\n const [actives] = await this.#db<[ActiveUsers]> /*sql*/ `\n SELECT \n COUNT(*) FILTER (WHERE \"lastActive\" >= ${since1day}) AS active_users_last_day,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since1day} AND starts_with(\"profileID\", 'p')) AS users_1da,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since7day} AND starts_with(\"profileID\", 'p')) AS users_7da,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE starts_with(\"profileID\", 'p')) AS users_30da,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since1day}) AS users_1da_legacy,\n COUNT(DISTINCT(\"profileID\")) FILTER (WHERE \"lastActive\" >= ${since7day}) AS users_7da_legacy,\n COUNT(DISTINCT(\"profileID\")) AS users_30da_legacy\n FROM ${this.#db(this.#schema)}.instances\n WHERE \"lastActive\" >= ${since30day}\n `;\n // Determine if the getter needs to be set (i.e. the first time).\n const setGetter = this.#lastActiveUsers === undefined;\n this.#lastActiveUsers = mapValues(actives, bigVal => Number(bigVal));\n\n // Set the getter after the first value is computed\n if (setGetter) {\n this.#setActiveUsersGetter(() => must(this.#lastActiveUsers));\n }\n\n this.#lc.debug?.(`updated active-users gauge`, this.#lastActiveUsers);\n } catch (e) {\n this.#lc.warn?.('error updating active-users gauge', e);\n }\n\n await this.#state.sleep(this.#updateIntervalMs);\n }\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return this.#state.stopped();\n }\n}\n"],"mappings":";;;;;;AAaA,IAAM,MAAM;AAMZ,IAAa,mBAAb,MAAiD;CAC/C,KAAc;CAEd;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,oBAAoB;CACvD;CAGA;CAEA,YACE,IACA,IACA,OACA,OAAgB,CAAC,GACjB,yBAAyB,sBACzB;EACA,KAAK,MAAM;EACX,KAAK,MAAM;EACX,KAAK,UAAU,UAAU,KAAK;EAC9B,KAAK,oBAAoB,KAAK,oBAAoB;EAClD,KAAK,wBAAwB;CAC/B;CAEA,MAAM,MAAqB;EACzB,OAAO,KAAK,OAAO,UAAU,GAAG;GAC9B,IAAI;IACF,MAAM,MAAM,KAAK,IAAI;IACrB,MAAM,aAAa,MAAM,MAAM;IAC/B,MAAM,YAAY,MAAM,MAAM;IAC9B,MAAM,YAAY,MAAM;IAUxB,MAAM,CAAC,WAAW,MAAM,KAAK,GAA2B;;qDAEX,UAAU;yEACU,UAAU;yEACV,UAAU;;yEAEV,UAAU;yEACV,UAAU;;mBAEhE,KAAK,IAAI,KAAK,OAAO,EAAE;oCACN,WAAW;;IAGvC,MAAM,YAAY,KAAK,qBAAqB,KAAA;IAC5C,KAAK,mBAAmB,UAAU,UAAS,WAAU,OAAO,MAAM,CAAC;IAGnE,IAAI,WACF,KAAK,4BAA4B,KAAK,KAAK,gBAAgB,CAAC;IAG9D,KAAK,IAAI,QAAQ,8BAA8B,KAAK,gBAAgB;GACtE,SAAS,GAAG;IACV,KAAK,IAAI,OAAO,qCAAqC,CAAC;GACxD;GAEA,MAAM,KAAK,OAAO,MAAM,KAAK,iBAAiB;EAChD;CACF;CAEA,OAAsB;EACpB,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,OAAO,KAAK,OAAO,QAAQ;CAC7B;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"advancement-metrics.js","names":["#processingTime","#setupTime","#wallTime"],"sources":["../../../../../../zero-cache/src/services/view-syncer/advancement-metrics.ts"],"sourcesContent":["import {getOrCreateLatencyHistogram} from '../../observability/metrics.ts';\nimport type {ResetPipelinesReason} from './snapshotter.ts';\n\nexport type AdvancementOutcome =\n | {readonly outcome: 'success' | 'error'}\n | {readonly outcome: 'reset'; readonly reason: ResetPipelinesReason};\n\nexport type AdvancementTimings = {\n /** Setup plus time between time-slice yields, including asynchronous I/O. */\n readonly processingTimeMs: number;\n /** Synchronous snapshot, change-count, CVR updater, and poke preparation. */\n readonly setupTimeMs: number;\n /** Elapsed wall time, including time-slice waits and output cleanup. */\n readonly wallTimeMs: number;\n};\n\n/**\n * Counts and times every advancement attempt, including work abandoned by a\n * reset. The existing sync.advance-time histogram measures successes only.\n * Outcome and reset reason are bounded labels; client and query IDs are not\n * recorded here. Recording once per attempt adds no clocks to row processing.\n */\nexport class AdvancementMetrics {\n readonly #processingTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-time',\n 'Processing time for an advancement attempt, including setup and ' +\n 'asynchronous I/O. Recorded for successful, reset, and failed attempts; ' +\n 'excludes only time-slice waits.',\n );\n readonly #setupTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-setup-time',\n 'Synchronous setup time for an advancement attempt: snapshot acquisition, ' +\n 'change counting, CVR updater construction, and poke preparation.',\n );\n readonly #wallTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-wall-time',\n 'Elapsed wall time for an advancement attempt, including time-slice ' +\n 'waits, CVR flush, and output cleanup.',\n );\n\n record(\n {processingTimeMs, setupTimeMs, wallTimeMs}: AdvancementTimings,\n outcome: AdvancementOutcome,\n ): void {\n const attributes = {\n outcome: outcome.outcome,\n ...(outcome.outcome === 'reset' && {reset_reason: outcome.reason}),\n };\n this.#processingTime.recordMs(processingTimeMs, attributes);\n this.#setupTime.recordMs(setupTimeMs, attributes);\n this.#wallTime.recordMs(wallTimeMs, attributes);\n }\n}\n"],"mappings":";;;;;;;;AAsBA,IAAa,qBAAb,MAAgC;CAC9B,kBAA2B,4BACzB,QACA,wBACA,wKAGF;CACA,aAAsB,4BACpB,QACA,8BACA,2IAEF;CACA,YAAqB,4BACnB,QACA,6BACA,0GAEF;CAEA,OACE,EAAC,kBAAkB,aAAa,cAChC,SACM;EACN,MAAM,aAAa;GACjB,SAAS,QAAQ;GACjB,GAAI,QAAQ,YAAY,WAAW,EAAC,cAAc,QAAQ,OAAM;EAClE;EACA,KAAKA,gBAAgB,SAAS,kBAAkB,UAAU;EAC1D,KAAKC,WAAW,SAAS,aAAa,UAAU;EAChD,KAAKC,UAAU,SAAS,YAAY,UAAU;CAChD;AACF"}
1
+ {"version":3,"file":"advancement-metrics.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/advancement-metrics.ts"],"sourcesContent":["import {getOrCreateLatencyHistogram} from '../../observability/metrics.ts';\nimport type {ResetPipelinesReason} from './snapshotter.ts';\n\nexport type AdvancementOutcome =\n | {readonly outcome: 'success' | 'error'}\n | {readonly outcome: 'reset'; readonly reason: ResetPipelinesReason};\n\nexport type AdvancementTimings = {\n /** Setup plus time between time-slice yields, including asynchronous I/O. */\n readonly processingTimeMs: number;\n /** Synchronous snapshot, change-count, CVR updater, and poke preparation. */\n readonly setupTimeMs: number;\n /** Elapsed wall time, including time-slice waits and output cleanup. */\n readonly wallTimeMs: number;\n};\n\n/**\n * Counts and times every advancement attempt, including work abandoned by a\n * reset. The existing sync.advance-time histogram measures successes only.\n * Outcome and reset reason are bounded labels; client and query IDs are not\n * recorded here. Recording once per attempt adds no clocks to row processing.\n */\nexport class AdvancementMetrics {\n readonly #processingTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-time',\n 'Processing time for an advancement attempt, including setup and ' +\n 'asynchronous I/O. Recorded for successful, reset, and failed attempts; ' +\n 'excludes only time-slice waits.',\n );\n readonly #setupTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-setup-time',\n 'Synchronous setup time for an advancement attempt: snapshot acquisition, ' +\n 'change counting, CVR updater construction, and poke preparation.',\n );\n readonly #wallTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-wall-time',\n 'Elapsed wall time for an advancement attempt, including time-slice ' +\n 'waits, CVR flush, and output cleanup.',\n );\n\n record(\n {processingTimeMs, setupTimeMs, wallTimeMs}: AdvancementTimings,\n outcome: AdvancementOutcome,\n ): void {\n const attributes = {\n outcome: outcome.outcome,\n ...(outcome.outcome === 'reset' && {reset_reason: outcome.reason}),\n };\n this.#processingTime.recordMs(processingTimeMs, attributes);\n this.#setupTime.recordMs(setupTimeMs, attributes);\n this.#wallTime.recordMs(wallTimeMs, attributes);\n }\n}\n"],"mappings":";;;;;;;;AAsBA,IAAa,qBAAb,MAAgC;CAC9B,kBAA2B,4BACzB,QACA,wBACA,wKAGF;CACA,aAAsB,4BACpB,QACA,8BACA,2IAEF;CACA,YAAqB,4BACnB,QACA,6BACA,0GAEF;CAEA,OACE,EAAC,kBAAkB,aAAa,cAChC,SACM;EACN,MAAM,aAAa;GACjB,SAAS,QAAQ;GACjB,GAAI,QAAQ,YAAY,WAAW,EAAC,cAAc,QAAQ,OAAM;EAClE;EACA,KAAK,gBAAgB,SAAS,kBAAkB,UAAU;EAC1D,KAAK,WAAW,SAAS,aAAa,UAAU;EAChD,KAAK,UAAU,SAAS,YAAY,UAAU;CAChD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"batch-pacer.js","names":["#shareTarget","#maxIntervalMs","#now","#utilization","#lc","#waiting","#intervalStart","#update","#multiplier","#waitsBelowMax","#waitsAtMax","#share","#saturatedSeconds","#logPacing","#notPacingSince","#pacingLoggedAt","#handingOutSlices","#nextSlice","#hydrationSlices"],"sources":["../../../../../../zero-cache/src/services/view-syncer/batch-pacer.ts"],"sourcesContent":["import {performance} from 'node:perf_hooks';\nimport type {LogContext} from '@rocicorp/logger';\n\n/** How often the multiplier is updated. */\nconst UPDATE_INTERVAL_MS = 1000;\n\n/**\n * The weight of the latest second in the worker's smoothed busy share.\n * Client groups that wait for seconds between batches make the share of\n * any one second jump between idle and saturated, and a worker that keeps\n * up has the odd busy second. Neither says what the load is.\n */\nconst SHARE_SMOOTHING = 0.3;\n\n/**\n * How much of the difference between the smoothed busy share and the target\n * each update corrects for. The smoothed share only shows part of what the\n * previous updates did, so correcting for all of the difference each time\n * would overshoot and oscillate.\n */\nconst CORRECTION_EXPONENT = 0.3;\n\n/**\n * The share of its time above which a worker has no time to spare. A share\n * target has to stay well clear of this to be held, which is why\n * `syncUpdateShareTarget` is at most 0.9 (or 1, which no worker is over).\n */\nconst SATURATED_SHARE = 0.95;\n\n/** The seconds that a worker has to be saturated for to be treated as such. */\nconst SATURATED_SECONDS = 2;\n\n/**\n * How much `1 + k` is scaled by each second that the worker is saturated.\n * A saturated worker does not show how much more its client groups would\n * use, which can be many times what it has, so the multiplier is raised by\n * more than the difference from the target calls for until it does.\n */\nconst SATURATED_CORRECTION = 1.3;\n\n/**\n * How often a worker that is pacing its client groups says so in its log,\n * and how long it has to have stopped for to say that it has.\n */\nconst PACING_LOG_INTERVAL_MS = 60_000;\n\n/**\n * The most updates that are made up for when nothing has asked the pacer\n * for a wait in a while, which is enough for the multiplier to get to 0.\n */\nconst MAX_CATCH_UP_UPDATES = 60;\n\n/**\n * Bounds the multiplier, which otherwise has no limit while a worker stays\n * over the target however long its client groups wait.\n */\nconst MAX_MULTIPLIER = 1_000_000;\n\nexport type BatchPacerOptions = {\n /**\n * The largest share of the worker's time that it may be busy before its\n * client groups are paced.\n */\n readonly shareTarget: number;\n /** The longest a client group waits between batches. */\n readonly maxIntervalMs: number;\n readonly now?: (() => number) | undefined;\n /**\n * The share of the time since the previous call that the worker was busy.\n * By default, the utilization of the event loop.\n */\n readonly utilization?: (() => number) | undefined;\n /** Where the worker says that it is pacing its client groups. */\n readonly lc?: LogContext | undefined;\n};\n\nfunction eventLoopUtilization(): () => number {\n let last = performance.eventLoopUtilization();\n return () => {\n const current = performance.eventLoopUtilization();\n const {utilization} = performance.eventLoopUtilization(current, last);\n last = current;\n return utilization;\n };\n}\n\n/**\n * Paces the batches (advancements, including the queries they hydrate\n * again) of the client groups of a sync worker, so that the worker is left\n * with time for new hydrations and connections, and gives the hydrations of\n * new queries priority over the batches.\n *\n * **Pacing.** After each batch, a client group waits {@link waitMs} before\n * it starts the next. The wait is a multiple of the batch's processing\n * time, so a client group that always has changes waiting is busy at most\n * `1 / (1 + k)` of the time, where `k` is the {@link multiplier}. Because\n * the wait is in proportion to a client group's own processing time, those\n * that cause the load are slowed the most, and cheap ones barely at all.\n *\n * The multiplier is shared by the client groups of the worker. Once per\n * second, it is scaled by how far the share of its time that the worker was\n * busy, smoothed over a few seconds, is from the target, and by more while\n * the worker has no time to spare. It never goes below 0, where no client\n * group waits. The share is the utilization of the worker's event loop, i.e. all\n * of what the worker does: what a batch costs is spread over the pipelines,\n * the CVR, the pokes and garbage collection, and is not measurable batch by\n * batch.\n *\n * **Priority of new hydrations.** The worker's time slices are handed out\n * by the pacer (see {@link yieldSlice}), and while the hydration of new\n * queries is waiting for one, most of them go to it rather than to batches.\n */\nexport class BatchPacer {\n readonly #shareTarget: number;\n readonly #maxIntervalMs: number;\n readonly #now: () => number;\n readonly #utilization: () => number;\n readonly #lc: LogContext | undefined;\n\n /** When the worker last logged that it is pacing, while it still is. */\n #pacingLoggedAt: number | undefined;\n /** Since when the worker has not been pacing, after it logged that it was. */\n #notPacingSince: number | undefined;\n\n #intervalStart: number;\n #multiplier = 0;\n /** The smoothed share of its time that the worker was busy. */\n #share = 0;\n #saturatedSeconds = 0;\n /** The waits handed out since the last update, by whether at the cap. */\n #waitsBelowMax = 0;\n #waitsAtMax = 0;\n\n /** The work that is waiting for a time slice, in the order it yielded. */\n readonly #waiting = {\n hydration: [] as (() => void)[],\n batch: [] as (() => void)[],\n };\n #handingOutSlices = false;\n /** The slices that have gone to hydrations since one went to a batch. */\n #hydrationSlices = 0;\n\n constructor({\n shareTarget,\n maxIntervalMs,\n now,\n utilization,\n lc,\n }: BatchPacerOptions) {\n this.#shareTarget = shareTarget;\n this.#maxIntervalMs = maxIntervalMs;\n this.#now = now ?? (() => performance.now());\n this.#utilization = utilization ?? eventLoopUtilization();\n this.#lc = lc;\n this.#intervalStart = this.#now();\n }\n\n /**\n * How long a client group should wait before its next batch, after one\n * that took `processingMs` to process. The multiplier changes while a\n * client group waits, so a long wait should be taken in steps, asking\n * again after each.\n */\n waitMs(processingMs: number): number {\n this.#update();\n const wait = this.#multiplier * processingMs;\n if (wait < this.#maxIntervalMs) {\n this.#waitsBelowMax++;\n return wait;\n }\n this.#waitsAtMax++;\n return this.#maxIntervalMs;\n }\n\n /** `k`: a client group waits `k` times the processing time of a batch. */\n get multiplier(): number {\n this.#update();\n return this.#multiplier;\n }\n\n /**\n * The share of its time that the worker was busy, smoothed over the last\n * few seconds, as of the last update of the multiplier.\n */\n get share(): number {\n this.#update();\n return this.#share;\n }\n\n /** Updates the multiplier for each interval that has ended. */\n #update(): void {\n const now = this.#now();\n const ended = Math.floor((now - this.#intervalStart) / UPDATE_INTERVAL_MS);\n if (ended <= 0) {\n return;\n }\n this.#intervalStart += ended * UPDATE_INTERVAL_MS;\n // If more than one interval has ended, nothing asked the pacer for a\n // wait in the meantime, and the utilization since then is that of each.\n const latest = this.#utilization();\n // Once every client group is told to wait the longest it can, raising\n // the multiplier changes nothing, and only makes it take longer to come\n // back down. A worker that is still over its target then is busy with\n // something that making its client groups wait does not relieve.\n const allAtMax = this.#waitsAtMax > 0 && this.#waitsBelowMax === 0;\n this.#waitsAtMax = 0;\n this.#waitsBelowMax = 0;\n for (let i = Math.min(ended, MAX_CATCH_UP_UPDATES); i > 0; i--) {\n this.#share += SHARE_SMOOTHING * (latest - this.#share);\n // A worker that is allowed to be this busy is only raised the\n // multiplier for what it is over its target by.\n this.#saturatedSeconds =\n latest >= SATURATED_SHARE && latest > this.#shareTarget\n ? this.#saturatedSeconds + 1\n : 0;\n let correction =\n this.#saturatedSeconds >= SATURATED_SECONDS\n ? SATURATED_CORRECTION\n : (this.#share / this.#shareTarget) ** CORRECTION_EXPONENT;\n if (allAtMax && correction > 1) {\n correction = 1;\n }\n this.#multiplier = Math.min(\n Math.max((1 + this.#multiplier) * correction - 1, 0),\n MAX_MULTIPLIER,\n );\n }\n this.#logPacing(now, allAtMax);\n }\n\n /**\n * Says in the worker's log when it starts to pace its client groups, once\n * a minute while it does, and when it has stopped. The pacing of a client\n * group is logged by its view syncer.\n */\n #logPacing(now: number, allAtMax: boolean): void {\n const lc = this.#lc;\n if (lc === undefined) {\n return;\n }\n const state = {\n busyShare: this.#share,\n shareTarget: this.#shareTarget,\n waitMultiplier: this.#multiplier,\n maxBatchIntervalMs: this.#maxIntervalMs,\n };\n if (this.#multiplier > 0) {\n this.#notPacingSince = undefined;\n if (this.#pacingLoggedAt === undefined) {\n lc.info?.(\n 'Sync worker is over its share target: its client groups wait between batches',\n state,\n );\n this.#pacingLoggedAt = now;\n } else if (now - this.#pacingLoggedAt >= PACING_LOG_INTERVAL_MS) {\n if (allAtMax && this.#share > this.#shareTarget) {\n // Waiting longer is not an option, so pacing cannot bring the\n // worker to its target: it has more to do than it has time for.\n lc.warn?.(\n 'Sync worker is over its share target although every client group waits the longest it may',\n state,\n );\n } else {\n lc.info?.('Sync worker is pacing its client groups', state);\n }\n this.#pacingLoggedAt = now;\n }\n } else if (this.#pacingLoggedAt !== undefined) {\n // A worker around its target goes in and out of pacing. It only says\n // that it has stopped once it has stayed out.\n this.#notPacingSince ??= now;\n if (now - this.#notPacingSince >= PACING_LOG_INTERVAL_MS) {\n lc.info?.(\n 'Sync worker is under its share target: its client groups no longer wait',\n state,\n );\n this.#pacingLoggedAt = undefined;\n this.#notPacingSince = undefined;\n }\n }\n }\n\n /**\n * Yields the worker to the other work that is waiting for a time slice,\n * and resolves when it is the caller's turn again. One piece of work runs\n * per turn of the event loop, as when the work of a worker's client groups\n * simply takes turns, except that the hydration of new queries goes first:\n * while both kinds of work are waiting, `HYDRATION_PRIORITY` slices go to\n * hydrations for each one that goes to a batch.\n *\n * Batches are never made to wait for anything but a slice. If no\n * hydration is waiting for one (because none is running, or because those\n * that are running are waiting for I/O), batches get every slice. And\n * however many hydrations there are, batches keep a share of the slices,\n * so they keep moving, and so do the snapshots of the replica that they\n * hold.\n */\n yieldSlice(kind: 'hydration' | 'batch'): Promise<void> {\n return new Promise(resolve => {\n this.#waiting[kind].push(resolve);\n if (!this.#handingOutSlices) {\n this.#handingOutSlices = true;\n setImmediate(this.#nextSlice);\n }\n });\n }\n\n readonly #nextSlice = () => {\n const {hydration, batch} = this.#waiting;\n let next: (() => void) | undefined;\n if (\n hydration.length > 0 &&\n (batch.length === 0 || this.#hydrationSlices < HYDRATION_PRIORITY)\n ) {\n next = hydration.shift();\n this.#hydrationSlices++;\n } else if (batch.length > 0) {\n next = batch.shift();\n this.#hydrationSlices = 0;\n }\n if (next === undefined) {\n this.#handingOutSlices = false;\n return;\n }\n next();\n // After the work that was just resumed has run its slice.\n setImmediate(this.#nextSlice);\n };\n}\n\n/**\n * How many time slices go to the hydration of new queries for each one that\n * goes to a batch, while both are waiting for one.\n */\nconst HYDRATION_PRIORITY = 9;\n"],"mappings":";;;AAIA,IAAM,qBAAqB;;;;;;;AAQ3B,IAAM,kBAAkB;;;;;;;AAQxB,IAAM,sBAAsB;;;;;;AAO5B,IAAM,kBAAkB;;AAGxB,IAAM,oBAAoB;;;;;;;AAQ1B,IAAM,uBAAuB;;;;;AAM7B,IAAM,yBAAyB;;;;;AAM/B,IAAM,uBAAuB;;;;;AAM7B,IAAM,iBAAiB;AAoBvB,SAAS,uBAAqC;CAC5C,IAAI,OAAO,YAAY,qBAAqB;CAC5C,aAAa;EACX,MAAM,UAAU,YAAY,qBAAqB;EACjD,MAAM,EAAC,gBAAe,YAAY,qBAAqB,SAAS,IAAI;EACpE,OAAO;EACP,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,aAAb,MAAwB;CACtB;CACA;CACA;CACA;CACA;;CAGA;;CAEA;CAEA;CACA,cAAc;;CAEd,SAAS;CACT,oBAAoB;;CAEpB,iBAAiB;CACjB,cAAc;;CAGd,WAAoB;EAClB,WAAW,CAAC;EACZ,OAAO,CAAC;CACV;CACA,oBAAoB;;CAEpB,mBAAmB;CAEnB,YAAY,EACV,aACA,eACA,KACA,aACA,MACoB;EACpB,KAAKA,eAAe;EACpB,KAAKC,iBAAiB;EACtB,KAAKC,OAAO,cAAc,YAAY,IAAI;EAC1C,KAAKC,eAAe,eAAe,qBAAqB;EACxD,KAAKC,MAAM;EACX,KAAKE,iBAAiB,KAAKJ,KAAK;CAClC;;;;;;;CAQA,OAAO,cAA8B;EACnC,KAAKK,QAAQ;EACb,MAAM,OAAO,KAAKC,cAAc;EAChC,IAAI,OAAO,KAAKP,gBAAgB;GAC9B,KAAKQ;GACL,OAAO;EACT;EACA,KAAKC;EACL,OAAO,KAAKT;CACd;;CAGA,IAAI,aAAqB;EACvB,KAAKM,QAAQ;EACb,OAAO,KAAKC;CACd;;;;;CAMA,IAAI,QAAgB;EAClB,KAAKD,QAAQ;EACb,OAAO,KAAKI;CACd;;CAGA,UAAgB;EACd,MAAM,MAAM,KAAKT,KAAK;EACtB,MAAM,QAAQ,KAAK,OAAO,MAAM,KAAKI,kBAAkB,kBAAkB;EACzE,IAAI,SAAS,GACX;EAEF,KAAKA,kBAAkB,QAAQ;EAG/B,MAAM,SAAS,KAAKH,aAAa;EAKjC,MAAM,WAAW,KAAKO,cAAc,KAAK,KAAKD,mBAAmB;EACjE,KAAKC,cAAc;EACnB,KAAKD,iBAAiB;EACtB,KAAK,IAAI,IAAI,KAAK,IAAI,OAAO,oBAAoB,GAAG,IAAI,GAAG,KAAK;GAC9D,KAAKE,UAAU,mBAAmB,SAAS,KAAKA;GAGhD,KAAKC,oBACH,UAAU,mBAAmB,SAAS,KAAKZ,eACvC,KAAKY,oBAAoB,IACzB;GACN,IAAI,aACF,KAAKA,qBAAqB,oBACtB,wBACC,KAAKD,SAAS,KAAKX,iBAAiB;GAC3C,IAAI,YAAY,aAAa,GAC3B,aAAa;GAEf,KAAKQ,cAAc,KAAK,IACtB,KAAK,KAAK,IAAI,KAAKA,eAAe,aAAa,GAAG,CAAC,GACnD,cACF;EACF;EACA,KAAKK,WAAW,KAAK,QAAQ;CAC/B;;;;;;CAOA,WAAW,KAAa,UAAyB;EAC/C,MAAM,KAAK,KAAKT;EAChB,IAAI,OAAO,KAAA,GACT;EAEF,MAAM,QAAQ;GACZ,WAAW,KAAKO;GAChB,aAAa,KAAKX;GAClB,gBAAgB,KAAKQ;GACrB,oBAAoB,KAAKP;EAC3B;EACA,IAAI,KAAKO,cAAc,GAAG;GACxB,KAAKM,kBAAkB,KAAA;GACvB,IAAI,KAAKC,oBAAoB,KAAA,GAAW;IACtC,GAAG,OACD,gFACA,KACF;IACA,KAAKA,kBAAkB;GACzB,OAAO,IAAI,MAAM,KAAKA,mBAAmB,wBAAwB;IAC/D,IAAI,YAAY,KAAKJ,SAAS,KAAKX,cAGjC,GAAG,OACD,6FACA,KACF;SAEA,GAAG,OAAO,2CAA2C,KAAK;IAE5D,KAAKe,kBAAkB;GACzB;EACF,OAAO,IAAI,KAAKA,oBAAoB,KAAA,GAAW;GAG7C,KAAKD,oBAAoB;GACzB,IAAI,MAAM,KAAKA,mBAAmB,wBAAwB;IACxD,GAAG,OACD,2EACA,KACF;IACA,KAAKC,kBAAkB,KAAA;IACvB,KAAKD,kBAAkB,KAAA;GACzB;EACF;CACF;;;;;;;;;;;;;;;;CAiBA,WAAW,MAA4C;EACrD,OAAO,IAAI,SAAQ,YAAW;GAC5B,KAAKT,SAAS,MAAM,KAAK,OAAO;GAChC,IAAI,CAAC,KAAKW,mBAAmB;IAC3B,KAAKA,oBAAoB;IACzB,aAAa,KAAKC,UAAU;GAC9B;EACF,CAAC;CACH;CAEA,mBAA4B;EAC1B,MAAM,EAAC,WAAW,UAAS,KAAKZ;EAChC,IAAI;EACJ,IACE,UAAU,SAAS,MAClB,MAAM,WAAW,KAAK,KAAKa,mBAAmB,qBAC/C;GACA,OAAO,UAAU,MAAM;GACvB,KAAKA;EACP,OAAO,IAAI,MAAM,SAAS,GAAG;GAC3B,OAAO,MAAM,MAAM;GACnB,KAAKA,mBAAmB;EAC1B;EACA,IAAI,SAAS,KAAA,GAAW;GACtB,KAAKF,oBAAoB;GACzB;EACF;EACA,KAAK;EAEL,aAAa,KAAKC,UAAU;CAC9B;AACF;;;;;AAMA,IAAM,qBAAqB"}
1
+ {"version":3,"file":"batch-pacer.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/batch-pacer.ts"],"sourcesContent":["import {performance} from 'node:perf_hooks';\nimport type {LogContext} from '@rocicorp/logger';\n\n/** How often the multiplier is updated. */\nconst UPDATE_INTERVAL_MS = 1000;\n\n/**\n * The weight of the latest second in the worker's smoothed busy share.\n * Client groups that wait for seconds between batches make the share of\n * any one second jump between idle and saturated, and a worker that keeps\n * up has the odd busy second. Neither says what the load is.\n */\nconst SHARE_SMOOTHING = 0.3;\n\n/**\n * How much of the difference between the smoothed busy share and the target\n * each update corrects for. The smoothed share only shows part of what the\n * previous updates did, so correcting for all of the difference each time\n * would overshoot and oscillate.\n */\nconst CORRECTION_EXPONENT = 0.3;\n\n/**\n * The share of its time above which a worker has no time to spare. A share\n * target has to stay well clear of this to be held, which is why\n * `syncUpdateShareTarget` is at most 0.9 (or 1, which no worker is over).\n */\nconst SATURATED_SHARE = 0.95;\n\n/** The seconds that a worker has to be saturated for to be treated as such. */\nconst SATURATED_SECONDS = 2;\n\n/**\n * How much `1 + k` is scaled by each second that the worker is saturated.\n * A saturated worker does not show how much more its client groups would\n * use, which can be many times what it has, so the multiplier is raised by\n * more than the difference from the target calls for until it does.\n */\nconst SATURATED_CORRECTION = 1.3;\n\n/**\n * How often a worker that is pacing its client groups says so in its log,\n * and how long it has to have stopped for to say that it has.\n */\nconst PACING_LOG_INTERVAL_MS = 60_000;\n\n/**\n * The most updates that are made up for when nothing has asked the pacer\n * for a wait in a while, which is enough for the multiplier to get to 0.\n */\nconst MAX_CATCH_UP_UPDATES = 60;\n\n/**\n * Bounds the multiplier, which otherwise has no limit while a worker stays\n * over the target however long its client groups wait.\n */\nconst MAX_MULTIPLIER = 1_000_000;\n\nexport type BatchPacerOptions = {\n /**\n * The largest share of the worker's time that it may be busy before its\n * client groups are paced.\n */\n readonly shareTarget: number;\n /** The longest a client group waits between batches. */\n readonly maxIntervalMs: number;\n readonly now?: (() => number) | undefined;\n /**\n * The share of the time since the previous call that the worker was busy.\n * By default, the utilization of the event loop.\n */\n readonly utilization?: (() => number) | undefined;\n /** Where the worker says that it is pacing its client groups. */\n readonly lc?: LogContext | undefined;\n};\n\nfunction eventLoopUtilization(): () => number {\n let last = performance.eventLoopUtilization();\n return () => {\n const current = performance.eventLoopUtilization();\n const {utilization} = performance.eventLoopUtilization(current, last);\n last = current;\n return utilization;\n };\n}\n\n/**\n * Paces the batches (advancements, including the queries they hydrate\n * again) of the client groups of a sync worker, so that the worker is left\n * with time for new hydrations and connections, and gives the hydrations of\n * new queries priority over the batches.\n *\n * **Pacing.** After each batch, a client group waits {@link waitMs} before\n * it starts the next. The wait is a multiple of the batch's processing\n * time, so a client group that always has changes waiting is busy at most\n * `1 / (1 + k)` of the time, where `k` is the {@link multiplier}. Because\n * the wait is in proportion to a client group's own processing time, those\n * that cause the load are slowed the most, and cheap ones barely at all.\n *\n * The multiplier is shared by the client groups of the worker. Once per\n * second, it is scaled by how far the share of its time that the worker was\n * busy, smoothed over a few seconds, is from the target, and by more while\n * the worker has no time to spare. It never goes below 0, where no client\n * group waits. The share is the utilization of the worker's event loop, i.e. all\n * of what the worker does: what a batch costs is spread over the pipelines,\n * the CVR, the pokes and garbage collection, and is not measurable batch by\n * batch.\n *\n * **Priority of new hydrations.** The worker's time slices are handed out\n * by the pacer (see {@link yieldSlice}), and while the hydration of new\n * queries is waiting for one, most of them go to it rather than to batches.\n */\nexport class BatchPacer {\n readonly #shareTarget: number;\n readonly #maxIntervalMs: number;\n readonly #now: () => number;\n readonly #utilization: () => number;\n readonly #lc: LogContext | undefined;\n\n /** When the worker last logged that it is pacing, while it still is. */\n #pacingLoggedAt: number | undefined;\n /** Since when the worker has not been pacing, after it logged that it was. */\n #notPacingSince: number | undefined;\n\n #intervalStart: number;\n #multiplier = 0;\n /** The smoothed share of its time that the worker was busy. */\n #share = 0;\n #saturatedSeconds = 0;\n /** The waits handed out since the last update, by whether at the cap. */\n #waitsBelowMax = 0;\n #waitsAtMax = 0;\n\n /** The work that is waiting for a time slice, in the order it yielded. */\n readonly #waiting = {\n hydration: [] as (() => void)[],\n batch: [] as (() => void)[],\n };\n #handingOutSlices = false;\n /** The slices that have gone to hydrations since one went to a batch. */\n #hydrationSlices = 0;\n\n constructor({\n shareTarget,\n maxIntervalMs,\n now,\n utilization,\n lc,\n }: BatchPacerOptions) {\n this.#shareTarget = shareTarget;\n this.#maxIntervalMs = maxIntervalMs;\n this.#now = now ?? (() => performance.now());\n this.#utilization = utilization ?? eventLoopUtilization();\n this.#lc = lc;\n this.#intervalStart = this.#now();\n }\n\n /**\n * How long a client group should wait before its next batch, after one\n * that took `processingMs` to process. The multiplier changes while a\n * client group waits, so a long wait should be taken in steps, asking\n * again after each.\n */\n waitMs(processingMs: number): number {\n this.#update();\n const wait = this.#multiplier * processingMs;\n if (wait < this.#maxIntervalMs) {\n this.#waitsBelowMax++;\n return wait;\n }\n this.#waitsAtMax++;\n return this.#maxIntervalMs;\n }\n\n /** `k`: a client group waits `k` times the processing time of a batch. */\n get multiplier(): number {\n this.#update();\n return this.#multiplier;\n }\n\n /**\n * The share of its time that the worker was busy, smoothed over the last\n * few seconds, as of the last update of the multiplier.\n */\n get share(): number {\n this.#update();\n return this.#share;\n }\n\n /** Updates the multiplier for each interval that has ended. */\n #update(): void {\n const now = this.#now();\n const ended = Math.floor((now - this.#intervalStart) / UPDATE_INTERVAL_MS);\n if (ended <= 0) {\n return;\n }\n this.#intervalStart += ended * UPDATE_INTERVAL_MS;\n // If more than one interval has ended, nothing asked the pacer for a\n // wait in the meantime, and the utilization since then is that of each.\n const latest = this.#utilization();\n // Once every client group is told to wait the longest it can, raising\n // the multiplier changes nothing, and only makes it take longer to come\n // back down. A worker that is still over its target then is busy with\n // something that making its client groups wait does not relieve.\n const allAtMax = this.#waitsAtMax > 0 && this.#waitsBelowMax === 0;\n this.#waitsAtMax = 0;\n this.#waitsBelowMax = 0;\n for (let i = Math.min(ended, MAX_CATCH_UP_UPDATES); i > 0; i--) {\n this.#share += SHARE_SMOOTHING * (latest - this.#share);\n // A worker that is allowed to be this busy is only raised the\n // multiplier for what it is over its target by.\n this.#saturatedSeconds =\n latest >= SATURATED_SHARE && latest > this.#shareTarget\n ? this.#saturatedSeconds + 1\n : 0;\n let correction =\n this.#saturatedSeconds >= SATURATED_SECONDS\n ? SATURATED_CORRECTION\n : (this.#share / this.#shareTarget) ** CORRECTION_EXPONENT;\n if (allAtMax && correction > 1) {\n correction = 1;\n }\n this.#multiplier = Math.min(\n Math.max((1 + this.#multiplier) * correction - 1, 0),\n MAX_MULTIPLIER,\n );\n }\n this.#logPacing(now, allAtMax);\n }\n\n /**\n * Says in the worker's log when it starts to pace its client groups, once\n * a minute while it does, and when it has stopped. The pacing of a client\n * group is logged by its view syncer.\n */\n #logPacing(now: number, allAtMax: boolean): void {\n const lc = this.#lc;\n if (lc === undefined) {\n return;\n }\n const state = {\n busyShare: this.#share,\n shareTarget: this.#shareTarget,\n waitMultiplier: this.#multiplier,\n maxBatchIntervalMs: this.#maxIntervalMs,\n };\n if (this.#multiplier > 0) {\n this.#notPacingSince = undefined;\n if (this.#pacingLoggedAt === undefined) {\n lc.info?.(\n 'Sync worker is over its share target: its client groups wait between batches',\n state,\n );\n this.#pacingLoggedAt = now;\n } else if (now - this.#pacingLoggedAt >= PACING_LOG_INTERVAL_MS) {\n if (allAtMax && this.#share > this.#shareTarget) {\n // Waiting longer is not an option, so pacing cannot bring the\n // worker to its target: it has more to do than it has time for.\n lc.warn?.(\n 'Sync worker is over its share target although every client group waits the longest it may',\n state,\n );\n } else {\n lc.info?.('Sync worker is pacing its client groups', state);\n }\n this.#pacingLoggedAt = now;\n }\n } else if (this.#pacingLoggedAt !== undefined) {\n // A worker around its target goes in and out of pacing. It only says\n // that it has stopped once it has stayed out.\n this.#notPacingSince ??= now;\n if (now - this.#notPacingSince >= PACING_LOG_INTERVAL_MS) {\n lc.info?.(\n 'Sync worker is under its share target: its client groups no longer wait',\n state,\n );\n this.#pacingLoggedAt = undefined;\n this.#notPacingSince = undefined;\n }\n }\n }\n\n /**\n * Yields the worker to the other work that is waiting for a time slice,\n * and resolves when it is the caller's turn again. One piece of work runs\n * per turn of the event loop, as when the work of a worker's client groups\n * simply takes turns, except that the hydration of new queries goes first:\n * while both kinds of work are waiting, `HYDRATION_PRIORITY` slices go to\n * hydrations for each one that goes to a batch.\n *\n * Batches are never made to wait for anything but a slice. If no\n * hydration is waiting for one (because none is running, or because those\n * that are running are waiting for I/O), batches get every slice. And\n * however many hydrations there are, batches keep a share of the slices,\n * so they keep moving, and so do the snapshots of the replica that they\n * hold.\n */\n yieldSlice(kind: 'hydration' | 'batch'): Promise<void> {\n return new Promise(resolve => {\n this.#waiting[kind].push(resolve);\n if (!this.#handingOutSlices) {\n this.#handingOutSlices = true;\n setImmediate(this.#nextSlice);\n }\n });\n }\n\n readonly #nextSlice = () => {\n const {hydration, batch} = this.#waiting;\n let next: (() => void) | undefined;\n if (\n hydration.length > 0 &&\n (batch.length === 0 || this.#hydrationSlices < HYDRATION_PRIORITY)\n ) {\n next = hydration.shift();\n this.#hydrationSlices++;\n } else if (batch.length > 0) {\n next = batch.shift();\n this.#hydrationSlices = 0;\n }\n if (next === undefined) {\n this.#handingOutSlices = false;\n return;\n }\n next();\n // After the work that was just resumed has run its slice.\n setImmediate(this.#nextSlice);\n };\n}\n\n/**\n * How many time slices go to the hydration of new queries for each one that\n * goes to a batch, while both are waiting for one.\n */\nconst HYDRATION_PRIORITY = 9;\n"],"mappings":"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"}
@@ -32,6 +32,7 @@ function startPoke(lc, clients, tentativeVersion) {
32
32
  end: (finalVersion) => settle("end", pokers.map((poker) => poker.end(finalVersion)))
33
33
  };
34
34
  }
35
+ var POKE_PART_FLUSH_THRESHOLD_CHARS = 1048576;
35
36
  var PART_COUNT_FLUSH_THRESHOLD = 100;
36
37
  /**
37
38
  * Handles a single `ViewSyncer` connection.
@@ -235,7 +236,8 @@ var ClientHandler = class {
235
236
  }
236
237
  #updateLMIDs(lmids, patch) {
237
238
  if (patch.op === "put") {
238
- const { clientGroupID, clientID, lastMutationID } = parse(ensureSafeJSON(patch.contents), lmidRowSchema, "passthrough");
239
+ const row = ensureSafeJSON(patch.contents);
240
+ const { clientGroupID, clientID, lastMutationID } = parse(row, lmidRowSchema, "passthrough");
239
241
  if (clientGroupID !== this.#clientGroupID) this.#lc.error?.(`Received clients row for wrong clientGroupID. Ignoring.`, clientGroupID);
240
242
  else lmids[clientID] = lastMutationID;
241
243
  } else patch.op;
@@ -289,6 +291,6 @@ function ensureSafeJSON(row) {
289
291
  } : row;
290
292
  }
291
293
  //#endregion
292
- export { ClientHandler, startPoke };
294
+ export { ClientHandler, POKE_PART_FLUSH_THRESHOLD_CHARS, ensureSafeJSON, startPoke };
293
295
 
294
296
  //# sourceMappingURL=client-handler.js.map