@rocicorp/zero 1.11.0-canary.13 → 1.11.0-canary.14

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 (249) hide show
  1. package/out/analyze-query/src/analyze-cli.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  3. package/out/ast-to-zql/src/ast-to-zql.d.ts +8 -1
  4. package/out/ast-to-zql/src/ast-to-zql.d.ts.map +1 -1
  5. package/out/ast-to-zql/src/ast-to-zql.js +25 -25
  6. package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
  7. package/out/otel/src/log-options.d.ts +16 -0
  8. package/out/otel/src/log-options.d.ts.map +1 -1
  9. package/out/otel/src/log-options.js +54 -1
  10. package/out/otel/src/log-options.js.map +1 -1
  11. package/out/replicache/src/get-kv-store-provider.d.ts +1 -1
  12. package/out/replicache/src/get-kv-store-provider.d.ts.map +1 -1
  13. package/out/replicache/src/get-kv-store-provider.js +4 -4
  14. package/out/replicache/src/get-kv-store-provider.js.map +1 -1
  15. package/out/replicache/src/kv/idb-store.d.ts +10 -1
  16. package/out/replicache/src/kv/idb-store.d.ts.map +1 -1
  17. package/out/replicache/src/kv/idb-store.js +18 -2
  18. package/out/replicache/src/kv/idb-store.js.map +1 -1
  19. package/out/replicache/src/kv/mem-fallback-store.d.ts +54 -0
  20. package/out/replicache/src/kv/mem-fallback-store.d.ts.map +1 -0
  21. package/out/replicache/src/kv/mem-fallback-store.js +177 -0
  22. package/out/replicache/src/kv/mem-fallback-store.js.map +1 -0
  23. package/out/replicache/src/kv/op-sqlite/store.js +3 -3
  24. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  25. package/out/replicache/src/kv/sqlite-store.d.ts +9 -0
  26. package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
  27. package/out/replicache/src/kv/sqlite-store.js +86 -6
  28. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  29. package/out/replicache/src/new-client-channel.js.map +1 -1
  30. package/out/replicache/src/persist/collect-idb-databases.d.ts +17 -3
  31. package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
  32. package/out/replicache/src/persist/collect-idb-databases.js +81 -11
  33. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  34. package/out/replicache/src/persist/idb-databases-store-db-name.js +1 -1
  35. package/out/replicache/src/persist/idb-databases-store-db-name.js.map +1 -1
  36. package/out/replicache/src/persist/persist.d.ts.map +1 -1
  37. package/out/replicache/src/persist/persist.js +6 -3
  38. package/out/replicache/src/persist/persist.js.map +1 -1
  39. package/out/replicache/src/replicache-impl.d.ts +43 -3
  40. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  41. package/out/replicache/src/replicache-impl.js +135 -17
  42. package/out/replicache/src/replicache-impl.js.map +1 -1
  43. package/out/replicache/src/storage-failure.d.ts +37 -0
  44. package/out/replicache/src/storage-failure.d.ts.map +1 -0
  45. package/out/replicache/src/storage-failure.js +31 -0
  46. package/out/replicache/src/storage-failure.js.map +1 -0
  47. package/out/shared/src/random-values.js +12 -1
  48. package/out/shared/src/random-values.js.map +1 -1
  49. package/out/zero/package.js +1 -1
  50. package/out/zero/package.js.map +1 -1
  51. package/out/zero/src/zero.js +2 -1
  52. package/out/zero-cache/src/config/zero-config.d.ts +26 -0
  53. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  54. package/out/zero-cache/src/config/zero-config.js +36 -0
  55. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  56. package/out/zero-cache/src/observability/log-throttle.d.ts +44 -0
  57. package/out/zero-cache/src/observability/log-throttle.d.ts.map +1 -0
  58. package/out/zero-cache/src/observability/log-throttle.js +76 -0
  59. package/out/zero-cache/src/observability/log-throttle.js.map +1 -0
  60. package/out/zero-cache/src/scripts/permissions.d.ts +16 -0
  61. package/out/zero-cache/src/scripts/permissions.d.ts.map +1 -1
  62. package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
  63. package/out/zero-cache/src/server/runner/zero-dispatcher.js +2 -0
  64. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  65. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  66. package/out/zero-cache/src/server/syncer.js +20 -2
  67. package/out/zero-cache/src/server/syncer.js.map +1 -1
  68. package/out/zero-cache/src/services/change-source/common/backfill-manager.d.ts.map +1 -1
  69. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +27 -15
  70. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  71. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts +1 -0
  72. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  73. package/out/zero-cache/src/services/change-source/common/replica-schema.js +15 -3
  74. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  75. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  76. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  77. package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts +9 -0
  78. package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts.map +1 -1
  79. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +204 -18
  80. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  81. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +11 -2
  82. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +11 -2
  84. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  85. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.d.ts +68 -0
  86. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.d.ts.map +1 -0
  87. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +141 -0
  88. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -0
  89. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +43 -0
  90. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/change-source/protocol/current/data.js +10 -1
  92. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  93. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +57 -0
  94. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
  95. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +20 -6
  96. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
  97. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js +17 -7
  98. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  99. package/out/zero-cache/src/services/change-source/protocol/current.js +2 -1
  100. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts +66 -0
  101. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts.map +1 -0
  102. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js +154 -0
  103. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -0
  104. package/out/zero-cache/src/services/change-streamer/backfill-state.d.ts +50 -0
  105. package/out/zero-cache/src/services/change-streamer/backfill-state.d.ts.map +1 -0
  106. package/out/zero-cache/src/services/change-streamer/backfill-state.js +268 -0
  107. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -0
  108. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +7 -6
  109. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  110. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  111. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +20 -4
  112. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  113. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +26 -1
  114. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  115. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  116. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  117. package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
  118. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  119. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +88 -0
  120. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
  121. package/out/zero-cache/src/services/change-streamer/subscribe.js +9 -1
  122. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  123. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +29 -0
  124. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  125. package/out/zero-cache/src/services/change-streamer/subscriber.js +34 -0
  126. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  127. package/out/zero-cache/src/services/plannerz.d.ts +159 -0
  128. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -0
  129. package/out/zero-cache/src/services/plannerz.js +263 -0
  130. package/out/zero-cache/src/services/plannerz.js.map +1 -0
  131. package/out/zero-cache/src/services/replicator/change-log-cookies.js +1 -1
  132. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  133. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  134. package/out/zero-cache/src/services/replicator/change-processor.js +44 -12
  135. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  136. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  137. package/out/zero-cache/src/services/replicator/incremental-sync.js +4 -3
  138. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  139. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts +23 -2
  140. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts.map +1 -1
  141. package/out/zero-cache/src/services/replicator/schema/backfilling.js +54 -2
  142. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  143. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts +11 -3
  144. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts.map +1 -1
  145. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  146. package/out/zero-cache/src/services/replicator/write-worker.js +5 -1
  147. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  148. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.d.ts +81 -0
  149. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.d.ts.map +1 -0
  150. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +128 -0
  151. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -0
  152. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -1
  153. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +36 -1
  154. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  155. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +362 -20
  156. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  157. package/out/zero-cache/src/services/view-syncer/plan-warnings.d.ts +7 -0
  158. package/out/zero-cache/src/services/view-syncer/plan-warnings.d.ts.map +1 -0
  159. package/out/zero-cache/src/services/view-syncer/plan-warnings.js +35 -0
  160. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -0
  161. package/out/zero-cache/src/services/view-syncer/query-shape.d.ts +25 -0
  162. package/out/zero-cache/src/services/view-syncer/query-shape.d.ts.map +1 -0
  163. package/out/zero-cache/src/services/view-syncer/query-shape.js +28 -0
  164. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -0
  165. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts +77 -0
  166. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts.map +1 -0
  167. package/out/zero-cache/src/services/view-syncer/query-stats.js +206 -0
  168. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -0
  169. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +11 -1
  170. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  171. package/out/zero-cache/src/services/view-syncer/snapshotter.js +7 -1
  172. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  173. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  174. package/out/zero-cache/src/services/view-syncer/view-syncer.js +49 -3
  175. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  176. package/out/zero-cache/src/workers/connection.js +3 -3
  177. package/out/zero-cache/src/workers/connection.js.map +1 -1
  178. package/out/zero-cache/src/workers/syncer.js +2 -2
  179. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  180. package/out/zero-client/src/client/options.d.ts +29 -0
  181. package/out/zero-client/src/client/options.d.ts.map +1 -1
  182. package/out/zero-client/src/client/options.js.map +1 -1
  183. package/out/zero-client/src/client/version.js +1 -1
  184. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  185. package/out/zero-client/src/client/zero.js +8 -23
  186. package/out/zero-client/src/client/zero.js.map +1 -1
  187. package/out/zero-client/src/mod.d.ts +1 -0
  188. package/out/zero-client/src/mod.d.ts.map +1 -1
  189. package/out/zero-client/src/mod.js +1 -0
  190. package/out/zero-protocol/src/down.d.ts +2 -2
  191. package/out/zero-react/src/zero.js +1 -0
  192. package/out/zero-solid/src/zero.js +1 -0
  193. package/out/zql/src/builder/builder.d.ts +6 -0
  194. package/out/zql/src/builder/builder.d.ts.map +1 -1
  195. package/out/zql/src/builder/builder.js +4 -3
  196. package/out/zql/src/builder/builder.js.map +1 -1
  197. package/out/zql/src/ivm/memory-source.d.ts +13 -0
  198. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  199. package/out/zql/src/ivm/memory-source.js +1 -1
  200. package/out/zql/src/ivm/memory-source.js.map +1 -1
  201. package/out/zql/src/planner/planner-builder.d.ts +12 -1
  202. package/out/zql/src/planner/planner-builder.d.ts.map +1 -1
  203. package/out/zql/src/planner/planner-builder.js +8 -1
  204. package/out/zql/src/planner/planner-builder.js.map +1 -1
  205. package/out/zql/src/planner/planner-connection.d.ts +43 -0
  206. package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
  207. package/out/zql/src/planner/planner-connection.js +31 -4
  208. package/out/zql/src/planner/planner-connection.js.map +1 -1
  209. package/out/zql/src/planner/planner-graph.d.ts +10 -0
  210. package/out/zql/src/planner/planner-graph.d.ts.map +1 -1
  211. package/out/zql/src/planner/planner-graph.js +14 -0
  212. package/out/zql/src/planner/planner-graph.js.map +1 -1
  213. package/out/zql/src/planner/planner-warnings.d.ts +83 -0
  214. package/out/zql/src/planner/planner-warnings.d.ts.map +1 -0
  215. package/out/zql/src/planner/planner-warnings.js +78 -0
  216. package/out/zql/src/planner/planner-warnings.js.map +1 -0
  217. package/out/zql/src/query/measure-push-operator.d.ts.map +1 -1
  218. package/out/zql/src/query/measure-push-operator.js +30 -8
  219. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  220. package/out/zqlite/src/pending-delta.d.ts +110 -0
  221. package/out/zqlite/src/pending-delta.d.ts.map +1 -0
  222. package/out/zqlite/src/pending-delta.js +278 -0
  223. package/out/zqlite/src/pending-delta.js.map +1 -0
  224. package/out/zqlite/src/query-delegate.d.ts.map +1 -1
  225. package/out/zqlite/src/query-delegate.js +5 -1
  226. package/out/zqlite/src/query-delegate.js.map +1 -1
  227. package/out/zqlite/src/sqlite-cost-model.d.ts.map +1 -1
  228. package/out/zqlite/src/sqlite-cost-model.js +52 -1
  229. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  230. package/out/zqlite/src/sqlite-stat-fanout.d.ts.map +1 -1
  231. package/out/zqlite/src/sqlite-stat-fanout.js +2 -26
  232. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  233. package/out/zqlite/src/sqlite-stat4-sample.d.ts +37 -0
  234. package/out/zqlite/src/sqlite-stat4-sample.d.ts.map +1 -0
  235. package/out/zqlite/src/sqlite-stat4-sample.js +61 -0
  236. package/out/zqlite/src/sqlite-stat4-sample.js.map +1 -0
  237. package/out/zqlite/src/table-source.d.ts +68 -0
  238. package/out/zqlite/src/table-source.d.ts.map +1 -1
  239. package/out/zqlite/src/table-source.js +206 -7
  240. package/out/zqlite/src/table-source.js.map +1 -1
  241. package/package.json +1 -1
  242. package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts +0 -35
  243. package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts.map +0 -1
  244. package/out/replicache/src/kv/idb-store-with-mem-fallback.js +0 -83
  245. package/out/replicache/src/kv/idb-store-with-mem-fallback.js.map +0 -1
  246. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts +0 -156
  247. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts.map +0 -1
  248. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js +0 -15
  249. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js.map +0 -1
@@ -31,6 +31,10 @@ var Subscriber = class {
31
31
  #backlogBackpressure;
32
32
  #backlogFullWaiters = /* @__PURE__ */ new Set();
33
33
  #onAck;
34
+ #backfills;
35
+ #onAligned;
36
+ #onBackfillIgnored;
37
+ #aligned = false;
34
38
  constructor(protocolVersion, id, mode, watermark, downstream, latestStatus, options = {}) {
35
39
  this.#protocolVersion = protocolVersion;
36
40
  this.id = id;
@@ -43,6 +47,29 @@ var Subscriber = class {
43
47
  this.#backlog = new RingBuffer();
44
48
  this.#backlogBackpressure = new ByteBackpressureGate(options.backlogHighWaterBytes ?? DEFAULT_BACKLOG_HIGH_WATER_BYTES, options.backlogLowWaterRatio ?? DEFAULT_BACKLOG_LOW_WATER_RATIO);
45
49
  this.#onAck = options.onAck;
50
+ this.#backfills = options.backfills;
51
+ this.#onAligned = options.onAligned;
52
+ this.#onBackfillIgnored = options.onBackfillIgnored;
53
+ }
54
+ /**
55
+ * The subscriber's tracked pending backfills, or `undefined` if the
56
+ * subscriber does not report them (i.e. protocol < v8).
57
+ */
58
+ get backfills() {
59
+ return this.#backfills;
60
+ }
61
+ /**
62
+ * Whether the subscriber has been sent all changes up to the head of the
63
+ * stream (see {@link SubscriberOptions.onAligned}).
64
+ */
65
+ get aligned() {
66
+ return this.#aligned;
67
+ }
68
+ #trackBackfills(change) {
69
+ if (this.#backfills) {
70
+ const ignored = this.#backfills.applySerialized(change);
71
+ if (ignored.length && this.#aligned) this.#onBackfillIgnored?.(ignored);
72
+ }
46
73
  }
47
74
  get watermark() {
48
75
  return this.#watermark;
@@ -106,6 +133,7 @@ var Subscriber = class {
106
133
  break;
107
134
  }
108
135
  if (commitWatermark) this.#watermark = commitWatermark;
136
+ for (const change of changes) this.#trackBackfills(change);
109
137
  return this.#sendPreSerializedDownstream(preSerialized, commitWatermark);
110
138
  }
111
139
  const promises = [];
@@ -154,6 +182,7 @@ var Subscriber = class {
154
182
  if (watermark <= this.watermark) return;
155
183
  if (!this.supportsMessage(tag)) return;
156
184
  if (tag === "commit") this.#watermark = watermark;
185
+ this.#trackBackfills(change);
157
186
  const result = await this.#sendStringifiedDownstream(json);
158
187
  if (tag === "commit" && result === "consumed") {
159
188
  const acked = max(this.#acked, watermark);
@@ -342,9 +371,14 @@ var Subscriber = class {
342
371
  for (;;) {
343
372
  const change = this.#backlog?.shift();
344
373
  if (!change) {
374
+ const closed = this.#backlog === null;
345
375
  this.#backlog = null;
346
376
  this.#backlogBytes = 0;
347
377
  this.#backlogBackpressure.releaseIfUnderLowWater(this.#bufferedBacklogBytes);
378
+ if (!closed && !this.#aligned) {
379
+ this.#aligned = true;
380
+ this.#onAligned?.(this);
381
+ }
348
382
  break;
349
383
  }
350
384
  const bytes = change[2].length;
@@ -1 +1 @@
1
- {"version":3,"file":"subscriber.js","names":["#protocolVersion","#downstream","#latestStatus","#wsBatched","#backlogBackpressure","#backlogFullWaiters","#onAck","#watermark","#acked","#backlog","#bufferedBacklogBytes","#pushBacklog","#maybeWaitForBacklogSpace","#sendChange","#initialized","#sendPreSerializedDownstream","#sendDownstream","#initialize","#backlogDrain","#drainBacklog","#sendStringifiedDownstream","#pending","#pendingBytes","#processed","#backlogCount","#samples","#totalBufferedBytes","#missedLastTimeout","#laggingSinceMs","#backlogBytes","#resolveBacklogFullWaiters","#backlogInFlightBytes","#lowWaterBytes","#waiters"],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {\n ChangeTag,\n Downstream,\n Status,\n WatermarkedChange,\n} from './change-streamer.ts';\nimport type * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst DEFAULT_BACKLOG_HIGH_WATER_BYTES = 16 * 1024 * 1024;\nconst DEFAULT_BACKLOG_LOW_WATER_RATIO = 0.8;\n\nexport type SubscriberOptions = {\n backlogHighWaterBytes?: number | undefined;\n backlogLowWaterRatio?: number | undefined;\n wsBatched?: boolean | undefined;\n\n /**\n * Called whenever the subscriber's acked watermark advances, i.e. when it\n * has confirmed that a commit was durably applied to its replica. The ACK\n * therefore lags the subscriber's replica rather than leading it.\n */\n onAck?: ((watermark: string) => void) | undefined;\n};\n\nexport type BacklogFullWait = {\n readonly promise: Promise<void>;\n cancel(): void;\n};\n\nexport type SubscriberStats = {\n processRate: number;\n pending: number;\n backlog: number;\n backlogBytes: number;\n totalBufferedBytes: number;\n missedLastTimeout: boolean;\n};\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly mode: ReplicatorMode;\n readonly #downstream: Subscription<string | PreSerializedBatch>;\n readonly #latestStatus: () => Status;\n readonly #wsBatched: boolean;\n #watermark: string;\n #acked: string;\n #backlog: RingBuffer<WatermarkedChange> | null;\n // While catchup is running, live changes are buffered here instead of being\n // pushed downstream. RingBuffer lets drainBacklog consume that backlog without\n // shifting an array, which matters when a subscriber is far behind.\n #backlogBytes = 0;\n #backlogInFlightBytes = 0;\n #backlogDrain: Promise<void> | null = null;\n readonly #backlogBackpressure: ByteBackpressureGate;\n readonly #backlogFullWaiters = new Set<Resolver<void>>();\n readonly #onAck: ((watermark: string) => void) | undefined;\n\n constructor(\n protocolVersion: number,\n id: string,\n mode: ReplicatorMode,\n watermark: string,\n downstream: Subscription<string | PreSerializedBatch>,\n latestStatus: () => Status,\n options: SubscriberOptions = {},\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.mode = mode;\n this.#downstream = downstream;\n this.#latestStatus = latestStatus;\n this.#wsBatched = options.wsBatched ?? false;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = new RingBuffer();\n this.#backlogBackpressure = new ByteBackpressureGate(\n options.backlogHighWaterBytes ?? DEFAULT_BACKLOG_HIGH_WATER_BYTES,\n options.backlogLowWaterRatio ?? DEFAULT_BACKLOG_LOW_WATER_RATIO,\n );\n this.#onAck = options.onAck;\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n /**\n * Whether the backlog of live changes buffered during catchup has reached the\n * point at which {@link send()} stops resolving. Past it the subscriber is no\n * longer free: it holds up every subsequent flush, and with no other\n * subscriber to form a majority it stalls replication outright.\n */\n get backlogFull() {\n return (\n this.#bufferedBacklogBytes >= this.#backlogBackpressure.highWaterBytes\n );\n }\n\n /**\n * @returns whether the subscriber is currently sending backlogged messages,\n * vs caught up and sending the \"head\" of the replication stream.\n */\n isBacklogged() {\n return this.#backlog !== null || this.#bufferedBacklogBytes > 0;\n }\n\n /**\n * Resolves the first time {@link backlogFull} becomes true, so that a caller\n * holding the subscriber in catchup can give up at the moment the subscriber\n * starts costing replication rather than on a timer. Call cancel() when the\n * wait is no longer needed so the subscriber does not retain it.\n */\n whenBacklogFull(): BacklogFullWait {\n if (this.backlogFull) {\n return {promise: promiseVoid, cancel() {}};\n }\n const r = resolver<void>();\n this.#backlogFullWaiters.add(r);\n return {\n promise: r.promise,\n cancel: () => {\n this.#backlogFullWaiters.delete(r);\n },\n };\n }\n\n send(change: WatermarkedChange): Promise<void> {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n // During catchup, buffer live changes behind the durable catchup stream.\n // The returned promise applies backpressure if the buffered bytes cross\n // the high water mark.\n this.#pushBacklog(change);\n return this.#maybeWaitForBacklogSpace();\n }\n return this.#sendChange(change);\n }\n return promiseVoid;\n }\n\n sendBatch(\n changes: readonly WatermarkedChange[],\n preSerialized?: PreSerializedBatch | undefined,\n ): Promise<void> {\n if (changes.length === 0) {\n return promiseVoid;\n }\n\n if (\n this.#wsBatched &&\n preSerialized &&\n !this.#backlog &&\n this.#initialized &&\n changes[0][0] > this.#watermark &&\n (this.#protocolVersion >= 5 ||\n changes.every(c => this.supportsMessage(c[1])))\n ) {\n let commitWatermark: string | undefined;\n for (let i = changes.length - 1; i >= 0; i--) {\n if (changes[i][1] === 'commit') {\n commitWatermark = changes[i][0];\n break;\n }\n }\n if (commitWatermark) {\n this.#watermark = commitWatermark;\n }\n return this.#sendPreSerializedDownstream(preSerialized, commitWatermark);\n }\n\n const promises: Promise<void>[] = [];\n for (const change of changes) {\n const p = this.send(change);\n if (p !== promiseVoid) {\n promises.push(p);\n }\n }\n if (promises.length === 0) {\n return promiseVoid;\n }\n if (promises.length === 1) {\n return promises[0];\n }\n return Promise.all(promises).then(() => {});\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n this.sendStatus(this.#latestStatus());\n }\n }\n\n sendStatus(status: Status) {\n if (this.#initialized) {\n void this.#sendDownstream(['status', status]);\n }\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp(): Promise<void> {\n this.#initialize();\n if (!this.#backlog) {\n return this.#backlogDrain ?? promiseVoid;\n }\n if (!this.#backlogDrain) {\n // Keep #backlog non-null while queued entries are being handed to\n // downstream. That preserves ordering for sends that race with\n // setCaughtUp(): they append to the same backlog instead of bypassing\n // older buffered changes.\n this.#backlogDrain = this.#drainBacklog();\n void this.#backlogDrain.catch(e => this.fail(e));\n }\n return this.#backlogDrain;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n // Sends can complete out of order (e.g. the bounded window in\n // #drainBacklog), so the ack only advances monotonically, and listeners\n // are only notified when it does.\n const acked = max(this.#acked, watermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n }\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n const size = json.length;\n this.#pending++;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#pendingBytes -= size;\n this.#processed++;\n }\n }\n\n async #sendPreSerializedDownstream(\n batch: PreSerializedBatch,\n commitWatermark?: string | undefined,\n ): Promise<void> {\n const size = batch.byteLength;\n this.#pending += batch.changes.length;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(batch);\n try {\n const outcome = await result;\n if (commitWatermark && outcome === 'consumed') {\n const acked = max(this.#acked, commitWatermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n }\n }\n } finally {\n this.#pending -= batch.changes.length;\n this.#pendingBytes -= size;\n this.#processed += batch.changes.length;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #pendingBytes = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending + this.#backlogCount;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): SubscriberStats {\n const pending = this.numPending;\n if (this.#samples.length < 2) {\n return {\n processRate: 0,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {\n processRate,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n\n #missedLastTimeout = false;\n #laggingSinceMs: number | undefined;\n\n trackResponseResult(result: 'on-time' | 'timed-out') {\n this.#missedLastTimeout = result === 'timed-out';\n if (result === 'on-time') {\n this.#laggingSinceMs = undefined;\n }\n }\n\n /**\n * Reports the change rate of a slow subscriber (i.e. that missed the last\n * timeout) compared to the change rate of the (slowest) subscriber that\n * responded on time.\n *\n * * `lagging` indicates that this subscriber is slower\n * * `catching-up` indicates that it is faster\n *\n * Returns the total duration in which subscriber has been continuously\n * reported as `lagging`.\n */\n reportChangeRate(now: number, status: 'lagging' | 'catching-up') {\n assert(\n this.#missedLastTimeout,\n `reportChangeRate should only be called for slow subscribers`,\n );\n if (status === 'catching-up') {\n this.#laggingSinceMs = undefined;\n return 0;\n }\n this.#laggingSinceMs ??= now;\n return now - this.#laggingSinceMs;\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n /**\n * Ends the subscription without sending a downstream `['error', ...]`.\n *\n * This is deliberate, and not the same as reporting the failure to the\n * subscriber: `IncrementalSyncer` treats any `['error', ...]` as terminal and\n * shuts down to restore a fresh replica from litestream, whereas a clean end\n * backs off and re-subscribes. The failures routed here -- storer catchup\n * errors, backlog drain errors, backup-monitor errors -- are transient, so a\n * reconnect is the proportionate response and a fleet-wide restore is not.\n *\n * Callers are responsible for logging `err`; it is not carried downstream.\n */\n fail(_err?: unknown) {\n this.close();\n }\n\n close(error?: ErrorType, message?: string) {\n // Closing the subscriber must also release producers that are blocked on\n // backlog capacity; there is no future drain that could wake them.\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseAll();\n // The backlog can no longer grow, so nothing would ever resolve these.\n // Waiters re-check backlogFull, which is now false, and see the close.\n this.#resolveBacklogFullWaiters();\n\n if (error !== undefined) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream(['error', {type: error, message}]).finally(() =>\n this.#downstream.cancel(),\n );\n } else {\n this.#downstream.cancel();\n }\n }\n\n get #backlogCount() {\n return this.#backlog?.size ?? 0;\n }\n\n get #bufferedBacklogBytes() {\n // Include entries already handed to downstream but not yet consumed. Without\n // this, setCaughtUp() could move bytes out of #backlog faster than the\n // downstream Subscription can process them and release producers too early.\n return this.#backlogBytes + this.#backlogInFlightBytes;\n }\n\n get #totalBufferedBytes() {\n return this.#bufferedBacklogBytes + this.#pendingBytes;\n }\n\n #pushBacklog(change: WatermarkedChange) {\n assert(this.#backlog, 'cannot push to backlog after catchup completed');\n this.#backlog.push(change);\n this.#backlogBytes += change[2].length;\n if (this.backlogFull) {\n this.#resolveBacklogFullWaiters();\n }\n }\n\n #resolveBacklogFullWaiters() {\n const waiters = [...this.#backlogFullWaiters];\n this.#backlogFullWaiters.clear();\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n\n #maybeWaitForBacklogSpace(): Promise<void> {\n return this.#backlogBackpressure.waitForSpace(this.#bufferedBacklogBytes);\n }\n\n async #drainBacklog() {\n const inFlight: {promise: Promise<void>; bytes: number}[] = [];\n let inFlightBytes = 0;\n\n try {\n for (;;) {\n const change = this.#backlog?.shift();\n if (!change) {\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n break;\n }\n\n const bytes = change[2].length;\n this.#backlogBytes -= bytes;\n this.#backlogInFlightBytes += bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n\n // Send backlog entries in order, but keep only a bounded byte window in\n // flight. This avoids replacing one unbounded buffer with another inside\n // the downstream Subscription during catchup completion.\n const promise = this.#sendChange(change).finally(() => {\n this.#backlogInFlightBytes -= bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n });\n inFlight.push({promise, bytes});\n inFlightBytes += bytes;\n\n while (inFlightBytes >= this.#backlogBackpressure.highWaterBytes) {\n const next = must(inFlight.shift());\n await next.promise;\n inFlightBytes -= next.bytes;\n }\n }\n\n for (const {promise} of inFlight) {\n await promise;\n }\n } finally {\n this.#backlogDrain = null;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n }\n }\n}\n\nclass ByteBackpressureGate {\n readonly highWaterBytes: number;\n readonly #lowWaterBytes: number;\n readonly #waiters: Resolver<void>[] = [];\n\n constructor(highWaterBytes: number, lowWaterRatio: number) {\n this.highWaterBytes = Math.max(1, highWaterBytes);\n this.#lowWaterBytes =\n this.highWaterBytes * Math.min(1, Math.max(0, lowWaterRatio));\n }\n\n waitForSpace(bufferedBytes: number): Promise<void> {\n if (bufferedBytes < this.highWaterBytes) {\n return promiseVoid;\n }\n\n // One waiter represents one send() call that has already appended its\n // change. The producer is released when the backlog falls back below the low\n // water mark or the subscriber closes.\n const r = resolver<void>();\n this.#waiters.push(r);\n return r.promise;\n }\n\n releaseIfUnderLowWater(bufferedBytes: number) {\n if (this.#waiters.length === 0 || bufferedBytes > this.#lowWaterBytes) {\n return;\n }\n\n // Use a low water mark so waiting producers are released in batches instead\n // of waking one at a time around the high water boundary.\n this.releaseAll();\n }\n\n releaseAll() {\n const waiters = this.#waiters.splice(0);\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subscriber.js","names":["#protocolVersion","#downstream","#latestStatus","#wsBatched","#backlogBackpressure","#backlogFullWaiters","#onAck","#backfills","#onAligned","#onBackfillIgnored","#watermark","#acked","#backlog","#aligned","#bufferedBacklogBytes","#pushBacklog","#maybeWaitForBacklogSpace","#sendChange","#initialized","#trackBackfills","#sendPreSerializedDownstream","#sendDownstream","#initialize","#backlogDrain","#drainBacklog","#sendStringifiedDownstream","#pending","#pendingBytes","#processed","#backlogCount","#samples","#totalBufferedBytes","#missedLastTimeout","#laggingSinceMs","#backlogBytes","#resolveBacklogFullWaiters","#backlogInFlightBytes","#lowWaterBytes","#waiters"],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {BackfillState} from './backfill-state.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {\n ChangeTag,\n Downstream,\n Status,\n WatermarkedChange,\n} from './change-streamer.ts';\nimport type * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst DEFAULT_BACKLOG_HIGH_WATER_BYTES = 16 * 1024 * 1024;\nconst DEFAULT_BACKLOG_LOW_WATER_RATIO = 0.8;\n\nexport type SubscriberOptions = {\n backlogHighWaterBytes?: number | undefined;\n backlogLowWaterRatio?: number | undefined;\n wsBatched?: boolean | undefined;\n\n /**\n * Called whenever the subscriber's acked watermark advances, i.e. when it\n * has confirmed that a commit was durably applied to its replica. The ACK\n * therefore lags the subscriber's replica rather than leading it.\n */\n onAck?: ((watermark: string) => void) | undefined;\n\n /**\n * The subscriber's pending backfills (protocol v8+), which are tracked\n * over the changes sent to the subscriber.\n */\n backfills?: BackfillState | undefined;\n\n /**\n * Called once the subscriber has been sent all changes up to the head of\n * the stream, i.e. its catchup and backlog have been flushed. From this\n * point on, the subscriber's tracked {@link backfills} reflect the same\n * position in the stream as that of the change-streamer.\n */\n onAligned?: ((subscriber: Subscriber) => void) | undefined;\n\n /**\n * Called when a `backfill` or `backfill-completed` message is ignored for\n * some of the subscriber's pending backfill columns, after the subscriber\n * is aligned.\n */\n onBackfillIgnored?: ((columns: string[]) => void) | undefined;\n};\n\nexport type BacklogFullWait = {\n readonly promise: Promise<void>;\n cancel(): void;\n};\n\nexport type SubscriberStats = {\n processRate: number;\n pending: number;\n backlog: number;\n backlogBytes: number;\n totalBufferedBytes: number;\n missedLastTimeout: boolean;\n};\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly mode: ReplicatorMode;\n readonly #downstream: Subscription<string | PreSerializedBatch>;\n readonly #latestStatus: () => Status;\n readonly #wsBatched: boolean;\n #watermark: string;\n #acked: string;\n #backlog: RingBuffer<WatermarkedChange> | null;\n // While catchup is running, live changes are buffered here instead of being\n // pushed downstream. RingBuffer lets drainBacklog consume that backlog without\n // shifting an array, which matters when a subscriber is far behind.\n #backlogBytes = 0;\n #backlogInFlightBytes = 0;\n #backlogDrain: Promise<void> | null = null;\n readonly #backlogBackpressure: ByteBackpressureGate;\n readonly #backlogFullWaiters = new Set<Resolver<void>>();\n readonly #onAck: ((watermark: string) => void) | undefined;\n readonly #backfills: BackfillState | undefined;\n readonly #onAligned: ((subscriber: Subscriber) => void) | undefined;\n readonly #onBackfillIgnored: ((columns: string[]) => void) | undefined;\n #aligned = false;\n\n constructor(\n protocolVersion: number,\n id: string,\n mode: ReplicatorMode,\n watermark: string,\n downstream: Subscription<string | PreSerializedBatch>,\n latestStatus: () => Status,\n options: SubscriberOptions = {},\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.mode = mode;\n this.#downstream = downstream;\n this.#latestStatus = latestStatus;\n this.#wsBatched = options.wsBatched ?? false;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = new RingBuffer();\n this.#backlogBackpressure = new ByteBackpressureGate(\n options.backlogHighWaterBytes ?? DEFAULT_BACKLOG_HIGH_WATER_BYTES,\n options.backlogLowWaterRatio ?? DEFAULT_BACKLOG_LOW_WATER_RATIO,\n );\n this.#onAck = options.onAck;\n this.#backfills = options.backfills;\n this.#onAligned = options.onAligned;\n this.#onBackfillIgnored = options.onBackfillIgnored;\n }\n\n /**\n * The subscriber's tracked pending backfills, or `undefined` if the\n * subscriber does not report them (i.e. protocol < v8).\n */\n get backfills(): BackfillState | undefined {\n return this.#backfills;\n }\n\n /**\n * Whether the subscriber has been sent all changes up to the head of the\n * stream (see {@link SubscriberOptions.onAligned}).\n */\n get aligned(): boolean {\n return this.#aligned;\n }\n\n #trackBackfills(change: WatermarkedChange) {\n if (this.#backfills) {\n const ignored = this.#backfills.applySerialized(change);\n if (ignored.length && this.#aligned) {\n this.#onBackfillIgnored?.(ignored);\n }\n }\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n /**\n * Whether the backlog of live changes buffered during catchup has reached the\n * point at which {@link send()} stops resolving. Past it the subscriber is no\n * longer free: it holds up every subsequent flush, and with no other\n * subscriber to form a majority it stalls replication outright.\n */\n get backlogFull() {\n return (\n this.#bufferedBacklogBytes >= this.#backlogBackpressure.highWaterBytes\n );\n }\n\n /**\n * @returns whether the subscriber is currently sending backlogged messages,\n * vs caught up and sending the \"head\" of the replication stream.\n */\n isBacklogged() {\n return this.#backlog !== null || this.#bufferedBacklogBytes > 0;\n }\n\n /**\n * Resolves the first time {@link backlogFull} becomes true, so that a caller\n * holding the subscriber in catchup can give up at the moment the subscriber\n * starts costing replication rather than on a timer. Call cancel() when the\n * wait is no longer needed so the subscriber does not retain it.\n */\n whenBacklogFull(): BacklogFullWait {\n if (this.backlogFull) {\n return {promise: promiseVoid, cancel() {}};\n }\n const r = resolver<void>();\n this.#backlogFullWaiters.add(r);\n return {\n promise: r.promise,\n cancel: () => {\n this.#backlogFullWaiters.delete(r);\n },\n };\n }\n\n send(change: WatermarkedChange): Promise<void> {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n // During catchup, buffer live changes behind the durable catchup stream.\n // The returned promise applies backpressure if the buffered bytes cross\n // the high water mark.\n this.#pushBacklog(change);\n return this.#maybeWaitForBacklogSpace();\n }\n return this.#sendChange(change);\n }\n return promiseVoid;\n }\n\n sendBatch(\n changes: readonly WatermarkedChange[],\n preSerialized?: PreSerializedBatch | undefined,\n ): Promise<void> {\n if (changes.length === 0) {\n return promiseVoid;\n }\n\n if (\n this.#wsBatched &&\n preSerialized &&\n !this.#backlog &&\n this.#initialized &&\n changes[0][0] > this.#watermark &&\n (this.#protocolVersion >= 5 ||\n changes.every(c => this.supportsMessage(c[1])))\n ) {\n let commitWatermark: string | undefined;\n for (let i = changes.length - 1; i >= 0; i--) {\n if (changes[i][1] === 'commit') {\n commitWatermark = changes[i][0];\n break;\n }\n }\n if (commitWatermark) {\n this.#watermark = commitWatermark;\n }\n for (const change of changes) {\n this.#trackBackfills(change);\n }\n return this.#sendPreSerializedDownstream(preSerialized, commitWatermark);\n }\n\n const promises: Promise<void>[] = [];\n for (const change of changes) {\n const p = this.send(change);\n if (p !== promiseVoid) {\n promises.push(p);\n }\n }\n if (promises.length === 0) {\n return promiseVoid;\n }\n if (promises.length === 1) {\n return promises[0];\n }\n return Promise.all(promises).then(() => {});\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n this.sendStatus(this.#latestStatus());\n }\n }\n\n sendStatus(status: Status) {\n if (this.#initialized) {\n void this.#sendDownstream(['status', status]);\n }\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp(): Promise<void> {\n this.#initialize();\n if (!this.#backlog) {\n return this.#backlogDrain ?? promiseVoid;\n }\n if (!this.#backlogDrain) {\n // Keep #backlog non-null while queued entries are being handed to\n // downstream. That preserves ordering for sends that race with\n // setCaughtUp(): they append to the same backlog instead of bypassing\n // older buffered changes.\n this.#backlogDrain = this.#drainBacklog();\n void this.#backlogDrain.catch(e => this.fail(e));\n }\n return this.#backlogDrain;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n this.#trackBackfills(change);\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n // Sends can complete out of order (e.g. the bounded window in\n // #drainBacklog), so the ack only advances monotonically, and listeners\n // are only notified when it does.\n const acked = max(this.#acked, watermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n }\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n const size = json.length;\n this.#pending++;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#pendingBytes -= size;\n this.#processed++;\n }\n }\n\n async #sendPreSerializedDownstream(\n batch: PreSerializedBatch,\n commitWatermark?: string | undefined,\n ): Promise<void> {\n const size = batch.byteLength;\n this.#pending += batch.changes.length;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(batch);\n try {\n const outcome = await result;\n if (commitWatermark && outcome === 'consumed') {\n const acked = max(this.#acked, commitWatermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n }\n }\n } finally {\n this.#pending -= batch.changes.length;\n this.#pendingBytes -= size;\n this.#processed += batch.changes.length;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #pendingBytes = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending + this.#backlogCount;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): SubscriberStats {\n const pending = this.numPending;\n if (this.#samples.length < 2) {\n return {\n processRate: 0,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {\n processRate,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n\n #missedLastTimeout = false;\n #laggingSinceMs: number | undefined;\n\n trackResponseResult(result: 'on-time' | 'timed-out') {\n this.#missedLastTimeout = result === 'timed-out';\n if (result === 'on-time') {\n this.#laggingSinceMs = undefined;\n }\n }\n\n /**\n * Reports the change rate of a slow subscriber (i.e. that missed the last\n * timeout) compared to the change rate of the (slowest) subscriber that\n * responded on time.\n *\n * * `lagging` indicates that this subscriber is slower\n * * `catching-up` indicates that it is faster\n *\n * Returns the total duration in which subscriber has been continuously\n * reported as `lagging`.\n */\n reportChangeRate(now: number, status: 'lagging' | 'catching-up') {\n assert(\n this.#missedLastTimeout,\n `reportChangeRate should only be called for slow subscribers`,\n );\n if (status === 'catching-up') {\n this.#laggingSinceMs = undefined;\n return 0;\n }\n this.#laggingSinceMs ??= now;\n return now - this.#laggingSinceMs;\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n /**\n * Ends the subscription without sending a downstream `['error', ...]`.\n *\n * This is deliberate, and not the same as reporting the failure to the\n * subscriber: `IncrementalSyncer` treats any `['error', ...]` as terminal and\n * shuts down to restore a fresh replica from litestream, whereas a clean end\n * backs off and re-subscribes. The failures routed here -- storer catchup\n * errors, backlog drain errors, backup-monitor errors -- are transient, so a\n * reconnect is the proportionate response and a fleet-wide restore is not.\n *\n * Callers are responsible for logging `err`; it is not carried downstream.\n */\n fail(_err?: unknown) {\n this.close();\n }\n\n close(error?: ErrorType, message?: string) {\n // Closing the subscriber must also release producers that are blocked on\n // backlog capacity; there is no future drain that could wake them.\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseAll();\n // The backlog can no longer grow, so nothing would ever resolve these.\n // Waiters re-check backlogFull, which is now false, and see the close.\n this.#resolveBacklogFullWaiters();\n\n if (error !== undefined) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream(['error', {type: error, message}]).finally(() =>\n this.#downstream.cancel(),\n );\n } else {\n this.#downstream.cancel();\n }\n }\n\n get #backlogCount() {\n return this.#backlog?.size ?? 0;\n }\n\n get #bufferedBacklogBytes() {\n // Include entries already handed to downstream but not yet consumed. Without\n // this, setCaughtUp() could move bytes out of #backlog faster than the\n // downstream Subscription can process them and release producers too early.\n return this.#backlogBytes + this.#backlogInFlightBytes;\n }\n\n get #totalBufferedBytes() {\n return this.#bufferedBacklogBytes + this.#pendingBytes;\n }\n\n #pushBacklog(change: WatermarkedChange) {\n assert(this.#backlog, 'cannot push to backlog after catchup completed');\n this.#backlog.push(change);\n this.#backlogBytes += change[2].length;\n if (this.backlogFull) {\n this.#resolveBacklogFullWaiters();\n }\n }\n\n #resolveBacklogFullWaiters() {\n const waiters = [...this.#backlogFullWaiters];\n this.#backlogFullWaiters.clear();\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n\n #maybeWaitForBacklogSpace(): Promise<void> {\n return this.#backlogBackpressure.waitForSpace(this.#bufferedBacklogBytes);\n }\n\n async #drainBacklog() {\n const inFlight: {promise: Promise<void>; bytes: number}[] = [];\n let inFlightBytes = 0;\n\n try {\n for (;;) {\n const change = this.#backlog?.shift();\n if (!change) {\n const closed = this.#backlog === null;\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n if (!closed && !this.#aligned) {\n // All changes up to the head have been handed to #sendChange()\n // (and thus tracked), and subsequent changes are sent directly.\n this.#aligned = true;\n this.#onAligned?.(this);\n }\n break;\n }\n\n const bytes = change[2].length;\n this.#backlogBytes -= bytes;\n this.#backlogInFlightBytes += bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n\n // Send backlog entries in order, but keep only a bounded byte window in\n // flight. This avoids replacing one unbounded buffer with another inside\n // the downstream Subscription during catchup completion.\n const promise = this.#sendChange(change).finally(() => {\n this.#backlogInFlightBytes -= bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n });\n inFlight.push({promise, bytes});\n inFlightBytes += bytes;\n\n while (inFlightBytes >= this.#backlogBackpressure.highWaterBytes) {\n const next = must(inFlight.shift());\n await next.promise;\n inFlightBytes -= next.bytes;\n }\n }\n\n for (const {promise} of inFlight) {\n await promise;\n }\n } finally {\n this.#backlogDrain = null;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n }\n }\n}\n\nclass ByteBackpressureGate {\n readonly highWaterBytes: number;\n readonly #lowWaterBytes: number;\n readonly #waiters: Resolver<void>[] = [];\n\n constructor(highWaterBytes: number, lowWaterRatio: number) {\n this.highWaterBytes = Math.max(1, highWaterBytes);\n this.#lowWaterBytes =\n this.highWaterBytes * Math.min(1, Math.max(0, lowWaterRatio));\n }\n\n waitForSpace(bufferedBytes: number): Promise<void> {\n if (bufferedBytes < this.highWaterBytes) {\n return promiseVoid;\n }\n\n // One waiter represents one send() call that has already appended its\n // change. The producer is released when the backlog falls back below the low\n // water mark or the subscriber closes.\n const r = resolver<void>();\n this.#waiters.push(r);\n return r.promise;\n }\n\n releaseIfUnderLowWater(bufferedBytes: number) {\n if (this.#waiters.length === 0 || bufferedBytes > this.#lowWaterBytes) {\n return;\n }\n\n // Use a low water mark so waiting producers are released in batches instead\n // of waking one at a time around the high water boundary.\n this.releaseAll();\n }\n\n releaseAll() {\n const waiters = this.#waiters.splice(0);\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ import type { LogContext } from '@rocicorp/logger';
2
+ import type { FastifyReply, FastifyRequest } from 'fastify';
3
+ import { Database } from '../../../zqlite/src/db.ts';
4
+ import { type SampleValueKind } from '../../../zqlite/src/sqlite-stat4-sample.ts';
5
+ import type { NormalizedZeroConfig as ZeroConfig } from '../config/normalize.ts';
6
+ /**
7
+ * A fixed glossary, sent with every bundle. The audience is a language model
8
+ * that has the user's codebase but has never seen these statistics, and would
9
+ * otherwise read a sampled estimate as an exact count.
10
+ */
11
+ declare const ABOUT: {
12
+ readonly whatThisIs: string;
13
+ readonly stat1Format: string;
14
+ readonly stat4Format: string;
15
+ readonly howThePlannerUsesThis: readonly [string, string, string];
16
+ readonly whatYouCanChange: readonly [string, 'A `limit`, which lets the planner stop early.', string, 'The planner flags in `server.planner`.'];
17
+ readonly caveats: readonly [string, string, string];
18
+ readonly nextStep: string;
19
+ };
20
+ export type Stat1 = {
21
+ /** The raw `sqlite_stat1.stat` text, in case a flag is not parsed here. */
22
+ raw: string;
23
+ /** Estimated rows in the table. */
24
+ rows: number;
25
+ /**
26
+ * Average rows per distinct key prefix: the first entry is for a 1-column
27
+ * prefix, the second for 2 columns, and so on.
28
+ */
29
+ avgRowsPerPrefix: number[];
30
+ /** Non-numeric tokens, e.g. `unordered`, `noskipscan`, `sz=N`. */
31
+ flags: string[];
32
+ };
33
+ /** A `sqlite_stat4` sample with its values replaced by their types. */
34
+ export type Stat4Sample = {
35
+ nEq: number[];
36
+ nLt: number[];
37
+ nDLt: number[];
38
+ /** One entry per key column, plus the trailing rowid. */
39
+ sample: SampleValueKind[];
40
+ };
41
+ /**
42
+ * A digest of an index's `sqlite_stat4` histogram, one entry per key prefix
43
+ * depth. This is what the default response carries: a full histogram runs to
44
+ * hundreds of samples per index, which is a lot of context to spend on data an
45
+ * LLM would only summarize anyway.
46
+ */
47
+ export type Stat4PrefixSummary = {
48
+ /** Rows equal to the most common sampled key at this depth. */
49
+ maxRowsPerKey: number;
50
+ /** Median rows per sampled key at this depth. */
51
+ medianRowsPerKey: number;
52
+ /**
53
+ * Lower bound on distinct keys at this depth, from the last sample's count of
54
+ * distinct values less than it.
55
+ */
56
+ estimatedDistinctKeys: number;
57
+ };
58
+ export type Stat4Summary = {
59
+ samples: number;
60
+ /** The type of each key column, and of the trailing rowid. */
61
+ sampleKinds: SampleValueKind[];
62
+ /** One entry per key prefix depth: 1 column, then 2, and so on. */
63
+ perPrefix: Stat4PrefixSummary[];
64
+ };
65
+ export type IndexStats = {
66
+ name: string;
67
+ columns: {
68
+ name: string;
69
+ dir: 'ASC' | 'DESC';
70
+ }[];
71
+ unique: boolean;
72
+ partial: boolean;
73
+ stat1?: Stat1 | undefined;
74
+ stat4?: Stat4Summary | undefined;
75
+ /** The full histogram, only with `?stat4=full`. */
76
+ stat4Samples?: Stat4Sample[] | undefined;
77
+ };
78
+ export type ColumnStats = {
79
+ name: string;
80
+ dataType: string;
81
+ nullable: boolean;
82
+ /** The ZQL type, or `null` for a column clients cannot see. */
83
+ zqlType: string | null;
84
+ backfilling?: true | undefined;
85
+ };
86
+ export type TableStats = {
87
+ name: string;
88
+ columns: ColumnStats[];
89
+ primaryKey: string[] | undefined;
90
+ estimatedRows: number | undefined;
91
+ syncable: boolean;
92
+ indexes: IndexStats[];
93
+ };
94
+ export type PlannerzBundle = {
95
+ about: typeof ABOUT;
96
+ server: {
97
+ zeroVersion: string;
98
+ sqliteVersion: string;
99
+ replicaWatermark: string | undefined;
100
+ generatedAt: string;
101
+ planner: {
102
+ enableQueryPlanner: boolean;
103
+ enableCorrelatedPredicatePushdown: boolean;
104
+ enablePlannerAwarePushdown: boolean;
105
+ };
106
+ };
107
+ statsQuality: {
108
+ stat1Present: boolean;
109
+ stat4Rows: number;
110
+ method: string;
111
+ tablesWithoutStats: string[];
112
+ };
113
+ tables: TableStats[];
114
+ };
115
+ /**
116
+ * Parses a `sqlite_stat1.stat` value.
117
+ *
118
+ * @visibleForTesting
119
+ */
120
+ export declare function parseStat1(raw: string): Stat1;
121
+ /**
122
+ * Builds the `/plannerz` bundle from the replica's catalog, `sqlite_stat*`
123
+ * tables and the server config. No user table is read, so the cost does not
124
+ * depend on how much data the replica holds.
125
+ *
126
+ * @visibleForTesting
127
+ */
128
+ export declare function buildPlannerzBundle(lc: LogContext, config: ZeroConfig, opts?: BundleOptions): PlannerzBundle;
129
+ export type BundleOptions = {
130
+ /** Include the full `sqlite_stat4` histogram, not just the digest. */
131
+ fullStat4?: boolean | undefined;
132
+ };
133
+ /** @visibleForTesting */
134
+ export declare function buildPlannerzBundleFromDB(lc: LogContext, db: Database, config: ZeroConfig, opts?: BundleOptions): PlannerzBundle;
135
+ /**
136
+ * Digests an index's stat4 samples.
137
+ *
138
+ * Note that `nLt` and `nDLt` are stored as text, so SQL cannot order the
139
+ * samples by them numerically. This works off the whole set rather than
140
+ * relying on the samples' order: the highest `nDLt` is a lower bound on the
141
+ * distinct keys, since the sample holding it has that many distinct keys below
142
+ * it plus itself.
143
+ *
144
+ * @visibleForTesting
145
+ */
146
+ export declare function summarizeStat4(samples: Stat4Sample[]): Stat4Summary;
147
+ /**
148
+ * Serves the replica's schema, indexes and SQLite statistics for consumption by
149
+ * a language model, so that it can advise on query rewrites and indexes.
150
+ *
151
+ * Row data is never returned. See `designs/003_replica_stats_endpoint.md`.
152
+ *
153
+ * HTTP query parameters:
154
+ * * `pretty`: indents the JSON
155
+ * * `stat4=full`: includes the full sqlite_stat4 histogram, which is large
156
+ */
157
+ export declare function handlePlannerzRequest(lc: LogContext, config: ZeroConfig, req: FastifyRequest, res: FastifyReply): Promise<void>;
158
+ export {};
159
+ //# sourceMappingURL=plannerz.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plannerz.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/plannerz.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,263 @@
1
+ import { getServerVersion, isAdminPasswordValid } from "../config/zero-config.js";
2
+ import { Database } from "../../../zqlite/src/db.js";
3
+ import { computeZqlSpecsFromLiteSpecs, listIndexes, listTables } from "../db/lite-tables.js";
4
+ import { decodeSampleKinds } from "../../../zqlite/src/sqlite-stat4-sample.js";
5
+ import { getReplicationState } from "./replicator/schema/replication-state.js";
6
+ import { StatementRunner } from "../db/statements.js";
7
+ import { _usingCtx } from "../../../_virtual/_@oxc-project_runtime@0.130.0/helpers/usingCtx.js";
8
+ import auth from "basic-auth";
9
+ //#region ../zero-cache/src/services/plannerz.ts
10
+ /**
11
+ * How the statistics in the replica were gathered. zero-cache only ever runs
12
+ * `PRAGMA optimize`, and always under an `analysis_limit`, which samples rather
13
+ * than scanning. A replica that someone ran `ANALYZE` on by hand has better
14
+ * statistics, which is visible in `sqlite_stat4` having rows at all.
15
+ */
16
+ var APPROXIMATE_METHOD = "PRAGMA optimize under an analysis_limit. Row counts and per-key averages are sampled estimates, and sqlite_stat4 is not collected.";
17
+ var FULL_METHOD = "ANALYZE, with no analysis limit. sqlite_stat4 has rows, so a full ANALYZE ran against this replica at some point.";
18
+ /**
19
+ * A fixed glossary, sent with every bundle. The audience is a language model
20
+ * that has the user's codebase but has never seen these statistics, and would
21
+ * otherwise read a sampled estimate as an exact count.
22
+ */
23
+ var ABOUT = {
24
+ whatThisIs: "Schema, indexes and SQLite statistics for the SQLite replica that zero-cache queries. Use it to decide how to rewrite ZQL queries and which indexes to add. It contains no row data.",
25
+ stat1Format: "sqlite_stat1.stat is \"<rows> <avg rows per distinct 1-column prefix> <... per 2-column prefix> ...\", parsed here into `rows` and `avgRowsPerPrefix`. NULLs count as ordinary values, so a sparse column looks less selective than it is. Trailing flags such as \"unordered\", \"noskipscan\" and \"sz=N\" are kept in `flags`.",
26
+ stat4Format: "sqlite_stat4 is a per-index histogram. For each sample, `nEq` is the rows equal to it, `nLt` the rows less than it, and `nDLt` the distinct values less than it, one entry per key column. A large `nEq` next to a small `nDLt` means a skewed column, where the average in stat1 hides the common values. Sample values are never returned: `sample` reports only each key column’s type.",
27
+ howThePlannerUsesThis: [
28
+ "Cost comes from SQLite scanstatus estimates for the SQL each source connection would run, so the statistics here are what the planner sees.",
29
+ "Join fanout comes from sqlite_stat4, else sqlite_stat1, else a default of 3. With no stat4 (the usual case) fanout on a nullable foreign key is overestimated, because stat1 counts NULLs.",
30
+ "The planner chooses per join whether to run it as a semi-join or to flip it so the child drives. An index that matches the child’s constraint plus ordering is what makes a flip cheap."
31
+ ],
32
+ whatYouCanChange: [
33
+ "The ZQL query: `whereExists` vs `related`, the order of conditions, and ordering that matches an existing index.",
34
+ "A `limit`, which lets the planner stop early.",
35
+ "Indexes: add them in Postgres, not on the replica. The replica copies upstream indexes, so a new index reaches it through replication.",
36
+ "The planner flags in `server.planner`."
37
+ ],
38
+ caveats: [
39
+ "Row counts are estimates from the statistics, not COUNT(*). This endpoint never scans a table.",
40
+ "Statistics are as of the last time they were gathered, which SQLite does not record. A table whose size changed a lot since then can be far off.",
41
+ "Tables in `tables` with `syncable: false` cannot be queried by clients: they lack a primary key or unique index, or every column has a type ZQL does not support."
42
+ ],
43
+ nextStep: "POST an AST to /plannerz/analyze to see the plan the planner picks for one query, with the SQLite query plans it generates."
44
+ };
45
+ /**
46
+ * Parses a `sqlite_stat1.stat` value.
47
+ *
48
+ * @visibleForTesting
49
+ */
50
+ function parseStat1(raw) {
51
+ const tokens = raw.trim().split(SEPARATOR).filter(Boolean);
52
+ const numbers = [];
53
+ const flags = [];
54
+ for (const token of tokens) if (flags.length === 0 && COUNT.test(token)) numbers.push(parseInt(token, 10));
55
+ else flags.push(token);
56
+ return {
57
+ raw,
58
+ rows: numbers[0] ?? 0,
59
+ avgRowsPerPrefix: numbers.slice(1),
60
+ flags
61
+ };
62
+ }
63
+ /** Whitespace between the counts in a `sqlite_stat1` or `sqlite_stat4` value. */
64
+ var SEPARATOR = /\s+/;
65
+ /** A count, as opposed to a trailing flag such as `unordered` or `sz=N`. */
66
+ var COUNT = /^\d+$/;
67
+ function tableExists(db, name) {
68
+ return db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).all(name).length > 0;
69
+ }
70
+ function parseCounts(raw) {
71
+ return raw.trim().split(SEPARATOR).filter(Boolean).map((n) => parseInt(n, 10));
72
+ }
73
+ /**
74
+ * Builds the `/plannerz` bundle from the replica's catalog, `sqlite_stat*`
75
+ * tables and the server config. No user table is read, so the cost does not
76
+ * depend on how much data the replica holds.
77
+ *
78
+ * @visibleForTesting
79
+ */
80
+ function buildPlannerzBundle(lc, config, opts = {}) {
81
+ try {
82
+ var _usingCtx$1 = _usingCtx();
83
+ return buildPlannerzBundleFromDB(lc, _usingCtx$1.u(new Database(lc, config.replica.file, { readonly: true })), config, opts);
84
+ } catch (_) {
85
+ _usingCtx$1.e = _;
86
+ } finally {
87
+ _usingCtx$1.d();
88
+ }
89
+ }
90
+ /** @visibleForTesting */
91
+ function buildPlannerzBundleFromDB(lc, db, config, opts = {}) {
92
+ const tables = listReplicaTables(lc, db);
93
+ const indexes = listIndexes(db);
94
+ const zqlSpecs = computeZqlSpecsFromLiteSpecs(tables, indexes, { includeBackfillingColumns: false }, void 0, void 0, lc);
95
+ const stat1Present = tableExists(db, "sqlite_stat1");
96
+ const stat1ByTableAndIndex = /* @__PURE__ */ new Map();
97
+ const statsByTable = /* @__PURE__ */ new Map();
98
+ if (stat1Present) for (const { tbl, idx, stat } of db.prepare(`SELECT tbl, idx, stat FROM sqlite_stat1`).all()) {
99
+ const parsed = parseStat1(stat);
100
+ if (idx !== null) stat1ByTableAndIndex.set(`${tbl}.${idx}`, parsed);
101
+ if (idx === null || !statsByTable.has(tbl)) statsByTable.set(tbl, parsed);
102
+ }
103
+ const stat4Present = tableExists(db, "sqlite_stat4");
104
+ const stat4ByTableAndIndex = /* @__PURE__ */ new Map();
105
+ let stat4Rows = 0;
106
+ if (stat4Present) for (const { tbl, idx, neq, nlt, ndlt, sample } of db.prepare(`SELECT tbl, idx, neq, nlt, ndlt, sample FROM sqlite_stat4`).all()) {
107
+ stat4Rows++;
108
+ const samples = stat4ByTableAndIndex.get(`${tbl}.${idx}`) ?? [];
109
+ samples.push({
110
+ nEq: parseCounts(neq),
111
+ nLt: parseCounts(nlt),
112
+ nDLt: parseCounts(ndlt),
113
+ sample: decodeSampleKinds(sample)
114
+ });
115
+ stat4ByTableAndIndex.set(`${tbl}.${idx}`, samples);
116
+ }
117
+ const indexesByTable = /* @__PURE__ */ new Map();
118
+ for (const index of indexes) {
119
+ const key = `${index.tableName}.${index.name}`;
120
+ const stat4 = stat4ByTableAndIndex.get(key);
121
+ const forTable = indexesByTable.get(index.tableName) ?? [];
122
+ forTable.push({
123
+ name: index.name,
124
+ columns: Object.entries(index.columns).map(([name, dir]) => ({
125
+ name,
126
+ dir
127
+ })),
128
+ unique: index.unique,
129
+ partial: index.partial === true,
130
+ stat1: stat1ByTableAndIndex.get(key),
131
+ stat4: stat4?.length ? summarizeStat4(stat4) : void 0,
132
+ stat4Samples: opts.fullStat4 && stat4?.length ? stat4 : void 0
133
+ });
134
+ indexesByTable.set(index.tableName, forTable);
135
+ }
136
+ const tablesWithoutStats = [];
137
+ const tableStats = tables.map((table) => {
138
+ const zqlSpec = zqlSpecs.get(table.name)?.zqlSpec;
139
+ if (!statsByTable.has(table.name)) tablesWithoutStats.push(table.name);
140
+ const backfilling = new Set(table.backfilling ?? []);
141
+ return {
142
+ name: table.name,
143
+ columns: Object.entries(table.columns).map(([name, spec]) => ({
144
+ name,
145
+ dataType: spec.dataType,
146
+ nullable: !spec.notNull,
147
+ zqlType: zqlSpec?.[name]?.type ?? null,
148
+ ...backfilling.has(name) ? { backfilling: true } : {}
149
+ })),
150
+ primaryKey: table.primaryKey ? [...table.primaryKey] : void 0,
151
+ estimatedRows: statsByTable.get(table.name)?.rows,
152
+ syncable: zqlSpec !== void 0,
153
+ indexes: indexesByTable.get(table.name) ?? []
154
+ };
155
+ });
156
+ return {
157
+ about: ABOUT,
158
+ server: {
159
+ zeroVersion: getServerVersion(config),
160
+ sqliteVersion: sqliteVersion(db),
161
+ replicaWatermark: replicaWatermark(lc, db),
162
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
163
+ planner: {
164
+ enableQueryPlanner: config.enableQueryPlanner,
165
+ enableCorrelatedPredicatePushdown: config.enableCorrelatedPredicatePushdown !== false,
166
+ enablePlannerAwarePushdown: config.enablePlannerAwarePushdown !== false
167
+ }
168
+ },
169
+ statsQuality: {
170
+ stat1Present,
171
+ stat4Rows,
172
+ method: stat4Rows > 0 ? FULL_METHOD : APPROXIMATE_METHOD,
173
+ tablesWithoutStats
174
+ },
175
+ tables: tableStats
176
+ };
177
+ }
178
+ /**
179
+ * Lists the replica's tables, tolerating a replica that has no metadata tables
180
+ * yet. `listTables` reads `_zero.tableMetadata` for backfill status, which does
181
+ * not exist until initial sync has created it. Reporting the schema without
182
+ * backfill status beats failing the whole request.
183
+ */
184
+ function listReplicaTables(lc, db) {
185
+ try {
186
+ return listTables(db);
187
+ } catch (e) {
188
+ lc.debug?.("reading table metadata failed; listing tables without it", e);
189
+ return listTables(db, true, false);
190
+ }
191
+ }
192
+ /**
193
+ * Digests an index's stat4 samples.
194
+ *
195
+ * Note that `nLt` and `nDLt` are stored as text, so SQL cannot order the
196
+ * samples by them numerically. This works off the whole set rather than
197
+ * relying on the samples' order: the highest `nDLt` is a lower bound on the
198
+ * distinct keys, since the sample holding it has that many distinct keys below
199
+ * it plus itself.
200
+ *
201
+ * @visibleForTesting
202
+ */
203
+ function summarizeStat4(samples) {
204
+ const depth = Math.max(...samples.map((s) => s.nEq.length));
205
+ const perPrefix = [];
206
+ for (let d = 0; d < depth; d++) {
207
+ const counts = samples.map((s) => s.nEq[d]).filter((n) => n !== void 0).sort((a, b) => a - b);
208
+ const maxDistinctBelow = Math.max(...samples.map((s) => s.nDLt[d]).filter((n) => n !== void 0));
209
+ perPrefix.push({
210
+ maxRowsPerKey: counts.at(-1),
211
+ medianRowsPerKey: counts[Math.floor((counts.length - 1) / 2)],
212
+ estimatedDistinctKeys: maxDistinctBelow + 1
213
+ });
214
+ }
215
+ return {
216
+ samples: samples.length,
217
+ sampleKinds: samples[0].sample,
218
+ perPrefix
219
+ };
220
+ }
221
+ function sqliteVersion(db) {
222
+ const [{ version }] = db.prepare(`SELECT sqlite_version() AS version`).all();
223
+ return version;
224
+ }
225
+ function replicaWatermark(lc, db) {
226
+ try {
227
+ return getReplicationState(new StatementRunner(db)).stateVersion;
228
+ } catch (e) {
229
+ lc.debug?.("could not read replication state", e);
230
+ return;
231
+ }
232
+ }
233
+ /**
234
+ * Serves the replica's schema, indexes and SQLite statistics for consumption by
235
+ * a language model, so that it can advise on query rewrites and indexes.
236
+ *
237
+ * Row data is never returned. See `designs/003_replica_stats_endpoint.md`.
238
+ *
239
+ * HTTP query parameters:
240
+ * * `pretty`: indents the JSON
241
+ * * `stat4=full`: includes the full sqlite_stat4 histogram, which is large
242
+ */
243
+ async function handlePlannerzRequest(lc, config, req, res) {
244
+ if (!isAdminPasswordValid(lc, config, auth(req)?.pass)) {
245
+ res.code(401).header("WWW-Authenticate", "Basic realm=\"Plannerz Protected Area\"").send("Unauthorized");
246
+ return;
247
+ }
248
+ const query = req.query;
249
+ const indent = query.pretty !== void 0 ? 2 : void 0;
250
+ let bundle;
251
+ try {
252
+ bundle = buildPlannerzBundle(lc, config, { fullStat4: query.stat4 === "full" });
253
+ } catch (e) {
254
+ lc.error?.("error building plannerz bundle", e);
255
+ await res.code(500).send({ error: String(e) });
256
+ return;
257
+ }
258
+ await res.header("Content-Type", "application/json").send(JSON.stringify(bundle, null, indent));
259
+ }
260
+ //#endregion
261
+ export { handlePlannerzRequest };
262
+
263
+ //# sourceMappingURL=plannerz.js.map