@rocicorp/zero 1.11.0-head-86ca5eff-20261014 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (750) hide show
  1. package/out/_virtual/{_@oxc-project_runtime@0.130.0/helpers → _@oxc-project_runtime@0.154.0/helpers/esm}/usingCtx.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js +2 -1
  3. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  4. package/out/analyze-query/src/bin-analyze.js +5 -3
  5. package/out/analyze-query/src/bin-analyze.js.map +1 -1
  6. package/out/analyze-query/src/bin-transform.js +3 -2
  7. package/out/analyze-query/src/bin-transform.js.map +1 -1
  8. package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
  9. package/out/ast-to-zql/src/bin.js +3 -2
  10. package/out/ast-to-zql/src/bin.js.map +1 -1
  11. package/out/ast-to-zql/src/format.js.map +1 -1
  12. package/out/datadog/src/datadog-log-sink.js +5 -4
  13. package/out/datadog/src/datadog-log-sink.js.map +1 -1
  14. package/out/otel/src/log-options.js.map +1 -1
  15. package/out/replicache/src/btree/node.js +13 -11
  16. package/out/replicache/src/btree/node.js.map +1 -1
  17. package/out/replicache/src/btree/read.js +4 -2
  18. package/out/replicache/src/btree/read.js.map +1 -1
  19. package/out/replicache/src/btree/splice.js.map +1 -1
  20. package/out/replicache/src/btree/write.js +9 -5
  21. package/out/replicache/src/btree/write.js.map +1 -1
  22. package/out/replicache/src/config.js +1 -2
  23. package/out/replicache/src/connection-loop.js.map +1 -1
  24. package/out/replicache/src/dag/chunk.js +1 -1
  25. package/out/replicache/src/dag/gc.js.map +1 -1
  26. package/out/replicache/src/dag/lazy-store.js +8 -7
  27. package/out/replicache/src/dag/lazy-store.js.map +1 -1
  28. package/out/replicache/src/dag/store-impl.js +1 -1
  29. package/out/replicache/src/dag/store-impl.js.map +1 -1
  30. package/out/replicache/src/dag/visitor.js.map +1 -1
  31. package/out/replicache/src/db/commit.js +5 -3
  32. package/out/replicache/src/db/commit.js.map +1 -1
  33. package/out/replicache/src/db/read.js +1 -1
  34. package/out/replicache/src/db/read.js.map +1 -1
  35. package/out/replicache/src/db/rebase.js.map +1 -1
  36. package/out/replicache/src/db/write.js +5 -2
  37. package/out/replicache/src/db/write.js.map +1 -1
  38. package/out/replicache/src/deleted-clients.js +3 -2
  39. package/out/replicache/src/deleted-clients.js.map +1 -1
  40. package/out/replicache/src/expo-sqlite.js +1 -2
  41. package/out/replicache/src/frozen-json.js +1 -1
  42. package/out/replicache/src/frozen-json.js.map +1 -1
  43. package/out/replicache/src/get-default-puller.js +1 -1
  44. package/out/replicache/src/hash.js +1 -1
  45. package/out/replicache/src/hash.js.map +1 -1
  46. package/out/replicache/src/impl.js +1 -2
  47. package/out/replicache/src/index-defs.js +5 -3
  48. package/out/replicache/src/index-defs.js.map +1 -1
  49. package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
  50. package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
  51. package/out/replicache/src/kv/idb-store.js +1 -1
  52. package/out/replicache/src/kv/idb-store.js.map +1 -1
  53. package/out/replicache/src/kv/mem-fallback-store.js +1 -1
  54. package/out/replicache/src/kv/mem-fallback-store.js.map +1 -1
  55. package/out/replicache/src/kv/mem-store.js.map +1 -1
  56. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  57. package/out/replicache/src/kv/op-sqlite/types.js.map +1 -1
  58. package/out/replicache/src/kv/read-impl.js.map +1 -1
  59. package/out/replicache/src/kv/sqlite-store.js +1 -1
  60. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  61. package/out/replicache/src/kv/write-impl-base.js +4 -1
  62. package/out/replicache/src/kv/write-impl-base.js.map +1 -1
  63. package/out/replicache/src/kv/write-impl.js.map +1 -1
  64. package/out/replicache/src/log-options.js +2 -1
  65. package/out/replicache/src/log-options.js.map +1 -1
  66. package/out/replicache/src/make-idb-name.js +1 -1
  67. package/out/replicache/src/new-client-channel.js +9 -7
  68. package/out/replicache/src/new-client-channel.js.map +1 -1
  69. package/out/replicache/src/on-persist-channel.js +1 -1
  70. package/out/replicache/src/on-persist-channel.js.map +1 -1
  71. package/out/replicache/src/op-sqlite.js +1 -2
  72. package/out/replicache/src/pending-mutations.js +2 -1
  73. package/out/replicache/src/pending-mutations.js.map +1 -1
  74. package/out/replicache/src/persist/client-gc.js +2 -2
  75. package/out/replicache/src/persist/client-gc.js.map +1 -1
  76. package/out/replicache/src/persist/client-group-gc.js +2 -2
  77. package/out/replicache/src/persist/client-group-gc.js.map +1 -1
  78. package/out/replicache/src/persist/client-groups.js +1 -1
  79. package/out/replicache/src/persist/client-groups.js.map +1 -1
  80. package/out/replicache/src/persist/clients.js +1 -1
  81. package/out/replicache/src/persist/clients.js.map +1 -1
  82. package/out/replicache/src/persist/collect-idb-databases.js +5 -4
  83. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  84. package/out/replicache/src/persist/gather-mem-only-visitor.js.map +1 -1
  85. package/out/replicache/src/persist/gather-not-cached-visitor.js.map +1 -1
  86. package/out/replicache/src/persist/heartbeat.js +2 -2
  87. package/out/replicache/src/persist/heartbeat.js.map +1 -1
  88. package/out/replicache/src/persist/idb-databases-store.js +1 -1
  89. package/out/replicache/src/persist/idb-databases-store.js.map +1 -1
  90. package/out/replicache/src/persist/make-client-id.js +1 -1
  91. package/out/replicache/src/persist/make-client-id.js.map +1 -1
  92. package/out/replicache/src/persist/persist.js +3 -2
  93. package/out/replicache/src/persist/persist.js.map +1 -1
  94. package/out/replicache/src/persist/refresh.js +8 -5
  95. package/out/replicache/src/persist/refresh.js.map +1 -1
  96. package/out/replicache/src/process-scheduler.js.map +1 -1
  97. package/out/replicache/src/replicache-impl.js +3 -3
  98. package/out/replicache/src/replicache-impl.js.map +1 -1
  99. package/out/replicache/src/scan-iterator.js.map +1 -1
  100. package/out/replicache/src/scan-options.js +7 -6
  101. package/out/replicache/src/scan-options.js.map +1 -1
  102. package/out/replicache/src/sqlite.js +1 -2
  103. package/out/replicache/src/subscriptions.js +1 -1
  104. package/out/replicache/src/subscriptions.js.map +1 -1
  105. package/out/replicache/src/sync/diff.js +3 -1
  106. package/out/replicache/src/sync/diff.js.map +1 -1
  107. package/out/replicache/src/sync/ids.js.map +1 -1
  108. package/out/replicache/src/sync/patch.js +2 -4
  109. package/out/replicache/src/sync/patch.js.map +1 -1
  110. package/out/replicache/src/sync/pull.js +3 -1
  111. package/out/replicache/src/sync/pull.js.map +1 -1
  112. package/out/replicache/src/sync/push.js.map +1 -1
  113. package/out/replicache/src/sync/request-id.js +1 -1
  114. package/out/replicache/src/sync/request-id.js.map +1 -1
  115. package/out/replicache/src/transactions.js +1 -1
  116. package/out/replicache/src/transactions.js.map +1 -1
  117. package/out/shared/src/arrays.js +4 -1
  118. package/out/shared/src/arrays.js.map +1 -1
  119. package/out/shared/src/asserts.js +1 -1
  120. package/out/shared/src/bigint-json.js.map +1 -1
  121. package/out/shared/src/broadcast-channel.js +1 -1
  122. package/out/shared/src/btree-set.js +11 -8
  123. package/out/shared/src/btree-set.js.map +1 -1
  124. package/out/shared/src/cache.js.map +1 -1
  125. package/out/shared/src/config.js +1 -1
  126. package/out/shared/src/custom-key-map.js.map +1 -1
  127. package/out/shared/src/custom-key-set.js.map +1 -1
  128. package/out/shared/src/deep-clone.js +1 -1
  129. package/out/shared/src/document-visible.js.map +1 -1
  130. package/out/shared/src/dotenv.js.map +1 -1
  131. package/out/shared/src/error.js +1 -1
  132. package/out/shared/src/error.js.map +1 -1
  133. package/out/shared/src/hash.js +2 -2
  134. package/out/shared/src/hash.js.map +1 -1
  135. package/out/shared/src/iterables.js.map +1 -1
  136. package/out/shared/src/json-schema.js.map +1 -1
  137. package/out/shared/src/json.js.map +1 -1
  138. package/out/shared/src/logging-test-utils.js +1 -1
  139. package/out/shared/src/logging.js +1 -1
  140. package/out/shared/src/logging.js.map +1 -1
  141. package/out/shared/src/navigator.js +1 -1
  142. package/out/shared/src/options.js +9 -12
  143. package/out/shared/src/options.js.map +1 -1
  144. package/out/shared/src/promise-race.js.map +1 -1
  145. package/out/shared/src/queue.js.map +1 -1
  146. package/out/shared/src/record-proxy.js.map +1 -1
  147. package/out/shared/src/ring-buffer.js.map +1 -1
  148. package/out/shared/src/size-of-value.js +7 -10
  149. package/out/shared/src/size-of-value.js.map +1 -1
  150. package/out/shared/src/sorted-entries.js.map +1 -1
  151. package/out/shared/src/tdigest-schema.js.map +1 -1
  152. package/out/shared/src/tdigest.js.map +1 -1
  153. package/out/shared/src/valita.js +1 -1
  154. package/out/shared/src/valita.js.map +1 -1
  155. package/out/z2s/src/compiler.js +2 -4
  156. package/out/z2s/src/compiler.js.map +1 -1
  157. package/out/z2s/src/sql.js +1 -1
  158. package/out/z2s/src/sql.js.map +1 -1
  159. package/out/zero/package.js +212 -189
  160. package/out/zero/package.js.map +1 -1
  161. package/out/zero/src/adapters/drizzle.js +1 -2
  162. package/out/zero/src/adapters/kysely.js +1 -2
  163. package/out/zero/src/adapters/pg.js +1 -2
  164. package/out/zero/src/adapters/postgresjs.js +1 -2
  165. package/out/zero/src/adapters/prisma.js +1 -2
  166. package/out/zero/src/analyze.js +1 -2
  167. package/out/zero/src/change-protocol/v0.js +1 -2
  168. package/out/zero/src/expo-sqlite.js +1 -2
  169. package/out/zero/src/op-sqlite.js +1 -2
  170. package/out/zero/src/react-native.js.map +1 -1
  171. package/out/zero/src/sqlite.js +1 -2
  172. package/out/zero/src/zero-cache-dev.js +10 -9
  173. package/out/zero/src/zero-cache-dev.js.map +1 -1
  174. package/out/zero/src/zero-out.js +2 -1
  175. package/out/zero/src/zero-out.js.map +1 -1
  176. package/out/zero-cache/src/auth/auth.js +1 -1
  177. package/out/zero-cache/src/auth/auth.js.map +1 -1
  178. package/out/zero-cache/src/auth/jwt.js +4 -1
  179. package/out/zero-cache/src/auth/jwt.js.map +1 -1
  180. package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
  181. package/out/zero-cache/src/auth/read-authorizer.js +3 -2
  182. package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
  183. package/out/zero-cache/src/auth/write-authorizer.js +5 -16
  184. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  185. package/out/zero-cache/src/config/network.js +1 -1
  186. package/out/zero-cache/src/config/network.js.map +1 -1
  187. package/out/zero-cache/src/config/server-context.js.map +1 -1
  188. package/out/zero-cache/src/config/zero-config.js +2 -2
  189. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  190. package/out/zero-cache/src/custom/fetch.js +1 -1
  191. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  192. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  193. package/out/zero-cache/src/db/create.js.map +1 -1
  194. package/out/zero-cache/src/db/index-predicate.js.map +1 -1
  195. package/out/zero-cache/src/db/lite-tables.js.map +1 -1
  196. package/out/zero-cache/src/db/migration-lite.js +1 -1
  197. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  198. package/out/zero-cache/src/db/migration.js +5 -3
  199. package/out/zero-cache/src/db/migration.js.map +1 -1
  200. package/out/zero-cache/src/db/pg-copy-binary.js +4 -3
  201. package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
  202. package/out/zero-cache/src/db/pg-copy.js +0 -1
  203. package/out/zero-cache/src/db/pg-copy.js.map +1 -1
  204. package/out/zero-cache/src/db/pg-to-lite.js +1 -1
  205. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  206. package/out/zero-cache/src/db/run-transaction.js.map +1 -1
  207. package/out/zero-cache/src/db/specs.js +7 -5
  208. package/out/zero-cache/src/db/specs.js.map +1 -1
  209. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
  210. package/out/zero-cache/src/db/transaction-pool.js +5 -3
  211. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  212. package/out/zero-cache/src/db/warmup.js.map +1 -1
  213. package/out/zero-cache/src/observability/events.js.map +1 -1
  214. package/out/zero-cache/src/observability/log-throttle.js.map +1 -1
  215. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  216. package/out/zero-cache/src/scripts/deploy-permissions.js +4 -2
  217. package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
  218. package/out/zero-cache/src/scripts/permissions.js.map +1 -1
  219. package/out/zero-cache/src/server/anonymous-otel-start.js +2 -1
  220. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  221. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  222. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  223. package/out/zero-cache/src/server/logging.js +1 -1
  224. package/out/zero-cache/src/server/logging.js.map +1 -1
  225. package/out/zero-cache/src/server/main.js.map +1 -1
  226. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  227. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  228. package/out/zero-cache/src/server/reaper.js +2 -2
  229. package/out/zero-cache/src/server/reaper.js.map +1 -1
  230. package/out/zero-cache/src/server/replicator.js.map +1 -1
  231. package/out/zero-cache/src/server/runner/run-worker.js +2 -1
  232. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  233. package/out/zero-cache/src/server/runner/runtime.js +2 -1
  234. package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
  235. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  236. package/out/zero-cache/src/server/shadow-syncer.js +1 -1
  237. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  238. package/out/zero-cache/src/server/slot-keeper.js.map +1 -1
  239. package/out/zero-cache/src/server/syncer-assigner.js.map +1 -1
  240. package/out/zero-cache/src/server/syncer.js.map +1 -1
  241. package/out/zero-cache/src/server/worker-dispatcher.js +1 -1
  242. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  243. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  244. package/out/zero-cache/src/services/analyze.js +1 -1
  245. package/out/zero-cache/src/services/analyze.js.map +1 -1
  246. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +1 -1
  247. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  248. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  249. package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -1
  250. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
  251. package/out/zero-cache/src/services/change-source/common/replica-schema.js +3 -3
  252. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  253. package/out/zero-cache/src/services/change-source/custom/change-source.js +1 -1
  254. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  255. package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
  256. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +3 -3
  257. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  258. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +1 -1
  259. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
  260. package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -6
  261. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  262. package/out/zero-cache/src/services/change-source/pg/decommission.js +2 -1
  263. package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
  264. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +5 -3
  265. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  266. package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
  267. package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
  268. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +3 -2
  269. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  270. package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
  271. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js +2 -2
  272. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js.map +1 -1
  273. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js +1 -1
  274. package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js.map +1 -1
  275. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +3 -3
  276. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  277. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +1 -1
  278. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  279. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +13 -12
  280. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
  281. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  282. package/out/zero-cache/src/services/change-source/pg/schema/published.js +1 -1
  283. package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
  284. package/out/zero-cache/src/services/change-source/pg/schema/replica-stage-enum.js.map +1 -1
  285. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +9 -6
  286. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  287. package/out/zero-cache/src/services/change-source/pg/schema/validation.js +1 -1
  288. package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
  289. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +1 -1
  290. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -1
  291. package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
  292. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  293. package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
  294. package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
  295. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  296. package/out/zero-cache/src/services/change-source/protocol/current.js +1 -1
  297. package/out/zero-cache/src/services/change-source/protocol/mod.js +1 -2
  298. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
  299. package/out/zero-cache/src/services/change-streamer/backfill-state.js +1 -1
  300. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -1
  301. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
  302. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
  303. package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
  304. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  305. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +2 -2
  306. package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
  307. package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -1
  308. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -1
  309. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  310. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +8 -3
  311. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  312. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +23 -23
  313. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  314. package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
  315. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  316. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts +4 -4
  317. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts.map +1 -1
  318. package/out/zero-cache/src/services/change-streamer/drain-tracker.js +4 -8
  319. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
  320. package/out/zero-cache/src/services/change-streamer/forwarder.js +11 -10
  321. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  322. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js +1 -1
  323. package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js.map +1 -1
  324. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +3 -3
  325. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
  326. package/out/zero-cache/src/services/change-streamer/replica-poller.js +1 -1
  327. package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -1
  328. package/out/zero-cache/src/services/change-streamer/restore-admission.js +12 -1
  329. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
  330. package/out/zero-cache/src/services/change-streamer/schema/init.js +1 -1
  331. package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
  332. package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
  333. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  334. package/out/zero-cache/src/services/change-streamer/snapshot-message.js +1 -1
  335. package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
  336. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  337. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +1 -1
  338. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
  339. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js +1 -1
  340. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js.map +1 -1
  341. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -1
  342. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
  343. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +1 -1
  344. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -1
  345. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +1 -3
  346. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
  347. package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
  348. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  349. package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
  350. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  351. package/out/zero-cache/src/services/change-streamer/subscriber.js +2 -2
  352. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  353. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +0 -1
  354. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
  355. package/out/zero-cache/src/services/heapz.js +2 -1
  356. package/out/zero-cache/src/services/heapz.js.map +1 -1
  357. package/out/zero-cache/src/services/http-service.d.ts +6 -0
  358. package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
  359. package/out/zero-cache/src/services/http-service.js +11 -0
  360. package/out/zero-cache/src/services/http-service.js.map +1 -1
  361. package/out/zero-cache/src/services/life-cycle.d.ts +3 -2
  362. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  363. package/out/zero-cache/src/services/life-cycle.js +18 -5
  364. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  365. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js +4 -2
  366. package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
  367. package/out/zero-cache/src/services/litestream/commands.js +1 -1
  368. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  369. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +2 -2
  370. package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
  371. package/out/zero-cache/src/services/litestream/litestream-controller.js.map +1 -1
  372. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  373. package/out/zero-cache/src/services/litestream/restore-progress.js.map +1 -1
  374. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  375. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  376. package/out/zero-cache/src/services/mutagen/pusher.js +3 -2
  377. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  378. package/out/zero-cache/src/services/plannerz.js +4 -3
  379. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  380. package/out/zero-cache/src/services/profz.js +5 -3
  381. package/out/zero-cache/src/services/profz.js.map +1 -1
  382. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  383. package/out/zero-cache/src/services/replicator/change-log-db.js +3 -2
  384. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
  385. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
  386. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
  387. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  388. package/out/zero-cache/src/services/replicator/drain-handler.d.ts +8 -0
  389. package/out/zero-cache/src/services/replicator/drain-handler.d.ts.map +1 -1
  390. package/out/zero-cache/src/services/replicator/drain-handler.js +38 -4
  391. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
  392. package/out/zero-cache/src/services/replicator/incremental-sync.js +1 -1
  393. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  394. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  395. package/out/zero-cache/src/services/replicator/replication-status.js +4 -3
  396. package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
  397. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  398. package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
  399. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  400. package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
  401. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
  402. package/out/zero-cache/src/services/replicator/schema/backfilling.js +1 -1
  403. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  404. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  405. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  406. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  407. package/out/zero-cache/src/services/replicator/schema/replication-state.js +10 -7
  408. package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
  409. package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
  410. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +1 -1
  411. package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
  412. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +1 -1
  413. package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -1
  414. package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
  415. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  416. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  417. package/out/zero-cache/src/services/run-ast.js.map +1 -1
  418. package/out/zero-cache/src/services/runner.js.map +1 -1
  419. package/out/zero-cache/src/services/running-state.js.map +1 -1
  420. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  421. package/out/zero-cache/src/services/statz.js +2 -1
  422. package/out/zero-cache/src/services/statz.js.map +1 -1
  423. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +2 -2
  424. package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
  425. package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -1
  426. package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -1
  427. package/out/zero-cache/src/services/view-syncer/client-handler.js +4 -2
  428. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  429. package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
  430. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  431. package/out/zero-cache/src/services/view-syncer/cvr-purger.js +2 -2
  432. package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
  433. package/out/zero-cache/src/services/view-syncer/cvr-store.js +9 -5
  434. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  435. package/out/zero-cache/src/services/view-syncer/cvr.js +3 -2
  436. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  437. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +1 -1
  438. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -1
  439. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  440. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -1
  441. package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -1
  442. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +2 -2
  443. package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -1
  444. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +5 -3
  445. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  446. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +6 -4
  447. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  448. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -1
  449. package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -1
  450. package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -1
  451. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
  452. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -1
  453. package/out/zero-cache/src/services/view-syncer/query-stats.js +1 -1
  454. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
  455. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  456. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
  457. package/out/zero-cache/src/services/view-syncer/schema/types.js +1 -1
  458. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  459. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js +7 -1
  460. package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js.map +1 -1
  461. package/out/zero-cache/src/services/view-syncer/snapshotter.js +3 -2
  462. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  463. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
  464. package/out/zero-cache/src/services/view-syncer/view-syncer.js +4 -6
  465. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  466. package/out/zero-cache/src/types/error-with-level.js.map +1 -1
  467. package/out/zero-cache/src/types/lexi-version.js.map +1 -1
  468. package/out/zero-cache/src/types/lite.js +1 -1
  469. package/out/zero-cache/src/types/lite.js.map +1 -1
  470. package/out/zero-cache/src/types/pg-data-type.js +1 -1
  471. package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
  472. package/out/zero-cache/src/types/pg.js +6 -4
  473. package/out/zero-cache/src/types/pg.js.map +1 -1
  474. package/out/zero-cache/src/types/poke-chunk.js +2 -2
  475. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  476. package/out/zero-cache/src/types/processes.js +1 -1
  477. package/out/zero-cache/src/types/processes.js.map +1 -1
  478. package/out/zero-cache/src/types/profiler.js.map +1 -1
  479. package/out/zero-cache/src/types/row-key.js.map +1 -1
  480. package/out/zero-cache/src/types/sql.js.map +1 -1
  481. package/out/zero-cache/src/types/streams.js +10 -9
  482. package/out/zero-cache/src/types/streams.js.map +1 -1
  483. package/out/zero-cache/src/types/strings.js.map +1 -1
  484. package/out/zero-cache/src/types/subscription.js.map +1 -1
  485. package/out/zero-cache/src/types/timeout.js +1 -1
  486. package/out/zero-cache/src/types/websocket-handoff.js +1 -1
  487. package/out/zero-cache/src/types/ws.js +1 -1
  488. package/out/zero-cache/src/workers/connection.js +2 -3
  489. package/out/zero-cache/src/workers/connection.js.map +1 -1
  490. package/out/zero-cache/src/workers/mutator.js.map +1 -1
  491. package/out/zero-cache/src/workers/replicator.js +5 -4
  492. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  493. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  494. package/out/zero-cache/src/workers/syncer.js +2 -2
  495. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  496. package/out/zero-client/src/client/active-clients-manager.js +1 -1
  497. package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
  498. package/out/zero-client/src/client/bindings.js +11 -11
  499. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  500. package/out/zero-client/src/client/connection.js +1 -1
  501. package/out/zero-client/src/client/connection.js.map +1 -1
  502. package/out/zero-client/src/client/context.js.map +1 -1
  503. package/out/zero-client/src/client/crud-impl.js +1 -1
  504. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  505. package/out/zero-client/src/client/crud.js +1 -1
  506. package/out/zero-client/src/client/crud.js.map +1 -1
  507. package/out/zero-client/src/client/custom.js +1 -1
  508. package/out/zero-client/src/client/custom.js.map +1 -1
  509. package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
  510. package/out/zero-client/src/client/enable-analytics.js +1 -1
  511. package/out/zero-client/src/client/enable-analytics.js.map +1 -1
  512. package/out/zero-client/src/client/error.js.map +1 -1
  513. package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
  514. package/out/zero-client/src/client/inspector/client.js.map +1 -1
  515. package/out/zero-client/src/client/inspector/html-dialog-prompt.js +1 -1
  516. package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
  517. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  518. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  519. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  520. package/out/zero-client/src/client/ivm-branch.js +1 -3
  521. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  522. package/out/zero-client/src/client/keys.js +2 -1
  523. package/out/zero-client/src/client/keys.js.map +1 -1
  524. package/out/zero-client/src/client/log-options.js.map +1 -1
  525. package/out/zero-client/src/client/make-replicache-mutators.js +5 -3
  526. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  527. package/out/zero-client/src/client/metrics.js +3 -5
  528. package/out/zero-client/src/client/metrics.js.map +1 -1
  529. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  530. package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
  531. package/out/zero-client/src/client/options.js.map +1 -1
  532. package/out/zero-client/src/client/query-manager.js +2 -2
  533. package/out/zero-client/src/client/query-manager.js.map +1 -1
  534. package/out/zero-client/src/client/reload-error-handler.js +4 -3
  535. package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
  536. package/out/zero-client/src/client/version.js +1 -1
  537. package/out/zero-client/src/client/zero-poke-handler.js +1 -1
  538. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  539. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  540. package/out/zero-client/src/client/zero.js +19 -11
  541. package/out/zero-client/src/client/zero.js.map +1 -1
  542. package/out/zero-client/src/mod.js +23 -23
  543. package/out/zero-client/src/util/nanoid.js.map +1 -1
  544. package/out/zero-permissions/src/permissions.js +3 -2
  545. package/out/zero-permissions/src/permissions.js.map +1 -1
  546. package/out/zero-pg/src/mod.js +9 -9
  547. package/out/zero-protocol/src/analyze-query-result.js +1 -1
  548. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  549. package/out/zero-protocol/src/application-error.js.map +1 -1
  550. package/out/zero-protocol/src/ast.js +19 -9
  551. package/out/zero-protocol/src/ast.js.map +1 -1
  552. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  553. package/out/zero-protocol/src/client-schema.js +1 -1
  554. package/out/zero-protocol/src/client-schema.js.map +1 -1
  555. package/out/zero-protocol/src/close-connection.js +1 -1
  556. package/out/zero-protocol/src/close-connection.js.map +1 -1
  557. package/out/zero-protocol/src/connect.js +1 -1
  558. package/out/zero-protocol/src/connect.js.map +1 -1
  559. package/out/zero-protocol/src/custom-queries.js +1 -1
  560. package/out/zero-protocol/src/custom-queries.js.map +1 -1
  561. package/out/zero-protocol/src/data.js +4 -2
  562. package/out/zero-protocol/src/data.js.map +1 -1
  563. package/out/zero-protocol/src/delete-clients.js.map +1 -1
  564. package/out/zero-protocol/src/down.js.map +1 -1
  565. package/out/zero-protocol/src/error.js.map +1 -1
  566. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  567. package/out/zero-protocol/src/inspect-down.js +1 -1
  568. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  569. package/out/zero-protocol/src/inspect-up.js +1 -1
  570. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  571. package/out/zero-protocol/src/mutate-server.js +1 -1
  572. package/out/zero-protocol/src/mutate-server.js.map +1 -1
  573. package/out/zero-protocol/src/mutation-id.js.map +1 -1
  574. package/out/zero-protocol/src/mutation.js +1 -1
  575. package/out/zero-protocol/src/mutation.js.map +1 -1
  576. package/out/zero-protocol/src/mutations-patch.js +1 -1
  577. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  578. package/out/zero-protocol/src/ping.js +1 -1
  579. package/out/zero-protocol/src/ping.js.map +1 -1
  580. package/out/zero-protocol/src/poke.js +1 -1
  581. package/out/zero-protocol/src/poke.js.map +1 -1
  582. package/out/zero-protocol/src/pong.js +1 -1
  583. package/out/zero-protocol/src/pong.js.map +1 -1
  584. package/out/zero-protocol/src/primary-key.js.map +1 -1
  585. package/out/zero-protocol/src/protocol-version.js +1 -0
  586. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  587. package/out/zero-protocol/src/pull.js +1 -1
  588. package/out/zero-protocol/src/pull.js.map +1 -1
  589. package/out/zero-protocol/src/push.js +1 -1
  590. package/out/zero-protocol/src/push.js.map +1 -1
  591. package/out/zero-protocol/src/queries-patch.js +1 -1
  592. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  593. package/out/zero-protocol/src/query-hash-visitor.js.map +1 -1
  594. package/out/zero-protocol/src/query-hash.js +2 -2
  595. package/out/zero-protocol/src/query-server.js +1 -1
  596. package/out/zero-protocol/src/query-server.js.map +1 -1
  597. package/out/zero-protocol/src/row-patch.js.map +1 -1
  598. package/out/zero-protocol/src/up.js.map +1 -1
  599. package/out/zero-protocol/src/update-auth.js.map +1 -1
  600. package/out/zero-protocol/src/version.js.map +1 -1
  601. package/out/zero-react/src/bindings.js +11 -11
  602. package/out/zero-react/src/mod.js +5 -5
  603. package/out/zero-react/src/use-query.js +1 -1
  604. package/out/zero-react/src/use-query.js.map +1 -1
  605. package/out/zero-react/src/zero-provider.d.ts +1 -1
  606. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  607. package/out/zero-react/src/zero-provider.js +2 -1
  608. package/out/zero-react/src/zero-provider.js.map +1 -1
  609. package/out/zero-react/src/zero.js +23 -23
  610. package/out/zero-schema/src/builder/schema-builder.js +3 -2
  611. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  612. package/out/zero-schema/src/builder/table-builder.js +1 -1
  613. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  614. package/out/zero-schema/src/compiled-permissions.js.map +1 -1
  615. package/out/zero-schema/src/name-mapper.js +7 -5
  616. package/out/zero-schema/src/name-mapper.js.map +1 -1
  617. package/out/zero-schema/src/schema-config.js +1 -1
  618. package/out/zero-schema/src/schema-config.js.map +1 -1
  619. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  620. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  621. package/out/zero-server/src/adapters/pg.js.map +1 -1
  622. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  623. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  624. package/out/zero-server/src/closable-db-transaction.js.map +1 -1
  625. package/out/zero-server/src/custom.js +1 -1
  626. package/out/zero-server/src/custom.js.map +1 -1
  627. package/out/zero-server/src/mod.js +8 -8
  628. package/out/zero-server/src/process-mutations.js.map +1 -1
  629. package/out/zero-server/src/push-processor.js.map +1 -1
  630. package/out/zero-server/src/queries/process-queries.js +2 -1
  631. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  632. package/out/zero-server/src/schema.js +7 -7
  633. package/out/zero-server/src/schema.js.map +1 -1
  634. package/out/zero-server/src/zql-database.js.map +1 -1
  635. package/out/zero-solid/src/bindings.js +11 -11
  636. package/out/zero-solid/src/mod.js +5 -5
  637. package/out/zero-solid/src/solid-view.js +1 -1
  638. package/out/zero-solid/src/solid-view.js.map +1 -1
  639. package/out/zero-solid/src/use-connection-state.js +2 -1
  640. package/out/zero-solid/src/use-connection-state.js.map +1 -1
  641. package/out/zero-solid/src/use-query.js.map +1 -1
  642. package/out/zero-solid/src/use-zero-online.js +2 -1
  643. package/out/zero-solid/src/use-zero-online.js.map +1 -1
  644. package/out/zero-solid/src/use-zero.js +3 -2
  645. package/out/zero-solid/src/use-zero.js.map +1 -1
  646. package/out/zero-solid/src/zero.js +23 -23
  647. package/out/zero-types/src/name-mapper.js.map +1 -1
  648. package/out/zql/src/builder/builder.js +15 -13
  649. package/out/zql/src/builder/builder.js.map +1 -1
  650. package/out/zql/src/builder/correlated-predicate-pushdown.js.map +1 -1
  651. package/out/zql/src/builder/debug-delegate.js.map +1 -1
  652. package/out/zql/src/builder/like.js +0 -1
  653. package/out/zql/src/builder/like.js.map +1 -1
  654. package/out/zql/src/ivm/array-view.js.map +1 -1
  655. package/out/zql/src/ivm/cap.js.map +1 -1
  656. package/out/zql/src/ivm/constraint.js +1 -1
  657. package/out/zql/src/ivm/constraint.js.map +1 -1
  658. package/out/zql/src/ivm/data.js.map +1 -1
  659. package/out/zql/src/ivm/deferred-input.js.map +1 -1
  660. package/out/zql/src/ivm/exists.js +20 -18
  661. package/out/zql/src/ivm/exists.js.map +1 -1
  662. package/out/zql/src/ivm/fan-in.js.map +1 -1
  663. package/out/zql/src/ivm/fan-out.js.map +1 -1
  664. package/out/zql/src/ivm/filter-operators.js +1 -1
  665. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  666. package/out/zql/src/ivm/filter-push.js.map +1 -1
  667. package/out/zql/src/ivm/filter.js.map +1 -1
  668. package/out/zql/src/ivm/flipped-join.js +1 -3
  669. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  670. package/out/zql/src/ivm/join-index-tap.js.map +1 -1
  671. package/out/zql/src/ivm/join-index.js.map +1 -1
  672. package/out/zql/src/ivm/join-utils.js +11 -13
  673. package/out/zql/src/ivm/join-utils.js.map +1 -1
  674. package/out/zql/src/ivm/join.js.map +1 -1
  675. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
  676. package/out/zql/src/ivm/memory-source.js +26 -22
  677. package/out/zql/src/ivm/memory-source.js.map +1 -1
  678. package/out/zql/src/ivm/memory-storage.js.map +1 -1
  679. package/out/zql/src/ivm/operator.js +2 -2
  680. package/out/zql/src/ivm/push-accumulated.js +0 -1
  681. package/out/zql/src/ivm/push-accumulated.js.map +1 -1
  682. package/out/zql/src/ivm/skip-yields.js.map +1 -1
  683. package/out/zql/src/ivm/skip.js.map +1 -1
  684. package/out/zql/src/ivm/take-gate.js.map +1 -1
  685. package/out/zql/src/ivm/take.js +1 -1
  686. package/out/zql/src/ivm/take.js.map +1 -1
  687. package/out/zql/src/ivm/union-fan-in.js +1 -1
  688. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  689. package/out/zql/src/ivm/union-fan-out.js.map +1 -1
  690. package/out/zql/src/ivm/view-apply-change.js +1 -1
  691. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  692. package/out/zql/src/mutate/crud.js +3 -2
  693. package/out/zql/src/mutate/crud.js.map +1 -1
  694. package/out/zql/src/planner/planner-builder.js +1 -1
  695. package/out/zql/src/planner/planner-builder.js.map +1 -1
  696. package/out/zql/src/planner/planner-connection.js.map +1 -1
  697. package/out/zql/src/planner/planner-debug.js.map +1 -1
  698. package/out/zql/src/planner/planner-fan-in.js.map +1 -1
  699. package/out/zql/src/planner/planner-fan-out.js.map +1 -1
  700. package/out/zql/src/planner/planner-filter.js.map +1 -1
  701. package/out/zql/src/planner/planner-graph.js +1 -5
  702. package/out/zql/src/planner/planner-graph.js.map +1 -1
  703. package/out/zql/src/planner/planner-join.js +1 -1
  704. package/out/zql/src/planner/planner-join.js.map +1 -1
  705. package/out/zql/src/planner/planner-source.js.map +1 -1
  706. package/out/zql/src/planner/planner-terminus.js.map +1 -1
  707. package/out/zql/src/query/create-builder.js +2 -1
  708. package/out/zql/src/query/create-builder.js.map +1 -1
  709. package/out/zql/src/query/expression.js +1 -1
  710. package/out/zql/src/query/expression.js.map +1 -1
  711. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  712. package/out/zql/src/query/named.js +2 -1
  713. package/out/zql/src/query/named.js.map +1 -1
  714. package/out/zql/src/query/query-delegate-base.js +1 -1
  715. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  716. package/out/zql/src/query/query-impl.js +3 -2
  717. package/out/zql/src/query/query-impl.js.map +1 -1
  718. package/out/zql/src/query/query-registry.js +1 -1
  719. package/out/zql/src/query/query-registry.js.map +1 -1
  720. package/out/zql/src/query/query-transitions.js +1 -1
  721. package/out/zql/src/query/query-transitions.js.map +1 -1
  722. package/out/zql/src/query/runnable-query-impl.js +3 -2
  723. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  724. package/out/zql/src/query/static-query.js.map +1 -1
  725. package/out/zql/src/query/ttl.js +8 -8
  726. package/out/zql/src/query/ttl.js.map +1 -1
  727. package/out/zql/src/query/validate-input.js.map +1 -1
  728. package/out/zqlite/src/database-storage.js +2 -2
  729. package/out/zqlite/src/database-storage.js.map +1 -1
  730. package/out/zqlite/src/db.js +2 -2
  731. package/out/zqlite/src/db.js.map +1 -1
  732. package/out/zqlite/src/explain-queries.js.map +1 -1
  733. package/out/zqlite/src/index-usage.js.map +1 -1
  734. package/out/zqlite/src/internal/sql-inline.js.map +1 -1
  735. package/out/zqlite/src/internal/sql.js.map +1 -1
  736. package/out/zqlite/src/internal/statement-cache.js +1 -1
  737. package/out/zqlite/src/internal/statement-cache.js.map +1 -1
  738. package/out/zqlite/src/mod.js +5 -5
  739. package/out/zqlite/src/pending-delta.js.map +1 -1
  740. package/out/zqlite/src/query-builder.js +1 -1
  741. package/out/zqlite/src/query-builder.js.map +1 -1
  742. package/out/zqlite/src/query-delegate.js.map +1 -1
  743. package/out/zqlite/src/resolve-scalar-subqueries.js +2 -5
  744. package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
  745. package/out/zqlite/src/sqlite-cost-model.js +3 -2
  746. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  747. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  748. package/out/zqlite/src/table-source.js +1 -1
  749. package/out/zqlite/src/table-source.js.map +1 -1
  750. package/package.json +16 -14
@@ -1 +1 @@
1
- {"version":3,"file":"batch-pacer.js","names":["#shareTarget","#maxIntervalMs","#now","#utilization","#lc","#waiting","#intervalStart","#update","#multiplier","#waitsBelowMax","#waitsAtMax","#share","#saturatedSeconds","#logPacing","#notPacingSince","#pacingLoggedAt","#handingOutSlices","#nextSlice","#hydrationSlices"],"sources":["../../../../../../zero-cache/src/services/view-syncer/batch-pacer.ts"],"sourcesContent":["import {performance} from 'node:perf_hooks';\nimport type {LogContext} from '@rocicorp/logger';\n\n/** How often the multiplier is updated. */\nconst UPDATE_INTERVAL_MS = 1000;\n\n/**\n * The weight of the latest second in the worker's smoothed busy share.\n * Client groups that wait for seconds between batches make the share of\n * any one second jump between idle and saturated, and a worker that keeps\n * up has the odd busy second. Neither says what the load is.\n */\nconst SHARE_SMOOTHING = 0.3;\n\n/**\n * How much of the difference between the smoothed busy share and the target\n * each update corrects for. The smoothed share only shows part of what the\n * previous updates did, so correcting for all of the difference each time\n * would overshoot and oscillate.\n */\nconst CORRECTION_EXPONENT = 0.3;\n\n/**\n * The share of its time above which a worker has no time to spare. A share\n * target has to stay well clear of this to be held, which is why\n * `syncUpdateShareTarget` is at most 0.9 (or 1, which no worker is over).\n */\nconst SATURATED_SHARE = 0.95;\n\n/** The seconds that a worker has to be saturated for to be treated as such. */\nconst SATURATED_SECONDS = 2;\n\n/**\n * How much `1 + k` is scaled by each second that the worker is saturated.\n * A saturated worker does not show how much more its client groups would\n * use, which can be many times what it has, so the multiplier is raised by\n * more than the difference from the target calls for until it does.\n */\nconst SATURATED_CORRECTION = 1.3;\n\n/**\n * How often a worker that is pacing its client groups says so in its log,\n * and how long it has to have stopped for to say that it has.\n */\nconst PACING_LOG_INTERVAL_MS = 60_000;\n\n/**\n * The most updates that are made up for when nothing has asked the pacer\n * for a wait in a while, which is enough for the multiplier to get to 0.\n */\nconst MAX_CATCH_UP_UPDATES = 60;\n\n/**\n * Bounds the multiplier, which otherwise has no limit while a worker stays\n * over the target however long its client groups wait.\n */\nconst MAX_MULTIPLIER = 1_000_000;\n\nexport type BatchPacerOptions = {\n /**\n * The largest share of the worker's time that it may be busy before its\n * client groups are paced.\n */\n readonly shareTarget: number;\n /** The longest a client group waits between batches. */\n readonly maxIntervalMs: number;\n readonly now?: (() => number) | undefined;\n /**\n * The share of the time since the previous call that the worker was busy.\n * By default, the utilization of the event loop.\n */\n readonly utilization?: (() => number) | undefined;\n /** Where the worker says that it is pacing its client groups. */\n readonly lc?: LogContext | undefined;\n};\n\nfunction eventLoopUtilization(): () => number {\n let last = performance.eventLoopUtilization();\n return () => {\n const current = performance.eventLoopUtilization();\n const {utilization} = performance.eventLoopUtilization(current, last);\n last = current;\n return utilization;\n };\n}\n\n/**\n * Paces the batches (advancements, including the queries they hydrate\n * again) of the client groups of a sync worker, so that the worker is left\n * with time for new hydrations and connections, and gives the hydrations of\n * new queries priority over the batches.\n *\n * **Pacing.** After each batch, a client group waits {@link waitMs} before\n * it starts the next. The wait is a multiple of the batch's processing\n * time, so a client group that always has changes waiting is busy at most\n * `1 / (1 + k)` of the time, where `k` is the {@link multiplier}. Because\n * the wait is in proportion to a client group's own processing time, those\n * that cause the load are slowed the most, and cheap ones barely at all.\n *\n * The multiplier is shared by the client groups of the worker. Once per\n * second, it is scaled by how far the share of its time that the worker was\n * busy, smoothed over a few seconds, is from the target, and by more while\n * the worker has no time to spare. It never goes below 0, where no client\n * group waits. The share is the utilization of the worker's event loop, i.e. all\n * of what the worker does: what a batch costs is spread over the pipelines,\n * the CVR, the pokes and garbage collection, and is not measurable batch by\n * batch.\n *\n * **Priority of new hydrations.** The worker's time slices are handed out\n * by the pacer (see {@link yieldSlice}), and while the hydration of new\n * queries is waiting for one, most of them go to it rather than to batches.\n */\nexport class BatchPacer {\n readonly #shareTarget: number;\n readonly #maxIntervalMs: number;\n readonly #now: () => number;\n readonly #utilization: () => number;\n readonly #lc: LogContext | undefined;\n\n /** When the worker last logged that it is pacing, while it still is. */\n #pacingLoggedAt: number | undefined;\n /** Since when the worker has not been pacing, after it logged that it was. */\n #notPacingSince: number | undefined;\n\n #intervalStart: number;\n #multiplier = 0;\n /** The smoothed share of its time that the worker was busy. */\n #share = 0;\n #saturatedSeconds = 0;\n /** The waits handed out since the last update, by whether at the cap. */\n #waitsBelowMax = 0;\n #waitsAtMax = 0;\n\n /** The work that is waiting for a time slice, in the order it yielded. */\n readonly #waiting = {\n hydration: [] as (() => void)[],\n batch: [] as (() => void)[],\n };\n #handingOutSlices = false;\n /** The slices that have gone to hydrations since one went to a batch. */\n #hydrationSlices = 0;\n\n constructor({\n shareTarget,\n maxIntervalMs,\n now,\n utilization,\n lc,\n }: BatchPacerOptions) {\n this.#shareTarget = shareTarget;\n this.#maxIntervalMs = maxIntervalMs;\n this.#now = now ?? (() => performance.now());\n this.#utilization = utilization ?? eventLoopUtilization();\n this.#lc = lc;\n this.#intervalStart = this.#now();\n }\n\n /**\n * How long a client group should wait before its next batch, after one\n * that took `processingMs` to process. The multiplier changes while a\n * client group waits, so a long wait should be taken in steps, asking\n * again after each.\n */\n waitMs(processingMs: number): number {\n this.#update();\n const wait = this.#multiplier * processingMs;\n if (wait < this.#maxIntervalMs) {\n this.#waitsBelowMax++;\n return wait;\n }\n this.#waitsAtMax++;\n return this.#maxIntervalMs;\n }\n\n /** `k`: a client group waits `k` times the processing time of a batch. */\n get multiplier(): number {\n this.#update();\n return this.#multiplier;\n }\n\n /**\n * The share of its time that the worker was busy, smoothed over the last\n * few seconds, as of the last update of the multiplier.\n */\n get share(): number {\n this.#update();\n return this.#share;\n }\n\n /** Updates the multiplier for each interval that has ended. */\n #update(): void {\n const now = this.#now();\n const ended = Math.floor((now - this.#intervalStart) / UPDATE_INTERVAL_MS);\n if (ended <= 0) {\n return;\n }\n this.#intervalStart += ended * UPDATE_INTERVAL_MS;\n // If more than one interval has ended, nothing asked the pacer for a\n // wait in the meantime, and the utilization since then is that of each.\n const latest = this.#utilization();\n // Once every client group is told to wait the longest it can, raising\n // the multiplier changes nothing, and only makes it take longer to come\n // back down. A worker that is still over its target then is busy with\n // something that making its client groups wait does not relieve.\n const allAtMax = this.#waitsAtMax > 0 && this.#waitsBelowMax === 0;\n this.#waitsAtMax = 0;\n this.#waitsBelowMax = 0;\n for (let i = Math.min(ended, MAX_CATCH_UP_UPDATES); i > 0; i--) {\n this.#share += SHARE_SMOOTHING * (latest - this.#share);\n // A worker that is allowed to be this busy is only raised the\n // multiplier for what it is over its target by.\n this.#saturatedSeconds =\n latest >= SATURATED_SHARE && latest > this.#shareTarget\n ? this.#saturatedSeconds + 1\n : 0;\n let correction =\n this.#saturatedSeconds >= SATURATED_SECONDS\n ? SATURATED_CORRECTION\n : (this.#share / this.#shareTarget) ** CORRECTION_EXPONENT;\n if (allAtMax && correction > 1) {\n correction = 1;\n }\n this.#multiplier = Math.min(\n Math.max((1 + this.#multiplier) * correction - 1, 0),\n MAX_MULTIPLIER,\n );\n }\n this.#logPacing(now, allAtMax);\n }\n\n /**\n * Says in the worker's log when it starts to pace its client groups, once\n * a minute while it does, and when it has stopped. The pacing of a client\n * group is logged by its view syncer.\n */\n #logPacing(now: number, allAtMax: boolean): void {\n const lc = this.#lc;\n if (lc === undefined) {\n return;\n }\n const state = {\n busyShare: this.#share,\n shareTarget: this.#shareTarget,\n waitMultiplier: this.#multiplier,\n maxBatchIntervalMs: this.#maxIntervalMs,\n };\n if (this.#multiplier > 0) {\n this.#notPacingSince = undefined;\n if (this.#pacingLoggedAt === undefined) {\n lc.info?.(\n 'Sync worker is over its share target: its client groups wait between batches',\n state,\n );\n this.#pacingLoggedAt = now;\n } else if (now - this.#pacingLoggedAt >= PACING_LOG_INTERVAL_MS) {\n if (allAtMax && this.#share > this.#shareTarget) {\n // Waiting longer is not an option, so pacing cannot bring the\n // worker to its target: it has more to do than it has time for.\n lc.warn?.(\n 'Sync worker is over its share target although every client group waits the longest it may',\n state,\n );\n } else {\n lc.info?.('Sync worker is pacing its client groups', state);\n }\n this.#pacingLoggedAt = now;\n }\n } else if (this.#pacingLoggedAt !== undefined) {\n // A worker around its target goes in and out of pacing. It only says\n // that it has stopped once it has stayed out.\n this.#notPacingSince ??= now;\n if (now - this.#notPacingSince >= PACING_LOG_INTERVAL_MS) {\n lc.info?.(\n 'Sync worker is under its share target: its client groups no longer wait',\n state,\n );\n this.#pacingLoggedAt = undefined;\n this.#notPacingSince = undefined;\n }\n }\n }\n\n /**\n * Yields the worker to the other work that is waiting for a time slice,\n * and resolves when it is the caller's turn again. One piece of work runs\n * per turn of the event loop, as when the work of a worker's client groups\n * simply takes turns, except that the hydration of new queries goes first:\n * while both kinds of work are waiting, `HYDRATION_PRIORITY` slices go to\n * hydrations for each one that goes to a batch.\n *\n * Batches are never made to wait for anything but a slice. If no\n * hydration is waiting for one (because none is running, or because those\n * that are running are waiting for I/O), batches get every slice. And\n * however many hydrations there are, batches keep a share of the slices,\n * so they keep moving, and so do the snapshots of the replica that they\n * hold.\n */\n yieldSlice(kind: 'hydration' | 'batch'): Promise<void> {\n return new Promise(resolve => {\n this.#waiting[kind].push(resolve);\n if (!this.#handingOutSlices) {\n this.#handingOutSlices = true;\n setImmediate(this.#nextSlice);\n }\n });\n }\n\n readonly #nextSlice = () => {\n const {hydration, batch} = this.#waiting;\n let next: (() => void) | undefined;\n if (\n hydration.length > 0 &&\n (batch.length === 0 || this.#hydrationSlices < HYDRATION_PRIORITY)\n ) {\n next = hydration.shift();\n this.#hydrationSlices++;\n } else if (batch.length > 0) {\n next = batch.shift();\n this.#hydrationSlices = 0;\n }\n if (next === undefined) {\n this.#handingOutSlices = false;\n return;\n }\n next();\n // After the work that was just resumed has run its slice.\n setImmediate(this.#nextSlice);\n };\n}\n\n/**\n * How many time slices go to the hydration of new queries for each one that\n * goes to a batch, while both are waiting for one.\n */\nconst HYDRATION_PRIORITY = 9;\n"],"mappings":"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"}
1
+ {"version":3,"file":"batch-pacer.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/batch-pacer.ts"],"sourcesContent":["import {performance} from 'node:perf_hooks';\nimport type {LogContext} from '@rocicorp/logger';\n\n/** How often the multiplier is updated. */\nconst UPDATE_INTERVAL_MS = 1000;\n\n/**\n * The weight of the latest second in the worker's smoothed busy share.\n * Client groups that wait for seconds between batches make the share of\n * any one second jump between idle and saturated, and a worker that keeps\n * up has the odd busy second. Neither says what the load is.\n */\nconst SHARE_SMOOTHING = 0.3;\n\n/**\n * How much of the difference between the smoothed busy share and the target\n * each update corrects for. The smoothed share only shows part of what the\n * previous updates did, so correcting for all of the difference each time\n * would overshoot and oscillate.\n */\nconst CORRECTION_EXPONENT = 0.3;\n\n/**\n * The share of its time above which a worker has no time to spare. A share\n * target has to stay well clear of this to be held, which is why\n * `syncUpdateShareTarget` is at most 0.9 (or 1, which no worker is over).\n */\nconst SATURATED_SHARE = 0.95;\n\n/** The seconds that a worker has to be saturated for to be treated as such. */\nconst SATURATED_SECONDS = 2;\n\n/**\n * How much `1 + k` is scaled by each second that the worker is saturated.\n * A saturated worker does not show how much more its client groups would\n * use, which can be many times what it has, so the multiplier is raised by\n * more than the difference from the target calls for until it does.\n */\nconst SATURATED_CORRECTION = 1.3;\n\n/**\n * How often a worker that is pacing its client groups says so in its log,\n * and how long it has to have stopped for to say that it has.\n */\nconst PACING_LOG_INTERVAL_MS = 60_000;\n\n/**\n * The most updates that are made up for when nothing has asked the pacer\n * for a wait in a while, which is enough for the multiplier to get to 0.\n */\nconst MAX_CATCH_UP_UPDATES = 60;\n\n/**\n * Bounds the multiplier, which otherwise has no limit while a worker stays\n * over the target however long its client groups wait.\n */\nconst MAX_MULTIPLIER = 1_000_000;\n\nexport type BatchPacerOptions = {\n /**\n * The largest share of the worker's time that it may be busy before its\n * client groups are paced.\n */\n readonly shareTarget: number;\n /** The longest a client group waits between batches. */\n readonly maxIntervalMs: number;\n readonly now?: (() => number) | undefined;\n /**\n * The share of the time since the previous call that the worker was busy.\n * By default, the utilization of the event loop.\n */\n readonly utilization?: (() => number) | undefined;\n /** Where the worker says that it is pacing its client groups. */\n readonly lc?: LogContext | undefined;\n};\n\nfunction eventLoopUtilization(): () => number {\n let last = performance.eventLoopUtilization();\n return () => {\n const current = performance.eventLoopUtilization();\n const {utilization} = performance.eventLoopUtilization(current, last);\n last = current;\n return utilization;\n };\n}\n\n/**\n * Paces the batches (advancements, including the queries they hydrate\n * again) of the client groups of a sync worker, so that the worker is left\n * with time for new hydrations and connections, and gives the hydrations of\n * new queries priority over the batches.\n *\n * **Pacing.** After each batch, a client group waits {@link waitMs} before\n * it starts the next. The wait is a multiple of the batch's processing\n * time, so a client group that always has changes waiting is busy at most\n * `1 / (1 + k)` of the time, where `k` is the {@link multiplier}. Because\n * the wait is in proportion to a client group's own processing time, those\n * that cause the load are slowed the most, and cheap ones barely at all.\n *\n * The multiplier is shared by the client groups of the worker. Once per\n * second, it is scaled by how far the share of its time that the worker was\n * busy, smoothed over a few seconds, is from the target, and by more while\n * the worker has no time to spare. It never goes below 0, where no client\n * group waits. The share is the utilization of the worker's event loop, i.e. all\n * of what the worker does: what a batch costs is spread over the pipelines,\n * the CVR, the pokes and garbage collection, and is not measurable batch by\n * batch.\n *\n * **Priority of new hydrations.** The worker's time slices are handed out\n * by the pacer (see {@link yieldSlice}), and while the hydration of new\n * queries is waiting for one, most of them go to it rather than to batches.\n */\nexport class BatchPacer {\n readonly #shareTarget: number;\n readonly #maxIntervalMs: number;\n readonly #now: () => number;\n readonly #utilization: () => number;\n readonly #lc: LogContext | undefined;\n\n /** When the worker last logged that it is pacing, while it still is. */\n #pacingLoggedAt: number | undefined;\n /** Since when the worker has not been pacing, after it logged that it was. */\n #notPacingSince: number | undefined;\n\n #intervalStart: number;\n #multiplier = 0;\n /** The smoothed share of its time that the worker was busy. */\n #share = 0;\n #saturatedSeconds = 0;\n /** The waits handed out since the last update, by whether at the cap. */\n #waitsBelowMax = 0;\n #waitsAtMax = 0;\n\n /** The work that is waiting for a time slice, in the order it yielded. */\n readonly #waiting = {\n hydration: [] as (() => void)[],\n batch: [] as (() => void)[],\n };\n #handingOutSlices = false;\n /** The slices that have gone to hydrations since one went to a batch. */\n #hydrationSlices = 0;\n\n constructor({\n shareTarget,\n maxIntervalMs,\n now,\n utilization,\n lc,\n }: BatchPacerOptions) {\n this.#shareTarget = shareTarget;\n this.#maxIntervalMs = maxIntervalMs;\n this.#now = now ?? (() => performance.now());\n this.#utilization = utilization ?? eventLoopUtilization();\n this.#lc = lc;\n this.#intervalStart = this.#now();\n }\n\n /**\n * How long a client group should wait before its next batch, after one\n * that took `processingMs` to process. The multiplier changes while a\n * client group waits, so a long wait should be taken in steps, asking\n * again after each.\n */\n waitMs(processingMs: number): number {\n this.#update();\n const wait = this.#multiplier * processingMs;\n if (wait < this.#maxIntervalMs) {\n this.#waitsBelowMax++;\n return wait;\n }\n this.#waitsAtMax++;\n return this.#maxIntervalMs;\n }\n\n /** `k`: a client group waits `k` times the processing time of a batch. */\n get multiplier(): number {\n this.#update();\n return this.#multiplier;\n }\n\n /**\n * The share of its time that the worker was busy, smoothed over the last\n * few seconds, as of the last update of the multiplier.\n */\n get share(): number {\n this.#update();\n return this.#share;\n }\n\n /** Updates the multiplier for each interval that has ended. */\n #update(): void {\n const now = this.#now();\n const ended = Math.floor((now - this.#intervalStart) / UPDATE_INTERVAL_MS);\n if (ended <= 0) {\n return;\n }\n this.#intervalStart += ended * UPDATE_INTERVAL_MS;\n // If more than one interval has ended, nothing asked the pacer for a\n // wait in the meantime, and the utilization since then is that of each.\n const latest = this.#utilization();\n // Once every client group is told to wait the longest it can, raising\n // the multiplier changes nothing, and only makes it take longer to come\n // back down. A worker that is still over its target then is busy with\n // something that making its client groups wait does not relieve.\n const allAtMax = this.#waitsAtMax > 0 && this.#waitsBelowMax === 0;\n this.#waitsAtMax = 0;\n this.#waitsBelowMax = 0;\n for (let i = Math.min(ended, MAX_CATCH_UP_UPDATES); i > 0; i--) {\n this.#share += SHARE_SMOOTHING * (latest - this.#share);\n // A worker that is allowed to be this busy is only raised the\n // multiplier for what it is over its target by.\n this.#saturatedSeconds =\n latest >= SATURATED_SHARE && latest > this.#shareTarget\n ? this.#saturatedSeconds + 1\n : 0;\n let correction =\n this.#saturatedSeconds >= SATURATED_SECONDS\n ? SATURATED_CORRECTION\n : (this.#share / this.#shareTarget) ** CORRECTION_EXPONENT;\n if (allAtMax && correction > 1) {\n correction = 1;\n }\n this.#multiplier = Math.min(\n Math.max((1 + this.#multiplier) * correction - 1, 0),\n MAX_MULTIPLIER,\n );\n }\n this.#logPacing(now, allAtMax);\n }\n\n /**\n * Says in the worker's log when it starts to pace its client groups, once\n * a minute while it does, and when it has stopped. The pacing of a client\n * group is logged by its view syncer.\n */\n #logPacing(now: number, allAtMax: boolean): void {\n const lc = this.#lc;\n if (lc === undefined) {\n return;\n }\n const state = {\n busyShare: this.#share,\n shareTarget: this.#shareTarget,\n waitMultiplier: this.#multiplier,\n maxBatchIntervalMs: this.#maxIntervalMs,\n };\n if (this.#multiplier > 0) {\n this.#notPacingSince = undefined;\n if (this.#pacingLoggedAt === undefined) {\n lc.info?.(\n 'Sync worker is over its share target: its client groups wait between batches',\n state,\n );\n this.#pacingLoggedAt = now;\n } else if (now - this.#pacingLoggedAt >= PACING_LOG_INTERVAL_MS) {\n if (allAtMax && this.#share > this.#shareTarget) {\n // Waiting longer is not an option, so pacing cannot bring the\n // worker to its target: it has more to do than it has time for.\n lc.warn?.(\n 'Sync worker is over its share target although every client group waits the longest it may',\n state,\n );\n } else {\n lc.info?.('Sync worker is pacing its client groups', state);\n }\n this.#pacingLoggedAt = now;\n }\n } else if (this.#pacingLoggedAt !== undefined) {\n // A worker around its target goes in and out of pacing. It only says\n // that it has stopped once it has stayed out.\n this.#notPacingSince ??= now;\n if (now - this.#notPacingSince >= PACING_LOG_INTERVAL_MS) {\n lc.info?.(\n 'Sync worker is under its share target: its client groups no longer wait',\n state,\n );\n this.#pacingLoggedAt = undefined;\n this.#notPacingSince = undefined;\n }\n }\n }\n\n /**\n * Yields the worker to the other work that is waiting for a time slice,\n * and resolves when it is the caller's turn again. One piece of work runs\n * per turn of the event loop, as when the work of a worker's client groups\n * simply takes turns, except that the hydration of new queries goes first:\n * while both kinds of work are waiting, `HYDRATION_PRIORITY` slices go to\n * hydrations for each one that goes to a batch.\n *\n * Batches are never made to wait for anything but a slice. If no\n * hydration is waiting for one (because none is running, or because those\n * that are running are waiting for I/O), batches get every slice. And\n * however many hydrations there are, batches keep a share of the slices,\n * so they keep moving, and so do the snapshots of the replica that they\n * hold.\n */\n yieldSlice(kind: 'hydration' | 'batch'): Promise<void> {\n return new Promise(resolve => {\n this.#waiting[kind].push(resolve);\n if (!this.#handingOutSlices) {\n this.#handingOutSlices = true;\n setImmediate(this.#nextSlice);\n }\n });\n }\n\n readonly #nextSlice = () => {\n const {hydration, batch} = this.#waiting;\n let next: (() => void) | undefined;\n if (\n hydration.length > 0 &&\n (batch.length === 0 || this.#hydrationSlices < HYDRATION_PRIORITY)\n ) {\n next = hydration.shift();\n this.#hydrationSlices++;\n } else if (batch.length > 0) {\n next = batch.shift();\n this.#hydrationSlices = 0;\n }\n if (next === undefined) {\n this.#handingOutSlices = false;\n return;\n }\n next();\n // After the work that was just resumed has run its slice.\n setImmediate(this.#nextSlice);\n };\n}\n\n/**\n * How many time slices go to the hydration of new queries for each one that\n * goes to a batch, while both are waiting for one.\n */\nconst HYDRATION_PRIORITY = 9;\n"],"mappings":"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"}
@@ -32,6 +32,7 @@ function startPoke(lc, clients, tentativeVersion) {
32
32
  end: (finalVersion) => settle("end", pokers.map((poker) => poker.end(finalVersion)))
33
33
  };
34
34
  }
35
+ var POKE_PART_FLUSH_THRESHOLD_CHARS = 1048576;
35
36
  var PART_COUNT_FLUSH_THRESHOLD = 100;
36
37
  /**
37
38
  * Handles a single `ViewSyncer` connection.
@@ -235,7 +236,8 @@ var ClientHandler = class {
235
236
  }
236
237
  #updateLMIDs(lmids, patch) {
237
238
  if (patch.op === "put") {
238
- const { clientGroupID, clientID, lastMutationID } = parse(ensureSafeJSON(patch.contents), lmidRowSchema, "passthrough");
239
+ const row = ensureSafeJSON(patch.contents);
240
+ const { clientGroupID, clientID, lastMutationID } = parse(row, lmidRowSchema, "passthrough");
239
241
  if (clientGroupID !== this.#clientGroupID) this.#lc.error?.(`Received clients row for wrong clientGroupID. Ignoring.`, clientGroupID);
240
242
  else lmids[clientID] = lastMutationID;
241
243
  } else patch.op;
@@ -289,6 +291,6 @@ function ensureSafeJSON(row) {
289
291
  } : row;
290
292
  }
291
293
  //#endregion
292
- export { ClientHandler, startPoke };
294
+ export { ClientHandler, POKE_PART_FLUSH_THRESHOLD_CHARS, ensureSafeJSON, startPoke };
293
295
 
294
296
  //# sourceMappingURL=client-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-handler.js","names":["#clientGroupID","#zeroClientsTable","#zeroMutationsTable","#lc","#downstream","#pokeTime","#pokeTransactions","#pokedRows","#baseVersion","#push","#everPoked","#updateLMIDs"],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../shared/src/bigint-json.ts';\nimport {\n assertJSONValue,\n type JSONObject as SafeJSONObject,\n} from '../../../../shared/src/json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Writable} from '../../../../shared/src/writable.ts';\nimport type {ErroredQuery} from '../../../../zero-protocol/src/custom-queries.ts';\nimport {rowSchema} from '../../../../zero-protocol/src/data.ts';\nimport type {DeleteClientsBody} from '../../../../zero-protocol/src/delete-clients.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {\n ProtocolError,\n type TransformFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport type {InspectDownBody} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {mutationResultSchema} from '../../../../zero-protocol/src/mutation.ts';\nimport type {\n PokePartBody,\n PokePartMessage,\n PokeStartBody,\n} from '../../../../zero-protocol/src/poke.ts';\nimport {primaryKeyValueRecordSchema} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {RowPatchOp} from '../../../../zero-protocol/src/row-patch.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport type {ViewSyncerDownstream} from '../../types/downstream.ts';\nimport {\n getLogLevel,\n wrapWithProtocolError,\n} from '../../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n cmpVersions,\n cookieToVersion,\n versionToCookie,\n versionToNullableCookie,\n type CVRVersion,\n type DelQueryPatch,\n type NullableCVRVersion,\n type PutQueryPatch,\n type RowID,\n} from './schema/types.ts';\n\nexport type PutRowPatch = {\n type: 'row';\n op: 'put';\n id: RowID;\n contents: JSONObject;\n};\n\nexport type DeleteRowPatch = {\n type: 'row';\n op: 'del';\n id: RowID;\n};\n\nexport type RowPatch = PutRowPatch | DeleteRowPatch;\nexport type ConfigPatch = DelQueryPatch | PutQueryPatch;\n\nexport type Patch = ConfigPatch | RowPatch;\n\nexport type PatchToVersion = {\n patch: Patch;\n toVersion: CVRVersion;\n};\n\nexport interface PokeHandler {\n addPatch(patch: PatchToVersion): Promise<void>;\n cancel(): Promise<void>;\n end(finalVersion: CVRVersion): Promise<void>;\n}\n\n/** A {@link PokeHandler} for a single client. */\nexport interface ClientPokeHandler extends PokeHandler {\n /**\n * Whether a patch has been sent to the client. Once one has, the poke can\n * only be ended with a `finalVersion` that is ahead of the client's base\n * version.\n */\n readonly patchesSent: boolean;\n}\n\n/**\n * A {@link PokeHandler} for multiple clients, as returned by\n * {@link startPoke}.\n */\nexport interface MultiPokeHandler extends PokeHandler {\n /** Whether a patch has been sent to any of the clients. */\n readonly patchesSent: boolean;\n}\n\nconst NOOP: ClientPokeHandler = {\n patchesSent: false,\n addPatch: () => promiseVoid,\n cancel: () => promiseVoid,\n end: () => promiseVoid,\n};\n\n/** Wraps PokeHandlers for multiple clients in a single PokeHandler. */\nexport function startPoke(\n lc: LogContext,\n clients: ClientHandler[],\n tentativeVersion: CVRVersion,\n): MultiPokeHandler {\n const pokers = clients.map(c => c.startPoke(tentativeVersion));\n\n // Promise.allSettled() ensures that a failed (e.g. disconnected) client\n // does not prevent other clients from receiving the pokes. However, the\n // rate (per client group) will be limited by the slowest connection.\n //\n // The individual pokers report their own failures (by failing the\n // connection), so a rejection here is not expected. Log it rather than\n // dropping it on the floor.\n const settle = async (op: string, results: Promise<void>[]) => {\n for (const result of await Promise.allSettled(results)) {\n if (result.status === 'rejected') {\n lc.error?.(`unhandled error poking client (${op})`, result.reason);\n }\n }\n };\n\n return {\n get patchesSent() {\n return pokers.some(poker => poker.patchesSent);\n },\n addPatch: patch =>\n settle(\n 'addPatch',\n pokers.map(poker => poker.addPatch(patch)),\n ),\n cancel: () =>\n settle(\n 'cancel',\n pokers.map(poker => poker.cancel()),\n ),\n end: finalVersion =>\n settle(\n 'end',\n pokers.map(poker => poker.end(finalVersion)),\n ),\n };\n}\n\n// Soft limit on the serialized row characters accumulated in a poke part.\n// This bounds view-syncer memory and keeps legacy JSON messages reasonably\n// sized. PokeChunkEncoder separately enforces the binary frame limit in bytes.\nexport const POKE_PART_FLUSH_THRESHOLD_CHARS = 1024 * 1024;\nconst PART_COUNT_FLUSH_THRESHOLD = 100;\n\n/**\n * Handles a single `ViewSyncer` connection.\n */\nexport class ClientHandler {\n readonly #clientGroupID: string;\n readonly clientID: string;\n readonly wsID: string;\n readonly #zeroClientsTable: string;\n readonly #zeroMutationsTable: string;\n readonly #lc: LogContext;\n readonly #downstream: Subscription<ViewSyncerDownstream>;\n #baseVersion: NullableCVRVersion;\n // We will send a poke on connect even if the client is already caught up, so that it can learn its\n // got-queries state has been reconciled with the server. After that, we will only send a poke if\n // the client is behind.\n #everPoked = false;\n\n readonly #pokeTime = getOrCreateLatencyHistogram(\n 'sync',\n 'poke.time',\n 'Time elapsed for each poke transaction. Canceled / noop pokes are excluded.',\n );\n\n readonly #pokeTransactions = getOrCreateCounter(\n 'sync',\n 'poke.transactions',\n 'Count of poke transactions.',\n );\n\n readonly #pokedRows = getOrCreateCounter(\n 'sync',\n 'poke.rows',\n 'Count of poked rows.',\n );\n\n constructor(\n lc: LogContext,\n clientGroupID: string,\n clientID: string,\n wsID: string,\n shard: ShardID,\n baseCookie: string | null,\n downstream: Subscription<ViewSyncerDownstream>,\n ) {\n lc.debug?.('new client handler');\n this.#clientGroupID = clientGroupID;\n this.clientID = clientID;\n this.wsID = wsID;\n this.#zeroClientsTable = `${upstreamSchema(shard)}.clients`;\n this.#zeroMutationsTable = `${upstreamSchema(shard)}.mutations`;\n this.#lc = lc;\n this.#downstream = downstream;\n this.#baseVersion = cookieToVersion(baseCookie);\n }\n\n version(): NullableCVRVersion {\n return this.#baseVersion;\n }\n\n async #push(msg: Downstream, serialized?: string | undefined): Promise<void> {\n const {result} = this.#downstream.push({message: msg, serialized});\n await result;\n }\n\n fail(e: unknown) {\n this.#lc[getLogLevel(e)]?.(\n `view-syncer closing connection with error: ${String(e)}`,\n e,\n );\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n\n close(reason: string) {\n this.#lc.debug?.(`view-syncer closing connection: ${reason}`);\n this.#downstream.cancel();\n }\n\n startPoke(tentativeVersion: CVRVersion): ClientPokeHandler {\n const pokeID = versionToCookie(tentativeVersion);\n const lc = this.#lc.withContext('pokeID', pokeID);\n\n // Force an (empty) initial poke even when already caught up; see #everPoked.\n const forceInitialPoke = !this.#everPoked;\n const cmp = cmpVersions(this.#baseVersion, tentativeVersion);\n if (cmp > 0 || (cmp === 0 && !forceInitialPoke)) {\n lc.info?.(`already caught up, not sending poke.`);\n return NOOP;\n }\n\n const baseCookie = versionToNullableCookie(this.#baseVersion);\n const cookie = versionToCookie(tentativeVersion);\n lc.debug?.(`starting poke from ${baseCookie} to ${cookie}`);\n\n const start = performance.now();\n\n const pokeStart: PokeStartBody = {pokeID, baseCookie};\n\n let pokeStarted = false;\n let body: PokePartBody | undefined;\n let partCount = 0;\n let serializedRowsLength = 0;\n\n const ensureBody = async () => {\n if (!pokeStarted) {\n await this.#push(['pokeStart', pokeStart]);\n pokeStarted = true;\n }\n return (body ??= {pokeID});\n };\n\n const flushBody = async () => {\n if (body) {\n const message = ['pokePart', body] satisfies PokePartMessage;\n await this.#push(message, JSON.stringify(message));\n body = undefined;\n partCount = 0;\n serializedRowsLength = 0;\n }\n };\n\n const addPatch = async (patchToVersion: PatchToVersion) => {\n const {patch, toVersion} = patchToVersion;\n if (cmpVersions(toVersion, this.#baseVersion) <= 0) {\n return;\n }\n const target = await ensureBody();\n\n const {type, op} = patch;\n switch (type) {\n case 'query': {\n const patches = patch.clientID\n ? ((target.desiredQueriesPatches ??= {})[patch.clientID] ??= [])\n : (target.gotQueriesPatch ??= []);\n patches.push({op, hash: patch.id});\n break;\n }\n case 'row':\n if (patch.id.table === this.#zeroClientsTable) {\n this.#updateLMIDs((target.lastMutationIDChanges ??= {}), patch);\n } else if (patch.id.table === this.#zeroMutationsTable) {\n const patches = (target.mutationsPatch ??= []);\n if (op === 'put') {\n const row = v.parse(\n ensureSafeJSON(patch.contents),\n mutationRowSchema,\n 'passthrough',\n );\n patches.push({\n op: 'put',\n mutation: {\n id: {\n clientID: row.clientID,\n id: row.mutationID,\n },\n result: row.result,\n },\n });\n } else {\n const {clientID, mutationID} = patch.id.rowKey;\n assert(\n typeof clientID === 'string',\n 'client id must be a string',\n );\n const id = Number(mutationID);\n assert(\n !Number.isNaN(id) && Number.isFinite(id) && id >= 0,\n 'mutation id must be a finite number',\n );\n patches.push({\n op: 'del',\n id: {\n clientID,\n id,\n },\n });\n }\n } else {\n const rowPatch = makeRowPatch(patch);\n (target.rowsPatch ??= []).push(rowPatch);\n serializedRowsLength += JSON.stringify(rowPatch).length;\n }\n break;\n default:\n unreachable(patch);\n }\n\n partCount++;\n if (\n serializedRowsLength >= POKE_PART_FLUSH_THRESHOLD_CHARS ||\n partCount >= PART_COUNT_FLUSH_THRESHOLD\n ) {\n await flushBody();\n }\n };\n\n // A poke that was started must always be ended. If it isn't, the\n // connection is left mid-poke and the *next* pokeStart fails the\n // DownstreamSender's in-progress check, surfacing the error far from\n // its cause. Fail the connection instead; the client reconnects and\n // resyncs from its baseVersion.\n return {\n get patchesSent() {\n return pokeStarted;\n },\n addPatch: async (patchToVersion: PatchToVersion) => {\n try {\n await addPatch(patchToVersion);\n if (patchToVersion.patch.type === 'row') {\n this.#pokedRows.add(1);\n }\n } catch (e) {\n this.fail(e);\n }\n },\n\n cancel: async () => {\n try {\n if (pokeStarted) {\n await this.#push(['pokeEnd', {pokeID, cookie: '', cancel: true}]);\n }\n } catch (e) {\n this.fail(e);\n }\n },\n\n end: async (finalVersion: CVRVersion) => {\n try {\n const cookie = versionToCookie(finalVersion);\n if (!pokeStarted) {\n if (\n cmpVersions(this.#baseVersion, finalVersion) === 0 &&\n !forceInitialPoke\n ) {\n return; // Nothing changed and nothing was sent.\n }\n await this.#push(['pokeStart', pokeStart]);\n } else if (cmpVersions(this.#baseVersion, finalVersion) >= 0) {\n // Sanity check: If the poke was started, the finalVersion\n // must be > #baseVersion.\n throw new Error(\n `Patches were sent but finalVersion ${cookie} is not ` +\n `greater than baseVersion ` +\n `${versionToNullableCookie(this.#baseVersion)}`,\n );\n }\n await flushBody();\n await this.#push(['pokeEnd', {pokeID, cookie}]);\n this.#baseVersion = finalVersion;\n this.#everPoked = true;\n\n const elapsed = performance.now() - start;\n this.#pokeTransactions.add(1);\n this.#pokeTime.recordMs(elapsed);\n } catch (e) {\n this.fail(e);\n }\n },\n };\n }\n\n async sendDeleteClients(\n lc: LogContext,\n deletedClientIDs: string[],\n deletedClientGroupIDs: string[],\n ) {\n const deleteClientsBody: Writable<DeleteClientsBody> = {};\n if (deletedClientIDs.length > 0) {\n deleteClientsBody.clientIDs = deletedClientIDs;\n }\n if (deletedClientGroupIDs.length > 0) {\n deleteClientsBody.clientGroupIDs = deletedClientGroupIDs;\n }\n lc.debug?.('sending deleteClients', deleteClientsBody);\n await this.#push(['deleteClients', deleteClientsBody]);\n }\n\n sendQueryTransformApplicationErrors(errors: ErroredQuery[]) {\n void this.#push(['transformError', errors]);\n }\n\n sendQueryTransformFailedError(error: TransformFailedBody) {\n this.fail(new ProtocolError(error));\n }\n\n sendInspectResponse(lc: LogContext, response: InspectDownBody): void {\n lc.debug?.('sending inspect response', response);\n this.#downstream.push({\n message: ['inspect', response],\n serialized: undefined,\n });\n }\n\n #updateLMIDs(lmids: Record<string, number>, patch: RowPatch) {\n if (patch.op === 'put') {\n const row = ensureSafeJSON(patch.contents);\n const {clientGroupID, clientID, lastMutationID} = v.parse(\n row,\n lmidRowSchema,\n 'passthrough',\n );\n if (clientGroupID !== this.#clientGroupID) {\n this.#lc.error?.(\n `Received clients row for wrong clientGroupID. Ignoring.`,\n clientGroupID,\n );\n } else {\n lmids[clientID] = lastMutationID;\n }\n } else {\n // The 'constrain' and 'del' ops for clients can be ignored.\n patch.op satisfies 'constrain' | 'del';\n }\n }\n}\n\n// Note: The {APP_ID}_{SHARD_ID}.clients table is set up in replicator/initial-sync.ts.\nconst lmidRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n lastMutationID: v.number(), // Actually returned as a bigint, but converted by ensureSafeJSON().\n});\n\nconst mutationRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n mutationID: v.number(),\n result: mutationResultSchema,\n});\n\nfunction makeRowPatch(patch: RowPatch): RowPatchOp {\n const {\n op,\n id: {table: tableName, rowKey: id},\n } = patch;\n\n switch (op) {\n case 'put':\n return {\n op: 'put',\n tableName,\n value: v.parse(ensureSafeJSON(patch.contents), rowSchema),\n };\n\n case 'del':\n return {\n op,\n tableName,\n id: v.parse(id, primaryKeyValueRecordSchema),\n };\n\n default:\n unreachable(op);\n }\n}\n\n/**\n * Column values of type INT8 are returned as the `bigint` from the\n * Postgres library. These are converted to `number` if they are within\n * the safe Number range, allowing the protocol to support numbers larger\n * than 32-bits. Values outside of the safe number range (e.g. > 2^53) will\n * result in an Error.\n */\nexport function ensureSafeJSON(row: JSONObject): SafeJSONObject {\n const modified = Object.entries(row)\n .filter(([k, v]) => {\n if (typeof v === 'bigint') {\n if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {\n return true; // send this entry onto the next map() step.\n }\n throw new Error(`Value of \"${k}\" exceeds safe Number range (${v})`);\n } else if (typeof v === 'object') {\n assertJSONValue(v);\n }\n return false;\n })\n .map(([k, v]) => [k, Number(v)]);\n\n return modified.length\n ? {...row, ...Object.fromEntries(modified)}\n : (row as SafeJSONObject);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-handler.js","names":["v.parse","v"],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../shared/src/bigint-json.ts';\nimport {\n assertJSONValue,\n type JSONObject as SafeJSONObject,\n} from '../../../../shared/src/json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Writable} from '../../../../shared/src/writable.ts';\nimport type {ErroredQuery} from '../../../../zero-protocol/src/custom-queries.ts';\nimport {rowSchema} from '../../../../zero-protocol/src/data.ts';\nimport type {DeleteClientsBody} from '../../../../zero-protocol/src/delete-clients.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {\n ProtocolError,\n type TransformFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport type {InspectDownBody} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {mutationResultSchema} from '../../../../zero-protocol/src/mutation.ts';\nimport type {\n PokePartBody,\n PokePartMessage,\n PokeStartBody,\n} from '../../../../zero-protocol/src/poke.ts';\nimport {primaryKeyValueRecordSchema} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {RowPatchOp} from '../../../../zero-protocol/src/row-patch.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport type {ViewSyncerDownstream} from '../../types/downstream.ts';\nimport {\n getLogLevel,\n wrapWithProtocolError,\n} from '../../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n cmpVersions,\n cookieToVersion,\n versionToCookie,\n versionToNullableCookie,\n type CVRVersion,\n type DelQueryPatch,\n type NullableCVRVersion,\n type PutQueryPatch,\n type RowID,\n} from './schema/types.ts';\n\nexport type PutRowPatch = {\n type: 'row';\n op: 'put';\n id: RowID;\n contents: JSONObject;\n};\n\nexport type DeleteRowPatch = {\n type: 'row';\n op: 'del';\n id: RowID;\n};\n\nexport type RowPatch = PutRowPatch | DeleteRowPatch;\nexport type ConfigPatch = DelQueryPatch | PutQueryPatch;\n\nexport type Patch = ConfigPatch | RowPatch;\n\nexport type PatchToVersion = {\n patch: Patch;\n toVersion: CVRVersion;\n};\n\nexport interface PokeHandler {\n addPatch(patch: PatchToVersion): Promise<void>;\n cancel(): Promise<void>;\n end(finalVersion: CVRVersion): Promise<void>;\n}\n\n/** A {@link PokeHandler} for a single client. */\nexport interface ClientPokeHandler extends PokeHandler {\n /**\n * Whether a patch has been sent to the client. Once one has, the poke can\n * only be ended with a `finalVersion` that is ahead of the client's base\n * version.\n */\n readonly patchesSent: boolean;\n}\n\n/**\n * A {@link PokeHandler} for multiple clients, as returned by\n * {@link startPoke}.\n */\nexport interface MultiPokeHandler extends PokeHandler {\n /** Whether a patch has been sent to any of the clients. */\n readonly patchesSent: boolean;\n}\n\nconst NOOP: ClientPokeHandler = {\n patchesSent: false,\n addPatch: () => promiseVoid,\n cancel: () => promiseVoid,\n end: () => promiseVoid,\n};\n\n/** Wraps PokeHandlers for multiple clients in a single PokeHandler. */\nexport function startPoke(\n lc: LogContext,\n clients: ClientHandler[],\n tentativeVersion: CVRVersion,\n): MultiPokeHandler {\n const pokers = clients.map(c => c.startPoke(tentativeVersion));\n\n // Promise.allSettled() ensures that a failed (e.g. disconnected) client\n // does not prevent other clients from receiving the pokes. However, the\n // rate (per client group) will be limited by the slowest connection.\n //\n // The individual pokers report their own failures (by failing the\n // connection), so a rejection here is not expected. Log it rather than\n // dropping it on the floor.\n const settle = async (op: string, results: Promise<void>[]) => {\n for (const result of await Promise.allSettled(results)) {\n if (result.status === 'rejected') {\n lc.error?.(`unhandled error poking client (${op})`, result.reason);\n }\n }\n };\n\n return {\n get patchesSent() {\n return pokers.some(poker => poker.patchesSent);\n },\n addPatch: patch =>\n settle(\n 'addPatch',\n pokers.map(poker => poker.addPatch(patch)),\n ),\n cancel: () =>\n settle(\n 'cancel',\n pokers.map(poker => poker.cancel()),\n ),\n end: finalVersion =>\n settle(\n 'end',\n pokers.map(poker => poker.end(finalVersion)),\n ),\n };\n}\n\n// Soft limit on the serialized row characters accumulated in a poke part.\n// This bounds view-syncer memory and keeps legacy JSON messages reasonably\n// sized. PokeChunkEncoder separately enforces the binary frame limit in bytes.\nexport const POKE_PART_FLUSH_THRESHOLD_CHARS = 1024 * 1024;\nconst PART_COUNT_FLUSH_THRESHOLD = 100;\n\n/**\n * Handles a single `ViewSyncer` connection.\n */\nexport class ClientHandler {\n readonly #clientGroupID: string;\n readonly clientID: string;\n readonly wsID: string;\n readonly #zeroClientsTable: string;\n readonly #zeroMutationsTable: string;\n readonly #lc: LogContext;\n readonly #downstream: Subscription<ViewSyncerDownstream>;\n #baseVersion: NullableCVRVersion;\n // We will send a poke on connect even if the client is already caught up, so that it can learn its\n // got-queries state has been reconciled with the server. After that, we will only send a poke if\n // the client is behind.\n #everPoked = false;\n\n readonly #pokeTime = getOrCreateLatencyHistogram(\n 'sync',\n 'poke.time',\n 'Time elapsed for each poke transaction. Canceled / noop pokes are excluded.',\n );\n\n readonly #pokeTransactions = getOrCreateCounter(\n 'sync',\n 'poke.transactions',\n 'Count of poke transactions.',\n );\n\n readonly #pokedRows = getOrCreateCounter(\n 'sync',\n 'poke.rows',\n 'Count of poked rows.',\n );\n\n constructor(\n lc: LogContext,\n clientGroupID: string,\n clientID: string,\n wsID: string,\n shard: ShardID,\n baseCookie: string | null,\n downstream: Subscription<ViewSyncerDownstream>,\n ) {\n lc.debug?.('new client handler');\n this.#clientGroupID = clientGroupID;\n this.clientID = clientID;\n this.wsID = wsID;\n this.#zeroClientsTable = `${upstreamSchema(shard)}.clients`;\n this.#zeroMutationsTable = `${upstreamSchema(shard)}.mutations`;\n this.#lc = lc;\n this.#downstream = downstream;\n this.#baseVersion = cookieToVersion(baseCookie);\n }\n\n version(): NullableCVRVersion {\n return this.#baseVersion;\n }\n\n async #push(msg: Downstream, serialized?: string | undefined): Promise<void> {\n const {result} = this.#downstream.push({message: msg, serialized});\n await result;\n }\n\n fail(e: unknown) {\n this.#lc[getLogLevel(e)]?.(\n `view-syncer closing connection with error: ${String(e)}`,\n e,\n );\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n\n close(reason: string) {\n this.#lc.debug?.(`view-syncer closing connection: ${reason}`);\n this.#downstream.cancel();\n }\n\n startPoke(tentativeVersion: CVRVersion): ClientPokeHandler {\n const pokeID = versionToCookie(tentativeVersion);\n const lc = this.#lc.withContext('pokeID', pokeID);\n\n // Force an (empty) initial poke even when already caught up; see #everPoked.\n const forceInitialPoke = !this.#everPoked;\n const cmp = cmpVersions(this.#baseVersion, tentativeVersion);\n if (cmp > 0 || (cmp === 0 && !forceInitialPoke)) {\n lc.info?.(`already caught up, not sending poke.`);\n return NOOP;\n }\n\n const baseCookie = versionToNullableCookie(this.#baseVersion);\n const cookie = versionToCookie(tentativeVersion);\n lc.debug?.(`starting poke from ${baseCookie} to ${cookie}`);\n\n const start = performance.now();\n\n const pokeStart: PokeStartBody = {pokeID, baseCookie};\n\n let pokeStarted = false;\n let body: PokePartBody | undefined;\n let partCount = 0;\n let serializedRowsLength = 0;\n\n const ensureBody = async () => {\n if (!pokeStarted) {\n await this.#push(['pokeStart', pokeStart]);\n pokeStarted = true;\n }\n return (body ??= {pokeID});\n };\n\n const flushBody = async () => {\n if (body) {\n const message = ['pokePart', body] satisfies PokePartMessage;\n await this.#push(message, JSON.stringify(message));\n body = undefined;\n partCount = 0;\n serializedRowsLength = 0;\n }\n };\n\n const addPatch = async (patchToVersion: PatchToVersion) => {\n const {patch, toVersion} = patchToVersion;\n if (cmpVersions(toVersion, this.#baseVersion) <= 0) {\n return;\n }\n const target = await ensureBody();\n\n const {type, op} = patch;\n switch (type) {\n case 'query': {\n const patches = patch.clientID\n ? ((target.desiredQueriesPatches ??= {})[patch.clientID] ??= [])\n : (target.gotQueriesPatch ??= []);\n patches.push({op, hash: patch.id});\n break;\n }\n case 'row':\n if (patch.id.table === this.#zeroClientsTable) {\n this.#updateLMIDs((target.lastMutationIDChanges ??= {}), patch);\n } else if (patch.id.table === this.#zeroMutationsTable) {\n const patches = (target.mutationsPatch ??= []);\n if (op === 'put') {\n const row = v.parse(\n ensureSafeJSON(patch.contents),\n mutationRowSchema,\n 'passthrough',\n );\n patches.push({\n op: 'put',\n mutation: {\n id: {\n clientID: row.clientID,\n id: row.mutationID,\n },\n result: row.result,\n },\n });\n } else {\n const {clientID, mutationID} = patch.id.rowKey;\n assert(\n typeof clientID === 'string',\n 'client id must be a string',\n );\n const id = Number(mutationID);\n assert(\n !Number.isNaN(id) && Number.isFinite(id) && id >= 0,\n 'mutation id must be a finite number',\n );\n patches.push({\n op: 'del',\n id: {\n clientID,\n id,\n },\n });\n }\n } else {\n const rowPatch = makeRowPatch(patch);\n (target.rowsPatch ??= []).push(rowPatch);\n serializedRowsLength += JSON.stringify(rowPatch).length;\n }\n break;\n default:\n unreachable(patch);\n }\n\n partCount++;\n if (\n serializedRowsLength >= POKE_PART_FLUSH_THRESHOLD_CHARS ||\n partCount >= PART_COUNT_FLUSH_THRESHOLD\n ) {\n await flushBody();\n }\n };\n\n // A poke that was started must always be ended. If it isn't, the\n // connection is left mid-poke and the *next* pokeStart fails the\n // DownstreamSender's in-progress check, surfacing the error far from\n // its cause. Fail the connection instead; the client reconnects and\n // resyncs from its baseVersion.\n return {\n get patchesSent() {\n return pokeStarted;\n },\n addPatch: async (patchToVersion: PatchToVersion) => {\n try {\n await addPatch(patchToVersion);\n if (patchToVersion.patch.type === 'row') {\n this.#pokedRows.add(1);\n }\n } catch (e) {\n this.fail(e);\n }\n },\n\n cancel: async () => {\n try {\n if (pokeStarted) {\n await this.#push(['pokeEnd', {pokeID, cookie: '', cancel: true}]);\n }\n } catch (e) {\n this.fail(e);\n }\n },\n\n end: async (finalVersion: CVRVersion) => {\n try {\n const cookie = versionToCookie(finalVersion);\n if (!pokeStarted) {\n if (\n cmpVersions(this.#baseVersion, finalVersion) === 0 &&\n !forceInitialPoke\n ) {\n return; // Nothing changed and nothing was sent.\n }\n await this.#push(['pokeStart', pokeStart]);\n } else if (cmpVersions(this.#baseVersion, finalVersion) >= 0) {\n // Sanity check: If the poke was started, the finalVersion\n // must be > #baseVersion.\n throw new Error(\n `Patches were sent but finalVersion ${cookie} is not ` +\n `greater than baseVersion ` +\n `${versionToNullableCookie(this.#baseVersion)}`,\n );\n }\n await flushBody();\n await this.#push(['pokeEnd', {pokeID, cookie}]);\n this.#baseVersion = finalVersion;\n this.#everPoked = true;\n\n const elapsed = performance.now() - start;\n this.#pokeTransactions.add(1);\n this.#pokeTime.recordMs(elapsed);\n } catch (e) {\n this.fail(e);\n }\n },\n };\n }\n\n async sendDeleteClients(\n lc: LogContext,\n deletedClientIDs: string[],\n deletedClientGroupIDs: string[],\n ) {\n const deleteClientsBody: Writable<DeleteClientsBody> = {};\n if (deletedClientIDs.length > 0) {\n deleteClientsBody.clientIDs = deletedClientIDs;\n }\n if (deletedClientGroupIDs.length > 0) {\n deleteClientsBody.clientGroupIDs = deletedClientGroupIDs;\n }\n lc.debug?.('sending deleteClients', deleteClientsBody);\n await this.#push(['deleteClients', deleteClientsBody]);\n }\n\n sendQueryTransformApplicationErrors(errors: ErroredQuery[]) {\n void this.#push(['transformError', errors]);\n }\n\n sendQueryTransformFailedError(error: TransformFailedBody) {\n this.fail(new ProtocolError(error));\n }\n\n sendInspectResponse(lc: LogContext, response: InspectDownBody): void {\n lc.debug?.('sending inspect response', response);\n this.#downstream.push({\n message: ['inspect', response],\n serialized: undefined,\n });\n }\n\n #updateLMIDs(lmids: Record<string, number>, patch: RowPatch) {\n if (patch.op === 'put') {\n const row = ensureSafeJSON(patch.contents);\n const {clientGroupID, clientID, lastMutationID} = v.parse(\n row,\n lmidRowSchema,\n 'passthrough',\n );\n if (clientGroupID !== this.#clientGroupID) {\n this.#lc.error?.(\n `Received clients row for wrong clientGroupID. Ignoring.`,\n clientGroupID,\n );\n } else {\n lmids[clientID] = lastMutationID;\n }\n } else {\n // The 'constrain' and 'del' ops for clients can be ignored.\n patch.op satisfies 'constrain' | 'del';\n }\n }\n}\n\n// Note: The {APP_ID}_{SHARD_ID}.clients table is set up in replicator/initial-sync.ts.\nconst lmidRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n lastMutationID: v.number(), // Actually returned as a bigint, but converted by ensureSafeJSON().\n});\n\nconst mutationRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n mutationID: v.number(),\n result: mutationResultSchema,\n});\n\nfunction makeRowPatch(patch: RowPatch): RowPatchOp {\n const {\n op,\n id: {table: tableName, rowKey: id},\n } = patch;\n\n switch (op) {\n case 'put':\n return {\n op: 'put',\n tableName,\n value: v.parse(ensureSafeJSON(patch.contents), rowSchema),\n };\n\n case 'del':\n return {\n op,\n tableName,\n id: v.parse(id, primaryKeyValueRecordSchema),\n };\n\n default:\n unreachable(op);\n }\n}\n\n/**\n * Column values of type INT8 are returned as the `bigint` from the\n * Postgres library. These are converted to `number` if they are within\n * the safe Number range, allowing the protocol to support numbers larger\n * than 32-bits. Values outside of the safe number range (e.g. > 2^53) will\n * result in an Error.\n */\nexport function ensureSafeJSON(row: JSONObject): SafeJSONObject {\n const modified = Object.entries(row)\n .filter(([k, v]) => {\n if (typeof v === 'bigint') {\n if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {\n return true; // send this entry onto the next map() step.\n }\n throw new Error(`Value of \"${k}\" exceeds safe Number range (${v})`);\n } else if (typeof v === 'object') {\n assertJSONValue(v);\n }\n return false;\n })\n .map(([k, v]) => [k, Number(v)]);\n\n return modified.length\n ? {...row, ...Object.fromEntries(modified)}\n : (row as SafeJSONObject);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"client-schema.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-schema.ts"],"sourcesContent":["import {toSorted} from '../../../../shared/src/iterables.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {\n difference,\n equals,\n intersection,\n} from '../../../../shared/src/set-utils.ts';\nimport type {ClientSchema} from '../../../../zero-protocol/src/client-schema.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ProtocolError} from '../../../../zero-protocol/src/error.ts';\nimport type {LiteAndZqlSpec, LiteTableSpec} from '../../db/specs.ts';\nimport {liteTypeToZqlValueType} from '../../types/lite.ts';\nimport {appSchema, upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport {ZERO_VERSION_COLUMN_NAME} from '../replicator/schema/constants.ts';\n\nexport function checkClientSchema(\n shardID: ShardID,\n clientSchema: ClientSchema,\n tableSpecs: Map<string, LiteAndZqlSpec>,\n fullTables: Map<string, LiteTableSpec>,\n) {\n if (fullTables.size === 0) {\n throw new ProtocolError({\n kind: 'Internal',\n message:\n `No tables have been synced from upstream. ` +\n `Please check that the ZERO_UPSTREAM_DB has been properly set.`,\n origin: ErrorOrigin.ZeroCache,\n });\n }\n const errors: string[] = [];\n const clientTables = new Set(Object.keys(clientSchema.tables));\n const missingTables = difference(clientTables, tableSpecs);\n for (const missing of toSorted(missingTables)) {\n if (fullTables.has(missing)) {\n const fullTable = must(fullTables.get(missing));\n const unsupportedPrimaryKeyColumns = (fullTable.primaryKey ?? []).filter(\n col => !liteTypeToZqlValueType(fullTable.columns[col]?.dataType ?? ''),\n );\n if (unsupportedPrimaryKeyColumns.length) {\n errors.push(\n `The \"${missing}\" table's primary key contains unsupported columns: ` +\n unsupportedPrimaryKeyColumns\n .map(\n col =>\n `\"${col}\" (${fullTable.columns[col]?.dataType ?? 'unknown'})`,\n )\n .join(', ') +\n `. These columns must use Zero-supported data types to sync the table to the client.`,\n );\n continue;\n }\n errors.push(\n `The \"${missing}\" table is missing a primary key or non-null ` +\n `unique index and thus cannot be synced to the client`,\n );\n } else {\n const app = appSchema(shardID) + '.';\n const shard = upstreamSchema(shardID) + '.';\n const syncedTables = [...tableSpecs.keys()]\n .filter(t => !t.startsWith(app) && !t.startsWith(shard))\n .sort()\n .map(t => `\"${t}\"`)\n .join(',');\n const schemaTip =\n missing.includes('.') && !syncedTables.includes('.')\n ? ` Note that zero does not sync tables from non-public schemas ` +\n `by default. Make sure you have defined a custom ` +\n `ZERO_APP_PUBLICATION to sync tables from non-public schemas.`\n : '';\n errors.push(\n `The \"${missing}\" table does not exist or is not ` +\n `one of the replicated tables: ${syncedTables}.${schemaTip}`,\n );\n }\n }\n const tables = intersection(tableSpecs, clientTables);\n for (const table of toSorted(tables)) {\n const clientSpec = clientSchema.tables[table];\n const serverSpec = must(tableSpecs.get(table)); // guaranteed by intersection\n const fullSpec = must(fullTables.get(table));\n\n const clientColumns = new Set(Object.keys(clientSpec.columns));\n const syncedColumns = new Set(Object.keys(serverSpec.zqlSpec));\n const missingColumns = difference(clientColumns, syncedColumns);\n for (const missing of toSorted(missingColumns)) {\n if (fullSpec.columns[missing]) {\n errors.push(\n `The \"${table}\".\"${missing}\" column cannot be synced because it ` +\n `is of an unsupported data type \"${fullSpec.columns[missing].dataType}\"`,\n );\n } else {\n const columns = [...syncedColumns]\n .filter(c => c !== ZERO_VERSION_COLUMN_NAME)\n .sort()\n .map(c => `\"${c}\"`)\n .join(',');\n\n errors.push(\n `The \"${table}\".\"${missing}\" column does not exist ` +\n `or is not one of the replicated columns: ${columns}.`,\n );\n }\n }\n const columns = intersection(clientColumns, syncedColumns);\n for (const column of columns) {\n const clientType = clientSpec.columns[column].type;\n const serverType = serverSpec.zqlSpec[column].type;\n if (clientSpec.columns[column].type !== serverSpec.zqlSpec[column].type) {\n errors.push(\n `The \"${table}\".\"${column}\" column's upstream type \"${serverType}\" ` +\n `does not match the client type \"${clientType}\"`,\n );\n }\n }\n if (!clientSpec.primaryKey) {\n errors.push(\n `The \"${table}\" table's client schema does not specify a primary key.`,\n );\n } else {\n const clientPrimaryKey = new Set(clientSpec.primaryKey);\n if (\n !serverSpec.tableSpec.allPotentialPrimaryKeys.some(key =>\n equals(clientPrimaryKey, new Set(key)),\n )\n ) {\n errors.push(\n `The \"${table}\" table's primaryKey <${clientSpec.primaryKey.join(',')}> ` +\n `is not associated with a non-null unique index.`,\n );\n }\n }\n }\n if (errors.length) {\n throw new ProtocolError({\n kind: 'SchemaVersionNotSupported',\n message: errors.join('\\n'),\n origin: ErrorOrigin.ZeroCache,\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-schema.js","names":["ErrorOrigin.ZeroCache"],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-schema.ts"],"sourcesContent":["import {toSorted} from '../../../../shared/src/iterables.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {\n difference,\n equals,\n intersection,\n} from '../../../../shared/src/set-utils.ts';\nimport type {ClientSchema} from '../../../../zero-protocol/src/client-schema.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ProtocolError} from '../../../../zero-protocol/src/error.ts';\nimport type {LiteAndZqlSpec, LiteTableSpec} from '../../db/specs.ts';\nimport {liteTypeToZqlValueType} from '../../types/lite.ts';\nimport {appSchema, upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport {ZERO_VERSION_COLUMN_NAME} from '../replicator/schema/constants.ts';\n\nexport function checkClientSchema(\n shardID: ShardID,\n clientSchema: ClientSchema,\n tableSpecs: Map<string, LiteAndZqlSpec>,\n fullTables: Map<string, LiteTableSpec>,\n) {\n if (fullTables.size === 0) {\n throw new ProtocolError({\n kind: 'Internal',\n message:\n `No tables have been synced from upstream. ` +\n `Please check that the ZERO_UPSTREAM_DB has been properly set.`,\n origin: ErrorOrigin.ZeroCache,\n });\n }\n const errors: string[] = [];\n const clientTables = new Set(Object.keys(clientSchema.tables));\n const missingTables = difference(clientTables, tableSpecs);\n for (const missing of toSorted(missingTables)) {\n if (fullTables.has(missing)) {\n const fullTable = must(fullTables.get(missing));\n const unsupportedPrimaryKeyColumns = (fullTable.primaryKey ?? []).filter(\n col => !liteTypeToZqlValueType(fullTable.columns[col]?.dataType ?? ''),\n );\n if (unsupportedPrimaryKeyColumns.length) {\n errors.push(\n `The \"${missing}\" table's primary key contains unsupported columns: ` +\n unsupportedPrimaryKeyColumns\n .map(\n col =>\n `\"${col}\" (${fullTable.columns[col]?.dataType ?? 'unknown'})`,\n )\n .join(', ') +\n `. These columns must use Zero-supported data types to sync the table to the client.`,\n );\n continue;\n }\n errors.push(\n `The \"${missing}\" table is missing a primary key or non-null ` +\n `unique index and thus cannot be synced to the client`,\n );\n } else {\n const app = appSchema(shardID) + '.';\n const shard = upstreamSchema(shardID) + '.';\n const syncedTables = [...tableSpecs.keys()]\n .filter(t => !t.startsWith(app) && !t.startsWith(shard))\n .sort()\n .map(t => `\"${t}\"`)\n .join(',');\n const schemaTip =\n missing.includes('.') && !syncedTables.includes('.')\n ? ` Note that zero does not sync tables from non-public schemas ` +\n `by default. Make sure you have defined a custom ` +\n `ZERO_APP_PUBLICATION to sync tables from non-public schemas.`\n : '';\n errors.push(\n `The \"${missing}\" table does not exist or is not ` +\n `one of the replicated tables: ${syncedTables}.${schemaTip}`,\n );\n }\n }\n const tables = intersection(tableSpecs, clientTables);\n for (const table of toSorted(tables)) {\n const clientSpec = clientSchema.tables[table];\n const serverSpec = must(tableSpecs.get(table)); // guaranteed by intersection\n const fullSpec = must(fullTables.get(table));\n\n const clientColumns = new Set(Object.keys(clientSpec.columns));\n const syncedColumns = new Set(Object.keys(serverSpec.zqlSpec));\n const missingColumns = difference(clientColumns, syncedColumns);\n for (const missing of toSorted(missingColumns)) {\n if (fullSpec.columns[missing]) {\n errors.push(\n `The \"${table}\".\"${missing}\" column cannot be synced because it ` +\n `is of an unsupported data type \"${fullSpec.columns[missing].dataType}\"`,\n );\n } else {\n const columns = [...syncedColumns]\n .filter(c => c !== ZERO_VERSION_COLUMN_NAME)\n .sort()\n .map(c => `\"${c}\"`)\n .join(',');\n\n errors.push(\n `The \"${table}\".\"${missing}\" column does not exist ` +\n `or is not one of the replicated columns: ${columns}.`,\n );\n }\n }\n const columns = intersection(clientColumns, syncedColumns);\n for (const column of columns) {\n const clientType = clientSpec.columns[column].type;\n const serverType = serverSpec.zqlSpec[column].type;\n if (clientSpec.columns[column].type !== serverSpec.zqlSpec[column].type) {\n errors.push(\n `The \"${table}\".\"${column}\" column's upstream type \"${serverType}\" ` +\n `does not match the client type \"${clientType}\"`,\n );\n }\n }\n if (!clientSpec.primaryKey) {\n errors.push(\n `The \"${table}\" table's client schema does not specify a primary key.`,\n );\n } else {\n const clientPrimaryKey = new Set(clientSpec.primaryKey);\n if (\n !serverSpec.tableSpec.allPotentialPrimaryKeys.some(key =>\n equals(clientPrimaryKey, new Set(key)),\n )\n ) {\n errors.push(\n `The \"${table}\" table's primaryKey <${clientSpec.primaryKey.join(',')}> ` +\n `is not associated with a non-null unique index.`,\n );\n }\n }\n }\n if (errors.length) {\n throw new ProtocolError({\n kind: 'SchemaVersionNotSupported',\n message: errors.join('\\n'),\n origin: ErrorOrigin.ZeroCache,\n });\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"connection-context-manager.js","names":["#lc","#connections","#validateLegacyJWT","#now","#revalidateIntervalMs","#retransformIntervalMs","#queryConfig","#pushConfig","#removeConnection","#nextInsertionOrder","#storeConnection","#refreshBackgroundConnectionContext","#updateBackgroundRetransformDeadline","#mustGetConnectionContext","#demoteConnection","#getConnectionContext","#group","#setGroup","#nextRevalidateAt","#getBackgroundConnectionContext","#sharedRetransformReady","#setBackgroundConnection"],"sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {InitConnectionBody} from '../../../../zero-protocol/src/connect.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {UpdateAuthBody} from '../../../../zero-protocol/src/update-auth.ts';\nimport {\n authEquals,\n resolveAuth,\n type Auth,\n type ValidateLegacyJWT,\n} from '../../auth/auth.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\nimport {compileUrlPattern} from '../../custom/fetch.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {ConnectParams} from '../../workers/connect-params.ts';\n\nexport type ConnectionState = 'provisional' | 'validated';\n\n/**\n * Normalized user identity shared by live connection state and group auth state.\n * `id: null` means logged out.\n */\nexport type UserState = {readonly id: string | null};\n\n/**\n * Delineates the two paths for validating a connection: either server can validate\n * the user's identity and return a definitive userID to trust, or we fall back to\n * trusting the one provided by the client in the incoming query params.\n */\nexport type ConnectionValidation =\n | {kind: 'client-fallback'}\n | {kind: 'server-validated'; validatedUserID: string | null};\n\n/**\n * Identifies one live websocket for a client slot.\n */\nexport type ConnectionSelector = {\n readonly clientID: string;\n readonly wsID: string;\n};\n\ntype FetchConfig = ZeroConfig['query'];\n\nexport type HeaderOptions = {\n readonly apiKey?: string | undefined;\n /** Allowlisted headers provided in the client options. */\n readonly customHeaders?: Readonly<Record<string, string>> | undefined;\n /** Allowlisted headers from the incoming HTTP request. */\n readonly requestHeaders?: Readonly<Record<string, string>> | undefined;\n readonly cookie?: string | undefined;\n readonly origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n readonly url: string | undefined;\n readonly allowedUrlPatterns: readonly URLPattern[] | undefined;\n readonly headerOptions: HeaderOptions;\n};\n\n/**\n * A snapshot of one live connection tracked by the manager.\n *\n * `revalidateAt` is only populated while the connection is `validated`.\n */\nexport type ConnectionContext = {\n readonly state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly user: UserState;\n\n readonly auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n\n readonly revision: number;\n\n readonly revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly mutateContext: ConnectionFetchContext;\n};\n\n/**\n * Group-scoped auth state shared across the live connections.\n *\n * The background connection is the validated connection currently used for\n * shared background work. Retransform happens on a group level, and uses\n * the background connection's credential to refetch the latest queries.\n */\nexport type GroupAuthState = {\n readonly pinnedUser: UserState | undefined;\n\n readonly backgroundConnection: ConnectionSelector | undefined;\n readonly retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n readonly maintenanceNotBeforeAt: number | undefined;\n};\n\nexport type ConnectionContextManager = {\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext>;\n\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext>;\n\n updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>>;\n\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined;\n\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): Readonly<ConnectionContext> | undefined;\n closeConnection(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void;\n\n setSharedRetransformReady(ready: boolean): void;\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void;\n\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext>;\n\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined;\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext>;\n\n getGroupState(): Readonly<GroupAuthState>;\n\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n };\n};\n\n/**\n * State machine for the auth state of a single `ViewSyncerService`.\n *\n * Connections are registered as `provisional`, optionally backfilled with\n * `initConnection` metadata, and then promoted to `validated` once their\n * effective `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\n */\nexport class ConnectionContextManagerImpl implements ConnectionContextManager {\n readonly #lc: LogContext;\n\n // The live connection records, keyed by clientID\n readonly #connections = new Map<string, ConnectionContext>();\n #group: GroupAuthState = {\n pinnedUser: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\n };\n\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n readonly #now: () => number;\n readonly #revalidateIntervalMs: number | undefined;\n readonly #retransformIntervalMs: number | undefined;\n readonly #queryConfig: FetchConfig | undefined;\n readonly #pushConfig: FetchConfig | undefined;\n #sharedRetransformReady = false;\n #nextInsertionOrder = 0;\n\n constructor(\n lc: LogContext,\n revalidateIntervalSeconds?: number,\n retransformIntervalSeconds?: number,\n queryConfig?: FetchConfig,\n pushConfig?: FetchConfig,\n validateLegacyJWT?: ValidateLegacyJWT,\n now?: () => number,\n ) {\n this.#lc = lc;\n this.#now = now ?? Date.now;\n this.#revalidateIntervalMs =\n revalidateIntervalSeconds === undefined\n ? undefined\n : revalidateIntervalSeconds * 1000;\n this.#retransformIntervalMs =\n retransformIntervalSeconds === undefined\n ? undefined\n : retransformIntervalSeconds * 1000;\n this.#queryConfig = queryConfig;\n this.#pushConfig = pushConfig;\n this.#validateLegacyJWT = validateLegacyJWT;\n }\n\n /**\n * Creates or replaces the live record for a websocket connection.\n *\n * Re-registering the same `clientID` drops the old socket record and starts\n * the replacement back in `provisional` state.\n */\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext> {\n this.#removeConnection(selector);\n\n const getContext = (type: 'query' | 'mutate'): ConnectionFetchContext => {\n const config = type === 'query' ? this.#queryConfig : this.#pushConfig;\n\n return {\n url: config?.url?.[0],\n allowedUrlPatterns: config?.url?.map(compileUrlPattern),\n headerOptions: {\n customHeaders: undefined,\n requestHeaders: filterHeaders(\n connectParams.requestHeaders,\n config?.allowedRequestHeaders,\n ),\n origin: connectParams.origin,\n apiKey: config?.apiKey,\n cookie: config?.forwardCookies ? connectParams.httpCookie : undefined,\n },\n };\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n user: {id: connectParams.userID ?? null},\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n\n revalidateAt: undefined,\n\n queryContext: getContext('query'),\n mutateContext: getContext('mutate'),\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#storeConnection(connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return connection;\n }\n\n /**\n * Backfills `initConnection` data for sockets that were registered before the\n * client could send its full init payload.\n *\n * This updates metadata only; it does not validate the connection.\n */\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext> {\n const connection = this.#mustGetConnectionContext(selector);\n\n let queryContext = connection.queryContext;\n let mutateContext = connection.mutateContext;\n\n if (body.userQueryURL) {\n queryContext = {\n ...queryContext,\n url: body.userQueryURL,\n };\n }\n if (body.userQueryHeaders) {\n queryContext = {\n ...queryContext,\n headerOptions: {\n ...queryContext.headerOptions,\n customHeaders: filterHeaders(\n body.userQueryHeaders,\n this.#queryConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n if (body.userPushURL) {\n mutateContext = {\n ...mutateContext,\n url: body.userPushURL,\n };\n }\n if (body.userPushHeaders) {\n mutateContext = {\n ...mutateContext,\n headerOptions: {\n ...mutateContext.headerOptions,\n customHeaders: filterHeaders(\n body.userPushHeaders,\n this.#pushConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n\n return this.#demoteConnection({\n ...connection,\n revision: connection.revision + 1,\n queryContext,\n mutateContext,\n });\n }\n\n /**\n * A material auth change demotes the connection back to provisional until it\n * is validated again.\n */\n async updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>> {\n const connection = this.#mustGetConnectionContext(selector);\n\n const nextAuth = await resolveAuth(\n this.#lc,\n connection.auth,\n connection.user.id,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n if (authChanged) {\n return this.#demoteConnection({\n ...connection,\n auth: nextAuth,\n revision: connection.revision + 1,\n });\n }\n\n if (nextAuth === connection.auth) {\n return connection;\n }\n\n return this.#storeConnection({\n ...connection,\n auth: nextAuth,\n });\n }\n\n /**\n * Validates one connection against the group's pinned `userID`.\n *\n * The first successful validation binds the group `userID`. Later\n * validations must match it. Validation also refreshes the connection's\n * revalidation deadline and may pick the connection as the group\n * background connection if none is currently available. If the websocket is\n * gone by the time async validation finishes, this becomes a no-op.\n */\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined {\n const connection = this.#getConnectionContext(selector);\n if (!connection) {\n return undefined;\n }\n\n if (connection.revision !== revision) {\n this.#lc.debug?.('Skipping validateConnection for stale revision', {\n clientID: selector.clientID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n let validatedUserState: UserState | undefined;\n\n // If the API server has validated the user's identity, we ensure that\n // the connection's claimed userID matches it.\n if (validation.kind === 'server-validated') {\n validatedUserState = {id: validation.validatedUserID};\n\n // Check that the ws connection userID provided by the client\n // matches the validated userID from the API server.\n if (connection.user.id !== validatedUserState.id) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Connection userID does not match validated server userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n }\n\n // The incoming user state is either the validated user state from the server\n // or the WS client's claimed user state if no server validation occurred.\n const incomingUserState = validatedUserState ?? connection.user;\n\n // Once a client group is validated, every later validated connection must\n // agree with that pinned identity.\n if (\n this.#group.pinnedUser !== undefined &&\n this.#group.pinnedUser.id !== incomingUserState.id\n ) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n if (this.#group.pinnedUser === undefined) {\n this.#setGroup({\n ...this.#group,\n pinnedUser: incomingUserState,\n });\n }\n\n const validatedConnection = this.#storeConnection({\n ...connection,\n state: 'validated',\n revalidateAt: this.#nextRevalidateAt(),\n });\n this.#refreshBackgroundConnectionContext(validatedConnection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: validatedConnection,\n group: this.getGroupState(),\n };\n }\n\n /** Removes one connection due to failed auth and updates all derived background/deadline state. */\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): ConnectionContext | undefined {\n return this.#removeConnection(selector, revision);\n }\n\n /** Removes one disconnected connection and updates all derived background/deadline state. */\n closeConnection(selector: ConnectionSelector): ConnectionContext | undefined {\n return this.#removeConnection(selector);\n }\n\n /**\n * Records a successful background retransform. This starts a fresh interval\n * from the manager clock when shared retransform is schedulable, or\n * clears the deadline if it is not.\n */\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n return;\n }\n if (\n backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n setSharedRetransformReady(ready: boolean): void {\n if (this.#sharedRetransformReady === ready) {\n return;\n }\n this.#sharedRetransformReady = ready;\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void {\n const intervalMs =\n kind === 'revalidate'\n ? this.#revalidateIntervalMs\n : this.#retransformIntervalMs;\n if (intervalMs === undefined) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n maintenanceNotBeforeAt: Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n ),\n });\n }\n\n /** Returns the current live record for a client slot, if any. */\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined {\n return this.#getConnectionContext(selector);\n }\n\n /** Returns the live record for one websocket or throws if it is unavailable. */\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> {\n return this.#mustGetConnectionContext(selector);\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return this.#getBackgroundConnectionContext();\n }\n\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext> {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'No validated connection is available for shared query work.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n return backgroundConnection;\n }\n\n /** Returns the shared group auth state. */\n getGroupState(): Readonly<GroupAuthState> {\n return this.#group;\n }\n\n /**\n * Reports which maintenance work is currently due.\n *\n * The result is a pure snapshot: callers decide which actions to run and\n * when to wake up next. `earliestDeadlineAt` is the earliest outstanding\n * maintenance deadline, including overdue work, unless a transient failure\n * has deferred all scheduled maintenance until `maintenanceNotBeforeAt`.\n */\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n } {\n const dueRevalidations: Readonly<ConnectionContext>[] = [];\n const now = this.#now();\n let earliestDeadlineAt = this.#group.retransformAt;\n\n for (const connection of this.#connections.values()) {\n if (\n connection.state !== 'validated' ||\n connection.revalidateAt === undefined\n ) {\n continue;\n }\n if (connection.revalidateAt <= now) {\n dueRevalidations.push(connection);\n }\n earliestDeadlineAt = minDefined(\n earliestDeadlineAt,\n connection.revalidateAt,\n );\n }\n\n const dueRetransform =\n this.#group.retransformAt !== undefined &&\n this.#group.retransformAt <= now;\n const maintenanceNotBeforeAt = this.#group.maintenanceNotBeforeAt;\n\n if (\n maintenanceNotBeforeAt !== undefined &&\n maintenanceNotBeforeAt > now &&\n earliestDeadlineAt !== undefined\n ) {\n return {\n dueRevalidations: [],\n dueRetransform: false,\n earliestDeadlineAt: Math.max(\n earliestDeadlineAt,\n maintenanceNotBeforeAt,\n ),\n };\n }\n\n return {\n dueRevalidations: dueRevalidations.sort(compareByInsertionOrder),\n dueRetransform,\n earliestDeadlineAt,\n };\n }\n\n #removeConnection(\n selector: ConnectionSelector,\n revision?: number,\n ): Readonly<ConnectionContext> | undefined {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n return undefined;\n }\n\n // If the revision has changed, we should not remove the connection\n if (revision !== undefined && connection.revision !== revision) {\n this.#lc.debug?.('Ignoring failConnection for stale revision', {\n clientID: selector.clientID,\n wsID: selector.wsID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return connection;\n }\n\n #demoteConnection(connection: ConnectionContext): ConnectionContext {\n const demotedConnection = this.#storeConnection({\n ...connection,\n state: 'provisional',\n revalidateAt: undefined,\n });\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return demotedConnection;\n }\n\n /**\n * Keeps the background connection sticky while it remains validated.\n *\n * When a newly validated `preferred` connection is provided, it is promoted\n * only if there is no current validated background connection. Otherwise the\n * existing background connection stays in place until it disappears or is\n * demoted, at which point the newest validated connection is selected.\n */\n #refreshBackgroundConnectionContext(preferred?: ConnectionContext): void {\n if (preferred?.state === 'validated') {\n const currentBackgroundConnection =\n this.#getBackgroundConnectionContext();\n if (\n currentBackgroundConnection?.clientID === preferred.clientID &&\n currentBackgroundConnection.wsID === preferred.wsID\n ) {\n return;\n }\n if (currentBackgroundConnection !== undefined) {\n return;\n }\n this.#setBackgroundConnection({\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n });\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n revision: preferred.revision,\n reason: 'preferred-validated',\n });\n return;\n }\n\n const currentBackgroundConnection = this.#getBackgroundConnectionContext();\n if (currentBackgroundConnection?.state === 'validated') {\n return;\n }\n\n const nextBackgroundConnection = [...this.#connections.values()]\n .filter(connection => connection.state === 'validated')\n .sort(comparePreferredValidatedConnection)\n .at(0);\n this.#setBackgroundConnection(\n nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined,\n );\n if (nextBackgroundConnection) {\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n revision: nextBackgroundConnection.revision,\n reason: 'fallback-validated',\n });\n }\n }\n\n #getBackgroundConnectionContext(): ConnectionContext | undefined {\n const backgroundConnection = this.#group.backgroundConnection;\n if (!backgroundConnection) {\n return undefined;\n }\n return this.#getConnectionContext(backgroundConnection);\n }\n\n #getConnectionContext(\n selector: ConnectionSelector,\n ): ConnectionContext | undefined {\n const connection = this.#connections.get(selector.clientID);\n if (!connection) {\n return undefined;\n }\n if (connection.wsID !== selector.wsID) {\n return undefined;\n }\n return connection;\n }\n\n #mustGetConnectionContext(selector: ConnectionSelector): ConnectionContext {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'Connection auth state was not available for this websocket.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n return connection;\n }\n\n #storeConnection(connection: ConnectionContext): ConnectionContext {\n this.#connections.set(connection.clientID, connection);\n return connection;\n }\n\n #setGroup(group: GroupAuthState): GroupAuthState {\n this.#group = group;\n return group;\n }\n\n #setBackgroundConnection(\n backgroundConnection: ConnectionSelector | undefined,\n ) {\n if (\n sameConnectionSelector(\n this.#group.backgroundConnection,\n backgroundConnection,\n )\n ) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n backgroundConnection: backgroundConnection\n ? {...backgroundConnection}\n : undefined,\n });\n }\n\n /**\n * Keeps the group background retransform deadline coherent with current\n * schedulability.\n *\n * When `reset` is false, this seeds a deadline only when shared retransform\n * is now possible and no deadline exists yet, preserving any existing\n * cadence. When `reset` is true, it starts a fresh interval from `#now()` if\n * retransform is schedulable for the current ready ViewSyncer instance, or\n * clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (\n !backgroundConnection ||\n this.#retransformIntervalMs === undefined ||\n !this.#sharedRetransformReady\n ) {\n if (this.#group.retransformAt !== undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: undefined,\n });\n }\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: this.#now() + this.#retransformIntervalMs,\n });\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\n }\n}\n\nfunction compareByInsertionOrder(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return a.insertionOrder - b.insertionOrder || a.wsID.localeCompare(b.wsID);\n}\n\nfunction comparePreferredValidatedConnection(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return b.insertionOrder - a.insertionOrder || b.wsID.localeCompare(a.wsID);\n}\n\nfunction minDefined(a: number | undefined, b: number | undefined) {\n if (a === undefined) {\n return b;\n }\n if (b === undefined) {\n return a;\n }\n return Math.min(a, b);\n}\n\nfunction sameConnectionSelector(\n a: ConnectionSelector | undefined,\n b: ConnectionSelector | undefined,\n) {\n return a?.clientID === b?.clientID && a?.wsID === b?.wsID;\n}\n\nfunction filterHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n allowedHeaders: readonly string[] | undefined,\n) {\n if (!headers || !allowedHeaders || allowedHeaders.length === 0) {\n return undefined;\n }\n\n const allowed = new Set(allowedHeaders.map(header => header.toLowerCase()));\n let filtered: Record<string, string> | undefined;\n for (const [key, value] of Object.entries(headers)) {\n if (allowed.has(key.toLowerCase())) {\n filtered ??= {};\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"connection-context-manager.js","names":["ErrorKind.Unauthorized","ErrorOrigin.ZeroCache","ErrorKind.InvalidConnectionRequest"],"sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {InitConnectionBody} from '../../../../zero-protocol/src/connect.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {UpdateAuthBody} from '../../../../zero-protocol/src/update-auth.ts';\nimport {\n authEquals,\n resolveAuth,\n type Auth,\n type ValidateLegacyJWT,\n} from '../../auth/auth.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\nimport {compileUrlPattern} from '../../custom/fetch.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {ConnectParams} from '../../workers/connect-params.ts';\n\nexport type ConnectionState = 'provisional' | 'validated';\n\n/**\n * Normalized user identity shared by live connection state and group auth state.\n * `id: null` means logged out.\n */\nexport type UserState = {readonly id: string | null};\n\n/**\n * Delineates the two paths for validating a connection: either server can validate\n * the user's identity and return a definitive userID to trust, or we fall back to\n * trusting the one provided by the client in the incoming query params.\n */\nexport type ConnectionValidation =\n | {kind: 'client-fallback'}\n | {kind: 'server-validated'; validatedUserID: string | null};\n\n/**\n * Identifies one live websocket for a client slot.\n */\nexport type ConnectionSelector = {\n readonly clientID: string;\n readonly wsID: string;\n};\n\ntype FetchConfig = ZeroConfig['query'];\n\nexport type HeaderOptions = {\n readonly apiKey?: string | undefined;\n /** Allowlisted headers provided in the client options. */\n readonly customHeaders?: Readonly<Record<string, string>> | undefined;\n /** Allowlisted headers from the incoming HTTP request. */\n readonly requestHeaders?: Readonly<Record<string, string>> | undefined;\n readonly cookie?: string | undefined;\n readonly origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n readonly url: string | undefined;\n readonly allowedUrlPatterns: readonly URLPattern[] | undefined;\n readonly headerOptions: HeaderOptions;\n};\n\n/**\n * A snapshot of one live connection tracked by the manager.\n *\n * `revalidateAt` is only populated while the connection is `validated`.\n */\nexport type ConnectionContext = {\n readonly state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly user: UserState;\n\n readonly auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n\n readonly revision: number;\n\n readonly revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly mutateContext: ConnectionFetchContext;\n};\n\n/**\n * Group-scoped auth state shared across the live connections.\n *\n * The background connection is the validated connection currently used for\n * shared background work. Retransform happens on a group level, and uses\n * the background connection's credential to refetch the latest queries.\n */\nexport type GroupAuthState = {\n readonly pinnedUser: UserState | undefined;\n\n readonly backgroundConnection: ConnectionSelector | undefined;\n readonly retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n readonly maintenanceNotBeforeAt: number | undefined;\n};\n\nexport type ConnectionContextManager = {\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext>;\n\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext>;\n\n updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>>;\n\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined;\n\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): Readonly<ConnectionContext> | undefined;\n closeConnection(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void;\n\n setSharedRetransformReady(ready: boolean): void;\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void;\n\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext>;\n\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined;\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext>;\n\n getGroupState(): Readonly<GroupAuthState>;\n\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n };\n};\n\n/**\n * State machine for the auth state of a single `ViewSyncerService`.\n *\n * Connections are registered as `provisional`, optionally backfilled with\n * `initConnection` metadata, and then promoted to `validated` once their\n * effective `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\n */\nexport class ConnectionContextManagerImpl implements ConnectionContextManager {\n readonly #lc: LogContext;\n\n // The live connection records, keyed by clientID\n readonly #connections = new Map<string, ConnectionContext>();\n #group: GroupAuthState = {\n pinnedUser: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\n };\n\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n readonly #now: () => number;\n readonly #revalidateIntervalMs: number | undefined;\n readonly #retransformIntervalMs: number | undefined;\n readonly #queryConfig: FetchConfig | undefined;\n readonly #pushConfig: FetchConfig | undefined;\n #sharedRetransformReady = false;\n #nextInsertionOrder = 0;\n\n constructor(\n lc: LogContext,\n revalidateIntervalSeconds?: number,\n retransformIntervalSeconds?: number,\n queryConfig?: FetchConfig,\n pushConfig?: FetchConfig,\n validateLegacyJWT?: ValidateLegacyJWT,\n now?: () => number,\n ) {\n this.#lc = lc;\n this.#now = now ?? Date.now;\n this.#revalidateIntervalMs =\n revalidateIntervalSeconds === undefined\n ? undefined\n : revalidateIntervalSeconds * 1000;\n this.#retransformIntervalMs =\n retransformIntervalSeconds === undefined\n ? undefined\n : retransformIntervalSeconds * 1000;\n this.#queryConfig = queryConfig;\n this.#pushConfig = pushConfig;\n this.#validateLegacyJWT = validateLegacyJWT;\n }\n\n /**\n * Creates or replaces the live record for a websocket connection.\n *\n * Re-registering the same `clientID` drops the old socket record and starts\n * the replacement back in `provisional` state.\n */\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext> {\n this.#removeConnection(selector);\n\n const getContext = (type: 'query' | 'mutate'): ConnectionFetchContext => {\n const config = type === 'query' ? this.#queryConfig : this.#pushConfig;\n\n return {\n url: config?.url?.[0],\n allowedUrlPatterns: config?.url?.map(compileUrlPattern),\n headerOptions: {\n customHeaders: undefined,\n requestHeaders: filterHeaders(\n connectParams.requestHeaders,\n config?.allowedRequestHeaders,\n ),\n origin: connectParams.origin,\n apiKey: config?.apiKey,\n cookie: config?.forwardCookies ? connectParams.httpCookie : undefined,\n },\n };\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n user: {id: connectParams.userID ?? null},\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n\n revalidateAt: undefined,\n\n queryContext: getContext('query'),\n mutateContext: getContext('mutate'),\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#storeConnection(connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return connection;\n }\n\n /**\n * Backfills `initConnection` data for sockets that were registered before the\n * client could send its full init payload.\n *\n * This updates metadata only; it does not validate the connection.\n */\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext> {\n const connection = this.#mustGetConnectionContext(selector);\n\n let queryContext = connection.queryContext;\n let mutateContext = connection.mutateContext;\n\n if (body.userQueryURL) {\n queryContext = {\n ...queryContext,\n url: body.userQueryURL,\n };\n }\n if (body.userQueryHeaders) {\n queryContext = {\n ...queryContext,\n headerOptions: {\n ...queryContext.headerOptions,\n customHeaders: filterHeaders(\n body.userQueryHeaders,\n this.#queryConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n if (body.userPushURL) {\n mutateContext = {\n ...mutateContext,\n url: body.userPushURL,\n };\n }\n if (body.userPushHeaders) {\n mutateContext = {\n ...mutateContext,\n headerOptions: {\n ...mutateContext.headerOptions,\n customHeaders: filterHeaders(\n body.userPushHeaders,\n this.#pushConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n\n return this.#demoteConnection({\n ...connection,\n revision: connection.revision + 1,\n queryContext,\n mutateContext,\n });\n }\n\n /**\n * A material auth change demotes the connection back to provisional until it\n * is validated again.\n */\n async updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>> {\n const connection = this.#mustGetConnectionContext(selector);\n\n const nextAuth = await resolveAuth(\n this.#lc,\n connection.auth,\n connection.user.id,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n if (authChanged) {\n return this.#demoteConnection({\n ...connection,\n auth: nextAuth,\n revision: connection.revision + 1,\n });\n }\n\n if (nextAuth === connection.auth) {\n return connection;\n }\n\n return this.#storeConnection({\n ...connection,\n auth: nextAuth,\n });\n }\n\n /**\n * Validates one connection against the group's pinned `userID`.\n *\n * The first successful validation binds the group `userID`. Later\n * validations must match it. Validation also refreshes the connection's\n * revalidation deadline and may pick the connection as the group\n * background connection if none is currently available. If the websocket is\n * gone by the time async validation finishes, this becomes a no-op.\n */\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined {\n const connection = this.#getConnectionContext(selector);\n if (!connection) {\n return undefined;\n }\n\n if (connection.revision !== revision) {\n this.#lc.debug?.('Skipping validateConnection for stale revision', {\n clientID: selector.clientID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n let validatedUserState: UserState | undefined;\n\n // If the API server has validated the user's identity, we ensure that\n // the connection's claimed userID matches it.\n if (validation.kind === 'server-validated') {\n validatedUserState = {id: validation.validatedUserID};\n\n // Check that the ws connection userID provided by the client\n // matches the validated userID from the API server.\n if (connection.user.id !== validatedUserState.id) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Connection userID does not match validated server userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n }\n\n // The incoming user state is either the validated user state from the server\n // or the WS client's claimed user state if no server validation occurred.\n const incomingUserState = validatedUserState ?? connection.user;\n\n // Once a client group is validated, every later validated connection must\n // agree with that pinned identity.\n if (\n this.#group.pinnedUser !== undefined &&\n this.#group.pinnedUser.id !== incomingUserState.id\n ) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n if (this.#group.pinnedUser === undefined) {\n this.#setGroup({\n ...this.#group,\n pinnedUser: incomingUserState,\n });\n }\n\n const validatedConnection = this.#storeConnection({\n ...connection,\n state: 'validated',\n revalidateAt: this.#nextRevalidateAt(),\n });\n this.#refreshBackgroundConnectionContext(validatedConnection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: validatedConnection,\n group: this.getGroupState(),\n };\n }\n\n /** Removes one connection due to failed auth and updates all derived background/deadline state. */\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): ConnectionContext | undefined {\n return this.#removeConnection(selector, revision);\n }\n\n /** Removes one disconnected connection and updates all derived background/deadline state. */\n closeConnection(selector: ConnectionSelector): ConnectionContext | undefined {\n return this.#removeConnection(selector);\n }\n\n /**\n * Records a successful background retransform. This starts a fresh interval\n * from the manager clock when shared retransform is schedulable, or\n * clears the deadline if it is not.\n */\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n return;\n }\n if (\n backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n setSharedRetransformReady(ready: boolean): void {\n if (this.#sharedRetransformReady === ready) {\n return;\n }\n this.#sharedRetransformReady = ready;\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void {\n const intervalMs =\n kind === 'revalidate'\n ? this.#revalidateIntervalMs\n : this.#retransformIntervalMs;\n if (intervalMs === undefined) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n maintenanceNotBeforeAt: Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n ),\n });\n }\n\n /** Returns the current live record for a client slot, if any. */\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined {\n return this.#getConnectionContext(selector);\n }\n\n /** Returns the live record for one websocket or throws if it is unavailable. */\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> {\n return this.#mustGetConnectionContext(selector);\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return this.#getBackgroundConnectionContext();\n }\n\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext> {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'No validated connection is available for shared query work.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n return backgroundConnection;\n }\n\n /** Returns the shared group auth state. */\n getGroupState(): Readonly<GroupAuthState> {\n return this.#group;\n }\n\n /**\n * Reports which maintenance work is currently due.\n *\n * The result is a pure snapshot: callers decide which actions to run and\n * when to wake up next. `earliestDeadlineAt` is the earliest outstanding\n * maintenance deadline, including overdue work, unless a transient failure\n * has deferred all scheduled maintenance until `maintenanceNotBeforeAt`.\n */\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n } {\n const dueRevalidations: Readonly<ConnectionContext>[] = [];\n const now = this.#now();\n let earliestDeadlineAt = this.#group.retransformAt;\n\n for (const connection of this.#connections.values()) {\n if (\n connection.state !== 'validated' ||\n connection.revalidateAt === undefined\n ) {\n continue;\n }\n if (connection.revalidateAt <= now) {\n dueRevalidations.push(connection);\n }\n earliestDeadlineAt = minDefined(\n earliestDeadlineAt,\n connection.revalidateAt,\n );\n }\n\n const dueRetransform =\n this.#group.retransformAt !== undefined &&\n this.#group.retransformAt <= now;\n const maintenanceNotBeforeAt = this.#group.maintenanceNotBeforeAt;\n\n if (\n maintenanceNotBeforeAt !== undefined &&\n maintenanceNotBeforeAt > now &&\n earliestDeadlineAt !== undefined\n ) {\n return {\n dueRevalidations: [],\n dueRetransform: false,\n earliestDeadlineAt: Math.max(\n earliestDeadlineAt,\n maintenanceNotBeforeAt,\n ),\n };\n }\n\n return {\n dueRevalidations: dueRevalidations.sort(compareByInsertionOrder),\n dueRetransform,\n earliestDeadlineAt,\n };\n }\n\n #removeConnection(\n selector: ConnectionSelector,\n revision?: number,\n ): Readonly<ConnectionContext> | undefined {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n return undefined;\n }\n\n // If the revision has changed, we should not remove the connection\n if (revision !== undefined && connection.revision !== revision) {\n this.#lc.debug?.('Ignoring failConnection for stale revision', {\n clientID: selector.clientID,\n wsID: selector.wsID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return connection;\n }\n\n #demoteConnection(connection: ConnectionContext): ConnectionContext {\n const demotedConnection = this.#storeConnection({\n ...connection,\n state: 'provisional',\n revalidateAt: undefined,\n });\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return demotedConnection;\n }\n\n /**\n * Keeps the background connection sticky while it remains validated.\n *\n * When a newly validated `preferred` connection is provided, it is promoted\n * only if there is no current validated background connection. Otherwise the\n * existing background connection stays in place until it disappears or is\n * demoted, at which point the newest validated connection is selected.\n */\n #refreshBackgroundConnectionContext(preferred?: ConnectionContext): void {\n if (preferred?.state === 'validated') {\n const currentBackgroundConnection =\n this.#getBackgroundConnectionContext();\n if (\n currentBackgroundConnection?.clientID === preferred.clientID &&\n currentBackgroundConnection.wsID === preferred.wsID\n ) {\n return;\n }\n if (currentBackgroundConnection !== undefined) {\n return;\n }\n this.#setBackgroundConnection({\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n });\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n revision: preferred.revision,\n reason: 'preferred-validated',\n });\n return;\n }\n\n const currentBackgroundConnection = this.#getBackgroundConnectionContext();\n if (currentBackgroundConnection?.state === 'validated') {\n return;\n }\n\n const nextBackgroundConnection = [...this.#connections.values()]\n .filter(connection => connection.state === 'validated')\n .sort(comparePreferredValidatedConnection)\n .at(0);\n this.#setBackgroundConnection(\n nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined,\n );\n if (nextBackgroundConnection) {\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n revision: nextBackgroundConnection.revision,\n reason: 'fallback-validated',\n });\n }\n }\n\n #getBackgroundConnectionContext(): ConnectionContext | undefined {\n const backgroundConnection = this.#group.backgroundConnection;\n if (!backgroundConnection) {\n return undefined;\n }\n return this.#getConnectionContext(backgroundConnection);\n }\n\n #getConnectionContext(\n selector: ConnectionSelector,\n ): ConnectionContext | undefined {\n const connection = this.#connections.get(selector.clientID);\n if (!connection) {\n return undefined;\n }\n if (connection.wsID !== selector.wsID) {\n return undefined;\n }\n return connection;\n }\n\n #mustGetConnectionContext(selector: ConnectionSelector): ConnectionContext {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'Connection auth state was not available for this websocket.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n return connection;\n }\n\n #storeConnection(connection: ConnectionContext): ConnectionContext {\n this.#connections.set(connection.clientID, connection);\n return connection;\n }\n\n #setGroup(group: GroupAuthState): GroupAuthState {\n this.#group = group;\n return group;\n }\n\n #setBackgroundConnection(\n backgroundConnection: ConnectionSelector | undefined,\n ) {\n if (\n sameConnectionSelector(\n this.#group.backgroundConnection,\n backgroundConnection,\n )\n ) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n backgroundConnection: backgroundConnection\n ? {...backgroundConnection}\n : undefined,\n });\n }\n\n /**\n * Keeps the group background retransform deadline coherent with current\n * schedulability.\n *\n * When `reset` is false, this seeds a deadline only when shared retransform\n * is now possible and no deadline exists yet, preserving any existing\n * cadence. When `reset` is true, it starts a fresh interval from `#now()` if\n * retransform is schedulable for the current ready ViewSyncer instance, or\n * clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (\n !backgroundConnection ||\n this.#retransformIntervalMs === undefined ||\n !this.#sharedRetransformReady\n ) {\n if (this.#group.retransformAt !== undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: undefined,\n });\n }\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: this.#now() + this.#retransformIntervalMs,\n });\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\n }\n}\n\nfunction compareByInsertionOrder(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return a.insertionOrder - b.insertionOrder || a.wsID.localeCompare(b.wsID);\n}\n\nfunction comparePreferredValidatedConnection(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return b.insertionOrder - a.insertionOrder || b.wsID.localeCompare(a.wsID);\n}\n\nfunction minDefined(a: number | undefined, b: number | undefined) {\n if (a === undefined) {\n return b;\n }\n if (b === undefined) {\n return a;\n }\n return Math.min(a, b);\n}\n\nfunction sameConnectionSelector(\n a: ConnectionSelector | undefined,\n b: ConnectionSelector | undefined,\n) {\n return a?.clientID === b?.clientID && a?.wsID === b?.wsID;\n}\n\nfunction filterHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n allowedHeaders: readonly string[] | undefined,\n) {\n if (!headers || !allowedHeaders || allowedHeaders.length === 0) {\n return undefined;\n }\n\n const allowed = new Set(allowedHeaders.map(header => header.toLowerCase()));\n let filtered: Record<string, string> | undefined;\n for (const [key, value] of Object.entries(headers)) {\n if (allowed.has(key.toLowerCase())) {\n filtered ??= {};\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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"}
@@ -5,8 +5,8 @@ import { RunningState } from "../running-state.js";
5
5
  import { READ_COMMITTED } from "../../db/mode-enum.js";
6
6
  import { runTx } from "../../db/run-transaction.js";
7
7
  //#region ../zero-cache/src/services/view-syncer/cvr-purger.ts
8
- var MAX_PURGE_INTERVAL_MS = 16 * (60 * 1e3);
9
- var TOMBSTONE_PURGE_THRESHOLD = 744 * 60 * 60 * 1e3;
8
+ var MAX_PURGE_INTERVAL_MS = 96e4;
9
+ var TOMBSTONE_PURGE_THRESHOLD = 26784e5;
10
10
  var CVRPurger = class {
11
11
  id = "reaper";
12
12
  #lc;
@@ -1 +1 @@
1
- {"version":3,"file":"cvr-purger.js","names":["#lc","#db","#schema","#inactivityThresholdMs","#tombstonePurgeThresholdMs","#initialBatchSize","#initialIntervalMs","#state"],"sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-purger.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {READ_COMMITTED} from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {type PostgresDB} from '../../types/pg.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nconst MINUTE = 60 * 1000;\nconst MAX_PURGE_INTERVAL_MS = 16 * MINUTE;\n\n// Purge tombstones after 31 days to facilitate up to a 30-day actives metric.\nconst TOMBSTONE_PURGE_THRESHOLD = 31 * 24 * 60 * 60 * 1000;\n\ntype Options = {\n inactivityThresholdMs: number;\n initialBatchSize: number;\n initialIntervalMs: number;\n};\n\nexport class CVRPurger implements Service {\n readonly id = 'reaper';\n\n readonly #lc: LogContext;\n readonly #db: PostgresDB;\n readonly #schema: string;\n readonly #inactivityThresholdMs: number;\n readonly #tombstonePurgeThresholdMs: number;\n readonly #initialBatchSize: number;\n readonly #initialIntervalMs: number;\n readonly #state = new RunningState('reaper');\n\n constructor(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n {inactivityThresholdMs, initialBatchSize, initialIntervalMs}: Options,\n tombstonePurgeThreshold = TOMBSTONE_PURGE_THRESHOLD,\n ) {\n this.#lc = lc;\n this.#db = db;\n this.#schema = cvrSchema(shard);\n this.#inactivityThresholdMs = inactivityThresholdMs;\n this.#tombstonePurgeThresholdMs = Math.max(\n tombstonePurgeThreshold,\n inactivityThresholdMs,\n );\n this.#initialBatchSize = initialBatchSize;\n this.#initialIntervalMs = initialIntervalMs;\n }\n\n async run() {\n let purgeable: number | undefined;\n let maxCVRsPerPurge = this.#initialBatchSize;\n let purgeInterval = this.#initialIntervalMs;\n\n if (this.#initialBatchSize === 0) {\n this.#lc.warn?.(\n `CVR garbage collection is disabled (initialBatchSize = 0)`,\n );\n // Do nothing and just wait to be stopped.\n await this.#state.stopped();\n } else {\n this.#lc.info?.(\n `running cvr-purger with`,\n await this.#db`SHOW statement_timeout`,\n );\n }\n\n while (this.#state.shouldRun()) {\n try {\n const start = performance.now();\n const {purged, remaining} =\n await this.purgeInactiveCVRs(maxCVRsPerPurge);\n this.#state.resetBackoff();\n\n if (purgeable !== undefined && remaining > purgeable) {\n // If the number of purgeable CVRs has grown even after the purge,\n // increase the number purged per round to achieve a steady state.\n maxCVRsPerPurge += this.#initialBatchSize;\n this.#lc.info?.(`increased CVRs per purge to ${maxCVRsPerPurge}`);\n }\n purgeable = remaining;\n\n purgeInterval =\n purgeable > 0\n ? this.#initialIntervalMs\n : Math.min(purgeInterval * 2, MAX_PURGE_INTERVAL_MS);\n const elapsed = performance.now() - start;\n this.#lc.info?.(\n `purged ${purged} inactive CVRs (${elapsed.toFixed(2)} ms). Next purge in ${purgeInterval} ms`,\n );\n await this.#state.sleep(purgeInterval);\n } catch (e) {\n await this.#state.backoff(this.#lc, e);\n }\n }\n }\n\n // Exported for testing.\n purgeInactiveCVRs(\n maxCVRs: number,\n ): Promise<{purged: number; remaining: number}> {\n return runTx(\n this.#db,\n async sql => {\n const now = Date.now();\n const threshold = now - this.#inactivityThresholdMs;\n const tombstonePurgeThreshold = now - this.#tombstonePurgeThresholdMs;\n // Implementation note: `FOR UPDATE` will prevent a syncer from\n // concurrently updating the CVR, since the update also performs\n // a `SELECT ... FOR UPDATE`, instead causing that update to\n // fail, which will cause the client to create a new CVR.\n //\n // `SKIP LOCKED` will skip over CVRs that a syncer is already\n // in the process of updating. In this manner, an in-progress\n // update effectively excludes the CVR from the purge.\n const ids = (\n await sql<{clientGroupID: string}[]>`\n SELECT \"clientGroupID\" FROM ${sql(this.#schema)}.instances\n WHERE NOT \"deleted\" AND \"lastActive\" < ${threshold}\n ORDER BY \"lastActive\" ASC\n LIMIT ${maxCVRs}\n FOR UPDATE SKIP LOCKED\n `.values()\n ).flat();\n\n if (ids.length > 0) {\n // Explicitly delete rows from cvr tables from \"bottom\" up. Relying on\n // foreign key cascading deletes can be suboptimal when the foreign key\n // is not a prefix of the primary key (e.g. the \"desires\" foreign key\n // reference to the \"queries\" table is not a prefix of the \"desires\"\n // primary key).\n const stmts = [\n 'desires',\n 'queries',\n 'clients',\n 'rows',\n 'rowsVersion',\n ].map(table =>\n sql`\n DELETE FROM ${sql(this.#schema)}.${sql(table)} \n WHERE \"clientGroupID\" IN ${sql(ids)}`.execute(),\n );\n // Tombstones are written for the `instances` rows, preserving the\n // \"profileID\" and \"lastActive\" columns for computing usage stats.\n //\n // For backwards compatibility (i.e. older zero-caches that do not\n // check the \"deleted\" column) reset the \"version\" to '00' to trigger\n // the ClientNotFound error via\n // view-syncer.ts:checkClientAndCVRVersions()\n stmts.push(\n sql`\n UPDATE ${sql(this.#schema)}.instances\n SET \"deleted\" = TRUE, \n \"version\" = '00', \n \"ttlClock\" = 0,\n \"replicaVersion\" = NULL, \n \"owner\" = NULL,\n \"grantedAt\" = NULL,\n \"clientSchema\" = NULL\n WHERE \"clientGroupID\" IN ${sql(ids)}`.execute(),\n );\n // Tombstone rows are deleted after the tombstonePurgeThreshold.\n stmts.push(\n sql`\n DELETE FROM ${sql(this.#schema)}.instances\n WHERE \"deleted\" AND \"lastActive\" < ${tombstonePurgeThreshold}\n `.execute(),\n );\n await Promise.all(stmts);\n }\n\n const [{remaining}] = await sql<[{remaining: bigint}]>`\n SELECT COUNT(*) AS remaining FROM ${sql(this.#schema)}.instances\n WHERE NOT \"deleted\" AND \"lastActive\" < ${threshold}\n `;\n\n return {purged: ids.length, remaining: Number(remaining)};\n },\n {mode: READ_COMMITTED},\n );\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n}\n"],"mappings":";;;;;;;AAUA,IAAM,wBAAwB,MADf,KAAK;AAIpB,IAAM,4BAA4B,MAAU,KAAK,KAAK;AAQtD,IAAa,YAAb,MAA0C;CACxC,KAAc;CAEd;CACA;CACA;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,QAAQ;CAE3C,YACE,IACA,IACA,OACA,EAAC,uBAAuB,kBAAkB,qBAC1C,0BAA0B,2BAC1B;EACA,KAAKA,MAAM;EACX,KAAKC,MAAM;EACX,KAAKC,UAAU,UAAU,KAAK;EAC9B,KAAKC,yBAAyB;EAC9B,KAAKC,6BAA6B,KAAK,IACrC,yBACA,qBACF;EACA,KAAKC,oBAAoB;EACzB,KAAKC,qBAAqB;CAC5B;CAEA,MAAM,MAAM;EACV,IAAI;EACJ,IAAI,kBAAkB,KAAKD;EAC3B,IAAI,gBAAgB,KAAKC;EAEzB,IAAI,KAAKD,sBAAsB,GAAG;GAChC,KAAKL,IAAI,OACP,2DACF;GAEA,MAAM,KAAKO,OAAO,QAAQ;EAC5B,OACE,KAAKP,IAAI,OACP,2BACA,MAAM,KAAKC,GAAG,wBAChB;EAGF,OAAO,KAAKM,OAAO,UAAU,GAC3B,IAAI;GACF,MAAM,QAAQ,YAAY,IAAI;GAC9B,MAAM,EAAC,QAAQ,cACb,MAAM,KAAK,kBAAkB,eAAe;GAC9C,KAAKA,OAAO,aAAa;GAEzB,IAAI,cAAc,KAAA,KAAa,YAAY,WAAW;IAGpD,mBAAmB,KAAKF;IACxB,KAAKL,IAAI,OAAO,+BAA+B,iBAAiB;GAClE;GACA,YAAY;GAEZ,gBACE,YAAY,IACR,KAAKM,qBACL,KAAK,IAAI,gBAAgB,GAAG,qBAAqB;GACvD,MAAM,UAAU,YAAY,IAAI,IAAI;GACpC,KAAKN,IAAI,OACP,UAAU,OAAO,kBAAkB,QAAQ,QAAQ,CAAC,EAAE,sBAAsB,cAAc,IAC5F;GACA,MAAM,KAAKO,OAAO,MAAM,aAAa;EACvC,SAAS,GAAG;GACV,MAAM,KAAKA,OAAO,QAAQ,KAAKP,KAAK,CAAC;EACvC;CAEJ;CAGA,kBACE,SAC8C;EAC9C,OAAO,MACL,KAAKC,KACL,OAAM,QAAO;GACX,MAAM,MAAM,KAAK,IAAI;GACrB,MAAM,YAAY,MAAM,KAAKE;GAC7B,MAAM,0BAA0B,MAAM,KAAKC;GAS3C,MAAM,OACJ,MAAM,GAA8B;wCACN,IAAI,KAAKF,OAAO,EAAE;qDACL,UAAU;;oBAE3C,QAAQ;;QAEpB,OAAO,GACL,KAAK;GAEP,IAAI,IAAI,SAAS,GAAG;IAMlB,MAAM,QAAQ;KACZ;KACA;KACA;KACA;KACA;IACF,EAAE,KAAI,UACJ,GAAG;0BACW,IAAI,KAAKA,OAAO,EAAE,GAAG,IAAI,KAAK,EAAE;yCACjB,IAAI,GAAG,IAAI,QAAQ,CAClD;IAQA,MAAM,KACJ,GAAG;qBACM,IAAI,KAAKA,OAAO,EAAE;;;;;;;;yCAQE,IAAI,GAAG,IAAI,QAAQ,CAClD;IAEA,MAAM,KACJ,GAAG;0BACW,IAAI,KAAKA,OAAO,EAAE;mDACO,wBAAwB;YAC/D,QAAQ,CACV;IACA,MAAM,QAAQ,IAAI,KAAK;GACzB;GAEA,MAAM,CAAC,EAAC,eAAc,MAAM,GAA0B;4CAClB,IAAI,KAAKA,OAAO,EAAE;mDACX,UAAU;;GAGrD,OAAO;IAAC,QAAQ,IAAI;IAAQ,WAAW,OAAO,SAAS;GAAC;EAC1D,GACA,EAAC,MAAM,eAAc,CACvB;CACF;CAEA,OAAsB;EACpB,KAAKK,OAAO,KAAK,KAAKP,GAAG;EACzB,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"cvr-purger.js","names":[],"sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-purger.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {READ_COMMITTED} from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {type PostgresDB} from '../../types/pg.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nconst MINUTE = 60 * 1000;\nconst MAX_PURGE_INTERVAL_MS = 16 * MINUTE;\n\n// Purge tombstones after 31 days to facilitate up to a 30-day actives metric.\nconst TOMBSTONE_PURGE_THRESHOLD = 31 * 24 * 60 * 60 * 1000;\n\ntype Options = {\n inactivityThresholdMs: number;\n initialBatchSize: number;\n initialIntervalMs: number;\n};\n\nexport class CVRPurger implements Service {\n readonly id = 'reaper';\n\n readonly #lc: LogContext;\n readonly #db: PostgresDB;\n readonly #schema: string;\n readonly #inactivityThresholdMs: number;\n readonly #tombstonePurgeThresholdMs: number;\n readonly #initialBatchSize: number;\n readonly #initialIntervalMs: number;\n readonly #state = new RunningState('reaper');\n\n constructor(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardID,\n {inactivityThresholdMs, initialBatchSize, initialIntervalMs}: Options,\n tombstonePurgeThreshold = TOMBSTONE_PURGE_THRESHOLD,\n ) {\n this.#lc = lc;\n this.#db = db;\n this.#schema = cvrSchema(shard);\n this.#inactivityThresholdMs = inactivityThresholdMs;\n this.#tombstonePurgeThresholdMs = Math.max(\n tombstonePurgeThreshold,\n inactivityThresholdMs,\n );\n this.#initialBatchSize = initialBatchSize;\n this.#initialIntervalMs = initialIntervalMs;\n }\n\n async run() {\n let purgeable: number | undefined;\n let maxCVRsPerPurge = this.#initialBatchSize;\n let purgeInterval = this.#initialIntervalMs;\n\n if (this.#initialBatchSize === 0) {\n this.#lc.warn?.(\n `CVR garbage collection is disabled (initialBatchSize = 0)`,\n );\n // Do nothing and just wait to be stopped.\n await this.#state.stopped();\n } else {\n this.#lc.info?.(\n `running cvr-purger with`,\n await this.#db`SHOW statement_timeout`,\n );\n }\n\n while (this.#state.shouldRun()) {\n try {\n const start = performance.now();\n const {purged, remaining} =\n await this.purgeInactiveCVRs(maxCVRsPerPurge);\n this.#state.resetBackoff();\n\n if (purgeable !== undefined && remaining > purgeable) {\n // If the number of purgeable CVRs has grown even after the purge,\n // increase the number purged per round to achieve a steady state.\n maxCVRsPerPurge += this.#initialBatchSize;\n this.#lc.info?.(`increased CVRs per purge to ${maxCVRsPerPurge}`);\n }\n purgeable = remaining;\n\n purgeInterval =\n purgeable > 0\n ? this.#initialIntervalMs\n : Math.min(purgeInterval * 2, MAX_PURGE_INTERVAL_MS);\n const elapsed = performance.now() - start;\n this.#lc.info?.(\n `purged ${purged} inactive CVRs (${elapsed.toFixed(2)} ms). Next purge in ${purgeInterval} ms`,\n );\n await this.#state.sleep(purgeInterval);\n } catch (e) {\n await this.#state.backoff(this.#lc, e);\n }\n }\n }\n\n // Exported for testing.\n purgeInactiveCVRs(\n maxCVRs: number,\n ): Promise<{purged: number; remaining: number}> {\n return runTx(\n this.#db,\n async sql => {\n const now = Date.now();\n const threshold = now - this.#inactivityThresholdMs;\n const tombstonePurgeThreshold = now - this.#tombstonePurgeThresholdMs;\n // Implementation note: `FOR UPDATE` will prevent a syncer from\n // concurrently updating the CVR, since the update also performs\n // a `SELECT ... FOR UPDATE`, instead causing that update to\n // fail, which will cause the client to create a new CVR.\n //\n // `SKIP LOCKED` will skip over CVRs that a syncer is already\n // in the process of updating. In this manner, an in-progress\n // update effectively excludes the CVR from the purge.\n const ids = (\n await sql<{clientGroupID: string}[]>`\n SELECT \"clientGroupID\" FROM ${sql(this.#schema)}.instances\n WHERE NOT \"deleted\" AND \"lastActive\" < ${threshold}\n ORDER BY \"lastActive\" ASC\n LIMIT ${maxCVRs}\n FOR UPDATE SKIP LOCKED\n `.values()\n ).flat();\n\n if (ids.length > 0) {\n // Explicitly delete rows from cvr tables from \"bottom\" up. Relying on\n // foreign key cascading deletes can be suboptimal when the foreign key\n // is not a prefix of the primary key (e.g. the \"desires\" foreign key\n // reference to the \"queries\" table is not a prefix of the \"desires\"\n // primary key).\n const stmts = [\n 'desires',\n 'queries',\n 'clients',\n 'rows',\n 'rowsVersion',\n ].map(table =>\n sql`\n DELETE FROM ${sql(this.#schema)}.${sql(table)} \n WHERE \"clientGroupID\" IN ${sql(ids)}`.execute(),\n );\n // Tombstones are written for the `instances` rows, preserving the\n // \"profileID\" and \"lastActive\" columns for computing usage stats.\n //\n // For backwards compatibility (i.e. older zero-caches that do not\n // check the \"deleted\" column) reset the \"version\" to '00' to trigger\n // the ClientNotFound error via\n // view-syncer.ts:checkClientAndCVRVersions()\n stmts.push(\n sql`\n UPDATE ${sql(this.#schema)}.instances\n SET \"deleted\" = TRUE, \n \"version\" = '00', \n \"ttlClock\" = 0,\n \"replicaVersion\" = NULL, \n \"owner\" = NULL,\n \"grantedAt\" = NULL,\n \"clientSchema\" = NULL\n WHERE \"clientGroupID\" IN ${sql(ids)}`.execute(),\n );\n // Tombstone rows are deleted after the tombstonePurgeThreshold.\n stmts.push(\n sql`\n DELETE FROM ${sql(this.#schema)}.instances\n WHERE \"deleted\" AND \"lastActive\" < ${tombstonePurgeThreshold}\n `.execute(),\n );\n await Promise.all(stmts);\n }\n\n const [{remaining}] = await sql<[{remaining: bigint}]>`\n SELECT COUNT(*) AS remaining FROM ${sql(this.#schema)}.instances\n WHERE NOT \"deleted\" AND \"lastActive\" < ${threshold}\n `;\n\n return {purged: ids.length, remaining: Number(remaining)};\n },\n {mode: READ_COMMITTED},\n );\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n}\n"],"mappings":";;;;;;;AAUA,IAAM,wBAAwB;AAG9B,IAAM,4BAA4B;AAQlC,IAAa,YAAb,MAA0C;CACxC,KAAc;CAEd;CACA;CACA;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,QAAQ;CAE3C,YACE,IACA,IACA,OACA,EAAC,uBAAuB,kBAAkB,qBAC1C,0BAA0B,2BAC1B;EACA,KAAK,MAAM;EACX,KAAK,MAAM;EACX,KAAK,UAAU,UAAU,KAAK;EAC9B,KAAK,yBAAyB;EAC9B,KAAK,6BAA6B,KAAK,IACrC,yBACA,qBACF;EACA,KAAK,oBAAoB;EACzB,KAAK,qBAAqB;CAC5B;CAEA,MAAM,MAAM;EACV,IAAI;EACJ,IAAI,kBAAkB,KAAK;EAC3B,IAAI,gBAAgB,KAAK;EAEzB,IAAI,KAAK,sBAAsB,GAAG;GAChC,KAAK,IAAI,OACP,2DACF;GAEA,MAAM,KAAK,OAAO,QAAQ;EAC5B,OACE,KAAK,IAAI,OACP,2BACA,MAAM,KAAK,GAAG,wBAChB;EAGF,OAAO,KAAK,OAAO,UAAU,GAC3B,IAAI;GACF,MAAM,QAAQ,YAAY,IAAI;GAC9B,MAAM,EAAC,QAAQ,cACb,MAAM,KAAK,kBAAkB,eAAe;GAC9C,KAAK,OAAO,aAAa;GAEzB,IAAI,cAAc,KAAA,KAAa,YAAY,WAAW;IAGpD,mBAAmB,KAAK;IACxB,KAAK,IAAI,OAAO,+BAA+B,iBAAiB;GAClE;GACA,YAAY;GAEZ,gBACE,YAAY,IACR,KAAK,qBACL,KAAK,IAAI,gBAAgB,GAAG,qBAAqB;GACvD,MAAM,UAAU,YAAY,IAAI,IAAI;GACpC,KAAK,IAAI,OACP,UAAU,OAAO,kBAAkB,QAAQ,QAAQ,CAAC,EAAE,sBAAsB,cAAc,IAC5F;GACA,MAAM,KAAK,OAAO,MAAM,aAAa;EACvC,SAAS,GAAG;GACV,MAAM,KAAK,OAAO,QAAQ,KAAK,KAAK,CAAC;EACvC;CAEJ;CAGA,kBACE,SAC8C;EAC9C,OAAO,MACL,KAAK,KACL,OAAM,QAAO;GACX,MAAM,MAAM,KAAK,IAAI;GACrB,MAAM,YAAY,MAAM,KAAK;GAC7B,MAAM,0BAA0B,MAAM,KAAK;GAS3C,MAAM,OACJ,MAAM,GAA8B;wCACN,IAAI,KAAK,OAAO,EAAE;qDACL,UAAU;;oBAE3C,QAAQ;;QAEpB,OAAO,EAAA,CACL,KAAK;GAEP,IAAI,IAAI,SAAS,GAAG;IAMlB,MAAM,QAAQ;KACZ;KACA;KACA;KACA;KACA;IACF,CAAC,CAAC,KAAI,UACJ,GAAG;0BACW,IAAI,KAAK,OAAO,EAAE,GAAG,IAAI,KAAK,EAAE;yCACjB,IAAI,GAAG,IAAI,QAAQ,CAClD;IAQA,MAAM,KACJ,GAAG;qBACM,IAAI,KAAK,OAAO,EAAE;;;;;;;;yCAQE,IAAI,GAAG,IAAI,QAAQ,CAClD;IAEA,MAAM,KACJ,GAAG;0BACW,IAAI,KAAK,OAAO,EAAE;mDACO,wBAAwB;YAC/D,QAAQ,CACV;IACA,MAAM,QAAQ,IAAI,KAAK;GACzB;GAEA,MAAM,CAAC,EAAC,eAAc,MAAM,GAA0B;4CAClB,IAAI,KAAK,OAAO,EAAE;mDACX,UAAU;;GAGrD,OAAO;IAAC,QAAQ,IAAI;IAAQ,WAAW,OAAO,SAAS;GAAC;EAC1D,GACA,EAAC,MAAM,eAAc,CACvB;CACF;CAEA,OAAsB;EACpB,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,OAAO;CACT;AACF"}
@@ -251,8 +251,9 @@ var CVRStore = class {
251
251
  assert(instance.length === 1, () => `Expected exactly one CVR instance, got ${instance.length}`);
252
252
  const { version, lastActive, ttlClock, replicaVersion, owner, grantedAt, rowsVersion, clientSchema, profileID, deleted } = instance[0];
253
253
  if (deleted) throw new ClientNotFoundError("Client has been purged due to inactivity");
254
- if (owner !== this.#taskID) if ((grantedAt ?? 0) > lastConnectTime) throw new OwnershipError(owner, grantedAt, lastConnectTime);
255
- else this.#db`
254
+ if (owner !== this.#taskID) {
255
+ if ((grantedAt ?? 0) > lastConnectTime) throw new OwnershipError(owner, grantedAt, lastConnectTime);
256
+ else this.#db`
256
257
  UPDATE ${this.#cvr("instances")}
257
258
  SET "owner" = ${this.#taskID},
258
259
  "grantedAt" = ${lastConnectTime}
@@ -260,6 +261,7 @@ var CVRStore = class {
260
261
  ("grantedAt" IS NULL OR
261
262
  "grantedAt" <= to_timestamp(${lastConnectTime / 1e3}))
262
263
  `.execute().catch(this.#failService);
264
+ }
263
265
  if (version !== (rowsVersion ?? EMPTY_CVR_VERSION.stateVersion)) return new RowsVersionBehindError(version, rowsVersion);
264
266
  cvr.version = versionFromString(version);
265
267
  cvr.lastActive = lastActive;
@@ -284,8 +286,10 @@ var CVRStore = class {
284
286
  }
285
287
  for (const row of desiresRows) {
286
288
  const client = cvr.clients[row.clientID];
287
- if (!row.deleted && row.inactivatedAt === null) if (client) client.desiredQueryIDs.push(row.queryHash);
288
- else lc.debug?.(`Not adding to desiredQueryIDs for client ${row.clientID} because it has been deleted.`);
289
+ if (!row.deleted && row.inactivatedAt === null) {
290
+ if (client) client.desiredQueryIDs.push(row.queryHash);
291
+ else lc.debug?.(`Not adding to desiredQueryIDs for client ${row.clientID} because it has been deleted.`);
292
+ }
289
293
  const query = cvr.queries[row.queryHash];
290
294
  if (query && query.type !== "internal" && (!row.deleted || row.inactivatedAt !== null)) query.clientState[row.clientID] = {
291
295
  inactivatedAt: row.inactivatedAt ?? void 0,
@@ -980,6 +984,6 @@ var RowsVersionBehindError = class extends Error {
980
984
  }
981
985
  };
982
986
  //#endregion
983
- export { CVRStore, ClientNotFoundError, checkVersion };
987
+ export { CVRStore, ClientNotFoundError, ConcurrentModificationException, InvalidClientSchemaError, OwnershipError, RowsVersionBehindError, checkVersion };
984
988
 
985
989
  //# sourceMappingURL=cvr-store.js.map