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

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 (785) 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 +4 -3
  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.d.ts +9 -0
  121. package/out/shared/src/bigint-json.d.ts.map +1 -1
  122. package/out/shared/src/bigint-json.js +27 -2
  123. package/out/shared/src/bigint-json.js.map +1 -1
  124. package/out/shared/src/broadcast-channel.js +1 -1
  125. package/out/shared/src/btree-set.js +11 -8
  126. package/out/shared/src/btree-set.js.map +1 -1
  127. package/out/shared/src/cache.js.map +1 -1
  128. package/out/shared/src/config.js +1 -1
  129. package/out/shared/src/custom-key-map.js.map +1 -1
  130. package/out/shared/src/custom-key-set.js.map +1 -1
  131. package/out/shared/src/deep-clone.js +1 -1
  132. package/out/shared/src/document-visible.js.map +1 -1
  133. package/out/shared/src/dotenv.js.map +1 -1
  134. package/out/shared/src/error.js +1 -1
  135. package/out/shared/src/error.js.map +1 -1
  136. package/out/shared/src/hash.js +2 -2
  137. package/out/shared/src/hash.js.map +1 -1
  138. package/out/shared/src/iterables.js.map +1 -1
  139. package/out/shared/src/json-schema.js.map +1 -1
  140. package/out/shared/src/json.js.map +1 -1
  141. package/out/shared/src/logging-test-utils.js +1 -1
  142. package/out/shared/src/logging.js +1 -1
  143. package/out/shared/src/logging.js.map +1 -1
  144. package/out/shared/src/navigator.js +1 -1
  145. package/out/shared/src/options.js +9 -12
  146. package/out/shared/src/options.js.map +1 -1
  147. package/out/shared/src/promise-race.js.map +1 -1
  148. package/out/shared/src/queue.js.map +1 -1
  149. package/out/shared/src/record-proxy.js.map +1 -1
  150. package/out/shared/src/ring-buffer.js.map +1 -1
  151. package/out/shared/src/size-of-value.js +7 -10
  152. package/out/shared/src/size-of-value.js.map +1 -1
  153. package/out/shared/src/sorted-entries.js.map +1 -1
  154. package/out/shared/src/tdigest-schema.js.map +1 -1
  155. package/out/shared/src/tdigest.js.map +1 -1
  156. package/out/shared/src/valita.js +1 -1
  157. package/out/shared/src/valita.js.map +1 -1
  158. package/out/z2s/src/compiler.js +2 -4
  159. package/out/z2s/src/compiler.js.map +1 -1
  160. package/out/z2s/src/sql.js +1 -1
  161. package/out/z2s/src/sql.js.map +1 -1
  162. package/out/zero/package.js +212 -189
  163. package/out/zero/package.js.map +1 -1
  164. package/out/zero/src/adapters/drizzle.js +1 -2
  165. package/out/zero/src/adapters/kysely.js +1 -2
  166. package/out/zero/src/adapters/pg.js +1 -2
  167. package/out/zero/src/adapters/postgresjs.js +1 -2
  168. package/out/zero/src/adapters/prisma.js +1 -2
  169. package/out/zero/src/analyze.js +1 -2
  170. package/out/zero/src/change-protocol/v0.js +1 -2
  171. package/out/zero/src/expo-sqlite.js +1 -2
  172. package/out/zero/src/op-sqlite.js +1 -2
  173. package/out/zero/src/react-native.js.map +1 -1
  174. package/out/zero/src/sqlite.js +1 -2
  175. package/out/zero/src/zero-cache-dev.js +10 -9
  176. package/out/zero/src/zero-cache-dev.js.map +1 -1
  177. package/out/zero/src/zero-out.js +2 -1
  178. package/out/zero/src/zero-out.js.map +1 -1
  179. package/out/zero-cache/src/auth/auth.js +1 -1
  180. package/out/zero-cache/src/auth/auth.js.map +1 -1
  181. package/out/zero-cache/src/auth/jwt.js +4 -1
  182. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  183. package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
  184. package/out/zero-cache/src/auth/read-authorizer.js +3 -2
  185. package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
  186. package/out/zero-cache/src/auth/write-authorizer.js +5 -16
  187. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  188. package/out/zero-cache/src/config/network.js +1 -1
  189. package/out/zero-cache/src/config/network.js.map +1 -1
  190. package/out/zero-cache/src/config/server-context.js.map +1 -1
  191. package/out/zero-cache/src/config/zero-config.d.ts +5 -0
  192. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  193. package/out/zero-cache/src/config/zero-config.js +13 -2
  194. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  195. package/out/zero-cache/src/custom/fetch.js +1 -1
  196. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  197. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  198. package/out/zero-cache/src/db/create.js.map +1 -1
  199. package/out/zero-cache/src/db/index-predicate.js.map +1 -1
  200. package/out/zero-cache/src/db/lite-tables.d.ts +2 -2
  201. package/out/zero-cache/src/db/lite-tables.d.ts.map +1 -1
  202. package/out/zero-cache/src/db/lite-tables.js.map +1 -1
  203. package/out/zero-cache/src/db/migration-lite.js +1 -1
  204. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  205. package/out/zero-cache/src/db/migration.js +5 -3
  206. package/out/zero-cache/src/db/migration.js.map +1 -1
  207. package/out/zero-cache/src/db/pg-copy-binary.js +4 -3
  208. package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
  209. package/out/zero-cache/src/db/pg-copy.js +0 -1
  210. package/out/zero-cache/src/db/pg-copy.js.map +1 -1
  211. package/out/zero-cache/src/db/pg-to-lite.js +1 -1
  212. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  213. package/out/zero-cache/src/db/run-transaction.js.map +1 -1
  214. package/out/zero-cache/src/db/specs.js +7 -5
  215. package/out/zero-cache/src/db/specs.js.map +1 -1
  216. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
  217. package/out/zero-cache/src/db/statements.d.ts.map +1 -1
  218. package/out/zero-cache/src/db/statements.js +9 -2
  219. package/out/zero-cache/src/db/statements.js.map +1 -1
  220. package/out/zero-cache/src/db/transaction-pool.js +5 -3
  221. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  222. package/out/zero-cache/src/db/warmup.js.map +1 -1
  223. package/out/zero-cache/src/observability/events.js.map +1 -1
  224. package/out/zero-cache/src/observability/log-throttle.js.map +1 -1
  225. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  226. package/out/zero-cache/src/scripts/deploy-permissions.js +4 -2
  227. package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
  228. package/out/zero-cache/src/scripts/permissions.js.map +1 -1
  229. package/out/zero-cache/src/server/anonymous-otel-start.js +2 -1
  230. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  231. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  232. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  233. package/out/zero-cache/src/server/logging.js +1 -1
  234. package/out/zero-cache/src/server/logging.js.map +1 -1
  235. package/out/zero-cache/src/server/main.js.map +1 -1
  236. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  237. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  238. package/out/zero-cache/src/server/reaper.js +2 -2
  239. package/out/zero-cache/src/server/reaper.js.map +1 -1
  240. package/out/zero-cache/src/server/replicator.js.map +1 -1
  241. package/out/zero-cache/src/server/runner/run-worker.js +2 -1
  242. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  243. package/out/zero-cache/src/server/runner/runtime.js +2 -1
  244. package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
  245. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  246. package/out/zero-cache/src/server/shadow-syncer.js +1 -1
  247. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  248. package/out/zero-cache/src/server/slot-keeper.js.map +1 -1
  249. package/out/zero-cache/src/server/syncer-assigner.js.map +1 -1
  250. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  251. package/out/zero-cache/src/server/syncer.js +6 -1
  252. package/out/zero-cache/src/server/syncer.js.map +1 -1
  253. package/out/zero-cache/src/server/worker-dispatcher.js +1 -1
  254. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  255. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  256. package/out/zero-cache/src/services/analyze.js +1 -1
  257. package/out/zero-cache/src/services/analyze.js.map +1 -1
  258. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +1 -1
  259. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  260. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  261. package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -1
  262. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
  263. package/out/zero-cache/src/services/change-source/common/replica-schema.js +3 -3
  264. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  265. package/out/zero-cache/src/services/change-source/custom/change-source.js +3 -3
  266. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  267. package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
  268. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +3 -3
  269. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  270. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +1 -1
  271. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
  272. package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -6
  273. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  274. package/out/zero-cache/src/services/change-source/pg/decommission.js +2 -1
  275. package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
  276. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +5 -3
  277. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  278. package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
  279. package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
  280. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +3 -2
  281. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  282. package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
  283. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js +2 -2
  284. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js.map +1 -1
  285. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js +1 -1
  286. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js.map +1 -1
  287. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +3 -3
  288. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  289. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +1 -1
  290. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  291. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +13 -12
  292. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
  293. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  294. package/out/zero-cache/src/services/change-source/pg/schema/published.js +1 -1
  295. package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
  296. package/out/zero-cache/src/services/change-source/pg/schema/replica-stage-enum.js.map +1 -1
  297. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +9 -6
  298. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  299. package/out/zero-cache/src/services/change-source/pg/schema/validation.js +1 -1
  300. package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
  301. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +1 -1
  302. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -1
  303. package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
  304. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  305. package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
  306. package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
  307. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  308. package/out/zero-cache/src/services/change-source/protocol/current.js +1 -1
  309. package/out/zero-cache/src/services/change-source/protocol/mod.js +1 -2
  310. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
  311. package/out/zero-cache/src/services/change-streamer/backfill-state.js +1 -1
  312. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -1
  313. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  314. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
  315. package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
  316. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  317. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +1 -1
  318. package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -1
  319. package/out/zero-cache/src/services/change-streamer/change-log-codec.js +2 -3
  320. package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -1
  321. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +2 -2
  322. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
  323. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -1
  324. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +5 -3
  325. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  326. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +21 -21
  327. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  328. package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
  329. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  330. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
  331. package/out/zero-cache/src/services/change-streamer/forwarder.js +11 -10
  332. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  333. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js +1 -1
  334. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js.map +1 -1
  335. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +3 -3
  336. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
  337. package/out/zero-cache/src/services/change-streamer/replica-poller.js +1 -1
  338. package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -1
  339. package/out/zero-cache/src/services/change-streamer/restore-admission.js +12 -1
  340. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
  341. package/out/zero-cache/src/services/change-streamer/schema/init.js +1 -1
  342. package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
  343. package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
  344. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  345. package/out/zero-cache/src/services/change-streamer/snapshot-message.js +1 -1
  346. package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
  347. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  348. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +1 -1
  349. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
  350. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js +2 -2
  351. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js.map +1 -1
  352. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -1
  353. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
  354. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts.map +1 -1
  355. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +4 -6
  356. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -1
  357. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +1 -3
  358. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
  359. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  360. package/out/zero-cache/src/services/change-streamer/storer.js +2 -2
  361. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  362. package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
  363. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  364. package/out/zero-cache/src/services/change-streamer/subscriber.js +2 -2
  365. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  366. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +0 -1
  367. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
  368. package/out/zero-cache/src/services/heapz.js +2 -1
  369. package/out/zero-cache/src/services/heapz.js.map +1 -1
  370. package/out/zero-cache/src/services/http-service.js.map +1 -1
  371. package/out/zero-cache/src/services/life-cycle.js +1 -1
  372. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  373. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js +4 -2
  374. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
  375. package/out/zero-cache/src/services/litestream/commands.js +1 -1
  376. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  377. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +2 -2
  378. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
  379. package/out/zero-cache/src/services/litestream/litestream-controller.js.map +1 -1
  380. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  381. package/out/zero-cache/src/services/litestream/restore-progress.js.map +1 -1
  382. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  383. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  384. package/out/zero-cache/src/services/mutagen/pusher.js +3 -2
  385. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  386. package/out/zero-cache/src/services/plannerz.js +4 -3
  387. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  388. package/out/zero-cache/src/services/profz.js +5 -3
  389. package/out/zero-cache/src/services/profz.js.map +1 -1
  390. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  391. package/out/zero-cache/src/services/replicator/change-log-db.js +3 -2
  392. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
  393. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
  394. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
  395. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  396. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
  397. package/out/zero-cache/src/services/replicator/incremental-sync.js +1 -1
  398. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  399. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  400. package/out/zero-cache/src/services/replicator/replication-status.js +4 -3
  401. package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
  402. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  403. package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
  404. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  405. package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
  406. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
  407. package/out/zero-cache/src/services/replicator/schema/backfilling.js +1 -1
  408. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  409. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +19 -7
  410. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts.map +1 -1
  411. package/out/zero-cache/src/services/replicator/schema/change-log.js +31 -15
  412. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  413. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  414. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  415. package/out/zero-cache/src/services/replicator/schema/replication-state.js +10 -7
  416. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  417. package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
  418. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +1 -1
  419. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
  420. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +1 -1
  421. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -1
  422. package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
  423. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  424. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  425. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  426. package/out/zero-cache/src/services/runner.js.map +1 -1
  427. package/out/zero-cache/src/services/running-state.js.map +1 -1
  428. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  429. package/out/zero-cache/src/services/statz.js +2 -1
  430. package/out/zero-cache/src/services/statz.js.map +1 -1
  431. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +2 -2
  432. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
  433. package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -1
  434. package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -1
  435. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  436. package/out/zero-cache/src/services/view-syncer/client-handler.js +24 -3
  437. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  438. package/out/zero-cache/src/services/view-syncer/client-schema.d.ts +15 -2
  439. package/out/zero-cache/src/services/view-syncer/client-schema.d.ts.map +1 -1
  440. package/out/zero-cache/src/services/view-syncer/client-schema.js +46 -6
  441. package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
  442. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  443. package/out/zero-cache/src/services/view-syncer/cvr-purger.js +2 -2
  444. package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
  445. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  446. package/out/zero-cache/src/services/view-syncer/cvr-store.js +10 -5
  447. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  448. package/out/zero-cache/src/services/view-syncer/cvr.js +12 -4
  449. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  450. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +1 -1
  451. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -1
  452. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  453. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -1
  454. package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -1
  455. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +2 -2
  456. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -1
  457. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +5 -3
  458. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  459. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  460. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +8 -5
  461. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  462. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -1
  463. package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -1
  464. package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -1
  465. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
  466. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -1
  467. package/out/zero-cache/src/services/view-syncer/query-stats.js +1 -1
  468. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
  469. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  470. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
  471. package/out/zero-cache/src/services/view-syncer/schema/types.js +1 -1
  472. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  473. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js +7 -1
  474. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js.map +1 -1
  475. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +8 -1
  476. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  477. package/out/zero-cache/src/services/view-syncer/snapshotter.js +6 -5
  478. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  479. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
  480. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  481. package/out/zero-cache/src/services/view-syncer/view-syncer.js +13 -9
  482. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  483. package/out/zero-cache/src/types/error-with-level.js.map +1 -1
  484. package/out/zero-cache/src/types/lexi-version.js.map +1 -1
  485. package/out/zero-cache/src/types/lite.js +1 -1
  486. package/out/zero-cache/src/types/lite.js.map +1 -1
  487. package/out/zero-cache/src/types/pg-data-type.js +1 -1
  488. package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
  489. package/out/zero-cache/src/types/pg.js +6 -4
  490. package/out/zero-cache/src/types/pg.js.map +1 -1
  491. package/out/zero-cache/src/types/poke-chunk.js +2 -2
  492. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  493. package/out/zero-cache/src/types/processes.js +1 -1
  494. package/out/zero-cache/src/types/processes.js.map +1 -1
  495. package/out/zero-cache/src/types/profiler.js.map +1 -1
  496. package/out/zero-cache/src/types/row-key.js.map +1 -1
  497. package/out/zero-cache/src/types/sql.js.map +1 -1
  498. package/out/zero-cache/src/types/streams.js +10 -9
  499. package/out/zero-cache/src/types/streams.js.map +1 -1
  500. package/out/zero-cache/src/types/strings.js.map +1 -1
  501. package/out/zero-cache/src/types/subscription.js.map +1 -1
  502. package/out/zero-cache/src/types/timeout.js +1 -1
  503. package/out/zero-cache/src/types/websocket-handoff.js +1 -1
  504. package/out/zero-cache/src/types/ws.js +1 -1
  505. package/out/zero-cache/src/workers/connection.js +5 -6
  506. package/out/zero-cache/src/workers/connection.js.map +1 -1
  507. package/out/zero-cache/src/workers/mutator.js.map +1 -1
  508. package/out/zero-cache/src/workers/replicator.js +5 -4
  509. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  510. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  511. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +9 -1
  512. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  513. package/out/zero-cache/src/workers/syncer.js +4 -4
  514. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  515. package/out/zero-client/src/client/active-clients-manager.js +1 -1
  516. package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
  517. package/out/zero-client/src/client/bindings.js +11 -11
  518. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  519. package/out/zero-client/src/client/connection.js +1 -1
  520. package/out/zero-client/src/client/connection.js.map +1 -1
  521. package/out/zero-client/src/client/context.js.map +1 -1
  522. package/out/zero-client/src/client/crud-impl.js +1 -1
  523. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  524. package/out/zero-client/src/client/crud.js +1 -1
  525. package/out/zero-client/src/client/crud.js.map +1 -1
  526. package/out/zero-client/src/client/custom.js +1 -1
  527. package/out/zero-client/src/client/custom.js.map +1 -1
  528. package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
  529. package/out/zero-client/src/client/enable-analytics.js +1 -1
  530. package/out/zero-client/src/client/enable-analytics.js.map +1 -1
  531. package/out/zero-client/src/client/error.js.map +1 -1
  532. package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
  533. package/out/zero-client/src/client/inspector/client.js.map +1 -1
  534. package/out/zero-client/src/client/inspector/html-dialog-prompt.js +1 -1
  535. package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
  536. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  537. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  538. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  539. package/out/zero-client/src/client/ivm-branch.js +1 -3
  540. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  541. package/out/zero-client/src/client/keys.js +2 -1
  542. package/out/zero-client/src/client/keys.js.map +1 -1
  543. package/out/zero-client/src/client/log-options.js.map +1 -1
  544. package/out/zero-client/src/client/make-replicache-mutators.js +5 -3
  545. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  546. package/out/zero-client/src/client/metrics.js +3 -5
  547. package/out/zero-client/src/client/metrics.js.map +1 -1
  548. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  549. package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
  550. package/out/zero-client/src/client/options.js.map +1 -1
  551. package/out/zero-client/src/client/query-manager.js +2 -2
  552. package/out/zero-client/src/client/query-manager.js.map +1 -1
  553. package/out/zero-client/src/client/reload-error-handler.js +4 -3
  554. package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
  555. package/out/zero-client/src/client/version.js +1 -1
  556. package/out/zero-client/src/client/zero-poke-handler.js +1 -1
  557. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  558. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  559. package/out/zero-client/src/client/zero.js +19 -11
  560. package/out/zero-client/src/client/zero.js.map +1 -1
  561. package/out/zero-client/src/mod.js +23 -23
  562. package/out/zero-client/src/util/nanoid.js.map +1 -1
  563. package/out/zero-permissions/src/permissions.js +3 -2
  564. package/out/zero-permissions/src/permissions.js.map +1 -1
  565. package/out/zero-pg/src/mod.js +9 -9
  566. package/out/zero-protocol/src/analyze-query-result.js +1 -1
  567. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  568. package/out/zero-protocol/src/application-error.js.map +1 -1
  569. package/out/zero-protocol/src/ast.js +19 -9
  570. package/out/zero-protocol/src/ast.js.map +1 -1
  571. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  572. package/out/zero-protocol/src/client-schema.js +1 -1
  573. package/out/zero-protocol/src/client-schema.js.map +1 -1
  574. package/out/zero-protocol/src/close-connection.js +1 -1
  575. package/out/zero-protocol/src/close-connection.js.map +1 -1
  576. package/out/zero-protocol/src/connect.js +1 -1
  577. package/out/zero-protocol/src/connect.js.map +1 -1
  578. package/out/zero-protocol/src/custom-queries.js +1 -1
  579. package/out/zero-protocol/src/custom-queries.js.map +1 -1
  580. package/out/zero-protocol/src/data.js +4 -2
  581. package/out/zero-protocol/src/data.js.map +1 -1
  582. package/out/zero-protocol/src/delete-clients.js.map +1 -1
  583. package/out/zero-protocol/src/down.d.ts +27 -27
  584. package/out/zero-protocol/src/down.js.map +1 -1
  585. package/out/zero-protocol/src/error.js.map +1 -1
  586. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  587. package/out/zero-protocol/src/inspect-down.js +1 -1
  588. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  589. package/out/zero-protocol/src/inspect-up.js +1 -1
  590. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  591. package/out/zero-protocol/src/mutate-server.js +1 -1
  592. package/out/zero-protocol/src/mutate-server.js.map +1 -1
  593. package/out/zero-protocol/src/mutation-id.js.map +1 -1
  594. package/out/zero-protocol/src/mutation.js +1 -1
  595. package/out/zero-protocol/src/mutation.js.map +1 -1
  596. package/out/zero-protocol/src/mutations-patch.js +1 -1
  597. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  598. package/out/zero-protocol/src/ping.js +1 -1
  599. package/out/zero-protocol/src/ping.js.map +1 -1
  600. package/out/zero-protocol/src/poke.d.ts +6 -6
  601. package/out/zero-protocol/src/poke.js +1 -1
  602. package/out/zero-protocol/src/poke.js.map +1 -1
  603. package/out/zero-protocol/src/pong.js +1 -1
  604. package/out/zero-protocol/src/pong.js.map +1 -1
  605. package/out/zero-protocol/src/primary-key.js.map +1 -1
  606. package/out/zero-protocol/src/protocol-version.js +1 -0
  607. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  608. package/out/zero-protocol/src/pull.js +1 -1
  609. package/out/zero-protocol/src/pull.js.map +1 -1
  610. package/out/zero-protocol/src/push.js +1 -1
  611. package/out/zero-protocol/src/push.js.map +1 -1
  612. package/out/zero-protocol/src/queries-patch.js +1 -1
  613. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  614. package/out/zero-protocol/src/query-hash-visitor.js.map +1 -1
  615. package/out/zero-protocol/src/query-hash.js +2 -2
  616. package/out/zero-protocol/src/query-server.js +1 -1
  617. package/out/zero-protocol/src/query-server.js.map +1 -1
  618. package/out/zero-protocol/src/row-patch.js.map +1 -1
  619. package/out/zero-protocol/src/up.d.ts +60 -60
  620. package/out/zero-protocol/src/up.js.map +1 -1
  621. package/out/zero-protocol/src/update-auth.js.map +1 -1
  622. package/out/zero-protocol/src/version.js.map +1 -1
  623. package/out/zero-react/src/bindings.js +11 -11
  624. package/out/zero-react/src/mod.js +5 -5
  625. package/out/zero-react/src/use-query.js +1 -1
  626. package/out/zero-react/src/use-query.js.map +1 -1
  627. package/out/zero-react/src/zero-provider.d.ts +1 -1
  628. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  629. package/out/zero-react/src/zero-provider.js +2 -1
  630. package/out/zero-react/src/zero-provider.js.map +1 -1
  631. package/out/zero-react/src/zero.js +23 -23
  632. package/out/zero-schema/src/builder/schema-builder.js +3 -2
  633. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  634. package/out/zero-schema/src/builder/table-builder.js +1 -1
  635. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  636. package/out/zero-schema/src/compiled-permissions.js.map +1 -1
  637. package/out/zero-schema/src/name-mapper.js +7 -5
  638. package/out/zero-schema/src/name-mapper.js.map +1 -1
  639. package/out/zero-schema/src/schema-config.js +1 -1
  640. package/out/zero-schema/src/schema-config.js.map +1 -1
  641. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  642. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  643. package/out/zero-server/src/adapters/pg.js.map +1 -1
  644. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  645. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  646. package/out/zero-server/src/closable-db-transaction.js.map +1 -1
  647. package/out/zero-server/src/custom.js +1 -1
  648. package/out/zero-server/src/custom.js.map +1 -1
  649. package/out/zero-server/src/mod.js +8 -8
  650. package/out/zero-server/src/process-mutations.js.map +1 -1
  651. package/out/zero-server/src/push-processor.js.map +1 -1
  652. package/out/zero-server/src/queries/process-queries.js +2 -1
  653. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  654. package/out/zero-server/src/schema.js +7 -7
  655. package/out/zero-server/src/schema.js.map +1 -1
  656. package/out/zero-server/src/zql-database.js.map +1 -1
  657. package/out/zero-solid/src/bindings.js +11 -11
  658. package/out/zero-solid/src/mod.js +5 -5
  659. package/out/zero-solid/src/solid-view.js +1 -1
  660. package/out/zero-solid/src/solid-view.js.map +1 -1
  661. package/out/zero-solid/src/use-connection-state.js +2 -1
  662. package/out/zero-solid/src/use-connection-state.js.map +1 -1
  663. package/out/zero-solid/src/use-query.js.map +1 -1
  664. package/out/zero-solid/src/use-zero-online.js +2 -1
  665. package/out/zero-solid/src/use-zero-online.js.map +1 -1
  666. package/out/zero-solid/src/use-zero.js +3 -2
  667. package/out/zero-solid/src/use-zero.js.map +1 -1
  668. package/out/zero-solid/src/zero.js +23 -23
  669. package/out/zero-types/src/name-mapper.js.map +1 -1
  670. package/out/zql/src/builder/builder.js +15 -13
  671. package/out/zql/src/builder/builder.js.map +1 -1
  672. package/out/zql/src/builder/correlated-predicate-pushdown.js.map +1 -1
  673. package/out/zql/src/builder/debug-delegate.js.map +1 -1
  674. package/out/zql/src/builder/like.js +0 -1
  675. package/out/zql/src/builder/like.js.map +1 -1
  676. package/out/zql/src/ivm/array-view.js.map +1 -1
  677. package/out/zql/src/ivm/cap.js.map +1 -1
  678. package/out/zql/src/ivm/constraint.js +1 -1
  679. package/out/zql/src/ivm/constraint.js.map +1 -1
  680. package/out/zql/src/ivm/data.js.map +1 -1
  681. package/out/zql/src/ivm/deferred-input.js.map +1 -1
  682. package/out/zql/src/ivm/exists.js +20 -18
  683. package/out/zql/src/ivm/exists.js.map +1 -1
  684. package/out/zql/src/ivm/fan-in.js.map +1 -1
  685. package/out/zql/src/ivm/fan-out.js.map +1 -1
  686. package/out/zql/src/ivm/filter-operators.js +1 -1
  687. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  688. package/out/zql/src/ivm/filter-push.js.map +1 -1
  689. package/out/zql/src/ivm/filter.js.map +1 -1
  690. package/out/zql/src/ivm/flipped-join.js +1 -3
  691. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  692. package/out/zql/src/ivm/join-index-tap.js.map +1 -1
  693. package/out/zql/src/ivm/join-index.js.map +1 -1
  694. package/out/zql/src/ivm/join-utils.js +11 -13
  695. package/out/zql/src/ivm/join-utils.js.map +1 -1
  696. package/out/zql/src/ivm/join.js.map +1 -1
  697. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
  698. package/out/zql/src/ivm/memory-source.js +26 -22
  699. package/out/zql/src/ivm/memory-source.js.map +1 -1
  700. package/out/zql/src/ivm/memory-storage.js.map +1 -1
  701. package/out/zql/src/ivm/operator.js +2 -2
  702. package/out/zql/src/ivm/push-accumulated.js +0 -1
  703. package/out/zql/src/ivm/push-accumulated.js.map +1 -1
  704. package/out/zql/src/ivm/skip-yields.js.map +1 -1
  705. package/out/zql/src/ivm/skip.js.map +1 -1
  706. package/out/zql/src/ivm/take-gate.js.map +1 -1
  707. package/out/zql/src/ivm/take.js +1 -1
  708. package/out/zql/src/ivm/take.js.map +1 -1
  709. package/out/zql/src/ivm/union-fan-in.js +1 -1
  710. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  711. package/out/zql/src/ivm/union-fan-out.js.map +1 -1
  712. package/out/zql/src/ivm/view-apply-change.js +1 -1
  713. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  714. package/out/zql/src/mutate/crud.js +3 -2
  715. package/out/zql/src/mutate/crud.js.map +1 -1
  716. package/out/zql/src/planner/planner-builder.js +1 -1
  717. package/out/zql/src/planner/planner-builder.js.map +1 -1
  718. package/out/zql/src/planner/planner-connection.js.map +1 -1
  719. package/out/zql/src/planner/planner-debug.js.map +1 -1
  720. package/out/zql/src/planner/planner-fan-in.js.map +1 -1
  721. package/out/zql/src/planner/planner-fan-out.js.map +1 -1
  722. package/out/zql/src/planner/planner-filter.js.map +1 -1
  723. package/out/zql/src/planner/planner-graph.js +1 -5
  724. package/out/zql/src/planner/planner-graph.js.map +1 -1
  725. package/out/zql/src/planner/planner-join.js +1 -1
  726. package/out/zql/src/planner/planner-join.js.map +1 -1
  727. package/out/zql/src/planner/planner-source.js.map +1 -1
  728. package/out/zql/src/planner/planner-terminus.js.map +1 -1
  729. package/out/zql/src/query/create-builder.js +2 -1
  730. package/out/zql/src/query/create-builder.js.map +1 -1
  731. package/out/zql/src/query/expression.js +1 -1
  732. package/out/zql/src/query/expression.js.map +1 -1
  733. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  734. package/out/zql/src/query/named.js +2 -1
  735. package/out/zql/src/query/named.js.map +1 -1
  736. package/out/zql/src/query/query-delegate-base.js +1 -1
  737. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  738. package/out/zql/src/query/query-impl.js +3 -2
  739. package/out/zql/src/query/query-impl.js.map +1 -1
  740. package/out/zql/src/query/query-registry.js +1 -1
  741. package/out/zql/src/query/query-registry.js.map +1 -1
  742. package/out/zql/src/query/query-transitions.js +1 -1
  743. package/out/zql/src/query/query-transitions.js.map +1 -1
  744. package/out/zql/src/query/runnable-query-impl.js +3 -2
  745. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  746. package/out/zql/src/query/static-query.js.map +1 -1
  747. package/out/zql/src/query/ttl.js +8 -8
  748. package/out/zql/src/query/ttl.js.map +1 -1
  749. package/out/zql/src/query/validate-input.js.map +1 -1
  750. package/out/zqlite/src/database-storage.d.ts +27 -8
  751. package/out/zqlite/src/database-storage.d.ts.map +1 -1
  752. package/out/zqlite/src/database-storage.js +190 -53
  753. package/out/zqlite/src/database-storage.js.map +1 -1
  754. package/out/zqlite/src/db.d.ts +47 -1
  755. package/out/zqlite/src/db.d.ts.map +1 -1
  756. package/out/zqlite/src/db.js +121 -21
  757. package/out/zqlite/src/db.js.map +1 -1
  758. package/out/zqlite/src/explain-queries.js.map +1 -1
  759. package/out/zqlite/src/fetch-sql.js.map +1 -1
  760. package/out/zqlite/src/index-usage.js.map +1 -1
  761. package/out/zqlite/src/internal/sql-inline.js.map +1 -1
  762. package/out/zqlite/src/internal/sql.js.map +1 -1
  763. package/out/zqlite/src/internal/statement-cache.d.ts +7 -0
  764. package/out/zqlite/src/internal/statement-cache.d.ts.map +1 -1
  765. package/out/zqlite/src/internal/statement-cache.js +11 -4
  766. package/out/zqlite/src/internal/statement-cache.js.map +1 -1
  767. package/out/zqlite/src/mod.js +5 -5
  768. package/out/zqlite/src/pending-delta.js.map +1 -1
  769. package/out/zqlite/src/query-builder.js +1 -1
  770. package/out/zqlite/src/query-builder.js.map +1 -1
  771. package/out/zqlite/src/query-delegate.js.map +1 -1
  772. package/out/zqlite/src/resolve-scalar-subqueries.js +2 -5
  773. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  774. package/out/zqlite/src/sqlite-cost-model.js +3 -2
  775. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  776. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  777. package/out/zqlite/src/table-source.d.ts +11 -0
  778. package/out/zqlite/src/table-source.d.ts.map +1 -1
  779. package/out/zqlite/src/table-source.js +62 -25
  780. package/out/zqlite/src/table-source.js.map +1 -1
  781. package/package.json +17 -15
  782. package/out/otel/src/maybe-time.d.ts +0 -3
  783. package/out/otel/src/maybe-time.d.ts.map +0 -1
  784. package/out/otel/src/maybe-time.js +0 -15
  785. package/out/otel/src/maybe-time.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"plan-warnings.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/plan-warnings.ts"],"sourcesContent":["import type {PlanWarning} from '../../../../zql/src/planner/planner-warnings.ts';\n\n/**\n * Explains a {@link PlanWarning} for a zero-cache operator, in terms of the\n * SQLite replica, whose indexes come from the upstream database.\n */\nexport function planWarningMessage(warning: PlanWarning): string {\n switch (warning.type) {\n case 'missing-index': {\n const {columns, rows, perRow} = warning;\n return (\n `${perRow ? 'Each lookup' : 'Each read'} of ${location(warning)} ` +\n `by ${list(columns)} scans all ~${count(rows)} rows because no ` +\n `index covers ${list(columns)}` +\n `${perRow ? ', and it runs once per parent row' : ''}. ` +\n suggestion(warning)\n );\n }\n case 'full-sort': {\n const {orderBy, rows, perRow} = warning;\n const ordering = orderBy\n .map(([column, direction]) => `${column} ${direction}`)\n .join(', ');\n return (\n `Each read of ${location(warning)} sorts ~${count(rows)} rows for ` +\n `ORDER BY ${ordering}, reading every matching row before returning ` +\n `the first, so a limit does not reduce the rows read` +\n `${perRow ? ', and it runs once per parent row' : ''}. ` +\n suggestion(warning)\n );\n }\n case 'high-cost':\n return (\n `The best plan found for the query is estimated to process ` +\n `~${count(warning.cost)} rows.`\n );\n }\n}\n\nfunction location({table, path}: {table: string; path: readonly string[]}) {\n return path.length === 0\n ? table\n : `${table} (related ${path.map(alias => `'${alias}'`).join(' > ')})`;\n}\n\nfunction suggestion({\n table,\n suggestedIndex,\n}: {\n table: string;\n suggestedIndex: readonly string[];\n}) {\n return `Consider adding an index on ${table} (${suggestedIndex.join(', ')}) upstream.`;\n}\n\nfunction list(columns: readonly string[]) {\n return columns.join(', ');\n}\n\nfunction count(n: number) {\n return Math.round(n).toLocaleString('en-US');\n}\n"],"mappings":";;;;;AAMA,SAAgB,mBAAmB,SAA8B;CAC/D,QAAQ,QAAQ,MAAhB;EACE,KAAK,iBAAiB;GACpB,MAAM,EAAC,SAAS,MAAM,WAAU;GAChC,OACE,GAAG,SAAS,gBAAgB,YAAY,MAAM,SAAS,OAAO,EAAE,MAC1D,KAAK,OAAO,EAAE,cAAc,MAAM,IAAI,EAAE,gCAC9B,KAAK,OAAO,IACzB,SAAS,sCAAsC,GAAG,MACrD,WAAW,OAAO;EAEtB;EACA,KAAK,aAAa;GAChB,MAAM,EAAC,SAAS,MAAM,WAAU;GAChC,MAAM,WAAW,QACd,KAAK,CAAC,QAAQ,eAAe,GAAG,OAAO,GAAG,WAAW,EACrD,KAAK,IAAI;GACZ,OACE,gBAAgB,SAAS,OAAO,EAAE,UAAU,MAAM,IAAI,EAAE,qBAC5C,SAAS,mGAElB,SAAS,sCAAsC,GAAG,MACrD,WAAW,OAAO;EAEtB;EACA,KAAK,aACH,OACE,8DACI,MAAM,QAAQ,IAAI,EAAE;CAE9B;AACF;AAEA,SAAS,SAAS,EAAC,OAAO,QAAiD;CACzE,OAAO,KAAK,WAAW,IACnB,QACA,GAAG,MAAM,YAAY,KAAK,KAAI,UAAS,IAAI,MAAM,EAAE,EAAE,KAAK,KAAK,EAAE;AACvE;AAEA,SAAS,WAAW,EAClB,OACA,kBAIC;CACD,OAAO,+BAA+B,MAAM,IAAI,eAAe,KAAK,IAAI,EAAE;AAC5E;AAEA,SAAS,KAAK,SAA4B;CACxC,OAAO,QAAQ,KAAK,IAAI;AAC1B;AAEA,SAAS,MAAM,GAAW;CACxB,OAAO,KAAK,MAAM,CAAC,EAAE,eAAe,OAAO;AAC7C"}
1
+ {"version":3,"file":"plan-warnings.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/plan-warnings.ts"],"sourcesContent":["import type {PlanWarning} from '../../../../zql/src/planner/planner-warnings.ts';\n\n/**\n * Explains a {@link PlanWarning} for a zero-cache operator, in terms of the\n * SQLite replica, whose indexes come from the upstream database.\n */\nexport function planWarningMessage(warning: PlanWarning): string {\n switch (warning.type) {\n case 'missing-index': {\n const {columns, rows, perRow} = warning;\n return (\n `${perRow ? 'Each lookup' : 'Each read'} of ${location(warning)} ` +\n `by ${list(columns)} scans all ~${count(rows)} rows because no ` +\n `index covers ${list(columns)}` +\n `${perRow ? ', and it runs once per parent row' : ''}. ` +\n suggestion(warning)\n );\n }\n case 'full-sort': {\n const {orderBy, rows, perRow} = warning;\n const ordering = orderBy\n .map(([column, direction]) => `${column} ${direction}`)\n .join(', ');\n return (\n `Each read of ${location(warning)} sorts ~${count(rows)} rows for ` +\n `ORDER BY ${ordering}, reading every matching row before returning ` +\n `the first, so a limit does not reduce the rows read` +\n `${perRow ? ', and it runs once per parent row' : ''}. ` +\n suggestion(warning)\n );\n }\n case 'high-cost':\n return (\n `The best plan found for the query is estimated to process ` +\n `~${count(warning.cost)} rows.`\n );\n }\n}\n\nfunction location({table, path}: {table: string; path: readonly string[]}) {\n return path.length === 0\n ? table\n : `${table} (related ${path.map(alias => `'${alias}'`).join(' > ')})`;\n}\n\nfunction suggestion({\n table,\n suggestedIndex,\n}: {\n table: string;\n suggestedIndex: readonly string[];\n}) {\n return `Consider adding an index on ${table} (${suggestedIndex.join(', ')}) upstream.`;\n}\n\nfunction list(columns: readonly string[]) {\n return columns.join(', ');\n}\n\nfunction count(n: number) {\n return Math.round(n).toLocaleString('en-US');\n}\n"],"mappings":";;;;;AAMA,SAAgB,mBAAmB,SAA8B;CAC/D,QAAQ,QAAQ,MAAhB;EACE,KAAK,iBAAiB;GACpB,MAAM,EAAC,SAAS,MAAM,WAAU;GAChC,OACE,GAAG,SAAS,gBAAgB,YAAY,MAAM,SAAS,OAAO,EAAE,MAC1D,KAAK,OAAO,EAAE,cAAc,MAAM,IAAI,EAAE,gCAC9B,KAAK,OAAO,IACzB,SAAS,sCAAsC,GAAG,MACrD,WAAW,OAAO;EAEtB;EACA,KAAK,aAAa;GAChB,MAAM,EAAC,SAAS,MAAM,WAAU;GAChC,MAAM,WAAW,QACd,KAAK,CAAC,QAAQ,eAAe,GAAG,OAAO,GAAG,WAAW,CAAC,CACtD,KAAK,IAAI;GACZ,OACE,gBAAgB,SAAS,OAAO,EAAE,UAAU,MAAM,IAAI,EAAE,qBAC5C,SAAS,mGAElB,SAAS,sCAAsC,GAAG,MACrD,WAAW,OAAO;EAEtB;EACA,KAAK,aACH,OACE,8DACI,MAAM,QAAQ,IAAI,EAAE;CAE9B;AACF;AAEA,SAAS,SAAS,EAAC,OAAO,QAAiD;CACzE,OAAO,KAAK,WAAW,IACnB,QACA,GAAG,MAAM,YAAY,KAAK,KAAI,UAAS,IAAI,MAAM,EAAE,CAAC,CAAC,KAAK,KAAK,EAAE;AACvE;AAEA,SAAS,WAAW,EAClB,OACA,kBAIC;CACD,OAAO,+BAA+B,MAAM,IAAI,eAAe,KAAK,IAAI,EAAE;AAC5E;AAEA,SAAS,KAAK,SAA4B;CACxC,OAAO,QAAQ,KAAK,IAAI;AAC1B;AAEA,SAAS,MAAM,GAAW;CACxB,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,eAAe,OAAO;AAC7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"push-cost.js","names":["#costs","#walkMsPerEntry","#parseMsPerEntry","#lookupMsPerEntry"],"sources":["../../../../../../zero-cache/src/services/view-syncer/push-cost.ts"],"sourcesContent":["import {MIN_PUSH_CUTOFF_MS} from './push-cutoff.ts';\n\n/**\n * The weight of the latest advancement in a query's cost per change. The\n * cost of a change depends on the row (one joined to many others costs more\n * than one that is filtered out), so it varies from one advancement to the\n * next, and workloads shift.\n */\nconst LATEST_WEIGHT = 0.3;\n\n/**\n * Learns what it costs to push a change of each table through each query of\n * a client group, and estimates from it what pushing a batch of changes\n * would cost.\n *\n * The estimates only decide whether a query skips pushing and starts an\n * advancement on rebuild (see {@link shouldStartOnRebuild}). Nothing depends\n * on their accuracy: a query that starts on push when it should not have is\n * cut off (see `PushCutoff`), and one that starts on rebuild costs its\n * hydration time, as it would have at the cutoff.\n */\nexport class PushCostEstimator {\n /** Milliseconds per change, by query ID and table. */\n readonly #costs = new Map<string, Map<string, number>>();\n\n /**\n * Records what an advancement cost a query.\n *\n * @param tables The tables the query reads.\n * @param timeMsByTable The time spent pushing the changes of each table\n * to the query. Absent for a table whose changes cost it nothing,\n * e.g. because the rows did not match its filters.\n * @param changesByTable The entries of each table that the advancement\n * read from the change log, whether or not they reached the query,\n * or changed anything at all (a row that is inserted and deleted\n * between two snapshots is an entry that changes nothing). These\n * are the entries that {@link estimate} is given the count of.\n */\n record(\n queryID: string,\n tables: Iterable<string>,\n timeMsByTable: ReadonlyMap<string, number> | undefined,\n changesByTable: ReadonlyMap<string, number>,\n ): void {\n let costs = this.#costs.get(queryID);\n for (const table of tables) {\n const changes = changesByTable.get(table);\n if (!changes) {\n continue;\n }\n if (costs === undefined) {\n costs = new Map();\n this.#costs.set(queryID, costs);\n }\n const latest = (timeMsByTable?.get(table) ?? 0) / changes;\n const previous = costs.get(table);\n const cost =\n previous === undefined\n ? latest\n : previous + LATEST_WEIGHT * (latest - previous);\n costs.set(table, cost);\n }\n }\n\n /**\n * The estimated time to push `changes` to the query, or `undefined` if it\n * has never been pushed any change. A table that the query has never been\n * pushed a change of is estimated to cost nothing.\n *\n * @param tables The tables the query reads.\n * @param changes The number of changes to push, by table.\n */\n estimate(\n queryID: string,\n tables: Iterable<string>,\n changes: ReadonlyMap<string, number>,\n ): number | undefined {\n const costs = this.#costs.get(queryID);\n if (costs === undefined) {\n return undefined;\n }\n let total = 0;\n for (const table of tables) {\n total += (changes.get(table) ?? 0) * (costs.get(table) ?? 0);\n }\n return total;\n }\n\n /** Forgets what was learned about a query. */\n forget(queryID: string): void {\n this.#costs.delete(queryID);\n }\n}\n\n/**\n * Learns what it costs a client group to read a batch of changes, apart from\n * pushing them through its queries, and estimates from it what reading the\n * next one would cost.\n *\n * Reading has three parts, which do not cost the same entries (see\n * {@link ReadParts}): walking the change log, parsing the entries that\n * the walk returns, and looking up the rows of the entries of the tables\n * that are read. What each costs is learned per entry that it was done for.\n * A cost learned per entry of some other count is only right for a batch\n * with the same mix of entries: charged to the entries that are pushed, for\n * instance, the parsing of a batch that is mostly of tables that no query\n * reads makes the next batch that is not look many times as expensive as it\n * is.\n *\n * No query can be cut off for what this costs, as it is not a cost of any\n * one query. It is what pushing costs all of them together, on top of what\n * it costs each (see {@link shouldRebuildInsteadOfReading}).\n */\nexport class ReadCostEstimator {\n #walkMsPerEntry: number | undefined;\n #parseMsPerEntry: number | undefined;\n #lookupMsPerEntry: number | undefined;\n\n /**\n * Records what reading a batch cost. Only for a batch that was read to\n * its end.\n *\n * @param timeMs The time spent on each part of reading the batch.\n * @param entries The entries that each part was done for.\n */\n record(timeMs: ReadParts, entries: ReadParts): void {\n this.#walkMsPerEntry = learn(\n this.#walkMsPerEntry,\n timeMs.walk,\n entries.walk,\n );\n this.#parseMsPerEntry = learn(\n this.#parseMsPerEntry,\n timeMs.parse,\n entries.parse,\n );\n this.#lookupMsPerEntry = learn(\n this.#lookupMsPerEntry,\n timeMs.lookup,\n entries.lookup,\n );\n }\n\n /**\n * The estimated time to read a batch, or `undefined` if none has been\n * read yet.\n *\n * @param entries The entries that each part of reading the batch would be\n * done for.\n */\n estimate(entries: ReadParts): number | undefined {\n if (this.#walkMsPerEntry === undefined) {\n return undefined;\n }\n return (\n entries.walk * this.#walkMsPerEntry +\n entries.parse * (this.#parseMsPerEntry ?? 0) +\n entries.lookup * (this.#lookupMsPerEntry ?? 0)\n );\n }\n}\n\n/**\n * A number for each part of reading a batch of changes: the entries of the\n * change log that the part is done for, or the time that it takes.\n */\nexport type ReadParts = {\n /**\n * Walking the change log, which is done for every entry since the\n * previous snapshot, whether or not the entry is of a table that is read:\n * the entries are in the order they were committed in, not grouped by\n * table.\n */\n readonly walk: number;\n /**\n * Parsing, which is done for every entry that the walk returns. That is\n * all of them, unless the walk is told which tables are read.\n */\n readonly parse: number;\n /**\n * Looking up the row in both snapshots, which is done for every entry of\n * a table that is read. This includes the entries that then turn out to\n * change nothing (e.g. a row inserted and deleted since the previous\n * snapshot).\n */\n readonly lookup: number;\n};\n\nfunction learn(\n msPerEntry: number | undefined,\n timeMs: number,\n entries: number,\n): number | undefined {\n return entries > 0 ? decay(msPerEntry, timeMs / entries) : msPerEntry;\n}\n\nfunction decay(previous: number | undefined, latest: number): number {\n return previous === undefined\n ? latest\n : previous + LATEST_WEIGHT * (latest - previous);\n}\n\n/**\n * Whether the queries that are left to push a batch to should all be\n * hydrated again instead, without the batch being read: when reading it and\n * pushing it to them is estimated to cost at least `margin` times what\n * hydrating them does.\n *\n * This is {@link shouldStartOnRebuild} for the queries together, with what\n * reading the batch costs added to what pushing it costs each of them. A\n * query that few of the batch's changes reach is cheap to push to, but the\n * whole batch still has to be walked to find those changes.\n *\n * @param estimatedReadMs `undefined` if there is no estimate, in which case\n * the batch is read.\n * @param queries What pushing the batch is estimated to cost each query\n * (nothing, if it has no estimate), and what hydrating it took.\n */\nexport function shouldRebuildInsteadOfReading(\n estimatedReadMs: number | undefined,\n queries: Iterable<{\n readonly estimatedPushMs: number | undefined;\n readonly hydrationTimeMs: number;\n }>,\n margin: number,\n): boolean {\n if (estimatedReadMs === undefined) {\n return false;\n }\n let pushMs = estimatedReadMs;\n let hydrationMs = 0;\n for (const query of queries) {\n pushMs += query.estimatedPushMs ?? 0;\n hydrationMs += Math.max(query.hydrationTimeMs, MIN_PUSH_CUTOFF_MS);\n }\n return hydrationMs > 0 && pushMs >= margin * hydrationMs;\n}\n\n/**\n * Whether a query should start an advancement on rebuild, i.e. be hydrated\n * again without being pushed any of the advancement's changes: when pushing\n * them is estimated to cost at least `margin` times what hydrating it does.\n *\n * The margin is for the imprecision of the estimate. A query without one\n * starts on push.\n */\nexport function shouldStartOnRebuild(\n estimatedPushMs: number | undefined,\n hydrationTimeMs: number,\n margin: number,\n): boolean {\n return (\n estimatedPushMs !== undefined &&\n estimatedPushMs >= margin * Math.max(hydrationTimeMs, MIN_PUSH_CUTOFF_MS)\n );\n}\n"],"mappings":";;;;;;;AAQA,IAAM,gBAAgB;;;;;;;;;;;;AAatB,IAAa,oBAAb,MAA+B;;CAE7B,yBAAkB,IAAI,IAAiC;;;;;;;;;;;;;;CAevD,OACE,SACA,QACA,eACA,gBACM;EACN,IAAI,QAAQ,KAAKA,OAAO,IAAI,OAAO;EACnC,KAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,UAAU,eAAe,IAAI,KAAK;GACxC,IAAI,CAAC,SACH;GAEF,IAAI,UAAU,KAAA,GAAW;IACvB,wBAAQ,IAAI,IAAI;IAChB,KAAKA,OAAO,IAAI,SAAS,KAAK;GAChC;GACA,MAAM,UAAU,eAAe,IAAI,KAAK,KAAK,KAAK;GAClD,MAAM,WAAW,MAAM,IAAI,KAAK;GAChC,MAAM,OACJ,aAAa,KAAA,IACT,SACA,WAAW,iBAAiB,SAAS;GAC3C,MAAM,IAAI,OAAO,IAAI;EACvB;CACF;;;;;;;;;CAUA,SACE,SACA,QACA,SACoB;EACpB,MAAM,QAAQ,KAAKA,OAAO,IAAI,OAAO;EACrC,IAAI,UAAU,KAAA,GACZ;EAEF,IAAI,QAAQ;EACZ,KAAK,MAAM,SAAS,QAClB,UAAU,QAAQ,IAAI,KAAK,KAAK,MAAM,MAAM,IAAI,KAAK,KAAK;EAE5D,OAAO;CACT;;CAGA,OAAO,SAAuB;EAC5B,KAAKA,OAAO,OAAO,OAAO;CAC5B;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CACA;;;;;;;;CASA,OAAO,QAAmB,SAA0B;EAClD,KAAKC,kBAAkB,MACrB,KAAKA,iBACL,OAAO,MACP,QAAQ,IACV;EACA,KAAKC,mBAAmB,MACtB,KAAKA,kBACL,OAAO,OACP,QAAQ,KACV;EACA,KAAKC,oBAAoB,MACvB,KAAKA,mBACL,OAAO,QACP,QAAQ,MACV;CACF;;;;;;;;CASA,SAAS,SAAwC;EAC/C,IAAI,KAAKF,oBAAoB,KAAA,GAC3B;EAEF,OACE,QAAQ,OAAO,KAAKA,kBACpB,QAAQ,SAAS,KAAKC,oBAAoB,KAC1C,QAAQ,UAAU,KAAKC,qBAAqB;CAEhD;AACF;AA4BA,SAAS,MACP,YACA,QACA,SACoB;CACpB,OAAO,UAAU,IAAI,MAAM,YAAY,SAAS,OAAO,IAAI;AAC7D;AAEA,SAAS,MAAM,UAA8B,QAAwB;CACnE,OAAO,aAAa,KAAA,IAChB,SACA,WAAW,iBAAiB,SAAS;AAC3C;;;;;;;;;;;;;;;;;AAkBA,SAAgB,8BACd,iBACA,SAIA,QACS;CACT,IAAI,oBAAoB,KAAA,GACtB,OAAO;CAET,IAAI,SAAS;CACb,IAAI,cAAc;CAClB,KAAK,MAAM,SAAS,SAAS;EAC3B,UAAU,MAAM,mBAAmB;EACnC,eAAe,KAAK,IAAI,MAAM,iBAAA,CAAmC;CACnE;CACA,OAAO,cAAc,KAAK,UAAU,SAAS;AAC/C;;;;;;;;;AAUA,SAAgB,qBACd,iBACA,iBACA,QACS;CACT,OACE,oBAAoB,KAAA,KACpB,mBAAmB,SAAS,KAAK,IAAI,iBAAA,CAAmC;AAE5E"}
1
+ {"version":3,"file":"push-cost.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/push-cost.ts"],"sourcesContent":["import {MIN_PUSH_CUTOFF_MS} from './push-cutoff.ts';\n\n/**\n * The weight of the latest advancement in a query's cost per change. The\n * cost of a change depends on the row (one joined to many others costs more\n * than one that is filtered out), so it varies from one advancement to the\n * next, and workloads shift.\n */\nconst LATEST_WEIGHT = 0.3;\n\n/**\n * Learns what it costs to push a change of each table through each query of\n * a client group, and estimates from it what pushing a batch of changes\n * would cost.\n *\n * The estimates only decide whether a query skips pushing and starts an\n * advancement on rebuild (see {@link shouldStartOnRebuild}). Nothing depends\n * on their accuracy: a query that starts on push when it should not have is\n * cut off (see `PushCutoff`), and one that starts on rebuild costs its\n * hydration time, as it would have at the cutoff.\n */\nexport class PushCostEstimator {\n /** Milliseconds per change, by query ID and table. */\n readonly #costs = new Map<string, Map<string, number>>();\n\n /**\n * Records what an advancement cost a query.\n *\n * @param tables The tables the query reads.\n * @param timeMsByTable The time spent pushing the changes of each table\n * to the query. Absent for a table whose changes cost it nothing,\n * e.g. because the rows did not match its filters.\n * @param changesByTable The entries of each table that the advancement\n * read from the change log, whether or not they reached the query,\n * or changed anything at all (a row that is inserted and deleted\n * between two snapshots is an entry that changes nothing). These\n * are the entries that {@link estimate} is given the count of.\n */\n record(\n queryID: string,\n tables: Iterable<string>,\n timeMsByTable: ReadonlyMap<string, number> | undefined,\n changesByTable: ReadonlyMap<string, number>,\n ): void {\n let costs = this.#costs.get(queryID);\n for (const table of tables) {\n const changes = changesByTable.get(table);\n if (!changes) {\n continue;\n }\n if (costs === undefined) {\n costs = new Map();\n this.#costs.set(queryID, costs);\n }\n const latest = (timeMsByTable?.get(table) ?? 0) / changes;\n const previous = costs.get(table);\n const cost =\n previous === undefined\n ? latest\n : previous + LATEST_WEIGHT * (latest - previous);\n costs.set(table, cost);\n }\n }\n\n /**\n * The estimated time to push `changes` to the query, or `undefined` if it\n * has never been pushed any change. A table that the query has never been\n * pushed a change of is estimated to cost nothing.\n *\n * @param tables The tables the query reads.\n * @param changes The number of changes to push, by table.\n */\n estimate(\n queryID: string,\n tables: Iterable<string>,\n changes: ReadonlyMap<string, number>,\n ): number | undefined {\n const costs = this.#costs.get(queryID);\n if (costs === undefined) {\n return undefined;\n }\n let total = 0;\n for (const table of tables) {\n total += (changes.get(table) ?? 0) * (costs.get(table) ?? 0);\n }\n return total;\n }\n\n /** Forgets what was learned about a query. */\n forget(queryID: string): void {\n this.#costs.delete(queryID);\n }\n}\n\n/**\n * Learns what it costs a client group to read a batch of changes, apart from\n * pushing them through its queries, and estimates from it what reading the\n * next one would cost.\n *\n * Reading has three parts, which do not cost the same entries (see\n * {@link ReadParts}): walking the change log, parsing the entries that\n * the walk returns, and looking up the rows of the entries of the tables\n * that are read. What each costs is learned per entry that it was done for.\n * A cost learned per entry of some other count is only right for a batch\n * with the same mix of entries: charged to the entries that are pushed, for\n * instance, the parsing of a batch that is mostly of tables that no query\n * reads makes the next batch that is not look many times as expensive as it\n * is.\n *\n * No query can be cut off for what this costs, as it is not a cost of any\n * one query. It is what pushing costs all of them together, on top of what\n * it costs each (see {@link shouldRebuildInsteadOfReading}).\n */\nexport class ReadCostEstimator {\n #walkMsPerEntry: number | undefined;\n #parseMsPerEntry: number | undefined;\n #lookupMsPerEntry: number | undefined;\n\n /**\n * Records what reading a batch cost. Only for a batch that was read to\n * its end.\n *\n * @param timeMs The time spent on each part of reading the batch.\n * @param entries The entries that each part was done for.\n */\n record(timeMs: ReadParts, entries: ReadParts): void {\n this.#walkMsPerEntry = learn(\n this.#walkMsPerEntry,\n timeMs.walk,\n entries.walk,\n );\n this.#parseMsPerEntry = learn(\n this.#parseMsPerEntry,\n timeMs.parse,\n entries.parse,\n );\n this.#lookupMsPerEntry = learn(\n this.#lookupMsPerEntry,\n timeMs.lookup,\n entries.lookup,\n );\n }\n\n /**\n * The estimated time to read a batch, or `undefined` if none has been\n * read yet.\n *\n * @param entries The entries that each part of reading the batch would be\n * done for.\n */\n estimate(entries: ReadParts): number | undefined {\n if (this.#walkMsPerEntry === undefined) {\n return undefined;\n }\n return (\n entries.walk * this.#walkMsPerEntry +\n entries.parse * (this.#parseMsPerEntry ?? 0) +\n entries.lookup * (this.#lookupMsPerEntry ?? 0)\n );\n }\n}\n\n/**\n * A number for each part of reading a batch of changes: the entries of the\n * change log that the part is done for, or the time that it takes.\n */\nexport type ReadParts = {\n /**\n * Walking the change log, which is done for every entry since the\n * previous snapshot, whether or not the entry is of a table that is read:\n * the entries are in the order they were committed in, not grouped by\n * table.\n */\n readonly walk: number;\n /**\n * Parsing, which is done for every entry that the walk returns. That is\n * all of them, unless the walk is told which tables are read.\n */\n readonly parse: number;\n /**\n * Looking up the row in both snapshots, which is done for every entry of\n * a table that is read. This includes the entries that then turn out to\n * change nothing (e.g. a row inserted and deleted since the previous\n * snapshot).\n */\n readonly lookup: number;\n};\n\nfunction learn(\n msPerEntry: number | undefined,\n timeMs: number,\n entries: number,\n): number | undefined {\n return entries > 0 ? decay(msPerEntry, timeMs / entries) : msPerEntry;\n}\n\nfunction decay(previous: number | undefined, latest: number): number {\n return previous === undefined\n ? latest\n : previous + LATEST_WEIGHT * (latest - previous);\n}\n\n/**\n * Whether the queries that are left to push a batch to should all be\n * hydrated again instead, without the batch being read: when reading it and\n * pushing it to them is estimated to cost at least `margin` times what\n * hydrating them does.\n *\n * This is {@link shouldStartOnRebuild} for the queries together, with what\n * reading the batch costs added to what pushing it costs each of them. A\n * query that few of the batch's changes reach is cheap to push to, but the\n * whole batch still has to be walked to find those changes.\n *\n * @param estimatedReadMs `undefined` if there is no estimate, in which case\n * the batch is read.\n * @param queries What pushing the batch is estimated to cost each query\n * (nothing, if it has no estimate), and what hydrating it took.\n */\nexport function shouldRebuildInsteadOfReading(\n estimatedReadMs: number | undefined,\n queries: Iterable<{\n readonly estimatedPushMs: number | undefined;\n readonly hydrationTimeMs: number;\n }>,\n margin: number,\n): boolean {\n if (estimatedReadMs === undefined) {\n return false;\n }\n let pushMs = estimatedReadMs;\n let hydrationMs = 0;\n for (const query of queries) {\n pushMs += query.estimatedPushMs ?? 0;\n hydrationMs += Math.max(query.hydrationTimeMs, MIN_PUSH_CUTOFF_MS);\n }\n return hydrationMs > 0 && pushMs >= margin * hydrationMs;\n}\n\n/**\n * Whether a query should start an advancement on rebuild, i.e. be hydrated\n * again without being pushed any of the advancement's changes: when pushing\n * them is estimated to cost at least `margin` times what hydrating it does.\n *\n * The margin is for the imprecision of the estimate. A query without one\n * starts on push.\n */\nexport function shouldStartOnRebuild(\n estimatedPushMs: number | undefined,\n hydrationTimeMs: number,\n margin: number,\n): boolean {\n return (\n estimatedPushMs !== undefined &&\n estimatedPushMs >= margin * Math.max(hydrationTimeMs, MIN_PUSH_CUTOFF_MS)\n );\n}\n"],"mappings":";;;;;;;AAQA,IAAM,gBAAgB;;;;;;;;;;;;AAatB,IAAa,oBAAb,MAA+B;;CAE7B,yBAAkB,IAAI,IAAiC;;;;;;;;;;;;;;CAevD,OACE,SACA,QACA,eACA,gBACM;EACN,IAAI,QAAQ,KAAK,OAAO,IAAI,OAAO;EACnC,KAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,UAAU,eAAe,IAAI,KAAK;GACxC,IAAI,CAAC,SACH;GAEF,IAAI,UAAU,KAAA,GAAW;IACvB,wBAAQ,IAAI,IAAI;IAChB,KAAK,OAAO,IAAI,SAAS,KAAK;GAChC;GACA,MAAM,UAAU,eAAe,IAAI,KAAK,KAAK,KAAK;GAClD,MAAM,WAAW,MAAM,IAAI,KAAK;GAChC,MAAM,OACJ,aAAa,KAAA,IACT,SACA,WAAW,iBAAiB,SAAS;GAC3C,MAAM,IAAI,OAAO,IAAI;EACvB;CACF;;;;;;;;;CAUA,SACE,SACA,QACA,SACoB;EACpB,MAAM,QAAQ,KAAK,OAAO,IAAI,OAAO;EACrC,IAAI,UAAU,KAAA,GACZ;EAEF,IAAI,QAAQ;EACZ,KAAK,MAAM,SAAS,QAClB,UAAU,QAAQ,IAAI,KAAK,KAAK,MAAM,MAAM,IAAI,KAAK,KAAK;EAE5D,OAAO;CACT;;CAGA,OAAO,SAAuB;EAC5B,KAAK,OAAO,OAAO,OAAO;CAC5B;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CACA;;;;;;;;CASA,OAAO,QAAmB,SAA0B;EAClD,KAAK,kBAAkB,MACrB,KAAK,iBACL,OAAO,MACP,QAAQ,IACV;EACA,KAAK,mBAAmB,MACtB,KAAK,kBACL,OAAO,OACP,QAAQ,KACV;EACA,KAAK,oBAAoB,MACvB,KAAK,mBACL,OAAO,QACP,QAAQ,MACV;CACF;;;;;;;;CASA,SAAS,SAAwC;EAC/C,IAAI,KAAK,oBAAoB,KAAA,GAC3B;EAEF,OACE,QAAQ,OAAO,KAAK,kBACpB,QAAQ,SAAS,KAAK,oBAAoB,KAC1C,QAAQ,UAAU,KAAK,qBAAqB;CAEhD;AACF;AA4BA,SAAS,MACP,YACA,QACA,SACoB;CACpB,OAAO,UAAU,IAAI,MAAM,YAAY,SAAS,OAAO,IAAI;AAC7D;AAEA,SAAS,MAAM,UAA8B,QAAwB;CACnE,OAAO,aAAa,KAAA,IAChB,SACA,WAAW,iBAAiB,SAAS;AAC3C;;;;;;;;;;;;;;;;;AAkBA,SAAgB,8BACd,iBACA,SAIA,QACS;CACT,IAAI,oBAAoB,KAAA,GACtB,OAAO;CAET,IAAI,SAAS;CACb,IAAI,cAAc;CAClB,KAAK,MAAM,SAAS,SAAS;EAC3B,UAAU,MAAM,mBAAmB;EACnC,eAAe,KAAK,IAAI,MAAM,iBAAA,CAAmC;CACnE;CACA,OAAO,cAAc,KAAK,UAAU,SAAS;AAC/C;;;;;;;;;AAUA,SAAgB,qBACd,iBACA,iBACA,QACS;CACT,OACE,oBAAoB,KAAA,KACpB,mBAAmB,SAAS,KAAK,IAAI,iBAAA,CAAmC;AAE5E"}
@@ -1 +1 @@
1
- {"version":3,"file":"push-cutoff.js","names":["#shouldCutOff","#pushed"],"sources":["../../../../../../zero-cache/src/services/view-syncer/push-cutoff.ts"],"sourcesContent":["/** What an advancement has pushed to a query so far. */\nexport type QueryPushProgress = {\n readonly queryID: string;\n /** The time spent processing pushes to the query in this advancement. */\n readonly pushTimeMs: number;\n /** The number of changes pushed to the query in this advancement. */\n readonly changes: number;\n /** The query's hydration time, i.e. what rebuilding it is expected to cost. */\n readonly hydrationTimeMs: number;\n};\n\n/** Whether to stop pushing an advancement's changes to a query. */\nexport type ShouldCutOffPush = (progress: QueryPushProgress) => boolean;\n\n/**\n * The least a query's hydration is taken to cost when deciding whether to\n * stop pushing to it. Small queries hydrate in well under a millisecond,\n * where one push can take longer than the hydration did on timer noise alone,\n * and hydrating again saves too little to be worth its fixed costs.\n */\nexport const MIN_PUSH_CUTOFF_MS = 2;\n\n/**\n * Whether to stop pushing an advancement's changes to a query and hydrate it\n * again instead: once the pushes have taken as long as the hydration is\n * expected to.\n *\n * This is the ski-rental strategy (rent until the rent paid equals the\n * purchase price, then buy). Whatever the actual costs turn out to be, the\n * query costs at most about twice the cheaper of pushing every change and\n * hydrating from the start, plus the one change that crossed the line. It\n * needs no estimate of what the remaining changes will cost.\n */\nexport function shouldCutOffPush({\n pushTimeMs,\n hydrationTimeMs,\n}: Pick<QueryPushProgress, 'pushTimeMs' | 'hydrationTimeMs'>): boolean {\n return pushTimeMs >= Math.max(hydrationTimeMs, MIN_PUSH_CUTOFF_MS);\n}\n\n/**\n * Bounds the cost of one advancement query by query: each query is pushed\n * changes until {@link ShouldCutOffPush} says to stop, and is then hydrated\n * again once the advancement is done.\n *\n * An advancement with a cutoff is not subject to the client group's\n * advancement time limit, which resets every pipeline of the client group.\n * The two are alternatives and are never applied together.\n */\nexport class PushCutoff {\n readonly #shouldCutOff: ShouldCutOffPush;\n /** The queries pushed to since the last {@link takeCutOff}. */\n readonly #pushed = new Set<string>();\n\n constructor(shouldCutOff: ShouldCutOffPush = shouldCutOffPush) {\n this.#shouldCutOff = shouldCutOff;\n }\n\n /** Called for each push to a query. */\n recordPush(queryID: string): void {\n this.#pushed.add(queryID);\n }\n\n /**\n * Called between changes, when no push is in progress. Returns the queries\n * to stop pushing to, of those pushed to since the last call.\n *\n * @param progressOf The progress of a query, or `undefined` if it is no\n * longer being pushed to.\n */\n takeCutOff(\n progressOf: (queryID: string) => QueryPushProgress | undefined,\n ): QueryPushProgress[] {\n if (this.#pushed.size === 0) {\n return [];\n }\n const cutOff: QueryPushProgress[] = [];\n for (const queryID of this.#pushed) {\n const progress = progressOf(queryID);\n if (progress !== undefined && this.#shouldCutOff(progress)) {\n cutOff.push(progress);\n }\n }\n this.#pushed.clear();\n return cutOff;\n }\n}\n"],"mappings":";;;;;;;;;;;AAiCA,SAAgB,iBAAiB,EAC/B,YACA,mBACqE;CACrE,OAAO,cAAc,KAAK,IAAI,iBAAA,CAAmC;AACnE;;;;;;;;;;AAWA,IAAa,aAAb,MAAwB;CACtB;;CAEA,0BAAmB,IAAI,IAAY;CAEnC,YAAY,eAAiC,kBAAkB;EAC7D,KAAKA,gBAAgB;CACvB;;CAGA,WAAW,SAAuB;EAChC,KAAKC,QAAQ,IAAI,OAAO;CAC1B;;;;;;;;CASA,WACE,YACqB;EACrB,IAAI,KAAKA,QAAQ,SAAS,GACxB,OAAO,CAAC;EAEV,MAAM,SAA8B,CAAC;EACrC,KAAK,MAAM,WAAW,KAAKA,SAAS;GAClC,MAAM,WAAW,WAAW,OAAO;GACnC,IAAI,aAAa,KAAA,KAAa,KAAKD,cAAc,QAAQ,GACvD,OAAO,KAAK,QAAQ;EAExB;EACA,KAAKC,QAAQ,MAAM;EACnB,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"push-cutoff.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/push-cutoff.ts"],"sourcesContent":["/** What an advancement has pushed to a query so far. */\nexport type QueryPushProgress = {\n readonly queryID: string;\n /** The time spent processing pushes to the query in this advancement. */\n readonly pushTimeMs: number;\n /** The number of changes pushed to the query in this advancement. */\n readonly changes: number;\n /** The query's hydration time, i.e. what rebuilding it is expected to cost. */\n readonly hydrationTimeMs: number;\n};\n\n/** Whether to stop pushing an advancement's changes to a query. */\nexport type ShouldCutOffPush = (progress: QueryPushProgress) => boolean;\n\n/**\n * The least a query's hydration is taken to cost when deciding whether to\n * stop pushing to it. Small queries hydrate in well under a millisecond,\n * where one push can take longer than the hydration did on timer noise alone,\n * and hydrating again saves too little to be worth its fixed costs.\n */\nexport const MIN_PUSH_CUTOFF_MS = 2;\n\n/**\n * Whether to stop pushing an advancement's changes to a query and hydrate it\n * again instead: once the pushes have taken as long as the hydration is\n * expected to.\n *\n * This is the ski-rental strategy (rent until the rent paid equals the\n * purchase price, then buy). Whatever the actual costs turn out to be, the\n * query costs at most about twice the cheaper of pushing every change and\n * hydrating from the start, plus the one change that crossed the line. It\n * needs no estimate of what the remaining changes will cost.\n */\nexport function shouldCutOffPush({\n pushTimeMs,\n hydrationTimeMs,\n}: Pick<QueryPushProgress, 'pushTimeMs' | 'hydrationTimeMs'>): boolean {\n return pushTimeMs >= Math.max(hydrationTimeMs, MIN_PUSH_CUTOFF_MS);\n}\n\n/**\n * Bounds the cost of one advancement query by query: each query is pushed\n * changes until {@link ShouldCutOffPush} says to stop, and is then hydrated\n * again once the advancement is done.\n *\n * An advancement with a cutoff is not subject to the client group's\n * advancement time limit, which resets every pipeline of the client group.\n * The two are alternatives and are never applied together.\n */\nexport class PushCutoff {\n readonly #shouldCutOff: ShouldCutOffPush;\n /** The queries pushed to since the last {@link takeCutOff}. */\n readonly #pushed = new Set<string>();\n\n constructor(shouldCutOff: ShouldCutOffPush = shouldCutOffPush) {\n this.#shouldCutOff = shouldCutOff;\n }\n\n /** Called for each push to a query. */\n recordPush(queryID: string): void {\n this.#pushed.add(queryID);\n }\n\n /**\n * Called between changes, when no push is in progress. Returns the queries\n * to stop pushing to, of those pushed to since the last call.\n *\n * @param progressOf The progress of a query, or `undefined` if it is no\n * longer being pushed to.\n */\n takeCutOff(\n progressOf: (queryID: string) => QueryPushProgress | undefined,\n ): QueryPushProgress[] {\n if (this.#pushed.size === 0) {\n return [];\n }\n const cutOff: QueryPushProgress[] = [];\n for (const queryID of this.#pushed) {\n const progress = progressOf(queryID);\n if (progress !== undefined && this.#shouldCutOff(progress)) {\n cutOff.push(progress);\n }\n }\n this.#pushed.clear();\n return cutOff;\n }\n}\n"],"mappings":";;;;;;;;;;;AAiCA,SAAgB,iBAAiB,EAC/B,YACA,mBACqE;CACrE,OAAO,cAAc,KAAK,IAAI,iBAAA,CAAmC;AACnE;;;;;;;;;;AAWA,IAAa,aAAb,MAAwB;CACtB;;CAEA,0BAAmB,IAAI,IAAY;CAEnC,YAAY,eAAiC,kBAAkB;EAC7D,KAAK,gBAAgB;CACvB;;CAGA,WAAW,SAAuB;EAChC,KAAK,QAAQ,IAAI,OAAO;CAC1B;;;;;;;;CASA,WACE,YACqB;EACrB,IAAI,KAAK,QAAQ,SAAS,GACxB,OAAO,CAAC;EAEV,MAAM,SAA8B,CAAC;EACrC,KAAK,MAAM,WAAW,KAAK,SAAS;GAClC,MAAM,WAAW,WAAW,OAAO;GACnC,IAAI,aAAa,KAAA,KAAa,KAAK,cAAc,QAAQ,GACvD,OAAO,KAAK,QAAQ;EAExB;EACA,KAAK,QAAQ,MAAM;EACnB,OAAO;CACT;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"query-covering.js","names":["#byRoot","#queryIDToRoot"],"sources":["../../../../../../zero-cache/src/services/view-syncer/query-covering.ts"],"sourcesContent":["import {\n deepEqual,\n type ReadonlyJSONValue,\n} from '../../../../shared/src/json.ts';\nimport {getOrInsertComputed, newMap} from '../../../../shared/src/map.ts';\nimport {\n normalizeAST,\n type AST,\n type Condition,\n type CorrelatedSubquery,\n type CorrelatedSubqueryCondition,\n type LiteralValue,\n type NormalizedAST,\n type SimpleCondition,\n} from '../../../../zero-protocol/src/ast.ts';\n\nexport type RunningQuery = {\n readonly transformedAst: AST;\n readonly transformationHash: string;\n readonly queryName?: string | undefined;\n};\n\nexport type CoveringQuery = {\n readonly queryID: string;\n readonly transformationHash: string;\n readonly queryName?: string | undefined;\n};\n\ntype NonNullScalarLiteralValue = string | number | boolean;\ntype IndexedRunningQuery = RunningQuery & {\n readonly normalizedAst: NormalizedAST;\n};\n\n/**\n * Returns true when every row that can be produced by `covered` is also\n * produced by `covering`.\n *\n * This is intentionally conservative: unsupported cases return false rather\n * than guessing. It is used for shadow logging only.\n */\nexport function isQueryCoveredBy(covered: AST, covering: AST): boolean {\n return astCoveredBy(normalizeAST(covered), normalizeAST(covering));\n}\n\nexport function findCoveringQuery(\n coveredQueryID: string,\n coveredAst: AST,\n runningQueries: ReadonlyMap<string, RunningQuery>,\n): CoveringQuery | undefined {\n return new QueryCoveringIndex(runningQueries).findCoveringQuery(\n coveredQueryID,\n coveredAst,\n );\n}\n\nexport class QueryCoveringIndex {\n readonly #byRoot = new Map<string, Map<string, IndexedRunningQuery>>();\n readonly #queryIDToRoot = new Map<string, string>();\n\n constructor(runningQueries?: ReadonlyMap<string, RunningQuery>) {\n if (runningQueries) {\n for (const [queryID, query] of runningQueries) {\n this.add(queryID, query);\n }\n }\n }\n\n add(queryID: string, query: RunningQuery): void {\n this.remove(queryID);\n\n const normalizedAst = normalizeAST(query.transformedAst);\n const root = rootKey(normalizedAst);\n getOrInsertComputed(this.#byRoot, root, newMap).set(queryID, {\n ...query,\n normalizedAst,\n });\n this.#queryIDToRoot.set(queryID, root);\n }\n\n remove(queryID: string): void {\n const root = this.#queryIDToRoot.get(queryID);\n if (root === undefined) {\n return;\n }\n\n this.#queryIDToRoot.delete(queryID);\n const queries = this.#byRoot.get(root);\n if (!queries) {\n return;\n }\n\n queries.delete(queryID);\n if (queries.size === 0) {\n this.#byRoot.delete(root);\n }\n }\n\n findCoveringQuery(\n coveredQueryID: string,\n coveredAst: AST,\n ): CoveringQuery | undefined {\n const normalizedCoveredAst = normalizeAST(coveredAst);\n const queries = this.#byRoot.get(rootKey(normalizedCoveredAst));\n if (!queries) {\n return undefined;\n }\n\n for (const [queryID, query] of queries) {\n if (queryID === coveredQueryID) {\n continue;\n }\n if (astCoveredBy(normalizedCoveredAst, query.normalizedAst)) {\n return {\n queryID,\n transformationHash: query.transformationHash,\n ...(query.queryName !== undefined && {queryName: query.queryName}),\n };\n }\n }\n return undefined;\n }\n}\n\nfunction rootKey(ast: NormalizedAST): string {\n return JSON.stringify([ast.schema, ast.table, ast.alias]);\n}\n\nfunction astCoveredBy(\n covered: NormalizedAST,\n covering: NormalizedAST,\n): boolean {\n return (\n covered.schema === covering.schema &&\n covered.table === covering.table &&\n covered.alias === covering.alias &&\n conditionImplies(covered.where, covering.where) &&\n relatedCoveredBy(covered.related, covering.related) &&\n boundsCoveredBy(covered, covering)\n );\n}\n\nfunction boundsCoveredBy(\n covered: NormalizedAST,\n covering: NormalizedAST,\n): boolean {\n if (covering.limit === undefined) {\n if (covering.start === undefined) {\n return true;\n }\n return (\n jsonEqual(covered.start, covering.start) &&\n jsonEqual(covered.orderBy, covering.orderBy)\n );\n }\n\n if (covered.limit === undefined || covering.limit < covered.limit) {\n return false;\n }\n\n // A limited broader query does not necessarily contain a limited narrower\n // query. The ordered input to the limit must be equivalent.\n return (\n conditionEquivalent(covered.where, covering.where) &&\n jsonEqual(covered.start, covering.start) &&\n jsonEqual(covered.orderBy, covering.orderBy)\n );\n}\n\nfunction relatedCoveredBy(\n covered: readonly CorrelatedSubquery[] | undefined,\n covering: readonly CorrelatedSubquery[] | undefined,\n): boolean {\n if (covered === undefined || covered.length === 0) {\n return true;\n }\n if (covering === undefined) {\n return false;\n }\n return covered.every(coveredRelated =>\n covering.some(\n coveringRelated =>\n sameRelatedEdge(coveredRelated, coveringRelated) &&\n astCoveredBy(\n coveredRelated.subquery as NormalizedAST,\n coveringRelated.subquery as NormalizedAST,\n ),\n ),\n );\n}\n\nfunction conditionEquivalent(\n a: Condition | undefined,\n b: Condition | undefined,\n): boolean {\n return conditionImplies(a, b) && conditionImplies(b, a);\n}\n\nfunction conditionImplies(\n covered: Condition | undefined,\n covering: Condition | undefined,\n): boolean {\n if (covering === undefined) {\n return true;\n }\n if (covered === undefined) {\n return false;\n }\n if (jsonEqual(covered, covering)) {\n return true;\n }\n\n if (covered.type === 'or') {\n return covered.conditions.every(c => conditionImplies(c, covering));\n }\n if (covering.type === 'or') {\n return covering.conditions.some(c => conditionImplies(covered, c));\n }\n if (covering.type === 'and') {\n return covering.conditions.every(c => conditionImplies(covered, c));\n }\n if (covered.type === 'and') {\n return covered.conditions.some(c => conditionImplies(c, covering));\n }\n if (covered.type === 'simple' && covering.type === 'simple') {\n return simpleConditionImplies(covered, covering);\n }\n if (\n covered.type === 'correlatedSubquery' &&\n covering.type === 'correlatedSubquery'\n ) {\n return correlatedConditionImplies(covered, covering);\n }\n return false;\n}\n\nfunction correlatedConditionImplies(\n covered: CorrelatedSubqueryCondition,\n covering: CorrelatedSubqueryCondition,\n): boolean {\n if (\n covered.op !== covering.op ||\n covered.scalar !== covering.scalar ||\n !sameRelatedEdge(covered.related, covering.related)\n ) {\n return false;\n }\n\n if (covered.op === 'EXISTS') {\n return astCoveredBy(\n covered.related.subquery as NormalizedAST,\n covering.related.subquery as NormalizedAST,\n );\n }\n\n return astCoveredBy(\n covering.related.subquery as NormalizedAST,\n covered.related.subquery as NormalizedAST,\n );\n}\n\nfunction sameRelatedEdge(\n a: CorrelatedSubquery,\n b: CorrelatedSubquery,\n): boolean {\n return (\n jsonEqual(a.correlation, b.correlation) &&\n a.hidden === b.hidden &&\n a.system === b.system &&\n a.subquery.alias === b.subquery.alias\n );\n}\n\nfunction simpleConditionImplies(\n covered: SimpleCondition,\n covering: SimpleCondition,\n): boolean {\n const coveredParts = columnLiteralParts(covered);\n const coveringParts = columnLiteralParts(covering);\n if (!coveredParts || !coveringParts) {\n return false;\n }\n if (coveredParts.column !== coveringParts.column) {\n return false;\n }\n\n const {op: coveredOp, value: coveredValue} = coveredParts;\n const {op: coveringOp, value: coveringValue} = coveringParts;\n\n if (isEqualityOp(coveredOp) && isNonNullScalarLiteralValue(coveredValue)) {\n return equalityImplies(coveredValue, coveringOp, coveringValue);\n }\n\n if (\n coveredOp === 'IN' &&\n coveringOp === 'IN' &&\n Array.isArray(coveredValue) &&\n Array.isArray(coveringValue)\n ) {\n return coveredValue.every(v => literalArrayIncludes(coveringValue, v));\n }\n\n if (isNumericOrderOp(coveredOp) && isNumericOrderOp(coveringOp)) {\n return orderConditionImplies(\n coveredOp,\n coveredValue,\n coveringOp,\n coveringValue,\n );\n }\n\n return false;\n}\n\nfunction equalityImplies(\n value: NonNullScalarLiteralValue,\n coveringOp: SimpleCondition['op'],\n coveringValue: LiteralValue,\n): boolean {\n switch (coveringOp) {\n case '=':\n case 'IS':\n return jsonEqual(value, coveringValue);\n case '!=':\n case 'IS NOT':\n return !jsonEqual(value, coveringValue);\n case 'IN':\n return (\n Array.isArray(coveringValue) &&\n literalArrayIncludes(coveringValue, value)\n );\n case '<':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value < coveringValue\n );\n case '<=':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value <= coveringValue\n );\n case '>':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value > coveringValue\n );\n case '>=':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value >= coveringValue\n );\n case 'NOT IN':\n case 'LIKE':\n case 'NOT LIKE':\n case 'ILIKE':\n case 'NOT ILIKE':\n return false;\n }\n}\n\nfunction orderConditionImplies(\n coveredOp: '<' | '>' | '<=' | '>=',\n coveredValue: LiteralValue,\n coveringOp: '<' | '>' | '<=' | '>=',\n coveringValue: LiteralValue,\n): boolean {\n if (typeof coveredValue !== 'number' || typeof coveringValue !== 'number') {\n return false;\n }\n\n switch (coveredOp) {\n case '>':\n return (\n (coveringOp === '>' && coveredValue >= coveringValue) ||\n (coveringOp === '>=' && coveredValue >= coveringValue)\n );\n case '>=':\n return (\n (coveringOp === '>' && coveredValue > coveringValue) ||\n (coveringOp === '>=' && coveredValue >= coveringValue)\n );\n case '<':\n return (\n (coveringOp === '<' && coveredValue <= coveringValue) ||\n (coveringOp === '<=' && coveredValue <= coveringValue)\n );\n case '<=':\n return (\n (coveringOp === '<' && coveredValue < coveringValue) ||\n (coveringOp === '<=' && coveredValue <= coveringValue)\n );\n }\n}\n\nfunction columnLiteralParts(condition: SimpleCondition):\n | {\n readonly column: string;\n readonly op: SimpleCondition['op'];\n readonly value: LiteralValue;\n }\n | undefined {\n if (condition.left.type !== 'column' || condition.right.type !== 'literal') {\n return undefined;\n }\n return {\n column: condition.left.name,\n op: condition.op,\n value: condition.right.value,\n };\n}\n\nfunction isEqualityOp(op: SimpleCondition['op']): op is '=' | 'IS' {\n return op === '=' || op === 'IS';\n}\n\nfunction isNumericOrderOp(\n op: SimpleCondition['op'],\n): op is '<' | '>' | '<=' | '>=' {\n return op === '<' || op === '>' || op === '<=' || op === '>=';\n}\n\nfunction isNonNullScalarLiteralValue(\n value: LiteralValue,\n): value is NonNullScalarLiteralValue {\n return value !== null && !Array.isArray(value);\n}\n\nfunction literalArrayIncludes(\n values: ReadonlyArray<string | number | boolean>,\n value: string | number | boolean | null,\n): boolean {\n return values.some(v => jsonEqual(v, value));\n}\n\nfunction jsonEqual(a: unknown, b: unknown): boolean {\n return deepEqual(\n a as ReadonlyJSONValue | undefined,\n b as ReadonlyJSONValue | undefined,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"query-covering.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/query-covering.ts"],"sourcesContent":["import {\n deepEqual,\n type ReadonlyJSONValue,\n} from '../../../../shared/src/json.ts';\nimport {getOrInsertComputed, newMap} from '../../../../shared/src/map.ts';\nimport {\n normalizeAST,\n type AST,\n type Condition,\n type CorrelatedSubquery,\n type CorrelatedSubqueryCondition,\n type LiteralValue,\n type NormalizedAST,\n type SimpleCondition,\n} from '../../../../zero-protocol/src/ast.ts';\n\nexport type RunningQuery = {\n readonly transformedAst: AST;\n readonly transformationHash: string;\n readonly queryName?: string | undefined;\n};\n\nexport type CoveringQuery = {\n readonly queryID: string;\n readonly transformationHash: string;\n readonly queryName?: string | undefined;\n};\n\ntype NonNullScalarLiteralValue = string | number | boolean;\ntype IndexedRunningQuery = RunningQuery & {\n readonly normalizedAst: NormalizedAST;\n};\n\n/**\n * Returns true when every row that can be produced by `covered` is also\n * produced by `covering`.\n *\n * This is intentionally conservative: unsupported cases return false rather\n * than guessing. It is used for shadow logging only.\n */\nexport function isQueryCoveredBy(covered: AST, covering: AST): boolean {\n return astCoveredBy(normalizeAST(covered), normalizeAST(covering));\n}\n\nexport function findCoveringQuery(\n coveredQueryID: string,\n coveredAst: AST,\n runningQueries: ReadonlyMap<string, RunningQuery>,\n): CoveringQuery | undefined {\n return new QueryCoveringIndex(runningQueries).findCoveringQuery(\n coveredQueryID,\n coveredAst,\n );\n}\n\nexport class QueryCoveringIndex {\n readonly #byRoot = new Map<string, Map<string, IndexedRunningQuery>>();\n readonly #queryIDToRoot = new Map<string, string>();\n\n constructor(runningQueries?: ReadonlyMap<string, RunningQuery>) {\n if (runningQueries) {\n for (const [queryID, query] of runningQueries) {\n this.add(queryID, query);\n }\n }\n }\n\n add(queryID: string, query: RunningQuery): void {\n this.remove(queryID);\n\n const normalizedAst = normalizeAST(query.transformedAst);\n const root = rootKey(normalizedAst);\n getOrInsertComputed(this.#byRoot, root, newMap).set(queryID, {\n ...query,\n normalizedAst,\n });\n this.#queryIDToRoot.set(queryID, root);\n }\n\n remove(queryID: string): void {\n const root = this.#queryIDToRoot.get(queryID);\n if (root === undefined) {\n return;\n }\n\n this.#queryIDToRoot.delete(queryID);\n const queries = this.#byRoot.get(root);\n if (!queries) {\n return;\n }\n\n queries.delete(queryID);\n if (queries.size === 0) {\n this.#byRoot.delete(root);\n }\n }\n\n findCoveringQuery(\n coveredQueryID: string,\n coveredAst: AST,\n ): CoveringQuery | undefined {\n const normalizedCoveredAst = normalizeAST(coveredAst);\n const queries = this.#byRoot.get(rootKey(normalizedCoveredAst));\n if (!queries) {\n return undefined;\n }\n\n for (const [queryID, query] of queries) {\n if (queryID === coveredQueryID) {\n continue;\n }\n if (astCoveredBy(normalizedCoveredAst, query.normalizedAst)) {\n return {\n queryID,\n transformationHash: query.transformationHash,\n ...(query.queryName !== undefined && {queryName: query.queryName}),\n };\n }\n }\n return undefined;\n }\n}\n\nfunction rootKey(ast: NormalizedAST): string {\n return JSON.stringify([ast.schema, ast.table, ast.alias]);\n}\n\nfunction astCoveredBy(\n covered: NormalizedAST,\n covering: NormalizedAST,\n): boolean {\n return (\n covered.schema === covering.schema &&\n covered.table === covering.table &&\n covered.alias === covering.alias &&\n conditionImplies(covered.where, covering.where) &&\n relatedCoveredBy(covered.related, covering.related) &&\n boundsCoveredBy(covered, covering)\n );\n}\n\nfunction boundsCoveredBy(\n covered: NormalizedAST,\n covering: NormalizedAST,\n): boolean {\n if (covering.limit === undefined) {\n if (covering.start === undefined) {\n return true;\n }\n return (\n jsonEqual(covered.start, covering.start) &&\n jsonEqual(covered.orderBy, covering.orderBy)\n );\n }\n\n if (covered.limit === undefined || covering.limit < covered.limit) {\n return false;\n }\n\n // A limited broader query does not necessarily contain a limited narrower\n // query. The ordered input to the limit must be equivalent.\n return (\n conditionEquivalent(covered.where, covering.where) &&\n jsonEqual(covered.start, covering.start) &&\n jsonEqual(covered.orderBy, covering.orderBy)\n );\n}\n\nfunction relatedCoveredBy(\n covered: readonly CorrelatedSubquery[] | undefined,\n covering: readonly CorrelatedSubquery[] | undefined,\n): boolean {\n if (covered === undefined || covered.length === 0) {\n return true;\n }\n if (covering === undefined) {\n return false;\n }\n return covered.every(coveredRelated =>\n covering.some(\n coveringRelated =>\n sameRelatedEdge(coveredRelated, coveringRelated) &&\n astCoveredBy(\n coveredRelated.subquery as NormalizedAST,\n coveringRelated.subquery as NormalizedAST,\n ),\n ),\n );\n}\n\nfunction conditionEquivalent(\n a: Condition | undefined,\n b: Condition | undefined,\n): boolean {\n return conditionImplies(a, b) && conditionImplies(b, a);\n}\n\nfunction conditionImplies(\n covered: Condition | undefined,\n covering: Condition | undefined,\n): boolean {\n if (covering === undefined) {\n return true;\n }\n if (covered === undefined) {\n return false;\n }\n if (jsonEqual(covered, covering)) {\n return true;\n }\n\n if (covered.type === 'or') {\n return covered.conditions.every(c => conditionImplies(c, covering));\n }\n if (covering.type === 'or') {\n return covering.conditions.some(c => conditionImplies(covered, c));\n }\n if (covering.type === 'and') {\n return covering.conditions.every(c => conditionImplies(covered, c));\n }\n if (covered.type === 'and') {\n return covered.conditions.some(c => conditionImplies(c, covering));\n }\n if (covered.type === 'simple' && covering.type === 'simple') {\n return simpleConditionImplies(covered, covering);\n }\n if (\n covered.type === 'correlatedSubquery' &&\n covering.type === 'correlatedSubquery'\n ) {\n return correlatedConditionImplies(covered, covering);\n }\n return false;\n}\n\nfunction correlatedConditionImplies(\n covered: CorrelatedSubqueryCondition,\n covering: CorrelatedSubqueryCondition,\n): boolean {\n if (\n covered.op !== covering.op ||\n covered.scalar !== covering.scalar ||\n !sameRelatedEdge(covered.related, covering.related)\n ) {\n return false;\n }\n\n if (covered.op === 'EXISTS') {\n return astCoveredBy(\n covered.related.subquery as NormalizedAST,\n covering.related.subquery as NormalizedAST,\n );\n }\n\n return astCoveredBy(\n covering.related.subquery as NormalizedAST,\n covered.related.subquery as NormalizedAST,\n );\n}\n\nfunction sameRelatedEdge(\n a: CorrelatedSubquery,\n b: CorrelatedSubquery,\n): boolean {\n return (\n jsonEqual(a.correlation, b.correlation) &&\n a.hidden === b.hidden &&\n a.system === b.system &&\n a.subquery.alias === b.subquery.alias\n );\n}\n\nfunction simpleConditionImplies(\n covered: SimpleCondition,\n covering: SimpleCondition,\n): boolean {\n const coveredParts = columnLiteralParts(covered);\n const coveringParts = columnLiteralParts(covering);\n if (!coveredParts || !coveringParts) {\n return false;\n }\n if (coveredParts.column !== coveringParts.column) {\n return false;\n }\n\n const {op: coveredOp, value: coveredValue} = coveredParts;\n const {op: coveringOp, value: coveringValue} = coveringParts;\n\n if (isEqualityOp(coveredOp) && isNonNullScalarLiteralValue(coveredValue)) {\n return equalityImplies(coveredValue, coveringOp, coveringValue);\n }\n\n if (\n coveredOp === 'IN' &&\n coveringOp === 'IN' &&\n Array.isArray(coveredValue) &&\n Array.isArray(coveringValue)\n ) {\n return coveredValue.every(v => literalArrayIncludes(coveringValue, v));\n }\n\n if (isNumericOrderOp(coveredOp) && isNumericOrderOp(coveringOp)) {\n return orderConditionImplies(\n coveredOp,\n coveredValue,\n coveringOp,\n coveringValue,\n );\n }\n\n return false;\n}\n\nfunction equalityImplies(\n value: NonNullScalarLiteralValue,\n coveringOp: SimpleCondition['op'],\n coveringValue: LiteralValue,\n): boolean {\n switch (coveringOp) {\n case '=':\n case 'IS':\n return jsonEqual(value, coveringValue);\n case '!=':\n case 'IS NOT':\n return !jsonEqual(value, coveringValue);\n case 'IN':\n return (\n Array.isArray(coveringValue) &&\n literalArrayIncludes(coveringValue, value)\n );\n case '<':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value < coveringValue\n );\n case '<=':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value <= coveringValue\n );\n case '>':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value > coveringValue\n );\n case '>=':\n return (\n typeof value === 'number' &&\n typeof coveringValue === 'number' &&\n value >= coveringValue\n );\n case 'NOT IN':\n case 'LIKE':\n case 'NOT LIKE':\n case 'ILIKE':\n case 'NOT ILIKE':\n return false;\n }\n}\n\nfunction orderConditionImplies(\n coveredOp: '<' | '>' | '<=' | '>=',\n coveredValue: LiteralValue,\n coveringOp: '<' | '>' | '<=' | '>=',\n coveringValue: LiteralValue,\n): boolean {\n if (typeof coveredValue !== 'number' || typeof coveringValue !== 'number') {\n return false;\n }\n\n switch (coveredOp) {\n case '>':\n return (\n (coveringOp === '>' && coveredValue >= coveringValue) ||\n (coveringOp === '>=' && coveredValue >= coveringValue)\n );\n case '>=':\n return (\n (coveringOp === '>' && coveredValue > coveringValue) ||\n (coveringOp === '>=' && coveredValue >= coveringValue)\n );\n case '<':\n return (\n (coveringOp === '<' && coveredValue <= coveringValue) ||\n (coveringOp === '<=' && coveredValue <= coveringValue)\n );\n case '<=':\n return (\n (coveringOp === '<' && coveredValue < coveringValue) ||\n (coveringOp === '<=' && coveredValue <= coveringValue)\n );\n }\n}\n\nfunction columnLiteralParts(condition: SimpleCondition):\n | {\n readonly column: string;\n readonly op: SimpleCondition['op'];\n readonly value: LiteralValue;\n }\n | undefined {\n if (condition.left.type !== 'column' || condition.right.type !== 'literal') {\n return undefined;\n }\n return {\n column: condition.left.name,\n op: condition.op,\n value: condition.right.value,\n };\n}\n\nfunction isEqualityOp(op: SimpleCondition['op']): op is '=' | 'IS' {\n return op === '=' || op === 'IS';\n}\n\nfunction isNumericOrderOp(\n op: SimpleCondition['op'],\n): op is '<' | '>' | '<=' | '>=' {\n return op === '<' || op === '>' || op === '<=' || op === '>=';\n}\n\nfunction isNonNullScalarLiteralValue(\n value: LiteralValue,\n): value is NonNullScalarLiteralValue {\n return value !== null && !Array.isArray(value);\n}\n\nfunction literalArrayIncludes(\n values: ReadonlyArray<string | number | boolean>,\n value: string | number | boolean | null,\n): boolean {\n return values.some(v => jsonEqual(v, value));\n}\n\nfunction jsonEqual(a: unknown, b: unknown): boolean {\n return deepEqual(\n a as ReadonlyJSONValue | undefined,\n b as ReadonlyJSONValue | undefined,\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"query-shape.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/query-shape.ts"],"sourcesContent":["// @circular-dep-ignore\nimport {astToZQL} from '../../../../ast-to-zql/src/ast-to-zql.ts';\nimport {h64} from '../../../../shared/src/hash.ts';\nimport type {AST} from '../../../../zero-protocol/src/ast.ts';\n\n/**\n * A query with its literal values redacted, for logging.\n */\nexport type QueryShape = {\n /**\n * The query as ZQL with every literal (and the `start` row) rendered as\n * `?`, so it can be logged without leaking row data, query arguments, or\n * auth data substituted in by permissions.\n */\n readonly zql: string;\n\n /**\n * A hash of {@link zql}. Queries that differ only in their literal values,\n * e.g. the same named query called with different arguments, share a hash,\n * which makes it a key for grouping and throttling logs.\n */\n readonly hash: string;\n};\n\n/**\n * Never throws: it is computed for every hydration, and a query that cannot be\n * rendered as ZQL (e.g. a subquery without an alias) must still hydrate. Such\n * a query is shaped by its AST with the literal values redacted instead.\n */\nexport function queryShape(ast: AST): QueryShape {\n let zql: string;\n try {\n zql = ast.table + astToZQL(ast, {redactLiterals: true});\n } catch {\n zql = JSON.stringify(ast, redactLiterals);\n }\n return {zql, hash: h64(zql).toString(36)};\n}\n\nfunction redactLiterals(this: unknown, key: string, value: unknown) {\n if (\n (key === 'value' && (this as {type?: unknown}).type === 'literal') ||\n key === 'start'\n ) {\n return '?';\n }\n return value;\n}\n"],"mappings":";;;;;;;;AA6BA,SAAgB,WAAW,KAAsB;CAC/C,IAAI;CACJ,IAAI;EACF,MAAM,IAAI,QAAQ,SAAS,KAAK,EAAC,gBAAgB,KAAI,CAAC;CACxD,QAAQ;EACN,MAAM,KAAK,UAAU,KAAK,cAAc;CAC1C;CACA,OAAO;EAAC;EAAK,MAAM,IAAI,GAAG,EAAE,SAAS,EAAE;CAAC;AAC1C;AAEA,SAAS,eAA8B,KAAa,OAAgB;CAClE,IACG,QAAQ,WAAY,KAA0B,SAAS,aACxD,QAAQ,SAER,OAAO;CAET,OAAO;AACT"}
1
+ {"version":3,"file":"query-shape.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/query-shape.ts"],"sourcesContent":["// @circular-dep-ignore\nimport {astToZQL} from '../../../../ast-to-zql/src/ast-to-zql.ts';\nimport {h64} from '../../../../shared/src/hash.ts';\nimport type {AST} from '../../../../zero-protocol/src/ast.ts';\n\n/**\n * A query with its literal values redacted, for logging.\n */\nexport type QueryShape = {\n /**\n * The query as ZQL with every literal (and the `start` row) rendered as\n * `?`, so it can be logged without leaking row data, query arguments, or\n * auth data substituted in by permissions.\n */\n readonly zql: string;\n\n /**\n * A hash of {@link zql}. Queries that differ only in their literal values,\n * e.g. the same named query called with different arguments, share a hash,\n * which makes it a key for grouping and throttling logs.\n */\n readonly hash: string;\n};\n\n/**\n * Never throws: it is computed for every hydration, and a query that cannot be\n * rendered as ZQL (e.g. a subquery without an alias) must still hydrate. Such\n * a query is shaped by its AST with the literal values redacted instead.\n */\nexport function queryShape(ast: AST): QueryShape {\n let zql: string;\n try {\n zql = ast.table + astToZQL(ast, {redactLiterals: true});\n } catch {\n zql = JSON.stringify(ast, redactLiterals);\n }\n return {zql, hash: h64(zql).toString(36)};\n}\n\nfunction redactLiterals(this: unknown, key: string, value: unknown) {\n if (\n (key === 'value' && (this as {type?: unknown}).type === 'literal') ||\n key === 'start'\n ) {\n return '?';\n }\n return value;\n}\n"],"mappings":";;;;;;;;AA6BA,SAAgB,WAAW,KAAsB;CAC/C,IAAI;CACJ,IAAI;EACF,MAAM,IAAI,QAAQ,SAAS,KAAK,EAAC,gBAAgB,KAAI,CAAC;CACxD,QAAQ;EACN,MAAM,KAAK,UAAU,KAAK,cAAc;CAC1C;CACA,OAAO;EAAC;EAAK,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE;CAAC;AAC1C;AAEA,SAAS,eAA8B,KAAa,OAAgB;CAClE,IACG,QAAQ,WAAY,KAA0B,SAAS,aACxD,QAAQ,SAER,OAAO;CAET,OAAO;AACT"}
@@ -79,7 +79,7 @@ var ShapeStats = class extends Totals {
79
79
  };
80
80
  /** A fixed-size linear-counting sketch for shapes beyond the tracking limit. */
81
81
  var OverflowShapeCounter = class {
82
- #buckets = new Uint8Array(4096);
82
+ #buckets = /* @__PURE__ */ new Uint8Array(4096);
83
83
  #occupied = 0;
84
84
  add(key) {
85
85
  let hash = 2166136261;
@@ -1 +1 @@
1
- {"version":3,"file":"query-stats.js","names":["#buckets","#occupied","#maxShapes","#maxReported","#now","#takeIndexUsage","#intervalStart","#totals","#stats","#shapes","#overflowShapes"],"sources":["../../../../../../zero-cache/src/services/view-syncer/query-stats.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {PlanWarning} from '../../../../zql/src/planner/planner-warnings.ts';\nimport {takeIndexUsage} from '../../../../zqlite/src/index-usage.ts';\nimport type {QueryShape} from './query-shape.ts';\n\n/** The query that work is accounted to. */\nexport type QueryIdentity = {\n readonly queryName?: string | undefined;\n readonly shape: QueryShape;\n};\n\nexport type HydrationOutcome = 'finished' | 'aborted' | 'failed';\n\nexport type HydrationRecord = {\n readonly outcome: HydrationOutcome;\n readonly timeMs: number;\n /** Only for a finished hydration. */\n readonly rowCount?: number | undefined;\n /** Only for a finished hydration. */\n readonly rowsRead?: number | undefined;\n readonly planWarnings?: readonly PlanWarning[] | undefined;\n};\n\nexport type AdvanceRecord = {\n /** The time spent processing the pushes to the query. */\n readonly timeMs: number;\n /** The number of changes pushed to the query. */\n readonly changes: number;\n /** Whether the advancement timed out and reset the pipelines. */\n readonly timedOut: boolean;\n};\n\n/** Why an advancement stopped pushing to a query and hydrated it again. */\nexport type AbandonReason = 'estimate' | 'read' | 'cutoff';\n\nexport type QueryStatsOptions = {\n /**\n * The most query shapes tracked per interval. Work on shapes beyond this is\n * accounted to the interval's totals only, which bounds memory when the\n * shape space is unbounded (e.g. dynamically built queries).\n */\n readonly maxShapes?: number | undefined;\n\n /**\n * The most query shapes logged per interval, those that took the most\n * time. The rest are included in the interval's totals.\n */\n readonly maxReported?: number | undefined;\n\n readonly now?: (() => number) | undefined;\n\n /** Returns the index uses since the last call. See `recordIndexUsage()`. */\n readonly takeIndexUsage?: (() => Map<string, number>) | undefined;\n};\n\nclass Distribution {\n count = 0;\n sumMs = 0;\n minMs = Infinity;\n maxMs = 0;\n\n add(ms: number) {\n this.count++;\n this.sumMs += ms;\n this.minMs = Math.min(this.minMs, ms);\n this.maxMs = Math.max(this.maxMs, ms);\n }\n\n toJSON() {\n return {\n count: this.count,\n sumMs: round(this.sumMs),\n minMs: round(this.count === 0 ? 0 : this.minMs),\n maxMs: round(this.maxMs),\n };\n }\n}\n\nclass Totals {\n readonly hydrations = new Distribution();\n hydrationsAborted = 0;\n hydrationsFailed = 0;\n hydrationRowCount = 0;\n hydrationRowsRead = 0;\n readonly advances = new Distribution();\n advanceChanges = 0;\n advanceTimeouts = 0;\n /**\n * The advancements that hydrated the query again instead of pushing (all\n * of) their changes to it, by why. The hydrations are in `hydrations`.\n */\n readonly advancesAbandoned: Record<AbandonReason, number> = {\n estimate: 0,\n read: 0,\n cutoff: 0,\n };\n\n get timeMs() {\n return this.hydrations.sumMs + this.advances.sumMs;\n }\n\n addHydration({outcome, timeMs, rowCount, rowsRead}: HydrationRecord) {\n this.hydrations.add(timeMs);\n if (outcome === 'aborted') {\n this.hydrationsAborted++;\n } else if (outcome === 'failed') {\n this.hydrationsFailed++;\n }\n this.hydrationRowCount += rowCount ?? 0;\n this.hydrationRowsRead += rowsRead ?? 0;\n }\n\n addAdvance({timeMs, changes, timedOut}: AdvanceRecord) {\n this.advances.add(timeMs);\n this.advanceChanges += changes;\n if (timedOut) {\n this.advanceTimeouts++;\n }\n }\n\n toJSON() {\n return {\n timeMs: round(this.timeMs),\n hydrations: this.hydrations.toJSON(),\n hydrationsAborted: this.hydrationsAborted,\n hydrationsFailed: this.hydrationsFailed,\n hydrationRowCount: this.hydrationRowCount,\n hydrationRowsRead: this.hydrationRowsRead,\n advances: this.advances.toJSON(),\n advanceChanges: this.advanceChanges,\n advanceTimeouts: this.advanceTimeouts,\n // Only with cost-based sync, without which there are none.\n ...(Object.values(this.advancesAbandoned).some(n => n > 0) && {\n advancesAbandoned: {...this.advancesAbandoned},\n }),\n };\n }\n}\n\nclass ShapeStats extends Totals {\n readonly query: QueryIdentity;\n planWarnings: readonly PlanWarning[] | undefined;\n\n constructor(query: QueryIdentity) {\n super();\n this.query = query;\n }\n}\n\n/** A fixed-size linear-counting sketch for shapes beyond the tracking limit. */\nclass OverflowShapeCounter {\n readonly #buckets = new Uint8Array(4096);\n #occupied = 0;\n\n add(key: string): void {\n // FNV-1a gives a stable bucket without retaining the key.\n let hash = 0x811c9dc5;\n for (let i = 0; i < key.length; i++) {\n hash = Math.imul(hash ^ key.charCodeAt(i), 0x01000193);\n }\n const bucket = (hash >>> 0) % this.#buckets.length;\n if (this.#buckets[bucket] === 0) {\n this.#buckets[bucket] = 1;\n this.#occupied++;\n }\n }\n\n get estimatedCount(): number {\n const empty = this.#buckets.length - this.#occupied;\n return empty === 0\n ? this.#buckets.length\n : Math.round(\n -this.#buckets.length * Math.log(empty / this.#buckets.length),\n );\n }\n}\n\n/**\n * Aggregates the work done for each query shape (the query with its values\n * redacted, see {@link QueryShape}) on a worker, and periodically logs it,\n * like `pg_stat_statements` does for Postgres.\n *\n * Unlike the slow query logs, which are throttled and only written above a\n * threshold, every hydration and advancement is counted, so the logs can be\n * summed over any time range and across workers to rank queries by the time\n * they take.\n *\n * Each {@link flush} logs the interval since the previous one:\n *\n * - a `query-stats` event per query shape, for the {@link\n * QueryStatsOptions.maxReported} shapes that took the most time, and\n * - a `query-stats-summary` event with the totals of all shapes, including\n * those not reported individually, and the `indexUses`: how often the\n * process read each index of the replica, like `pg_stat_user_indexes`. An\n * index that is never read is absent. The workers of a single-process\n * zero-cache share these counts, which the first of them to flush reports.\n *\n * The events carry counts, sums, minimums and maximums, which compose across\n * intervals and workers.\n */\nexport class QueryStats {\n readonly #maxShapes: number;\n readonly #maxReported: number;\n readonly #now: () => number;\n readonly #takeIndexUsage: () => Map<string, number>;\n\n #shapes = new Map<string, ShapeStats>();\n #totals = new Totals();\n #overflowShapes = new OverflowShapeCounter();\n #intervalStart: number;\n\n constructor({\n maxShapes = 1000,\n maxReported = 100,\n now = Date.now,\n takeIndexUsage: takeIndexUsageFn = takeIndexUsage,\n }: QueryStatsOptions = {}) {\n assert(maxShapes > 0, 'maxShapes must be positive');\n assert(maxReported > 0, 'maxReported must be positive');\n this.#maxShapes = maxShapes;\n this.#maxReported = maxReported;\n this.#now = now;\n this.#takeIndexUsage = takeIndexUsageFn;\n this.#intervalStart = now();\n }\n\n recordHydration(query: QueryIdentity, record: HydrationRecord): void {\n this.#totals.addHydration(record);\n const stats = this.#stats(query);\n if (stats) {\n stats.addHydration(record);\n if (record.planWarnings !== undefined) {\n stats.planWarnings = record.planWarnings;\n }\n }\n }\n\n recordAdvance(query: QueryIdentity, record: AdvanceRecord): void {\n this.#totals.addAdvance(record);\n this.#stats(query)?.addAdvance(record);\n }\n\n /**\n * Records that an advancement abandoned the query's pipeline, to hydrate\n * the query again.\n */\n recordAbandoned(query: QueryIdentity, reason: AbandonReason): void {\n this.#totals.advancesAbandoned[reason]++;\n const stats = this.#stats(query);\n if (stats) {\n stats.advancesAbandoned[reason]++;\n }\n }\n\n #stats(query: QueryIdentity): ShapeStats | undefined {\n const key = `${query.queryName ?? ''}:${query.shape.hash}`;\n let stats = this.#shapes.get(key);\n if (stats === undefined) {\n if (this.#shapes.size >= this.#maxShapes) {\n this.#overflowShapes.add(key);\n return undefined;\n }\n stats = new ShapeStats({queryName: query.queryName, shape: query.shape});\n this.#shapes.set(key, stats);\n }\n return stats;\n }\n\n /**\n * Logs the stats of the interval since the previous flush, and starts a\n * new interval. Nothing is logged for an interval without any work.\n */\n flush(lc: LogContext): void {\n const now = this.#now();\n const intervalMs = now - this.#intervalStart;\n const shapes = this.#shapes;\n const totals = this.#totals;\n const overflowShapes = this.#overflowShapes.estimatedCount;\n const indexUses = this.#takeIndexUsage();\n this.#shapes = new Map();\n this.#totals = new Totals();\n this.#overflowShapes = new OverflowShapeCounter();\n this.#intervalStart = now;\n\n if (shapes.size === 0 && overflowShapes === 0 && indexUses.size === 0) {\n return;\n }\n const reported = [...shapes.values()]\n .toSorted((a, b) => b.timeMs - a.timeMs)\n .slice(0, this.#maxReported);\n for (const stats of reported) {\n const {queryName, shape} = stats.query;\n lc.info?.('query stats', {\n zeroEvent: 'query-stats',\n intervalMs,\n ...(queryName !== undefined && {queryName}),\n queryShape: shape.hash,\n ...stats.toJSON(),\n ...(stats.planWarnings !== undefined &&\n stats.planWarnings.length > 0 && {planWarnings: stats.planWarnings}),\n zql: shape.zql,\n });\n }\n lc.info?.('query stats summary', {\n zeroEvent: 'query-stats-summary',\n intervalMs,\n shapes: shapes.size + overflowShapes,\n shapesReported: reported.length,\n ...totals.toJSON(),\n ...(indexUses.size > 0 && {indexUses: Object.fromEntries(indexUses)}),\n });\n }\n\n /**\n * Flushes every `intervalMs` until the returned function is called, which\n * flushes one last time.\n */\n start(lc: LogContext, intervalMs: number): () => void {\n const timer = setInterval(() => this.flush(lc), intervalMs);\n timer.unref?.();\n return () => {\n clearInterval(timer);\n this.flush(lc);\n };\n }\n}\n\nfunction round(ms: number) {\n return Math.round(ms * 100) / 100;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"query-stats.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/query-stats.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport type {PlanWarning} from '../../../../zql/src/planner/planner-warnings.ts';\nimport {takeIndexUsage} from '../../../../zqlite/src/index-usage.ts';\nimport type {QueryShape} from './query-shape.ts';\n\n/** The query that work is accounted to. */\nexport type QueryIdentity = {\n readonly queryName?: string | undefined;\n readonly shape: QueryShape;\n};\n\nexport type HydrationOutcome = 'finished' | 'aborted' | 'failed';\n\nexport type HydrationRecord = {\n readonly outcome: HydrationOutcome;\n readonly timeMs: number;\n /** Only for a finished hydration. */\n readonly rowCount?: number | undefined;\n /** Only for a finished hydration. */\n readonly rowsRead?: number | undefined;\n readonly planWarnings?: readonly PlanWarning[] | undefined;\n};\n\nexport type AdvanceRecord = {\n /** The time spent processing the pushes to the query. */\n readonly timeMs: number;\n /** The number of changes pushed to the query. */\n readonly changes: number;\n /** Whether the advancement timed out and reset the pipelines. */\n readonly timedOut: boolean;\n};\n\n/** Why an advancement stopped pushing to a query and hydrated it again. */\nexport type AbandonReason = 'estimate' | 'read' | 'cutoff';\n\nexport type QueryStatsOptions = {\n /**\n * The most query shapes tracked per interval. Work on shapes beyond this is\n * accounted to the interval's totals only, which bounds memory when the\n * shape space is unbounded (e.g. dynamically built queries).\n */\n readonly maxShapes?: number | undefined;\n\n /**\n * The most query shapes logged per interval, those that took the most\n * time. The rest are included in the interval's totals.\n */\n readonly maxReported?: number | undefined;\n\n readonly now?: (() => number) | undefined;\n\n /** Returns the index uses since the last call. See `recordIndexUsage()`. */\n readonly takeIndexUsage?: (() => Map<string, number>) | undefined;\n};\n\nclass Distribution {\n count = 0;\n sumMs = 0;\n minMs = Infinity;\n maxMs = 0;\n\n add(ms: number) {\n this.count++;\n this.sumMs += ms;\n this.minMs = Math.min(this.minMs, ms);\n this.maxMs = Math.max(this.maxMs, ms);\n }\n\n toJSON() {\n return {\n count: this.count,\n sumMs: round(this.sumMs),\n minMs: round(this.count === 0 ? 0 : this.minMs),\n maxMs: round(this.maxMs),\n };\n }\n}\n\nclass Totals {\n readonly hydrations = new Distribution();\n hydrationsAborted = 0;\n hydrationsFailed = 0;\n hydrationRowCount = 0;\n hydrationRowsRead = 0;\n readonly advances = new Distribution();\n advanceChanges = 0;\n advanceTimeouts = 0;\n /**\n * The advancements that hydrated the query again instead of pushing (all\n * of) their changes to it, by why. The hydrations are in `hydrations`.\n */\n readonly advancesAbandoned: Record<AbandonReason, number> = {\n estimate: 0,\n read: 0,\n cutoff: 0,\n };\n\n get timeMs() {\n return this.hydrations.sumMs + this.advances.sumMs;\n }\n\n addHydration({outcome, timeMs, rowCount, rowsRead}: HydrationRecord) {\n this.hydrations.add(timeMs);\n if (outcome === 'aborted') {\n this.hydrationsAborted++;\n } else if (outcome === 'failed') {\n this.hydrationsFailed++;\n }\n this.hydrationRowCount += rowCount ?? 0;\n this.hydrationRowsRead += rowsRead ?? 0;\n }\n\n addAdvance({timeMs, changes, timedOut}: AdvanceRecord) {\n this.advances.add(timeMs);\n this.advanceChanges += changes;\n if (timedOut) {\n this.advanceTimeouts++;\n }\n }\n\n toJSON() {\n return {\n timeMs: round(this.timeMs),\n hydrations: this.hydrations.toJSON(),\n hydrationsAborted: this.hydrationsAborted,\n hydrationsFailed: this.hydrationsFailed,\n hydrationRowCount: this.hydrationRowCount,\n hydrationRowsRead: this.hydrationRowsRead,\n advances: this.advances.toJSON(),\n advanceChanges: this.advanceChanges,\n advanceTimeouts: this.advanceTimeouts,\n // Only with cost-based sync, without which there are none.\n ...(Object.values(this.advancesAbandoned).some(n => n > 0) && {\n advancesAbandoned: {...this.advancesAbandoned},\n }),\n };\n }\n}\n\nclass ShapeStats extends Totals {\n readonly query: QueryIdentity;\n planWarnings: readonly PlanWarning[] | undefined;\n\n constructor(query: QueryIdentity) {\n super();\n this.query = query;\n }\n}\n\n/** A fixed-size linear-counting sketch for shapes beyond the tracking limit. */\nclass OverflowShapeCounter {\n readonly #buckets = new Uint8Array(4096);\n #occupied = 0;\n\n add(key: string): void {\n // FNV-1a gives a stable bucket without retaining the key.\n let hash = 0x811c9dc5;\n for (let i = 0; i < key.length; i++) {\n hash = Math.imul(hash ^ key.charCodeAt(i), 0x01000193);\n }\n const bucket = (hash >>> 0) % this.#buckets.length;\n if (this.#buckets[bucket] === 0) {\n this.#buckets[bucket] = 1;\n this.#occupied++;\n }\n }\n\n get estimatedCount(): number {\n const empty = this.#buckets.length - this.#occupied;\n return empty === 0\n ? this.#buckets.length\n : Math.round(\n -this.#buckets.length * Math.log(empty / this.#buckets.length),\n );\n }\n}\n\n/**\n * Aggregates the work done for each query shape (the query with its values\n * redacted, see {@link QueryShape}) on a worker, and periodically logs it,\n * like `pg_stat_statements` does for Postgres.\n *\n * Unlike the slow query logs, which are throttled and only written above a\n * threshold, every hydration and advancement is counted, so the logs can be\n * summed over any time range and across workers to rank queries by the time\n * they take.\n *\n * Each {@link flush} logs the interval since the previous one:\n *\n * - a `query-stats` event per query shape, for the {@link\n * QueryStatsOptions.maxReported} shapes that took the most time, and\n * - a `query-stats-summary` event with the totals of all shapes, including\n * those not reported individually, and the `indexUses`: how often the\n * process read each index of the replica, like `pg_stat_user_indexes`. An\n * index that is never read is absent. The workers of a single-process\n * zero-cache share these counts, which the first of them to flush reports.\n *\n * The events carry counts, sums, minimums and maximums, which compose across\n * intervals and workers.\n */\nexport class QueryStats {\n readonly #maxShapes: number;\n readonly #maxReported: number;\n readonly #now: () => number;\n readonly #takeIndexUsage: () => Map<string, number>;\n\n #shapes = new Map<string, ShapeStats>();\n #totals = new Totals();\n #overflowShapes = new OverflowShapeCounter();\n #intervalStart: number;\n\n constructor({\n maxShapes = 1000,\n maxReported = 100,\n now = Date.now,\n takeIndexUsage: takeIndexUsageFn = takeIndexUsage,\n }: QueryStatsOptions = {}) {\n assert(maxShapes > 0, 'maxShapes must be positive');\n assert(maxReported > 0, 'maxReported must be positive');\n this.#maxShapes = maxShapes;\n this.#maxReported = maxReported;\n this.#now = now;\n this.#takeIndexUsage = takeIndexUsageFn;\n this.#intervalStart = now();\n }\n\n recordHydration(query: QueryIdentity, record: HydrationRecord): void {\n this.#totals.addHydration(record);\n const stats = this.#stats(query);\n if (stats) {\n stats.addHydration(record);\n if (record.planWarnings !== undefined) {\n stats.planWarnings = record.planWarnings;\n }\n }\n }\n\n recordAdvance(query: QueryIdentity, record: AdvanceRecord): void {\n this.#totals.addAdvance(record);\n this.#stats(query)?.addAdvance(record);\n }\n\n /**\n * Records that an advancement abandoned the query's pipeline, to hydrate\n * the query again.\n */\n recordAbandoned(query: QueryIdentity, reason: AbandonReason): void {\n this.#totals.advancesAbandoned[reason]++;\n const stats = this.#stats(query);\n if (stats) {\n stats.advancesAbandoned[reason]++;\n }\n }\n\n #stats(query: QueryIdentity): ShapeStats | undefined {\n const key = `${query.queryName ?? ''}:${query.shape.hash}`;\n let stats = this.#shapes.get(key);\n if (stats === undefined) {\n if (this.#shapes.size >= this.#maxShapes) {\n this.#overflowShapes.add(key);\n return undefined;\n }\n stats = new ShapeStats({queryName: query.queryName, shape: query.shape});\n this.#shapes.set(key, stats);\n }\n return stats;\n }\n\n /**\n * Logs the stats of the interval since the previous flush, and starts a\n * new interval. Nothing is logged for an interval without any work.\n */\n flush(lc: LogContext): void {\n const now = this.#now();\n const intervalMs = now - this.#intervalStart;\n const shapes = this.#shapes;\n const totals = this.#totals;\n const overflowShapes = this.#overflowShapes.estimatedCount;\n const indexUses = this.#takeIndexUsage();\n this.#shapes = new Map();\n this.#totals = new Totals();\n this.#overflowShapes = new OverflowShapeCounter();\n this.#intervalStart = now;\n\n if (shapes.size === 0 && overflowShapes === 0 && indexUses.size === 0) {\n return;\n }\n const reported = [...shapes.values()]\n .toSorted((a, b) => b.timeMs - a.timeMs)\n .slice(0, this.#maxReported);\n for (const stats of reported) {\n const {queryName, shape} = stats.query;\n lc.info?.('query stats', {\n zeroEvent: 'query-stats',\n intervalMs,\n ...(queryName !== undefined && {queryName}),\n queryShape: shape.hash,\n ...stats.toJSON(),\n ...(stats.planWarnings !== undefined &&\n stats.planWarnings.length > 0 && {planWarnings: stats.planWarnings}),\n zql: shape.zql,\n });\n }\n lc.info?.('query stats summary', {\n zeroEvent: 'query-stats-summary',\n intervalMs,\n shapes: shapes.size + overflowShapes,\n shapesReported: reported.length,\n ...totals.toJSON(),\n ...(indexUses.size > 0 && {indexUses: Object.fromEntries(indexUses)}),\n });\n }\n\n /**\n * Flushes every `intervalMs` until the returned function is called, which\n * flushes one last time.\n */\n start(lc: LogContext, intervalMs: number): () => void {\n const timer = setInterval(() => this.flush(lc), intervalMs);\n timer.unref?.();\n return () => {\n clearInterval(timer);\n this.flush(lc);\n };\n }\n}\n\nfunction round(ms: number) {\n return Math.round(ms * 100) / 100;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"row-record-cache.js","names":["#lc","#db","#schema","#cvrID","#failService","#deferredRowFlushThreshold","#setTimeout","#pending","#cvrFlushTime","#cvrRowsFlushed","#cvrFlushes","#ensureLoaded","#cache","#cvr","#pendingRowsVersion","#flushing","#flush","#flushedRowsVersion","#recordAsyncFlushStats"],"sources":["../../../../../../zero-cache/src/services/view-syncer/row-record-cache.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {type Resolver, resolver} from '@rocicorp/resolver';\nimport type {PendingQuery, Row} from 'postgres';\nimport {startAsyncSpan} from '../../../../otel/src/span.ts';\nimport {CustomKeyMap} from '../../../../shared/src/custom-key-map.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../../types/pg.ts';\nimport {rowIDString} from '../../types/row-key.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport {checkVersion, type CVRFlushStats} from './cvr-store.ts';\nimport type {CVRSnapshot} from './cvr.ts';\nimport {\n rowRecordToRowsRow,\n type RowsRow,\n rowsRowToRowRecord,\n} from './schema/cvr.ts';\nimport {\n cmpVersions,\n type CVRVersion,\n type NullableCVRVersion,\n type RowID,\n type RowRecord,\n versionString,\n versionToNullableCookie,\n} from './schema/types.ts';\nimport {tracer} from './tracer.ts';\n\nconst FLUSH_TYPE_ATTRIBUTE = 'flush.type';\nconst RESULT_ATTRIBUTE = 'result';\nconst ERROR_KIND_ATTRIBUTE = 'error.kind';\n\n/**\n * The RowRecordCache is an in-memory cache of the `cvr.rows` tables that\n * operates as both a write-through and write-back cache.\n *\n * For \"small\" CVR updates (i.e. zero or small numbers of rows) the\n * RowRecordCache operates as write-through, executing commits in\n * {@link executeRowUpdates()} before they are {@link apply}-ed to the\n * in-memory state.\n *\n * For \"large\" CVR updates (i.e. with many rows), the cache switches to a\n * write-back mode of operation, in which {@link executeRowUpdates()} is a\n * no-op, and {@link apply()} initiates a background task to flush the pending\n * row changes to the store. This allows the client poke to be completed and\n * committed on the client without waiting for the heavyweight operation of\n * committing the row records to the CVR store.\n *\n * Note that when the cache is in write-back mode, all updates become\n * write-back (i.e. asynchronously flushed) until the pending update queue is\n * fully flushed. This is required because updates must be applied in version\n * order. As with all pending work systems in zero-cache, multiple pending\n * updates are coalesced to reduce buildup of work.\n *\n * ### High level consistency\n *\n * Note that the above caching scheme only applies to the row data in `cvr.rows`\n * and corresponding `cvr.rowsVersion` tables. CVR metadata and query\n * information, on the other hand, are always committed before completing the\n * client poke. In this manner, the difference between the `version` column in\n * `cvr.instances` and the analogous column in `cvr.rowsVersion` determines\n * whether the data in the store is consistent, or whether it is awaiting a\n * pending update.\n *\n * The logic in {@link CVRStore#load()} takes this into account by loading both\n * the `cvr.instances` version and the `cvr.rowsVersion` version and checking\n * if they are in sync, waiting for a configurable delay until they are.\n *\n * ### Eventual conversion\n *\n * In the event of a continual stream of mutations (e.g. an animation-style\n * app), it is conceivable that the row record data be continually behind\n * the CVR metadata. In order to effect eventual convergence, a new view-syncer\n * signals the current view-syncer to stop updating by writing new `owner`\n * information to the `cvr.instances` row. This effectively stops the mutation\n * processing (in {@link CVRStore.#checkVersionAndOwnership}) so that the row\n * data can eventually catch up, allowing the new view-syncer to take over.\n *\n * Of course, there is the pathological situation in which a view-syncer\n * process crashes before the pending row updates are flushed. In this case,\n * the wait timeout will elapse and the CVR considered invalid.\n */\nexport class RowRecordCache {\n // The state in the #cache is always in sync with the CVR metadata\n // (i.e. cvr.instances). It may contain information that has not yet\n // been flushed to cvr.rows.\n #cache: Promise<CustomKeyMap<RowID, RowRecord>> | undefined;\n readonly #lc: LogContext;\n readonly #db: PostgresDB;\n readonly #schema: string;\n readonly #cvrID: string;\n readonly #failService: (e: unknown) => void;\n readonly #deferredRowFlushThreshold: number;\n readonly #setTimeout: typeof setTimeout;\n\n // Write-back cache state.\n readonly #pending = new CustomKeyMap<RowID, RowRecord | null>(rowIDString);\n #pendingRowsVersion: CVRVersion | null = null;\n #flushedRowsVersion: CVRVersion | null = null;\n #flushing: Resolver<void> | null = null;\n\n readonly #cvrFlushTime = getOrCreateLatencyHistogram(\n 'sync',\n 'cvr.flush-time',\n 'Time to flush a CVR transaction. This includes both synchronous ' +\n 'and asynchronous flushes, distinguished by the flush.type attribute.',\n );\n readonly #cvrRowsFlushed = getOrCreateCounter(\n 'sync',\n 'cvr.rows-flushed',\n 'Number of (changed) rows flushed to a CVR',\n );\n readonly #cvrFlushes = getOrCreateCounter(\n 'sync',\n 'cvr.flush_attempts',\n 'CVR flush attempts, labeled by result and flush.type.',\n );\n\n constructor(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n cvrID: string,\n failService: (e: unknown) => void,\n deferredRowFlushThreshold = 100,\n setTimeoutFn = setTimeout,\n ) {\n this.#lc = lc;\n this.#db = db;\n this.#schema = cvrSchema(shard);\n this.#cvrID = cvrID;\n this.#failService = failService;\n this.#deferredRowFlushThreshold = deferredRowFlushThreshold;\n this.#setTimeout = setTimeoutFn;\n }\n\n recordSyncFlushStats(stats: CVRFlushStats, elapsedMs: number) {\n this.#cvrFlushTime.recordMs(elapsedMs, {\n [FLUSH_TYPE_ATTRIBUTE]: 'sync',\n });\n if (stats.rowsDeferred === 0) {\n this.#cvrRowsFlushed.add(stats.rows);\n }\n }\n\n #recordAsyncFlushStats(rows: number, elapsedMs: number) {\n this.#cvrFlushTime.recordMs(elapsedMs, {\n [FLUSH_TYPE_ATTRIBUTE]: 'async',\n });\n this.#cvrRowsFlushed.add(rows);\n }\n\n #cvr(table: string) {\n return this.#db(`${this.#schema}.${table}`);\n }\n\n async #ensureLoaded(): Promise<CustomKeyMap<RowID, RowRecord>> {\n if (this.#cache) {\n return this.#cache;\n }\n const start = Date.now();\n const r = resolver<CustomKeyMap<RowID, RowRecord>>();\n r.promise.catch(() => {});\n // Set this.#cache immediately (before await) so that only one db\n // query is made even if there are multiple callers.\n this.#cache = r.promise;\n try {\n const cache = await startAsyncSpan(\n tracer,\n 'RowRecordCache.load',\n async span => {\n const cache: CustomKeyMap<RowID, RowRecord> = new CustomKeyMap(\n rowIDString,\n );\n for await (const rows of this.#db<RowsRow[]>`\n SELECT\n \"clientGroupID\",\n \"schema\",\n \"table\",\n \"rowKey\",\n \"rowVersion\",\n \"patchVersion\",\n \"refCounts\"\n FROM ${this.#cvr(`rows`)}\n WHERE \"clientGroupID\" = ${this.#cvrID} AND \"refCounts\" IS NOT NULL`\n // TODO(arv): Arbitrary page size\n .cursor(5000)) {\n for (const row of rows) {\n const rowRecord = rowsRowToRowRecord(row);\n cache.set(rowRecord.id, rowRecord);\n }\n }\n span.setAttribute('rows', cache.size);\n return cache;\n },\n );\n this.#lc.info?.(\n `Loaded ${cache.size} row records in ${Date.now() - start} ms`,\n );\n r.resolve(cache);\n return this.#cache;\n } catch (e) {\n r.reject(e); // Make sure the error is reflected in the cached promise\n throw e;\n }\n }\n\n getRowRecords(): Promise<ReadonlyMap<RowID, RowRecord>> {\n return this.#ensureLoaded();\n }\n\n /**\n * Applies the `rowRecords` corresponding to the `rowsVersion`\n * to the cache, indicating whether the corresponding updates\n * (generated by {@link executeRowUpdates}) were `flushed`.\n *\n * If `flushed` is false, the RowRecordCache will flush the records\n * asynchronously.\n *\n * Note that `apply()` indicates that the CVR metadata associated with\n * the `rowRecords` was successfully committed, which essentially means\n * that this process has the unconditional right (and responsibility) of\n * following up with a flush of the `rowRecords`. In particular, the\n * commit of row records are not conditioned on the version or ownership\n * columns of the `cvr.instances` row.\n */\n async apply(\n rowRecords: Map<RowID, RowRecord | null>,\n rowsVersion: CVRVersion,\n flushed: boolean,\n ): Promise<number> {\n const cache = await this.#ensureLoaded();\n for (const [id, row] of rowRecords.entries()) {\n if (row === null || row.refCounts === null) {\n cache.delete(id);\n } else {\n cache.set(id, row);\n }\n if (!flushed) {\n this.#pending.set(id, row);\n }\n }\n this.#pendingRowsVersion = rowsVersion;\n // Initiate a flush if not already flushing.\n if (!flushed && this.#flushing === null) {\n this.#flushing = resolver();\n // The #flush() method handles propagating errors to #failService.\n // Attach a rejection handler to this promise to avoid unhandled\n // rejections.\n this.#flushing.promise.catch(() => {});\n this.#setTimeout(() => this.#flush(), 0);\n }\n return cache.size;\n }\n\n async #flush() {\n const flushing = must(this.#flushing);\n try {\n while (this.#pendingRowsVersion !== this.#flushedRowsVersion) {\n const start = performance.now();\n\n const {rows, rowsVersion} = await runTx(\n this.#db,\n tx => {\n // Note: This code block is synchronous, guaranteeing that the\n // #pendingRowsVersion is consistent with the #pending rows.\n const rows = this.#pending.size;\n const rowsVersion = must(this.#pendingRowsVersion);\n // Awaiting all of the individual statements incurs too much\n // overhead. Instead, just catch and log exception(s); the outer\n // transaction will properly fail.\n void Promise.all(\n this.executeRowUpdates(tx, rowsVersion, this.#pending, 'force'),\n ).catch(e => this.#lc.error?.(`error flushing cvr rows`, e));\n\n this.#pending.clear();\n return {rows, rowsVersion};\n },\n {mode: Mode.READ_COMMITTED},\n );\n const elapsed = performance.now() - start;\n this.#lc.info?.(\n `flushed ${rows} rows@${versionString(rowsVersion)} (${elapsed} ms)`,\n );\n this.#recordAsyncFlushStats(rows, elapsed);\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'success',\n [FLUSH_TYPE_ATTRIBUTE]: 'async',\n });\n this.#flushedRowsVersion = rowsVersion;\n // Note: apply() may have called while the transaction was committing,\n // which will result in looping to commit the next #pendingRowsVersion.\n }\n this.#lc.info?.(\n `up to date rows@${versionToNullableCookie(this.#flushedRowsVersion)}`,\n );\n flushing.resolve();\n this.#flushing = null;\n } catch (e) {\n this.#lc.info?.(`row record flush failed`, e);\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'error',\n [FLUSH_TYPE_ATTRIBUTE]: 'async',\n [ERROR_KIND_ATTRIBUTE]: 'error',\n });\n flushing.reject(e);\n this.#failService(e);\n }\n }\n\n hasPendingUpdates() {\n return this.#flushing !== null;\n }\n\n /**\n * Returns a promise that resolves when all outstanding row-records\n * have been committed.\n */\n flushed(lc: LogContext): Promise<void> {\n if (this.#flushing) {\n lc.debug?.('awaiting pending row flush');\n return this.#flushing.promise;\n }\n return promiseVoid;\n }\n\n clear() {\n // Note: Only the #cache is cleared. #pending updates, on the other hand,\n // comprise canonical (i.e. already flushed) data and must be flushed\n // even if the snapshot of the present state (the #cache) is cleared.\n this.#cache = undefined;\n }\n\n async *catchupRowPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n excludeQueryHashes: string[] = [],\n ): AsyncGenerator<RowsRow[], void, undefined> {\n if (cmpVersions(afterVersion, upToCVR.version) >= 0) {\n return;\n }\n\n const startMs = Date.now();\n const start = afterVersion ? versionString(afterVersion) : '';\n const end = versionString(upToCVR.version);\n lc.debug?.(`scanning row patches for clients from ${start}`);\n\n // Before accessing the CVR db, pending row records must be flushed.\n // Note that because catchupRowPatches() is called from within the\n // view syncer lock, this flush is guaranteed to complete since no\n // new CVR updates can happen while the lock is held.\n await this.flushed(lc);\n const flushMs = Date.now() - startMs;\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(this.#db);\n try {\n // Verify that we are reading the right version of the CVR.\n await reader.processReadTask(tx =>\n checkVersion(tx, this.#schema, this.#cvrID, current),\n );\n\n const {query} = await reader.processReadTask(tx => {\n const query =\n excludeQueryHashes.length === 0\n ? tx<RowsRow[]>`SELECT\n \"clientGroupID\",\n \"schema\",\n \"table\",\n \"rowKey\",\n \"rowVersion\",\n \"patchVersion\",\n \"refCounts\"\n FROM ${this.#cvr('rows')}\n WHERE \"clientGroupID\" = ${this.#cvrID}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`\n : // Exclude rows that were already sent as part of query hydration.\n tx<RowsRow[]>`SELECT\n \"clientGroupID\",\n \"schema\",\n \"table\",\n \"rowKey\",\n \"rowVersion\",\n \"patchVersion\",\n \"refCounts\"\n FROM ${this.#cvr('rows')}\n WHERE \"clientGroupID\" = ${this.#cvrID}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}\n AND (\"refCounts\" IS NULL OR NOT \"refCounts\" ?| ${excludeQueryHashes})`;\n return {query};\n });\n\n yield* query.cursor(10000);\n } finally {\n reader.setDone();\n }\n\n const totalMs = Date.now() - startMs;\n lc.info?.(\n `finished row catchup (flush: ${flushMs} ms, total: ${totalMs} ms)`,\n );\n }\n\n executeRowUpdates(\n tx: PostgresTransaction,\n version: CVRVersion,\n rowUpdates: Map<RowID, RowRecord | null>,\n mode: 'allow-defer' | 'force',\n lc = this.#lc,\n ): PendingQuery<Row[]>[] {\n if (\n mode === 'allow-defer' &&\n // defer if pending rows are being flushed\n (this.#flushing !== null ||\n // or if the new batch is above the limit.\n rowUpdates.size > this.#deferredRowFlushThreshold)\n ) {\n return [];\n }\n const rowsVersion = {\n clientGroupID: this.#cvrID,\n version: versionString(version),\n };\n const pending: PendingQuery<Row[]>[] = [\n tx`INSERT INTO ${this.#cvr('rowsVersion')} ${tx(rowsVersion)}\n ON CONFLICT (\"clientGroupID\") \n DO UPDATE SET ${tx(rowsVersion)}`,\n ];\n\n const rowRecordRows: RowsRow[] = [];\n for (const [id, row] of rowUpdates.entries()) {\n if (row === null) {\n pending.push(\n tx`\n DELETE FROM ${this.#cvr('rows')}\n WHERE \"clientGroupID\" = ${this.#cvrID}\n AND \"schema\" = ${id.schema}\n AND \"table\" = ${id.table}\n AND \"rowKey\" = ${id.rowKey}\n `,\n );\n } else {\n rowRecordRows.push(rowRecordToRowsRow(this.#cvrID, row));\n }\n }\n if (rowRecordRows.length) {\n pending.push(\n tx`\n INSERT INTO ${this.#cvr('rows')}(\n \"clientGroupID\", \"schema\", \"table\", \"rowKey\", \"rowVersion\", \"patchVersion\", \"refCounts\"\n ) SELECT\n \"clientGroupID\", \"schema\", \"table\", \"rowKey\", \"rowVersion\", \"patchVersion\", \"refCounts\"\n FROM json_to_recordset(${rowRecordRows}) AS x(\n \"clientGroupID\" TEXT,\n \"schema\" TEXT,\n \"table\" TEXT,\n \"rowKey\" JSONB,\n \"rowVersion\" TEXT,\n \"patchVersion\" TEXT,\n \"refCounts\" JSONB\n ) ON CONFLICT (\"clientGroupID\", \"schema\", \"table\", \"rowKey\")\n DO UPDATE SET \"rowVersion\" = excluded.\"rowVersion\",\n \"patchVersion\" = excluded.\"patchVersion\",\n \"refCounts\" = excluded.\"refCounts\"\n `,\n );\n lc.info?.(\n `flushing ${rowUpdates.size} rows (${rowRecordRows.length} inserts, ${\n rowUpdates.size - rowRecordRows.length\n } deletes)`,\n );\n }\n return pending;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmCA,IAAM,uBAAuB;AAC7B,IAAM,mBAAmB;AACzB,IAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoD7B,IAAa,iBAAb,MAA4B;CAI1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA,WAAoB,IAAI,aAAsC,WAAW;CACzE,sBAAyC;CACzC,sBAAyC;CACzC,YAAmC;CAEnC,gBAAyB,4BACvB,QACA,kBACA,sIAEF;CACA,kBAA2B,mBACzB,QACA,oBACA,2CACF;CACA,cAAuB,mBACrB,QACA,sBACA,uDACF;CAEA,YACE,IACA,IACA,OACA,OACA,aACA,4BAA4B,KAC5B,eAAe,YACf;EACA,KAAKA,MAAM;EACX,KAAKC,MAAM;EACX,KAAKC,UAAU,UAAU,KAAK;EAC9B,KAAKC,SAAS;EACd,KAAKC,eAAe;EACpB,KAAKC,6BAA6B;EAClC,KAAKC,cAAc;CACrB;CAEA,qBAAqB,OAAsB,WAAmB;EAC5D,KAAKE,cAAc,SAAS,WAAW,GACpC,uBAAuB,OAC1B,CAAC;EACD,IAAI,MAAM,iBAAiB,GACzB,KAAKC,gBAAgB,IAAI,MAAM,IAAI;CAEvC;CAEA,uBAAuB,MAAc,WAAmB;EACtD,KAAKD,cAAc,SAAS,WAAW,GACpC,uBAAuB,QAC1B,CAAC;EACD,KAAKC,gBAAgB,IAAI,IAAI;CAC/B;CAEA,KAAK,OAAe;EAClB,OAAO,KAAKR,IAAI,GAAG,KAAKC,QAAQ,GAAG,OAAO;CAC5C;CAEA,MAAMS,gBAAyD;EAC7D,IAAI,KAAKC,QACP,OAAO,KAAKA;EAEd,MAAM,QAAQ,KAAK,IAAI;EACvB,MAAM,IAAI,SAAyC;EACnD,EAAE,QAAQ,YAAY,CAAC,CAAC;EAGxB,KAAKA,SAAS,EAAE;EAChB,IAAI;GACF,MAAM,QAAQ,MAAM,eAClB,QACA,uBACA,OAAM,SAAQ;IACZ,MAAM,QAAwC,IAAI,aAChD,WACF;IACA,WAAW,MAAM,QAAQ,KAAKX,GAAc;;;;;;;;;mBASnC,KAAKY,KAAK,MAAM,EAAE;wCACG,KAAKV,OAAO,8BAEvC,OAAO,GAAI,GACZ,KAAK,MAAM,OAAO,MAAM;KACtB,MAAM,YAAY,mBAAmB,GAAG;KACxC,MAAM,IAAI,UAAU,IAAI,SAAS;IACnC;IAEF,KAAK,aAAa,QAAQ,MAAM,IAAI;IACpC,OAAO;GACT,CACF;GACA,KAAKH,IAAI,OACP,UAAU,MAAM,KAAK,kBAAkB,KAAK,IAAI,IAAI,MAAM,IAC5D;GACA,EAAE,QAAQ,KAAK;GACf,OAAO,KAAKY;EACd,SAAS,GAAG;GACV,EAAE,OAAO,CAAC;GACV,MAAM;EACR;CACF;CAEA,gBAAwD;EACtD,OAAO,KAAKD,cAAc;CAC5B;;;;;;;;;;;;;;;;CAiBA,MAAM,MACJ,YACA,aACA,SACiB;EACjB,MAAM,QAAQ,MAAM,KAAKA,cAAc;EACvC,KAAK,MAAM,CAAC,IAAI,QAAQ,WAAW,QAAQ,GAAG;GAC5C,IAAI,QAAQ,QAAQ,IAAI,cAAc,MACpC,MAAM,OAAO,EAAE;QAEf,MAAM,IAAI,IAAI,GAAG;GAEnB,IAAI,CAAC,SACH,KAAKJ,SAAS,IAAI,IAAI,GAAG;EAE7B;EACA,KAAKO,sBAAsB;EAE3B,IAAI,CAAC,WAAW,KAAKC,cAAc,MAAM;GACvC,KAAKA,YAAY,SAAS;GAI1B,KAAKA,UAAU,QAAQ,YAAY,CAAC,CAAC;GACrC,KAAKT,kBAAkB,KAAKU,OAAO,GAAG,CAAC;EACzC;EACA,OAAO,MAAM;CACf;CAEA,MAAMA,SAAS;EACb,MAAM,WAAW,KAAK,KAAKD,SAAS;EACpC,IAAI;GACF,OAAO,KAAKD,wBAAwB,KAAKG,qBAAqB;IAC5D,MAAM,QAAQ,YAAY,IAAI;IAE9B,MAAM,EAAC,MAAM,gBAAe,MAAM,MAChC,KAAKhB,MACL,OAAM;KAGJ,MAAM,OAAO,KAAKM,SAAS;KAC3B,MAAM,cAAc,KAAK,KAAKO,mBAAmB;KAIjD,QAAa,IACX,KAAK,kBAAkB,IAAI,aAAa,KAAKP,UAAU,OAAO,CAChE,EAAE,OAAM,MAAK,KAAKP,IAAI,QAAQ,2BAA2B,CAAC,CAAC;KAE3D,KAAKO,SAAS,MAAM;KACpB,OAAO;MAAC;MAAM;KAAW;IAC3B,GACA,EAAC,MAAM,eAAmB,CAC5B;IACA,MAAM,UAAU,YAAY,IAAI,IAAI;IACpC,KAAKP,IAAI,OACP,WAAW,KAAK,QAAQ,cAAc,WAAW,EAAE,IAAI,QAAQ,KACjE;IACA,KAAKkB,uBAAuB,MAAM,OAAO;IACzC,KAAKR,YAAY,IAAI,GAAG;MACrB,mBAAmB;MACnB,uBAAuB;IAC1B,CAAC;IACD,KAAKO,sBAAsB;GAG7B;GACA,KAAKjB,IAAI,OACP,mBAAmB,wBAAwB,KAAKiB,mBAAmB,GACrE;GACA,SAAS,QAAQ;GACjB,KAAKF,YAAY;EACnB,SAAS,GAAG;GACV,KAAKf,IAAI,OAAO,2BAA2B,CAAC;GAC5C,KAAKU,YAAY,IAAI,GAAG;KACrB,mBAAmB;KACnB,uBAAuB;KACvB,uBAAuB;GAC1B,CAAC;GACD,SAAS,OAAO,CAAC;GACjB,KAAKN,aAAa,CAAC;EACrB;CACF;CAEA,oBAAoB;EAClB,OAAO,KAAKW,cAAc;CAC5B;;;;;CAMA,QAAQ,IAA+B;EACrC,IAAI,KAAKA,WAAW;GAClB,GAAG,QAAQ,4BAA4B;GACvC,OAAO,KAAKA,UAAU;EACxB;EACA,OAAO;CACT;CAEA,QAAQ;EAIN,KAAKH,SAAS,KAAA;CAChB;CAEA,OAAO,kBACL,IACA,cACA,SACA,SACA,qBAA+B,CAAC,GACY;EAC5C,IAAI,YAAY,cAAc,QAAQ,OAAO,KAAK,GAChD;EAGF,MAAM,UAAU,KAAK,IAAI;EACzB,MAAM,QAAQ,eAAe,cAAc,YAAY,IAAI;EAC3D,MAAM,MAAM,cAAc,QAAQ,OAAO;EACzC,GAAG,QAAQ,yCAAyC,OAAO;EAM3D,MAAM,KAAK,QAAQ,EAAE;EACrB,MAAM,UAAU,KAAK,IAAI,IAAI;EAE7B,MAAM,SAAS,IAAI,gBAAgB,IAAI,EAAC,MAAM,SAAa,CAAC,EAAE,IAAI,KAAKX,GAAG;EAC1E,IAAI;GAEF,MAAM,OAAO,iBAAgB,OAC3B,aAAa,IAAI,KAAKC,SAAS,KAAKC,QAAQ,OAAO,CACrD;GAEA,MAAM,EAAC,UAAS,MAAM,OAAO,iBAAgB,OAAM;IA6BjD,OAAO,EAAC,OA3BN,mBAAmB,WAAW,IAC1B,EAAa;;;;;;;;mBAQR,KAAKU,KAAK,MAAM,EAAE;kCACH,KAAKV,OAAO;iCACb,MAAM;kCACL,QAEpB,EAAa;;;;;;;;mBAQR,KAAKU,KAAK,MAAM,EAAE;kCACH,KAAKV,OAAO;iCACb,MAAM;kCACL,IAAI;2DACqB,mBAAmB,GACzD;GACf,CAAC;GAED,OAAO,MAAM,OAAO,GAAK;EAC3B,UAAU;GACR,OAAO,QAAQ;EACjB;EAEA,MAAM,UAAU,KAAK,IAAI,IAAI;EAC7B,GAAG,OACD,gCAAgC,QAAQ,cAAc,QAAQ,KAChE;CACF;CAEA,kBACE,IACA,SACA,YACA,MACA,KAAK,KAAKH,KACa;EACvB,IACE,SAAS,kBAER,KAAKe,cAAc,QAElB,WAAW,OAAO,KAAKV,6BAEzB,OAAO,CAAC;EAEV,MAAM,cAAc;GAClB,eAAe,KAAKF;GACpB,SAAS,cAAc,OAAO;EAChC;EACA,MAAM,UAAiC,CACrC,EAAE,eAAe,KAAKU,KAAK,aAAa,EAAE,GAAG,GAAG,WAAW,EAAE;;2BAExC,GAAG,WAAW,GACrC;EAEA,MAAM,gBAA2B,CAAC;EAClC,KAAK,MAAM,CAAC,IAAI,QAAQ,WAAW,QAAQ,GACzC,IAAI,QAAQ,MACV,QAAQ,KACN,EAAE;wBACY,KAAKA,KAAK,MAAM,EAAE;sCACJ,KAAKV,OAAO;+BACnB,GAAG,OAAO;8BACX,GAAG,MAAM;+BACR,GAAG,OAAO;QAEjC;OAEA,cAAc,KAAK,mBAAmB,KAAKA,QAAQ,GAAG,CAAC;EAG3D,IAAI,cAAc,QAAQ;GACxB,QAAQ,KACN,EAAE;gBACM,KAAKU,KAAK,MAAM,EAAE;;;;6BAIL,cAAc;;;;;;;;;;;;KAarC;GACA,GAAG,OACD,YAAY,WAAW,KAAK,SAAS,cAAc,OAAO,YACxD,WAAW,OAAO,cAAc,OACjC,UACH;EACF;EACA,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"row-record-cache.js","names":["Mode.READ_COMMITTED","Mode.READONLY"],"sources":["../../../../../../zero-cache/src/services/view-syncer/row-record-cache.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {type Resolver, resolver} from '@rocicorp/resolver';\nimport type {PendingQuery, Row} from 'postgres';\nimport {startAsyncSpan} from '../../../../otel/src/span.ts';\nimport {CustomKeyMap} from '../../../../shared/src/custom-key-map.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../../types/pg.ts';\nimport {rowIDString} from '../../types/row-key.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport {checkVersion, type CVRFlushStats} from './cvr-store.ts';\nimport type {CVRSnapshot} from './cvr.ts';\nimport {\n rowRecordToRowsRow,\n type RowsRow,\n rowsRowToRowRecord,\n} from './schema/cvr.ts';\nimport {\n cmpVersions,\n type CVRVersion,\n type NullableCVRVersion,\n type RowID,\n type RowRecord,\n versionString,\n versionToNullableCookie,\n} from './schema/types.ts';\nimport {tracer} from './tracer.ts';\n\nconst FLUSH_TYPE_ATTRIBUTE = 'flush.type';\nconst RESULT_ATTRIBUTE = 'result';\nconst ERROR_KIND_ATTRIBUTE = 'error.kind';\n\n/**\n * The RowRecordCache is an in-memory cache of the `cvr.rows` tables that\n * operates as both a write-through and write-back cache.\n *\n * For \"small\" CVR updates (i.e. zero or small numbers of rows) the\n * RowRecordCache operates as write-through, executing commits in\n * {@link executeRowUpdates()} before they are {@link apply}-ed to the\n * in-memory state.\n *\n * For \"large\" CVR updates (i.e. with many rows), the cache switches to a\n * write-back mode of operation, in which {@link executeRowUpdates()} is a\n * no-op, and {@link apply()} initiates a background task to flush the pending\n * row changes to the store. This allows the client poke to be completed and\n * committed on the client without waiting for the heavyweight operation of\n * committing the row records to the CVR store.\n *\n * Note that when the cache is in write-back mode, all updates become\n * write-back (i.e. asynchronously flushed) until the pending update queue is\n * fully flushed. This is required because updates must be applied in version\n * order. As with all pending work systems in zero-cache, multiple pending\n * updates are coalesced to reduce buildup of work.\n *\n * ### High level consistency\n *\n * Note that the above caching scheme only applies to the row data in `cvr.rows`\n * and corresponding `cvr.rowsVersion` tables. CVR metadata and query\n * information, on the other hand, are always committed before completing the\n * client poke. In this manner, the difference between the `version` column in\n * `cvr.instances` and the analogous column in `cvr.rowsVersion` determines\n * whether the data in the store is consistent, or whether it is awaiting a\n * pending update.\n *\n * The logic in {@link CVRStore#load()} takes this into account by loading both\n * the `cvr.instances` version and the `cvr.rowsVersion` version and checking\n * if they are in sync, waiting for a configurable delay until they are.\n *\n * ### Eventual conversion\n *\n * In the event of a continual stream of mutations (e.g. an animation-style\n * app), it is conceivable that the row record data be continually behind\n * the CVR metadata. In order to effect eventual convergence, a new view-syncer\n * signals the current view-syncer to stop updating by writing new `owner`\n * information to the `cvr.instances` row. This effectively stops the mutation\n * processing (in {@link CVRStore.#checkVersionAndOwnership}) so that the row\n * data can eventually catch up, allowing the new view-syncer to take over.\n *\n * Of course, there is the pathological situation in which a view-syncer\n * process crashes before the pending row updates are flushed. In this case,\n * the wait timeout will elapse and the CVR considered invalid.\n */\nexport class RowRecordCache {\n // The state in the #cache is always in sync with the CVR metadata\n // (i.e. cvr.instances). It may contain information that has not yet\n // been flushed to cvr.rows.\n #cache: Promise<CustomKeyMap<RowID, RowRecord>> | undefined;\n readonly #lc: LogContext;\n readonly #db: PostgresDB;\n readonly #schema: string;\n readonly #cvrID: string;\n readonly #failService: (e: unknown) => void;\n readonly #deferredRowFlushThreshold: number;\n readonly #setTimeout: typeof setTimeout;\n\n // Write-back cache state.\n readonly #pending = new CustomKeyMap<RowID, RowRecord | null>(rowIDString);\n #pendingRowsVersion: CVRVersion | null = null;\n #flushedRowsVersion: CVRVersion | null = null;\n #flushing: Resolver<void> | null = null;\n\n readonly #cvrFlushTime = getOrCreateLatencyHistogram(\n 'sync',\n 'cvr.flush-time',\n 'Time to flush a CVR transaction. This includes both synchronous ' +\n 'and asynchronous flushes, distinguished by the flush.type attribute.',\n );\n readonly #cvrRowsFlushed = getOrCreateCounter(\n 'sync',\n 'cvr.rows-flushed',\n 'Number of (changed) rows flushed to a CVR',\n );\n readonly #cvrFlushes = getOrCreateCounter(\n 'sync',\n 'cvr.flush_attempts',\n 'CVR flush attempts, labeled by result and flush.type.',\n );\n\n constructor(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n cvrID: string,\n failService: (e: unknown) => void,\n deferredRowFlushThreshold = 100,\n setTimeoutFn = setTimeout,\n ) {\n this.#lc = lc;\n this.#db = db;\n this.#schema = cvrSchema(shard);\n this.#cvrID = cvrID;\n this.#failService = failService;\n this.#deferredRowFlushThreshold = deferredRowFlushThreshold;\n this.#setTimeout = setTimeoutFn;\n }\n\n recordSyncFlushStats(stats: CVRFlushStats, elapsedMs: number) {\n this.#cvrFlushTime.recordMs(elapsedMs, {\n [FLUSH_TYPE_ATTRIBUTE]: 'sync',\n });\n if (stats.rowsDeferred === 0) {\n this.#cvrRowsFlushed.add(stats.rows);\n }\n }\n\n #recordAsyncFlushStats(rows: number, elapsedMs: number) {\n this.#cvrFlushTime.recordMs(elapsedMs, {\n [FLUSH_TYPE_ATTRIBUTE]: 'async',\n });\n this.#cvrRowsFlushed.add(rows);\n }\n\n #cvr(table: string) {\n return this.#db(`${this.#schema}.${table}`);\n }\n\n async #ensureLoaded(): Promise<CustomKeyMap<RowID, RowRecord>> {\n if (this.#cache) {\n return this.#cache;\n }\n const start = Date.now();\n const r = resolver<CustomKeyMap<RowID, RowRecord>>();\n r.promise.catch(() => {});\n // Set this.#cache immediately (before await) so that only one db\n // query is made even if there are multiple callers.\n this.#cache = r.promise;\n try {\n const cache = await startAsyncSpan(\n tracer,\n 'RowRecordCache.load',\n async span => {\n const cache: CustomKeyMap<RowID, RowRecord> = new CustomKeyMap(\n rowIDString,\n );\n for await (const rows of this.#db<RowsRow[]>`\n SELECT\n \"clientGroupID\",\n \"schema\",\n \"table\",\n \"rowKey\",\n \"rowVersion\",\n \"patchVersion\",\n \"refCounts\"\n FROM ${this.#cvr(`rows`)}\n WHERE \"clientGroupID\" = ${this.#cvrID} AND \"refCounts\" IS NOT NULL`\n // TODO(arv): Arbitrary page size\n .cursor(5000)) {\n for (const row of rows) {\n const rowRecord = rowsRowToRowRecord(row);\n cache.set(rowRecord.id, rowRecord);\n }\n }\n span.setAttribute('rows', cache.size);\n return cache;\n },\n );\n this.#lc.info?.(\n `Loaded ${cache.size} row records in ${Date.now() - start} ms`,\n );\n r.resolve(cache);\n return this.#cache;\n } catch (e) {\n r.reject(e); // Make sure the error is reflected in the cached promise\n throw e;\n }\n }\n\n getRowRecords(): Promise<ReadonlyMap<RowID, RowRecord>> {\n return this.#ensureLoaded();\n }\n\n /**\n * Applies the `rowRecords` corresponding to the `rowsVersion`\n * to the cache, indicating whether the corresponding updates\n * (generated by {@link executeRowUpdates}) were `flushed`.\n *\n * If `flushed` is false, the RowRecordCache will flush the records\n * asynchronously.\n *\n * Note that `apply()` indicates that the CVR metadata associated with\n * the `rowRecords` was successfully committed, which essentially means\n * that this process has the unconditional right (and responsibility) of\n * following up with a flush of the `rowRecords`. In particular, the\n * commit of row records are not conditioned on the version or ownership\n * columns of the `cvr.instances` row.\n */\n async apply(\n rowRecords: Map<RowID, RowRecord | null>,\n rowsVersion: CVRVersion,\n flushed: boolean,\n ): Promise<number> {\n const cache = await this.#ensureLoaded();\n for (const [id, row] of rowRecords.entries()) {\n if (row === null || row.refCounts === null) {\n cache.delete(id);\n } else {\n cache.set(id, row);\n }\n if (!flushed) {\n this.#pending.set(id, row);\n }\n }\n this.#pendingRowsVersion = rowsVersion;\n // Initiate a flush if not already flushing.\n if (!flushed && this.#flushing === null) {\n this.#flushing = resolver();\n // The #flush() method handles propagating errors to #failService.\n // Attach a rejection handler to this promise to avoid unhandled\n // rejections.\n this.#flushing.promise.catch(() => {});\n this.#setTimeout(() => this.#flush(), 0);\n }\n return cache.size;\n }\n\n async #flush() {\n const flushing = must(this.#flushing);\n try {\n while (this.#pendingRowsVersion !== this.#flushedRowsVersion) {\n const start = performance.now();\n\n const {rows, rowsVersion} = await runTx(\n this.#db,\n tx => {\n // Note: This code block is synchronous, guaranteeing that the\n // #pendingRowsVersion is consistent with the #pending rows.\n const rows = this.#pending.size;\n const rowsVersion = must(this.#pendingRowsVersion);\n // Awaiting all of the individual statements incurs too much\n // overhead. Instead, just catch and log exception(s); the outer\n // transaction will properly fail.\n void Promise.all(\n this.executeRowUpdates(tx, rowsVersion, this.#pending, 'force'),\n ).catch(e => this.#lc.error?.(`error flushing cvr rows`, e));\n\n this.#pending.clear();\n return {rows, rowsVersion};\n },\n {mode: Mode.READ_COMMITTED},\n );\n const elapsed = performance.now() - start;\n this.#lc.info?.(\n `flushed ${rows} rows@${versionString(rowsVersion)} (${elapsed} ms)`,\n );\n this.#recordAsyncFlushStats(rows, elapsed);\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'success',\n [FLUSH_TYPE_ATTRIBUTE]: 'async',\n });\n this.#flushedRowsVersion = rowsVersion;\n // Note: apply() may have called while the transaction was committing,\n // which will result in looping to commit the next #pendingRowsVersion.\n }\n this.#lc.info?.(\n `up to date rows@${versionToNullableCookie(this.#flushedRowsVersion)}`,\n );\n flushing.resolve();\n this.#flushing = null;\n } catch (e) {\n this.#lc.info?.(`row record flush failed`, e);\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'error',\n [FLUSH_TYPE_ATTRIBUTE]: 'async',\n [ERROR_KIND_ATTRIBUTE]: 'error',\n });\n flushing.reject(e);\n this.#failService(e);\n }\n }\n\n hasPendingUpdates() {\n return this.#flushing !== null;\n }\n\n /**\n * Returns a promise that resolves when all outstanding row-records\n * have been committed.\n */\n flushed(lc: LogContext): Promise<void> {\n if (this.#flushing) {\n lc.debug?.('awaiting pending row flush');\n return this.#flushing.promise;\n }\n return promiseVoid;\n }\n\n clear() {\n // Note: Only the #cache is cleared. #pending updates, on the other hand,\n // comprise canonical (i.e. already flushed) data and must be flushed\n // even if the snapshot of the present state (the #cache) is cleared.\n this.#cache = undefined;\n }\n\n async *catchupRowPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n excludeQueryHashes: string[] = [],\n ): AsyncGenerator<RowsRow[], void, undefined> {\n if (cmpVersions(afterVersion, upToCVR.version) >= 0) {\n return;\n }\n\n const startMs = Date.now();\n const start = afterVersion ? versionString(afterVersion) : '';\n const end = versionString(upToCVR.version);\n lc.debug?.(`scanning row patches for clients from ${start}`);\n\n // Before accessing the CVR db, pending row records must be flushed.\n // Note that because catchupRowPatches() is called from within the\n // view syncer lock, this flush is guaranteed to complete since no\n // new CVR updates can happen while the lock is held.\n await this.flushed(lc);\n const flushMs = Date.now() - startMs;\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(this.#db);\n try {\n // Verify that we are reading the right version of the CVR.\n await reader.processReadTask(tx =>\n checkVersion(tx, this.#schema, this.#cvrID, current),\n );\n\n const {query} = await reader.processReadTask(tx => {\n const query =\n excludeQueryHashes.length === 0\n ? tx<RowsRow[]>`SELECT\n \"clientGroupID\",\n \"schema\",\n \"table\",\n \"rowKey\",\n \"rowVersion\",\n \"patchVersion\",\n \"refCounts\"\n FROM ${this.#cvr('rows')}\n WHERE \"clientGroupID\" = ${this.#cvrID}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`\n : // Exclude rows that were already sent as part of query hydration.\n tx<RowsRow[]>`SELECT\n \"clientGroupID\",\n \"schema\",\n \"table\",\n \"rowKey\",\n \"rowVersion\",\n \"patchVersion\",\n \"refCounts\"\n FROM ${this.#cvr('rows')}\n WHERE \"clientGroupID\" = ${this.#cvrID}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}\n AND (\"refCounts\" IS NULL OR NOT \"refCounts\" ?| ${excludeQueryHashes})`;\n return {query};\n });\n\n yield* query.cursor(10000);\n } finally {\n reader.setDone();\n }\n\n const totalMs = Date.now() - startMs;\n lc.info?.(\n `finished row catchup (flush: ${flushMs} ms, total: ${totalMs} ms)`,\n );\n }\n\n executeRowUpdates(\n tx: PostgresTransaction,\n version: CVRVersion,\n rowUpdates: Map<RowID, RowRecord | null>,\n mode: 'allow-defer' | 'force',\n lc = this.#lc,\n ): PendingQuery<Row[]>[] {\n if (\n mode === 'allow-defer' &&\n // defer if pending rows are being flushed\n (this.#flushing !== null ||\n // or if the new batch is above the limit.\n rowUpdates.size > this.#deferredRowFlushThreshold)\n ) {\n return [];\n }\n const rowsVersion = {\n clientGroupID: this.#cvrID,\n version: versionString(version),\n };\n const pending: PendingQuery<Row[]>[] = [\n tx`INSERT INTO ${this.#cvr('rowsVersion')} ${tx(rowsVersion)}\n ON CONFLICT (\"clientGroupID\") \n DO UPDATE SET ${tx(rowsVersion)}`,\n ];\n\n const rowRecordRows: RowsRow[] = [];\n for (const [id, row] of rowUpdates.entries()) {\n if (row === null) {\n pending.push(\n tx`\n DELETE FROM ${this.#cvr('rows')}\n WHERE \"clientGroupID\" = ${this.#cvrID}\n AND \"schema\" = ${id.schema}\n AND \"table\" = ${id.table}\n AND \"rowKey\" = ${id.rowKey}\n `,\n );\n } else {\n rowRecordRows.push(rowRecordToRowsRow(this.#cvrID, row));\n }\n }\n if (rowRecordRows.length) {\n pending.push(\n tx`\n INSERT INTO ${this.#cvr('rows')}(\n \"clientGroupID\", \"schema\", \"table\", \"rowKey\", \"rowVersion\", \"patchVersion\", \"refCounts\"\n ) SELECT\n \"clientGroupID\", \"schema\", \"table\", \"rowKey\", \"rowVersion\", \"patchVersion\", \"refCounts\"\n FROM json_to_recordset(${rowRecordRows}) AS x(\n \"clientGroupID\" TEXT,\n \"schema\" TEXT,\n \"table\" TEXT,\n \"rowKey\" JSONB,\n \"rowVersion\" TEXT,\n \"patchVersion\" TEXT,\n \"refCounts\" JSONB\n ) ON CONFLICT (\"clientGroupID\", \"schema\", \"table\", \"rowKey\")\n DO UPDATE SET \"rowVersion\" = excluded.\"rowVersion\",\n \"patchVersion\" = excluded.\"patchVersion\",\n \"refCounts\" = excluded.\"refCounts\"\n `,\n );\n lc.info?.(\n `flushing ${rowUpdates.size} rows (${rowRecordRows.length} inserts, ${\n rowUpdates.size - rowRecordRows.length\n } deletes)`,\n );\n }\n return pending;\n }\n}\n"],"mappings":"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"}
@@ -202,6 +202,6 @@ async function setupCVRTables(lc, db, shard) {
202
202
  await db.unsafe(createTables(shard));
203
203
  }
204
204
  //#endregion
205
- export { createRowsVersionTable, rowRecordToRowsRow, rowsRowToRowRecord, setupCVRTables };
205
+ export { createRowsVersionTable, rowRecordToRowsRow, rowsRowToRowID, rowsRowToRowRecord, setupCVRTables };
206
206
 
207
207
  //# sourceMappingURL=cvr.js.map
@@ -258,6 +258,6 @@ function queryRecordToQueryRow(clientGroupID, query, rowSetSignature) {
258
258
  }
259
259
  var maybeVersionString = (v) => v ? versionString(v) : null;
260
260
  //#endregion
261
- export { EMPTY_CVR_VERSION, cmpVersions, cookieToVersion, maxVersion, oneAfter, queryRecordToQueryRow, versionFromString, versionString, versionToCookie, versionToNullableCookie };
261
+ export { EMPTY_CVR_VERSION, baseQueryRecordSchema, clientQueryRecordSchema, cmpVersions, cookieToVersion, customQueryRecordSchema, cvrVersionSchema, delRowPatchSchema, internalQueryRecordSchema, maxVersion, maybeVersionString, oneAfter, patchSchema, putRowPatchSchema, queryRecordToQueryRow, rowIDSchema, versionFromString, versionString, versionToCookie, versionToNullableCookie };
262
262
 
263
263
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../../../../../zero-cache/src/services/view-syncer/schema/types.ts"],"sourcesContent":["import {jsonValueSchema} from '../../../../../shared/src/bigint-json.ts';\nimport {jsonSchema} from '../../../../../shared/src/json-schema.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport {astSchema} from '../../../../../zero-protocol/src/ast.ts';\nimport {versionFromLexi, versionToLexi} from '../../../types/lexi-version.ts';\nimport {\n majorVersionToString,\n stateVersionFromString,\n} from '../../../types/state-version.ts';\nimport {ttlClockSchema} from '../ttl-clock.ts';\nimport type {QueriesRow} from './cvr.ts';\n\nexport const cvrVersionSchema = v.object({\n /**\n * The database `stateVersion` with which the rows in the CVR are consistent.\n */\n stateVersion: v.string(), // LexiVersion\n\n /**\n * `configVersion` is subversion of `stateVersion` that is initially absent for each\n * `stateVersion`, and incremented for configuration changes that affect the contents\n * of the CVR such as:\n *\n * * query set changes\n * * query transformation changes (which may happen for changes\n * in server-side logic or authorization policies)\n *\n * Such configuration changes are always correlated with a change to one or more\n * `/meta/...` records in the CVR, often (but not always) with corresponding\n * patches in `/patches/meta/...`.\n *\n * When the `stateVersion` moves forward, the `minorVersion` is reset to absent.\n * In this manner it behaves like the analogous concept in semantic versioning.\n */\n configVersion: v.number().optional(),\n});\n\nexport type CVRVersion = v.Infer<typeof cvrVersionSchema>;\n\nexport const EMPTY_CVR_VERSION: CVRVersion = {\n stateVersion: majorVersionToString(0),\n} as const;\n\nexport function oneAfter(v: NullableCVRVersion): CVRVersion {\n return v === null\n ? {stateVersion: majorVersionToString(0)}\n : {\n stateVersion: v.stateVersion,\n configVersion: (v.configVersion ?? 0) + 1,\n };\n}\n\nexport type NullableCVRVersion = CVRVersion | null;\n\nexport function cmpVersions(\n a: NullableCVRVersion,\n b: NullableCVRVersion,\n): number {\n return a === null && b === null\n ? 0\n : a === null\n ? -1\n : b === null\n ? 1\n : a.stateVersion < b.stateVersion\n ? -1\n : a.stateVersion > b.stateVersion\n ? 1\n : (a.configVersion ?? 0) - (b.configVersion ?? 0);\n}\n\nexport function maxVersion(a: CVRVersion, b?: CVRVersion): CVRVersion {\n return !b ? a : cmpVersions(b, a) > 0 ? b : a;\n}\n\nexport function versionToCookie(v: CVRVersion): string {\n return versionString(v);\n}\n\nexport function versionToNullableCookie(v: NullableCVRVersion): string | null {\n return v === null ? null : versionToCookie(v);\n}\n\nexport function cookieToVersion(cookie: string | null): NullableCVRVersion {\n if (cookie === null) {\n return null;\n }\n return versionFromString(cookie);\n}\n\n// Last Active tracking.\n\nexport const cvrIDSchema = v.object({id: v.string()});\nexport type CvrID = v.Infer<typeof cvrIDSchema>;\n\nconst cvrRecordSchema = v.object({\n /**\n * CVR records store the CVRVersion at which the record was last patched into\n * the CVR, which corresponds with a patch row that is cleaned up when the\n * record is changed (updated, deleted, and re-added in the case of rows).\n *\n * Tombstones are stored for row records but not for config records. This means\n * that \"orphaned\" delete patches for config records may exist, and therefore\n * scans of config patches must always run until the end of the list. On the\n * contrary, for row patches, the row record tombstones allow cleanup of delete\n * patches.\n */\n patchVersion: cvrVersionSchema,\n});\n\nexport const clientRecordSchema = v.object({\n /** The client ID, of which there can be multiple for a client group view. */\n id: v.string(),\n\n /** The client's desired query IDs. Patch information is stored in the QueryRecord. */\n desiredQueryIDs: v.array(v.string()),\n});\n\nexport type ClientRecord = v.Infer<typeof clientRecordSchema>;\n\nexport const baseQueryRecordSchema = v.object({\n /** The client-specified ID used to identify this query. Typically a hash. */\n id: v.string(),\n\n /**\n * The hash of the query after server-side transformations, which include:\n *\n * * Normalization (which may differ from what the client does)\n * * Query \"expansion\" to include primary keys and query-execution-related columns\n * * Authorization transforms\n *\n * Transformations depend on conditions that are independent of the db state version,\n * such as server-side logic and authorization policies. As such, the version of a CVR\n * version may need to be advanced independent of db state changes. This is done\n * via the `minorVersion` counter of the CVRVersion object, which is used to account\n * for both changes to the query set and changes to query transformations (which are\n * effectively remove-old-query + add-new-query).\n *\n * Note that the transformed AST itself is **not** stored, as the result of the previous\n * transformation is not useful in and of itself. If the current transformation results in\n * a different hash than that of the transformation used for the last version of the CVR,\n * it is simply handled by invalidating the existing rows, re-executed the query with\n * the new transformation, and advancing the CVR's `minorVersion` and this query's\n * `transformationVersion`.\n *\n * Note that the transformationHash is only stored when the query has reached the \"gotten\"\n * state. If the query is in the \"desired\" but not yet \"gotten\" state, the field is absent.\n */\n transformationHash: v.string().optional(),\n\n /**\n * The CVR version corresponding to the `transformationHash`. This essentially tracks when\n * this version of the query was effectively added to the CVR (as opposed to the\n * `patchVersion`, which is simply when the client was notified that its query was added\n * to the gotten set). Catchup of clients from old CVR versions require executing all\n * queries with a newer `transformationVersion`.\n */\n transformationVersion: cvrVersionSchema.optional(),\n});\n\n/**\n * Internal queries track rows in the database for internal use, such as the\n * `lastMutationID`s in the `zero.clients` table. They participate in the standard\n * invalidation / update logic for row contents, but not in the desired/got or\n * size-based quota logic for client-requested queries.\n */\nexport const internalQueryRecordSchema = baseQueryRecordSchema.extend({\n type: v.literal('internal'),\n ast: astSchema,\n});\n\nexport type InternalQueryRecord = v.Infer<typeof internalQueryRecordSchema>;\n\nconst clientStateSchema = v.object({\n /**\n * The time at which the query was last inactivated. If this undefined or\n * missing then the query is active.\n *\n * Desired queries are always active and have an undefined inactivatedAt.\n */\n inactivatedAt: ttlClockSchema.optional(),\n\n /**\n * TTL, time to live in milliseconds. If the query is not updated within this\n * time. The time to live is the time after it has become inactive. Negative\n * values are treated as `'forever'`.\n *\n * We do clamp this to a maximum of 10 minutes, so that queries do not\n * live for a very long time in the CVR.\n */\n ttl: v.number(),\n\n /**\n * The version at which the client state changed (i.e. individual `patchVersion`s).\n */\n version: cvrVersionSchema,\n});\n\nconst externalQueryRecordSchema = baseQueryRecordSchema.extend({\n /**\n * The client state for this query, which includes the inactivatedAt, ttl and\n * version. The client state is stored in a record with the client ID as the\n * key.\n */\n clientState: v.record(clientStateSchema),\n\n // For queries, the `patchVersion` indicates when query was added to the got set,\n // and is absent if not yet gotten.\n patchVersion: cvrVersionSchema.optional(),\n});\n\nexport const clientQueryRecordSchema = externalQueryRecordSchema.extend({\n type: v.literal('client'),\n\n /** The original AST as supplied by the client. */\n ast: astSchema,\n});\n\nexport type ClientQueryRecord = v.Infer<typeof clientQueryRecordSchema>;\n\nexport const customQueryRecordSchema = externalQueryRecordSchema.extend({\n type: v.literal('custom'),\n name: v.string(),\n args: v.readonly(v.array(jsonSchema)),\n});\n\nexport type CustomQueryRecord = v.Infer<typeof customQueryRecordSchema>;\n\nexport const queryRecordSchema = v.union(\n clientQueryRecordSchema,\n customQueryRecordSchema,\n internalQueryRecordSchema,\n);\n\nexport type QueryRecord = v.Infer<typeof queryRecordSchema>;\n\nexport const rowIDSchema = v.object({\n schema: v.string(),\n table: v.string(),\n rowKey: v.record(jsonValueSchema),\n});\n\nexport type RowID = v.Infer<typeof rowIDSchema>;\n\nexport const rowRecordSchema = cvrRecordSchema.extend({\n id: rowIDSchema,\n rowVersion: v.string(), // '_0_version' of the row\n // query hashes => refCount, or `null` for a row that was removed from the\n // view (i.e. tombstone).\n refCounts: v.record(v.number()).nullable(),\n});\n\nexport type RowRecord = v.Infer<typeof rowRecordSchema>;\n\nexport const patchSchema = v.object({\n type: v.literalUnion('row', 'query'),\n op: v.literalUnion('put', 'del'),\n});\n\nexport const putRowPatchSchema = patchSchema.extend({\n type: v.literal('row'),\n op: v.literal('put'),\n id: rowIDSchema,\n rowVersion: v.string(), // '_0_version' of the row\n});\n\nexport type PutRowPatch = v.Infer<typeof putRowPatchSchema>;\n\nexport const delRowPatchSchema = patchSchema.extend({\n type: v.literal('row'),\n op: v.literal('del'),\n id: rowIDSchema,\n});\n\nexport type DelRowPatch = v.Infer<typeof delRowPatchSchema>;\n\nexport const rowPatchSchema = v.union(putRowPatchSchema, delRowPatchSchema);\n\nexport type RowPatch = v.Infer<typeof rowPatchSchema>;\n\nexport const queryPatchSchema = patchSchema.extend({\n type: v.literal('query'),\n id: v.string(),\n clientID: v.string().optional(), // defined for \"desired\", undefined for \"got\"\n});\n\nexport type QueryPatch = v.Infer<typeof queryPatchSchema>;\n\nexport type PutQueryPatch = QueryPatch & {op: 'put'};\nexport type DelQueryPatch = QueryPatch & {op: 'del'};\n\nexport const metadataPatchSchema = queryPatchSchema;\n\nexport type MetadataPatch = v.Infer<typeof metadataPatchSchema>;\n\nexport function versionString(v: CVRVersion) {\n // The separator (e.g. \":\") needs to be lexicographically greater than the\n // storage key path separator (e.g. \"/\") so that \"01/row-hash\" is less than \"01:01/row-hash\".\n // In particular, the traditional separator for major.minor versions (\".\") does not\n // satisfy this quality.\n return v.configVersion\n ? `${v.stateVersion}:${versionToLexi(v.configVersion)}`\n : v.stateVersion;\n}\n\nexport function versionFromString(str: string): CVRVersion {\n const parts = str.split(':');\n const stateVersion = parts[0];\n switch (parts.length) {\n case 1: {\n stateVersionFromString(stateVersion); // Purely for validation.\n return {stateVersion};\n }\n case 2: {\n const configVersion = versionFromLexi(parts[1]);\n if (configVersion > BigInt(Number.MAX_SAFE_INTEGER)) {\n throw new Error(`minorVersion ${parts[1]} exceeds max safe integer`);\n }\n return {stateVersion, configVersion: Number(configVersion)};\n }\n default:\n throw new TypeError(`Invalid version string ${str}`);\n }\n}\n\nexport function queryRecordToQueryRow(\n clientGroupID: string,\n query: QueryRecord,\n rowSetSignature: string | null,\n): QueriesRow {\n switch (query.type) {\n case 'internal':\n return {\n clientGroupID,\n queryHash: query.id,\n clientAST: query.ast,\n queryName: null,\n queryArgs: null,\n patchVersion: null,\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n internal: true,\n deleted: false, // put vs del \"got\" query\n rowSetSignature,\n };\n case 'client':\n return {\n clientGroupID,\n queryHash: query.id,\n clientAST: query.ast,\n queryName: null,\n queryArgs: null,\n patchVersion: maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n internal: null,\n deleted: false, // put vs del \"got\" query\n rowSetSignature,\n };\n case 'custom':\n return {\n clientGroupID,\n queryHash: query.id,\n clientAST: null,\n queryName: query.name,\n queryArgs: query.args,\n patchVersion: maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n internal: null,\n deleted: false, // put vs del \"got\" query\n rowSetSignature,\n };\n }\n}\n\nexport const maybeVersionString = (v: CVRVersion | undefined) =>\n v ? versionString(v) : null;\n"],"mappings":"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"}
1
+ {"version":3,"file":"types.js","names":["v","v.readonly","v.literalUnion"],"sources":["../../../../../../../zero-cache/src/services/view-syncer/schema/types.ts"],"sourcesContent":["import {jsonValueSchema} from '../../../../../shared/src/bigint-json.ts';\nimport {jsonSchema} from '../../../../../shared/src/json-schema.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport {astSchema} from '../../../../../zero-protocol/src/ast.ts';\nimport {versionFromLexi, versionToLexi} from '../../../types/lexi-version.ts';\nimport {\n majorVersionToString,\n stateVersionFromString,\n} from '../../../types/state-version.ts';\nimport {ttlClockSchema} from '../ttl-clock.ts';\nimport type {QueriesRow} from './cvr.ts';\n\nexport const cvrVersionSchema = v.object({\n /**\n * The database `stateVersion` with which the rows in the CVR are consistent.\n */\n stateVersion: v.string(), // LexiVersion\n\n /**\n * `configVersion` is subversion of `stateVersion` that is initially absent for each\n * `stateVersion`, and incremented for configuration changes that affect the contents\n * of the CVR such as:\n *\n * * query set changes\n * * query transformation changes (which may happen for changes\n * in server-side logic or authorization policies)\n *\n * Such configuration changes are always correlated with a change to one or more\n * `/meta/...` records in the CVR, often (but not always) with corresponding\n * patches in `/patches/meta/...`.\n *\n * When the `stateVersion` moves forward, the `minorVersion` is reset to absent.\n * In this manner it behaves like the analogous concept in semantic versioning.\n */\n configVersion: v.number().optional(),\n});\n\nexport type CVRVersion = v.Infer<typeof cvrVersionSchema>;\n\nexport const EMPTY_CVR_VERSION: CVRVersion = {\n stateVersion: majorVersionToString(0),\n} as const;\n\nexport function oneAfter(v: NullableCVRVersion): CVRVersion {\n return v === null\n ? {stateVersion: majorVersionToString(0)}\n : {\n stateVersion: v.stateVersion,\n configVersion: (v.configVersion ?? 0) + 1,\n };\n}\n\nexport type NullableCVRVersion = CVRVersion | null;\n\nexport function cmpVersions(\n a: NullableCVRVersion,\n b: NullableCVRVersion,\n): number {\n return a === null && b === null\n ? 0\n : a === null\n ? -1\n : b === null\n ? 1\n : a.stateVersion < b.stateVersion\n ? -1\n : a.stateVersion > b.stateVersion\n ? 1\n : (a.configVersion ?? 0) - (b.configVersion ?? 0);\n}\n\nexport function maxVersion(a: CVRVersion, b?: CVRVersion): CVRVersion {\n return !b ? a : cmpVersions(b, a) > 0 ? b : a;\n}\n\nexport function versionToCookie(v: CVRVersion): string {\n return versionString(v);\n}\n\nexport function versionToNullableCookie(v: NullableCVRVersion): string | null {\n return v === null ? null : versionToCookie(v);\n}\n\nexport function cookieToVersion(cookie: string | null): NullableCVRVersion {\n if (cookie === null) {\n return null;\n }\n return versionFromString(cookie);\n}\n\n// Last Active tracking.\n\nexport const cvrIDSchema = v.object({id: v.string()});\nexport type CvrID = v.Infer<typeof cvrIDSchema>;\n\nconst cvrRecordSchema = v.object({\n /**\n * CVR records store the CVRVersion at which the record was last patched into\n * the CVR, which corresponds with a patch row that is cleaned up when the\n * record is changed (updated, deleted, and re-added in the case of rows).\n *\n * Tombstones are stored for row records but not for config records. This means\n * that \"orphaned\" delete patches for config records may exist, and therefore\n * scans of config patches must always run until the end of the list. On the\n * contrary, for row patches, the row record tombstones allow cleanup of delete\n * patches.\n */\n patchVersion: cvrVersionSchema,\n});\n\nexport const clientRecordSchema = v.object({\n /** The client ID, of which there can be multiple for a client group view. */\n id: v.string(),\n\n /** The client's desired query IDs. Patch information is stored in the QueryRecord. */\n desiredQueryIDs: v.array(v.string()),\n});\n\nexport type ClientRecord = v.Infer<typeof clientRecordSchema>;\n\nexport const baseQueryRecordSchema = v.object({\n /** The client-specified ID used to identify this query. Typically a hash. */\n id: v.string(),\n\n /**\n * The hash of the query after server-side transformations, which include:\n *\n * * Normalization (which may differ from what the client does)\n * * Query \"expansion\" to include primary keys and query-execution-related columns\n * * Authorization transforms\n *\n * Transformations depend on conditions that are independent of the db state version,\n * such as server-side logic and authorization policies. As such, the version of a CVR\n * version may need to be advanced independent of db state changes. This is done\n * via the `minorVersion` counter of the CVRVersion object, which is used to account\n * for both changes to the query set and changes to query transformations (which are\n * effectively remove-old-query + add-new-query).\n *\n * Note that the transformed AST itself is **not** stored, as the result of the previous\n * transformation is not useful in and of itself. If the current transformation results in\n * a different hash than that of the transformation used for the last version of the CVR,\n * it is simply handled by invalidating the existing rows, re-executed the query with\n * the new transformation, and advancing the CVR's `minorVersion` and this query's\n * `transformationVersion`.\n *\n * Note that the transformationHash is only stored when the query has reached the \"gotten\"\n * state. If the query is in the \"desired\" but not yet \"gotten\" state, the field is absent.\n */\n transformationHash: v.string().optional(),\n\n /**\n * The CVR version corresponding to the `transformationHash`. This essentially tracks when\n * this version of the query was effectively added to the CVR (as opposed to the\n * `patchVersion`, which is simply when the client was notified that its query was added\n * to the gotten set). Catchup of clients from old CVR versions require executing all\n * queries with a newer `transformationVersion`.\n */\n transformationVersion: cvrVersionSchema.optional(),\n});\n\n/**\n * Internal queries track rows in the database for internal use, such as the\n * `lastMutationID`s in the `zero.clients` table. They participate in the standard\n * invalidation / update logic for row contents, but not in the desired/got or\n * size-based quota logic for client-requested queries.\n */\nexport const internalQueryRecordSchema = baseQueryRecordSchema.extend({\n type: v.literal('internal'),\n ast: astSchema,\n});\n\nexport type InternalQueryRecord = v.Infer<typeof internalQueryRecordSchema>;\n\nconst clientStateSchema = v.object({\n /**\n * The time at which the query was last inactivated. If this undefined or\n * missing then the query is active.\n *\n * Desired queries are always active and have an undefined inactivatedAt.\n */\n inactivatedAt: ttlClockSchema.optional(),\n\n /**\n * TTL, time to live in milliseconds. If the query is not updated within this\n * time. The time to live is the time after it has become inactive. Negative\n * values are treated as `'forever'`.\n *\n * We do clamp this to a maximum of 10 minutes, so that queries do not\n * live for a very long time in the CVR.\n */\n ttl: v.number(),\n\n /**\n * The version at which the client state changed (i.e. individual `patchVersion`s).\n */\n version: cvrVersionSchema,\n});\n\nconst externalQueryRecordSchema = baseQueryRecordSchema.extend({\n /**\n * The client state for this query, which includes the inactivatedAt, ttl and\n * version. The client state is stored in a record with the client ID as the\n * key.\n */\n clientState: v.record(clientStateSchema),\n\n // For queries, the `patchVersion` indicates when query was added to the got set,\n // and is absent if not yet gotten.\n patchVersion: cvrVersionSchema.optional(),\n});\n\nexport const clientQueryRecordSchema = externalQueryRecordSchema.extend({\n type: v.literal('client'),\n\n /** The original AST as supplied by the client. */\n ast: astSchema,\n});\n\nexport type ClientQueryRecord = v.Infer<typeof clientQueryRecordSchema>;\n\nexport const customQueryRecordSchema = externalQueryRecordSchema.extend({\n type: v.literal('custom'),\n name: v.string(),\n args: v.readonly(v.array(jsonSchema)),\n});\n\nexport type CustomQueryRecord = v.Infer<typeof customQueryRecordSchema>;\n\nexport const queryRecordSchema = v.union(\n clientQueryRecordSchema,\n customQueryRecordSchema,\n internalQueryRecordSchema,\n);\n\nexport type QueryRecord = v.Infer<typeof queryRecordSchema>;\n\nexport const rowIDSchema = v.object({\n schema: v.string(),\n table: v.string(),\n rowKey: v.record(jsonValueSchema),\n});\n\nexport type RowID = v.Infer<typeof rowIDSchema>;\n\nexport const rowRecordSchema = cvrRecordSchema.extend({\n id: rowIDSchema,\n rowVersion: v.string(), // '_0_version' of the row\n // query hashes => refCount, or `null` for a row that was removed from the\n // view (i.e. tombstone).\n refCounts: v.record(v.number()).nullable(),\n});\n\nexport type RowRecord = v.Infer<typeof rowRecordSchema>;\n\nexport const patchSchema = v.object({\n type: v.literalUnion('row', 'query'),\n op: v.literalUnion('put', 'del'),\n});\n\nexport const putRowPatchSchema = patchSchema.extend({\n type: v.literal('row'),\n op: v.literal('put'),\n id: rowIDSchema,\n rowVersion: v.string(), // '_0_version' of the row\n});\n\nexport type PutRowPatch = v.Infer<typeof putRowPatchSchema>;\n\nexport const delRowPatchSchema = patchSchema.extend({\n type: v.literal('row'),\n op: v.literal('del'),\n id: rowIDSchema,\n});\n\nexport type DelRowPatch = v.Infer<typeof delRowPatchSchema>;\n\nexport const rowPatchSchema = v.union(putRowPatchSchema, delRowPatchSchema);\n\nexport type RowPatch = v.Infer<typeof rowPatchSchema>;\n\nexport const queryPatchSchema = patchSchema.extend({\n type: v.literal('query'),\n id: v.string(),\n clientID: v.string().optional(), // defined for \"desired\", undefined for \"got\"\n});\n\nexport type QueryPatch = v.Infer<typeof queryPatchSchema>;\n\nexport type PutQueryPatch = QueryPatch & {op: 'put'};\nexport type DelQueryPatch = QueryPatch & {op: 'del'};\n\nexport const metadataPatchSchema = queryPatchSchema;\n\nexport type MetadataPatch = v.Infer<typeof metadataPatchSchema>;\n\nexport function versionString(v: CVRVersion) {\n // The separator (e.g. \":\") needs to be lexicographically greater than the\n // storage key path separator (e.g. \"/\") so that \"01/row-hash\" is less than \"01:01/row-hash\".\n // In particular, the traditional separator for major.minor versions (\".\") does not\n // satisfy this quality.\n return v.configVersion\n ? `${v.stateVersion}:${versionToLexi(v.configVersion)}`\n : v.stateVersion;\n}\n\nexport function versionFromString(str: string): CVRVersion {\n const parts = str.split(':');\n const stateVersion = parts[0];\n switch (parts.length) {\n case 1: {\n stateVersionFromString(stateVersion); // Purely for validation.\n return {stateVersion};\n }\n case 2: {\n const configVersion = versionFromLexi(parts[1]);\n if (configVersion > BigInt(Number.MAX_SAFE_INTEGER)) {\n throw new Error(`minorVersion ${parts[1]} exceeds max safe integer`);\n }\n return {stateVersion, configVersion: Number(configVersion)};\n }\n default:\n throw new TypeError(`Invalid version string ${str}`);\n }\n}\n\nexport function queryRecordToQueryRow(\n clientGroupID: string,\n query: QueryRecord,\n rowSetSignature: string | null,\n): QueriesRow {\n switch (query.type) {\n case 'internal':\n return {\n clientGroupID,\n queryHash: query.id,\n clientAST: query.ast,\n queryName: null,\n queryArgs: null,\n patchVersion: null,\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n internal: true,\n deleted: false, // put vs del \"got\" query\n rowSetSignature,\n };\n case 'client':\n return {\n clientGroupID,\n queryHash: query.id,\n clientAST: query.ast,\n queryName: null,\n queryArgs: null,\n patchVersion: maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n internal: null,\n deleted: false, // put vs del \"got\" query\n rowSetSignature,\n };\n case 'custom':\n return {\n clientGroupID,\n queryHash: query.id,\n clientAST: null,\n queryName: query.name,\n queryArgs: query.args,\n patchVersion: maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n internal: null,\n deleted: false, // put vs del \"got\" query\n rowSetSignature,\n };\n }\n}\n\nexport const maybeVersionString = (v: CVRVersion | undefined) =>\n v ? versionString(v) : null;\n"],"mappings":"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"}
@@ -31,6 +31,12 @@ function serialize(v) {
31
31
  default: return "u" + String(v);
32
32
  }
33
33
  }
34
+ /**
35
+ * The maximum number of distinct SQL statements interned by a
36
+ * {@link SnapshotRowCache} before it is cleared. There is one per table and
37
+ * lookup shape, so this is only reached after many schema changes.
38
+ */
39
+ var MAX_INTERNED_STATEMENTS = 1e4;
34
40
  var HIT = { result: "hit" };
35
41
  var MISS = { result: "miss" };
36
42
  /**
@@ -150,6 +156,6 @@ var SnapshotRowCache = class {
150
156
  }
151
157
  };
152
158
  //#endregion
153
- export { SnapshotRowCache };
159
+ export { DEFAULT_MAX_SNAPSHOT_ROW_CACHE_ENTRIES, MAX_INTERNED_STATEMENTS, SnapshotRowCache };
154
160
 
155
161
  //# sourceMappingURL=snapshot-row-cache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot-row-cache.js","names":["#maxEntries","#entries","#keys","#sqlIDs","#reads","#hits","#misses","#key"],"sources":["../../../../../../zero-cache/src/services/view-syncer/snapshot-row-cache.ts"],"sourcesContent":["import {stringify} from '../../../../shared/src/bigint-json.ts';\nimport {getOrInsert} from '../../../../shared/src/map.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\n\n/**\n * The default maximum number of entries retained by a {@link SnapshotRowCache}.\n *\n * Each entry is a raw SQLite row (or the list of rows conflicting with\n * a row on its unique keys) read while computing a snapshot diff. A\n * replicated transaction of N rows produces up to 2N entries (the new value\n * and the previous value(s) of each row), so the cache should be large enough\n * to hold the largest transactions that are commonly replicated in order for\n * all of the client groups on a worker to share the reads.\n */\nexport const DEFAULT_MAX_SNAPSHOT_ROW_CACHE_ENTRIES = 50_000;\n\n/**\n * Serializes a lookup key value (i.e. a SQLite-bound parameter). Numbers and\n * bigints serialize identically, as row keys parsed from the change log are\n * JSON numbers whereas the values read from SQLite are bigints; both address\n * the same row. Strings are prefixed to distinguish them from numbers.\n */\nfunction serialize(v: unknown): string {\n switch (typeof v) {\n case 'string':\n return 's' + v;\n case 'number':\n case 'bigint':\n return String(v);\n case 'boolean':\n return v ? 'true' : 'false';\n case 'object':\n return v === null ? 'null' : 'o' + stringify(v);\n default:\n return 'u' + String(v);\n }\n}\n\n/**\n * How a statement's results are read: a single row (`Statement.get()`) or\n * all rows (`Statement.all()`).\n */\nexport type ReadMode = 'get' | 'all';\n\n/**\n * The maximum number of distinct SQL statements interned by a\n * {@link SnapshotRowCache} before it is cleared. There is one per table and\n * lookup shape, so this is only reached after many schema changes.\n */\nexport const MAX_INTERNED_STATEMENTS = 10_000;\n\nconst HIT = {result: 'hit'} as const;\nconst MISS = {result: 'miss'} as const;\n\n/**\n * A worker-wide, bounded cache of the rows that a `Snapshotter` reads from\n * replica snapshots when computing the diff between two versions of the\n * replica.\n *\n * Every client group (i.e. view-syncer) on a sync worker computes its own\n * diff from the shared `_zero.changeLog2`, and each change in the diff costs\n * two SQLite reads: the new value of the row (from the `curr` snapshot) and\n * the previous value(s) that it replaces (from the `prev` snapshot). The\n * reads for a given change are identical for every client group that\n * traverses it, so a replicated transaction of N rows costs N × 2 × (client\n * groups) reads per worker, most of which are redundant. This cache lets the\n * client groups on a worker share those reads.\n *\n * ### Keys\n *\n * Entries are keyed by the exact SQL statement and arguments of the read\n * (which encodes the table, the selected columns and the lookup keys), plus\n * a `tag` supplied by the caller that pins the result to a version of the\n * replica:\n *\n * - The new value of a row is the row as of the `stateVersion` recorded in\n * its change log entry (its `_0_version` is asserted to equal that\n * version), and is tagged with that `stateVersion`. It is thus shareable\n * by all client groups regardless of the snapshots they are advancing\n * between.\n * - Previous values are read from the `prev` snapshot at its version, and\n * are tagged with that version (and, when the result can depend on the\n * changes applied to the snapshot before the read, the `curr` version\n * as well). See `Diff` in `snapshotter.ts` for the details.\n *\n * Because every key includes the version(s) that the result depends on,\n * entries never go stale and are never invalidated; they are only evicted.\n *\n * ### Eviction\n *\n * The cache is bounded by entry count and evicts in insertion (FIFO) order\n * rather than by recency. Diffs are traversed in change log order by every\n * client group, so the entries that will not be needed again are those that\n * were inserted the earliest (all groups have moved past them), while the\n * most recently inserted entries are the ones that the trailing groups have\n * yet to reach. Refreshing an entry on a hit would evict exactly the wrong\n * entries when a trailing group catches up.\n *\n * ### Sharing\n *\n * Cached values are shared by reference across client groups and must\n * therefore never be mutated by callers.\n */\nexport class SnapshotRowCache {\n readonly #maxEntries: number;\n readonly #entries = new Map<string, unknown>();\n // The keys of #entries in insertion order, so that the oldest can be\n // evicted in O(1). (Finding the oldest key with a fresh Map iterator\n // instead costs O(evictions) per insert, as V8 leaves deleted entries in\n // the table until it is compacted and every new iterator walks over them.)\n readonly #keys = new RingBuffer<string>();\n // Interns the SQL text of each distinct read so that entry keys\n // do not repeat the (long) column list of every statement.\n readonly #sqlIDs = new Map<string, number>();\n readonly #reads = getOrCreateCounter(\n 'sync',\n 'ivm.snapshot-row-reads',\n 'Row reads performed while computing snapshot diffs for IVM, labeled by whether the read was served by the worker-wide snapshot row cache.',\n );\n #hits = 0;\n #misses = 0;\n\n constructor(maxEntries = DEFAULT_MAX_SNAPSHOT_ROW_CACHE_ENTRIES) {\n this.#maxEntries = Math.max(0, maxEntries);\n }\n\n get size(): number {\n return this.#entries.size;\n }\n\n get maxEntries(): number {\n return this.#maxEntries;\n }\n\n stats(): {hits: number; misses: number; size: number} {\n return {hits: this.#hits, misses: this.#misses, size: this.#entries.size};\n }\n\n /**\n * Returns the cached result of the read identified by `tag`, `sql`, `mode`\n * and `args`, or performs the read via `read()`, caching and returning its\n * result. `undefined` results (i.e. a missing row) are not cached.\n *\n * `mode` is part of the key because the same statement can be read both\n * ways: for a table whose only unique key is its primary key, `getRow()`\n * and `getRows()` produce identical SQL, args and tags, but the former\n * caches a row and the latter an array of rows.\n */\n getOrRead<T>(\n tag: string,\n sql: string,\n mode: ReadMode,\n args: unknown[],\n read: () => T,\n ): T {\n const key = this.#key(tag, sql, mode, args);\n const cached = this.#entries.get(key);\n if (cached !== undefined) {\n this.#hits++;\n this.#reads.add(1, HIT);\n return cached as T;\n }\n this.#misses++;\n this.#reads.add(1, MISS);\n const value = read();\n if (value !== undefined && this.#maxEntries > 0) {\n // `key` is not in #entries (this is a miss), so it is always added.\n this.#entries.set(key, value);\n this.#keys.push(key);\n if (this.#keys.size > this.#maxEntries) {\n this.#entries.delete(must(this.#keys.shift()));\n }\n }\n return value;\n }\n\n clear(): void {\n this.#entries.clear();\n this.#keys.clear();\n this.#sqlIDs.clear();\n }\n\n #key(tag: string, sql: string, mode: ReadMode, args: unknown[]): string {\n if (\n this.#sqlIDs.size >= MAX_INTERNED_STATEMENTS &&\n !this.#sqlIDs.has(sql)\n ) {\n // Statements are only added (e.g. when schema changes produce new\n // column lists), so bound them by starting over. The entries refer to\n // statements by ID and are cleared with them.\n this.clear();\n }\n const sqlID = getOrInsert(this.#sqlIDs, sql, this.#sqlIDs.size);\n let key = `${tag}\\0${mode}${sqlID}`;\n // Include each value's length so separators inside string values cannot\n // make different argument arrays produce the same key.\n for (const arg of args) {\n const serialized = serialize(arg);\n key += `\\0${serialized.length}:${serialized}`;\n }\n return key;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,IAAa,yCAAyC;;;;;;;AAQtD,SAAS,UAAU,GAAoB;CACrC,QAAQ,OAAO,GAAf;EACE,KAAK,UACH,OAAO,MAAM;EACf,KAAK;EACL,KAAK,UACH,OAAO,OAAO,CAAC;EACjB,KAAK,WACH,OAAO,IAAI,SAAS;EACtB,KAAK,UACH,OAAO,MAAM,OAAO,SAAS,MAAM,UAAU,CAAC;EAChD,SACE,OAAO,MAAM,OAAO,CAAC;CACzB;AACF;AAeA,IAAM,MAAM,EAAC,QAAQ,MAAK;AAC1B,IAAM,OAAO,EAAC,QAAQ,OAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmD5B,IAAa,mBAAb,MAA8B;CAC5B;CACA,2BAAoB,IAAI,IAAqB;CAK7C,QAAiB,IAAI,WAAmB;CAGxC,0BAAmB,IAAI,IAAoB;CAC3C,SAAkB,mBAChB,QACA,0BACA,2IACF;CACA,QAAQ;CACR,UAAU;CAEV,YAAY,aAAa,wCAAwC;EAC/D,KAAKA,cAAc,KAAK,IAAI,GAAG,UAAU;CAC3C;CAEA,IAAI,OAAe;EACjB,OAAO,KAAKC,SAAS;CACvB;CAEA,IAAI,aAAqB;EACvB,OAAO,KAAKD;CACd;CAEA,QAAsD;EACpD,OAAO;GAAC,MAAM,KAAKK;GAAO,QAAQ,KAAKC;GAAS,MAAM,KAAKL,SAAS;EAAI;CAC1E;;;;;;;;;;;CAYA,UACE,KACA,KACA,MACA,MACA,MACG;EACH,MAAM,MAAM,KAAKM,KAAK,KAAK,KAAK,MAAM,IAAI;EAC1C,MAAM,SAAS,KAAKN,SAAS,IAAI,GAAG;EACpC,IAAI,WAAW,KAAA,GAAW;GACxB,KAAKI;GACL,KAAKD,OAAO,IAAI,GAAG,GAAG;GACtB,OAAO;EACT;EACA,KAAKE;EACL,KAAKF,OAAO,IAAI,GAAG,IAAI;EACvB,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,KAAa,KAAKJ,cAAc,GAAG;GAE/C,KAAKC,SAAS,IAAI,KAAK,KAAK;GAC5B,KAAKC,MAAM,KAAK,GAAG;GACnB,IAAI,KAAKA,MAAM,OAAO,KAAKF,aACzB,KAAKC,SAAS,OAAO,KAAK,KAAKC,MAAM,MAAM,CAAC,CAAC;EAEjD;EACA,OAAO;CACT;CAEA,QAAc;EACZ,KAAKD,SAAS,MAAM;EACpB,KAAKC,MAAM,MAAM;EACjB,KAAKC,QAAQ,MAAM;CACrB;CAEA,KAAK,KAAa,KAAa,MAAgB,MAAyB;EACtE,IACE,KAAKA,QAAQ,QAAA,OACb,CAAC,KAAKA,QAAQ,IAAI,GAAG,GAKrB,KAAK,MAAM;EAGb,IAAI,MAAM,GAAG,IAAI,IAAI,OADP,YAAY,KAAKA,SAAS,KAAK,KAAKA,QAAQ,IAC9B;EAG5B,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,aAAa,UAAU,GAAG;GAChC,OAAO,KAAK,WAAW,OAAO,GAAG;EACnC;EACA,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"snapshot-row-cache.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/snapshot-row-cache.ts"],"sourcesContent":["import {stringify} from '../../../../shared/src/bigint-json.ts';\nimport {getOrInsert} from '../../../../shared/src/map.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\n\n/**\n * The default maximum number of entries retained by a {@link SnapshotRowCache}.\n *\n * Each entry is a raw SQLite row (or the list of rows conflicting with\n * a row on its unique keys) read while computing a snapshot diff. A\n * replicated transaction of N rows produces up to 2N entries (the new value\n * and the previous value(s) of each row), so the cache should be large enough\n * to hold the largest transactions that are commonly replicated in order for\n * all of the client groups on a worker to share the reads.\n */\nexport const DEFAULT_MAX_SNAPSHOT_ROW_CACHE_ENTRIES = 50_000;\n\n/**\n * Serializes a lookup key value (i.e. a SQLite-bound parameter). Numbers and\n * bigints serialize identically, as row keys parsed from the change log are\n * JSON numbers whereas the values read from SQLite are bigints; both address\n * the same row. Strings are prefixed to distinguish them from numbers.\n */\nfunction serialize(v: unknown): string {\n switch (typeof v) {\n case 'string':\n return 's' + v;\n case 'number':\n case 'bigint':\n return String(v);\n case 'boolean':\n return v ? 'true' : 'false';\n case 'object':\n return v === null ? 'null' : 'o' + stringify(v);\n default:\n return 'u' + String(v);\n }\n}\n\n/**\n * How a statement's results are read: a single row (`Statement.get()`) or\n * all rows (`Statement.all()`).\n */\nexport type ReadMode = 'get' | 'all';\n\n/**\n * The maximum number of distinct SQL statements interned by a\n * {@link SnapshotRowCache} before it is cleared. There is one per table and\n * lookup shape, so this is only reached after many schema changes.\n */\nexport const MAX_INTERNED_STATEMENTS = 10_000;\n\nconst HIT = {result: 'hit'} as const;\nconst MISS = {result: 'miss'} as const;\n\n/**\n * A worker-wide, bounded cache of the rows that a `Snapshotter` reads from\n * replica snapshots when computing the diff between two versions of the\n * replica.\n *\n * Every client group (i.e. view-syncer) on a sync worker computes its own\n * diff from the shared `_zero.changeLog2`, and each change in the diff costs\n * two SQLite reads: the new value of the row (from the `curr` snapshot) and\n * the previous value(s) that it replaces (from the `prev` snapshot). The\n * reads for a given change are identical for every client group that\n * traverses it, so a replicated transaction of N rows costs N × 2 × (client\n * groups) reads per worker, most of which are redundant. This cache lets the\n * client groups on a worker share those reads.\n *\n * ### Keys\n *\n * Entries are keyed by the exact SQL statement and arguments of the read\n * (which encodes the table, the selected columns and the lookup keys), plus\n * a `tag` supplied by the caller that pins the result to a version of the\n * replica:\n *\n * - The new value of a row is the row as of the `stateVersion` recorded in\n * its change log entry (its `_0_version` is asserted to equal that\n * version), and is tagged with that `stateVersion`. It is thus shareable\n * by all client groups regardless of the snapshots they are advancing\n * between.\n * - Previous values are read from the `prev` snapshot at its version, and\n * are tagged with that version (and, when the result can depend on the\n * changes applied to the snapshot before the read, the `curr` version\n * as well). See `Diff` in `snapshotter.ts` for the details.\n *\n * Because every key includes the version(s) that the result depends on,\n * entries never go stale and are never invalidated; they are only evicted.\n *\n * ### Eviction\n *\n * The cache is bounded by entry count and evicts in insertion (FIFO) order\n * rather than by recency. Diffs are traversed in change log order by every\n * client group, so the entries that will not be needed again are those that\n * were inserted the earliest (all groups have moved past them), while the\n * most recently inserted entries are the ones that the trailing groups have\n * yet to reach. Refreshing an entry on a hit would evict exactly the wrong\n * entries when a trailing group catches up.\n *\n * ### Sharing\n *\n * Cached values are shared by reference across client groups and must\n * therefore never be mutated by callers.\n */\nexport class SnapshotRowCache {\n readonly #maxEntries: number;\n readonly #entries = new Map<string, unknown>();\n // The keys of #entries in insertion order, so that the oldest can be\n // evicted in O(1). (Finding the oldest key with a fresh Map iterator\n // instead costs O(evictions) per insert, as V8 leaves deleted entries in\n // the table until it is compacted and every new iterator walks over them.)\n readonly #keys = new RingBuffer<string>();\n // Interns the SQL text of each distinct read so that entry keys\n // do not repeat the (long) column list of every statement.\n readonly #sqlIDs = new Map<string, number>();\n readonly #reads = getOrCreateCounter(\n 'sync',\n 'ivm.snapshot-row-reads',\n 'Row reads performed while computing snapshot diffs for IVM, labeled by whether the read was served by the worker-wide snapshot row cache.',\n );\n #hits = 0;\n #misses = 0;\n\n constructor(maxEntries = DEFAULT_MAX_SNAPSHOT_ROW_CACHE_ENTRIES) {\n this.#maxEntries = Math.max(0, maxEntries);\n }\n\n get size(): number {\n return this.#entries.size;\n }\n\n get maxEntries(): number {\n return this.#maxEntries;\n }\n\n stats(): {hits: number; misses: number; size: number} {\n return {hits: this.#hits, misses: this.#misses, size: this.#entries.size};\n }\n\n /**\n * Returns the cached result of the read identified by `tag`, `sql`, `mode`\n * and `args`, or performs the read via `read()`, caching and returning its\n * result. `undefined` results (i.e. a missing row) are not cached.\n *\n * `mode` is part of the key because the same statement can be read both\n * ways: for a table whose only unique key is its primary key, `getRow()`\n * and `getRows()` produce identical SQL, args and tags, but the former\n * caches a row and the latter an array of rows.\n */\n getOrRead<T>(\n tag: string,\n sql: string,\n mode: ReadMode,\n args: unknown[],\n read: () => T,\n ): T {\n const key = this.#key(tag, sql, mode, args);\n const cached = this.#entries.get(key);\n if (cached !== undefined) {\n this.#hits++;\n this.#reads.add(1, HIT);\n return cached as T;\n }\n this.#misses++;\n this.#reads.add(1, MISS);\n const value = read();\n if (value !== undefined && this.#maxEntries > 0) {\n // `key` is not in #entries (this is a miss), so it is always added.\n this.#entries.set(key, value);\n this.#keys.push(key);\n if (this.#keys.size > this.#maxEntries) {\n this.#entries.delete(must(this.#keys.shift()));\n }\n }\n return value;\n }\n\n clear(): void {\n this.#entries.clear();\n this.#keys.clear();\n this.#sqlIDs.clear();\n }\n\n #key(tag: string, sql: string, mode: ReadMode, args: unknown[]): string {\n if (\n this.#sqlIDs.size >= MAX_INTERNED_STATEMENTS &&\n !this.#sqlIDs.has(sql)\n ) {\n // Statements are only added (e.g. when schema changes produce new\n // column lists), so bound them by starting over. The entries refer to\n // statements by ID and are cleared with them.\n this.clear();\n }\n const sqlID = getOrInsert(this.#sqlIDs, sql, this.#sqlIDs.size);\n let key = `${tag}\\0${mode}${sqlID}`;\n // Include each value's length so separators inside string values cannot\n // make different argument arrays produce the same key.\n for (const arg of args) {\n const serialized = serialize(arg);\n key += `\\0${serialized.length}:${serialized}`;\n }\n return key;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,IAAa,yCAAyC;;;;;;;AAQtD,SAAS,UAAU,GAAoB;CACrC,QAAQ,OAAO,GAAf;EACE,KAAK,UACH,OAAO,MAAM;EACf,KAAK;EACL,KAAK,UACH,OAAO,OAAO,CAAC;EACjB,KAAK,WACH,OAAO,IAAI,SAAS;EACtB,KAAK,UACH,OAAO,MAAM,OAAO,SAAS,MAAM,UAAU,CAAC;EAChD,SACE,OAAO,MAAM,OAAO,CAAC;CACzB;AACF;;;;;;AAaA,IAAa,0BAA0B;AAEvC,IAAM,MAAM,EAAC,QAAQ,MAAK;AAC1B,IAAM,OAAO,EAAC,QAAQ,OAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmD5B,IAAa,mBAAb,MAA8B;CAC5B;CACA,2BAAoB,IAAI,IAAqB;CAK7C,QAAiB,IAAI,WAAmB;CAGxC,0BAAmB,IAAI,IAAoB;CAC3C,SAAkB,mBAChB,QACA,0BACA,2IACF;CACA,QAAQ;CACR,UAAU;CAEV,YAAY,aAAa,wCAAwC;EAC/D,KAAK,cAAc,KAAK,IAAI,GAAG,UAAU;CAC3C;CAEA,IAAI,OAAe;EACjB,OAAO,KAAK,SAAS;CACvB;CAEA,IAAI,aAAqB;EACvB,OAAO,KAAK;CACd;CAEA,QAAsD;EACpD,OAAO;GAAC,MAAM,KAAK;GAAO,QAAQ,KAAK;GAAS,MAAM,KAAK,SAAS;EAAI;CAC1E;;;;;;;;;;;CAYA,UACE,KACA,KACA,MACA,MACA,MACG;EACH,MAAM,MAAM,KAAK,KAAK,KAAK,KAAK,MAAM,IAAI;EAC1C,MAAM,SAAS,KAAK,SAAS,IAAI,GAAG;EACpC,IAAI,WAAW,KAAA,GAAW;GACxB,KAAK;GACL,KAAK,OAAO,IAAI,GAAG,GAAG;GACtB,OAAO;EACT;EACA,KAAK;EACL,KAAK,OAAO,IAAI,GAAG,IAAI;EACvB,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,KAAa,KAAK,cAAc,GAAG;GAE/C,KAAK,SAAS,IAAI,KAAK,KAAK;GAC5B,KAAK,MAAM,KAAK,GAAG;GACnB,IAAI,KAAK,MAAM,OAAO,KAAK,aACzB,KAAK,SAAS,OAAO,KAAK,KAAK,MAAM,MAAM,CAAC,CAAC;EAEjD;EACA,OAAO;CACT;CAEA,QAAc;EACZ,KAAK,SAAS,MAAM;EACpB,KAAK,MAAM,MAAM;EACjB,KAAK,QAAQ,MAAM;CACrB;CAEA,KAAK,KAAa,KAAa,MAAgB,MAAyB;EACtE,IACE,KAAK,QAAQ,QAAA,OACb,CAAC,KAAK,QAAQ,IAAI,GAAG,GAKrB,KAAK,MAAM;EAGb,IAAI,MAAM,GAAG,IAAI,IAAI,OADP,YAAY,KAAK,SAAS,KAAK,KAAK,QAAQ,IAC9B;EAG5B,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,aAAa,UAAU,GAAG;GAChC,OAAO,KAAK,WAAW,OAAO,GAAG;EACnC;EACA,OAAO;CACT;AACF"}