@rocicorp/zero 0.22.2025071101 → 0.22.2025072500

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 (189) hide show
  1. package/out/{chunk-4FQJO7OM.js → chunk-CPTFADYI.js} +26 -7
  2. package/out/{chunk-4FQJO7OM.js.map → chunk-CPTFADYI.js.map} +2 -2
  3. package/out/{chunk-INLOZBST.js → chunk-P5M53J4D.js} +2 -3
  4. package/out/{chunk-INLOZBST.js.map → chunk-P5M53J4D.js.map} +2 -2
  5. package/out/{chunk-5A6QECBY.js → chunk-ROYQ5GXF.js} +230 -137
  6. package/out/chunk-ROYQ5GXF.js.map +7 -0
  7. package/out/{inspector-HDOYOVMS.js → inspector-TFZBDN6K.js} +2 -2
  8. package/out/otel/src/enabled.d.ts +5 -0
  9. package/out/otel/src/enabled.d.ts.map +1 -0
  10. package/out/otel/src/enabled.js +16 -0
  11. package/out/otel/src/enabled.js.map +1 -0
  12. package/out/react.js +2 -2
  13. package/out/replicache/src/kv/idb-store.d.ts.map +1 -1
  14. package/out/shared/src/options.d.ts +4 -1
  15. package/out/shared/src/options.d.ts.map +1 -1
  16. package/out/shared/src/options.js +1 -1
  17. package/out/shared/src/options.js.map +1 -1
  18. package/out/solid.js +7 -3
  19. package/out/solid.js.map +1 -1
  20. package/out/zero/package.json +161 -0
  21. package/out/zero-cache/src/config/normalize.d.ts +1 -0
  22. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  23. package/out/zero-cache/src/config/normalize.js +10 -1
  24. package/out/zero-cache/src/config/normalize.js.map +1 -1
  25. package/out/zero-cache/src/config/zero-config.d.ts +44 -6
  26. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  27. package/out/zero-cache/src/config/zero-config.js +48 -54
  28. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  29. package/out/zero-cache/src/custom/fetch.d.ts +14 -1
  30. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  31. package/out/zero-cache/src/custom/fetch.js +56 -1
  32. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  33. package/out/zero-cache/src/custom-queries/transform-query.d.ts +1 -1
  34. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  35. package/out/zero-cache/src/custom-queries/transform-query.js +2 -1
  36. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  37. package/out/zero-cache/src/server/anonymous-otel-start.d.ts +9 -0
  38. package/out/zero-cache/src/server/anonymous-otel-start.d.ts.map +1 -0
  39. package/out/zero-cache/src/server/anonymous-otel-start.js +293 -0
  40. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -0
  41. package/out/zero-cache/src/server/logging.d.ts.map +1 -1
  42. package/out/zero-cache/src/server/logging.js +3 -3
  43. package/out/zero-cache/src/server/logging.js.map +1 -1
  44. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  45. package/out/zero-cache/src/server/main.js +1 -5
  46. package/out/zero-cache/src/server/main.js.map +1 -1
  47. package/out/zero-cache/src/server/otel-start.d.ts.map +1 -1
  48. package/out/zero-cache/src/server/otel-start.js +24 -18
  49. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  50. package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
  51. package/out/zero-cache/src/server/runner/zero-dispatcher.js +2 -2
  52. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  53. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  54. package/out/zero-cache/src/server/syncer.js +7 -4
  55. package/out/zero-cache/src/server/syncer.js.map +1 -1
  56. package/out/zero-cache/src/server/worker-dispatcher.js +3 -3
  57. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  58. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +0 -7
  59. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  60. package/out/zero-cache/src/services/change-source/pg/change-source.js +43 -32
  61. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  62. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  63. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +24 -12
  64. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  65. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  66. package/out/zero-cache/src/services/change-source/pg/schema/init.js +12 -1
  67. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  68. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +10 -0
  69. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  70. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +21 -2
  71. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  72. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +1 -1
  73. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  74. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +4 -2
  75. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  76. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  77. package/out/zero-cache/src/services/litestream/commands.js +50 -23
  78. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  79. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  80. package/out/zero-cache/src/services/mutagen/mutagen.js +4 -0
  81. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  82. package/out/zero-cache/src/services/mutagen/pusher.d.ts +45 -6
  83. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  84. package/out/zero-cache/src/services/mutagen/pusher.js +22 -6
  85. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  86. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  87. package/out/zero-cache/src/services/view-syncer/client-handler.js +28 -0
  88. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  89. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +11 -8
  90. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/view-syncer/cvr-store.js +14 -13
  92. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  93. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +9 -16
  94. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  95. package/out/zero-cache/src/services/view-syncer/cvr.js +71 -28
  96. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  97. package/out/zero-cache/src/services/view-syncer/row-record-cache.d.ts.map +1 -1
  98. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +3 -0
  99. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  100. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts +3 -2
  101. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts.map +1 -1
  102. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  103. package/out/zero-cache/src/services/view-syncer/schema/types.d.ts +11 -11
  104. package/out/zero-cache/src/services/view-syncer/schema/types.d.ts.map +1 -1
  105. package/out/zero-cache/src/services/view-syncer/schema/types.js +3 -2
  106. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  107. package/out/zero-cache/src/services/view-syncer/ttl-clock.d.ts +10 -0
  108. package/out/zero-cache/src/services/view-syncer/ttl-clock.d.ts.map +1 -0
  109. package/out/zero-cache/src/services/view-syncer/ttl-clock.js +9 -0
  110. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -0
  111. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +14 -13
  112. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  113. package/out/zero-cache/src/services/view-syncer/view-syncer.js +81 -124
  114. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  115. package/out/zero-cache/src/types/processes.d.ts +6 -6
  116. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  117. package/out/zero-cache/src/types/processes.js +2 -2
  118. package/out/zero-cache/src/types/processes.js.map +1 -1
  119. package/out/zero-cache/src/types/websocket-handoff.d.ts +2 -2
  120. package/out/zero-cache/src/types/websocket-handoff.d.ts.map +1 -1
  121. package/out/zero-cache/src/types/websocket-handoff.js +4 -4
  122. package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
  123. package/out/zero-cache/src/workers/connection.d.ts +1 -1
  124. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  125. package/out/zero-cache/src/workers/connection.js +2 -0
  126. package/out/zero-cache/src/workers/connection.js.map +1 -1
  127. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  128. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +6 -1
  129. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  130. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  131. package/out/zero-cache/src/workers/syncer.js +3 -1
  132. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  133. package/out/zero-client/src/client/active-clients-manager.d.ts.map +1 -1
  134. package/out/zero-client/src/client/mutation-tracker.d.ts +7 -2
  135. package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
  136. package/out/zero-client/src/client/options.d.ts +9 -4
  137. package/out/zero-client/src/client/options.d.ts.map +1 -1
  138. package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
  139. package/out/zero-client/src/client/zero-poke-handler.d.ts +6 -2
  140. package/out/zero-client/src/client/zero-poke-handler.d.ts.map +1 -1
  141. package/out/zero-client/src/client/zero.d.ts +2 -2
  142. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  143. package/out/zero-client/src/mod.d.ts +1 -1
  144. package/out/zero-client/src/mod.d.ts.map +1 -1
  145. package/out/zero-protocol/src/connect.d.ts +24 -2
  146. package/out/zero-protocol/src/connect.d.ts.map +1 -1
  147. package/out/zero-protocol/src/connect.js +17 -2
  148. package/out/zero-protocol/src/connect.js.map +1 -1
  149. package/out/zero-protocol/src/down.d.ts +18 -0
  150. package/out/zero-protocol/src/down.d.ts.map +1 -1
  151. package/out/zero-protocol/src/mutations-patch.d.ts +65 -0
  152. package/out/zero-protocol/src/mutations-patch.d.ts.map +1 -0
  153. package/out/zero-protocol/src/mutations-patch.js +16 -0
  154. package/out/zero-protocol/src/mutations-patch.js.map +1 -0
  155. package/out/zero-protocol/src/poke.d.ts +36 -0
  156. package/out/zero-protocol/src/poke.d.ts.map +1 -1
  157. package/out/zero-protocol/src/poke.js +3 -0
  158. package/out/zero-protocol/src/poke.js.map +1 -1
  159. package/out/zero-protocol/src/protocol-version.d.ts +1 -1
  160. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  161. package/out/zero-protocol/src/protocol-version.js +4 -1
  162. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  163. package/out/zero-protocol/src/push.d.ts +8 -3
  164. package/out/zero-protocol/src/push.d.ts.map +1 -1
  165. package/out/zero-protocol/src/push.js +7 -3
  166. package/out/zero-protocol/src/push.js.map +1 -1
  167. package/out/zero-protocol/src/up.d.ts +9 -1
  168. package/out/zero-protocol/src/up.d.ts.map +1 -1
  169. package/out/zero-protocol/src/up.js +2 -2
  170. package/out/zero-protocol/src/up.js.map +1 -1
  171. package/out/zero-server/src/queries/process-queries.d.ts +1 -1
  172. package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
  173. package/out/zero-server/src/queries/process-queries.js +1 -1
  174. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  175. package/out/zero-solid/src/mod.d.ts +2 -2
  176. package/out/zero-solid/src/mod.d.ts.map +1 -1
  177. package/out/zero.js +9 -7
  178. package/out/zql/src/query/named.d.ts +11 -11
  179. package/out/zql/src/query/named.d.ts.map +1 -1
  180. package/out/zql/src/query/named.js +20 -2
  181. package/out/zql/src/query/named.js.map +1 -1
  182. package/out/zql/src/query/query-impl.js +1 -1
  183. package/out/zql/src/query/query-impl.js.map +1 -1
  184. package/out/zql/src/query/query.d.ts +12 -3
  185. package/out/zql/src/query/query.d.ts.map +1 -1
  186. package/out/zql/src/query/query.js.map +1 -1
  187. package/package.json +2 -2
  188. package/out/chunk-5A6QECBY.js.map +0 -7
  189. /package/out/{inspector-HDOYOVMS.js.map → inspector-TFZBDN6K.js.map} +0 -0
@@ -10,6 +10,7 @@ import { hasOwn } from "../../../../shared/src/has-own.js";
10
10
  import { must } from "../../../../shared/src/must.js";
11
11
  import { randInt } from "../../../../shared/src/rand.js";
12
12
  import { ErrorKind } from "../../../../zero-protocol/src/error-kind.js";
13
+ import { clampTTL, MAX_TTL_MS } from "../../../../zql/src/query/ttl.js";
13
14
  import { transformAndHashQuery, } from "../../auth/read-authorizer.js";
14
15
  import {} from "../../config/zero-config.js";
15
16
  import { CustomQueryTransformer } from "../../custom-queries/transform-query.js";
@@ -21,15 +22,13 @@ import { Subscription } from "../../types/subscription.js";
21
22
  import { ZERO_VERSION_COLUMN_NAME } from "../replicator/schema/replication-state.js";
22
23
  import { ClientHandler, startPoke, } from "./client-handler.js";
23
24
  import { CVRStore } from "./cvr-store.js";
24
- import { CVRConfigDrivenUpdater, CVRQueryDrivenUpdater, CVRUpdater, getInactiveQueries, nextEvictionTime, } from "./cvr.js";
25
+ import { CVRConfigDrivenUpdater, CVRQueryDrivenUpdater, CVRUpdater, nextEvictionTime, } from "./cvr.js";
25
26
  import { PipelineDriver } from "./pipeline-driver.js";
26
27
  import { cmpVersions, EMPTY_CVR_VERSION, versionFromString, versionString, versionToCookie, } from "./schema/types.js";
27
28
  import { ResetPipelinesSignal } from "./snapshotter.js";
29
+ import { ttlClockAsNumber, ttlClockFromNumber, } from "./ttl-clock.js";
28
30
  const tracer = trace.getTracer('view-syncer', version);
29
31
  const DEFAULT_KEEPALIVE_MS = 5_000;
30
- // We have previously said that the goal is to have 20MB on the client.
31
- // If we assume each row is ~1KB, then we can have 20,000 rows.
32
- const DEFAULT_MAX_ROW_COUNT = 20_000;
33
32
  function randomID() {
34
33
  return randInt(1, Number.MAX_SAFE_INTEGER).toString(36);
35
34
  }
@@ -38,7 +37,14 @@ function randomID() {
38
37
  * some flushes do not write to the CVR and in those cases we
39
38
  * use a timer to update the ttlClock every minute.
40
39
  */
41
- const TTL_CLOCK_INTERVAL = 60_000;
40
+ export const TTL_CLOCK_INTERVAL = 60_000;
41
+ /**
42
+ * This is some extra time we delay the TTL timer to allow for some
43
+ * slack in the timing of the timer. This is to allow multiple evictions
44
+ * to happen in a short period of time without having to wait for the
45
+ * next tick of the timer.
46
+ */
47
+ export const TTL_TIMER_HYSTERESIS = 50; // ms
42
48
  export class ViewSyncerService {
43
49
  id;
44
50
  #shard;
@@ -48,7 +54,7 @@ export class ViewSyncerService {
48
54
  #drainCoordinator;
49
55
  #keepaliveMs;
50
56
  #slowHydrateThreshold;
51
- #pullConfig;
57
+ #queryConfig;
52
58
  // The ViewSyncerService is only started in response to a connection,
53
59
  // so #lastConnectTime is always initialized to now(). This is necessary
54
60
  // to handle race conditions in which, e.g. the replica is ready and the
@@ -60,7 +66,7 @@ export class ViewSyncerService {
60
66
  * The TTL clock is used to determine the time at which queries are considered
61
67
  * expired.
62
68
  */
63
- #ttlClock = Date.now();
69
+ #ttlClock;
64
70
  /**
65
71
  * The base time for the TTL clock. This is used to compute the current TTL
66
72
  * clock value. The first time a connection is made, this is set to the
@@ -73,9 +79,6 @@ export class ViewSyncerService {
73
79
  * time that has passed since the last time we set it.
74
80
  */
75
81
  #ttlClockBase = Date.now();
76
- get ttlClockBase() {
77
- return this.#ttlClockBase;
78
- }
79
82
  /**
80
83
  * We update the ttlClock every minute to ensure that it is not too much
81
84
  * out of sync with the current time.
@@ -95,25 +98,13 @@ export class ViewSyncerService {
95
98
  // auth and cookie headers directly
96
99
  #authData;
97
100
  #httpCookie;
98
- /**
99
- * The {@linkcode maxRowCount} is used for the eviction of inactive queries.
100
- * An inactive query is a query that is no longer desired but is kept alive
101
- * due to its TTL. When the number of rows in the CVR exceeds
102
- * {@linkcode maxRowCount} we keep removing inactive queries (even if they are
103
- * not expired yet) until the actual row count is below the max row count.
104
- *
105
- * There is no guarantee that the number of rows in the CVR will be below this
106
- * if there are active queries that have a lot of rows.
107
- */
108
- maxRowCount;
109
101
  #expiredQueriesTimer = 0;
110
- #nextExpiredQueryTime = 0;
111
102
  #setTimeout;
112
103
  #customQueryTransformer;
113
- constructor(pullConfig, lc, shard, taskID, clientGroupID, db, pipelineDriver, versionChanges, drainCoordinator, slowHydrateThreshold, keepaliveMs = DEFAULT_KEEPALIVE_MS, maxRowCount = DEFAULT_MAX_ROW_COUNT, setTimeoutFn = setTimeout.bind(globalThis)) {
104
+ constructor(pullConfig, lc, shard, taskID, clientGroupID, db, pipelineDriver, versionChanges, drainCoordinator, slowHydrateThreshold, keepaliveMs = DEFAULT_KEEPALIVE_MS, setTimeoutFn = setTimeout.bind(globalThis)) {
114
105
  this.id = clientGroupID;
115
106
  this.#shard = shard;
116
- this.#pullConfig = pullConfig;
107
+ this.#queryConfig = pullConfig;
117
108
  this.#lc = lc;
118
109
  this.#pipelines = pipelineDriver;
119
110
  this.#stateChanges = versionChanges;
@@ -124,7 +115,6 @@ export class ViewSyncerService {
124
115
  // On failure, cancel the #stateChanges subscription. The run()
125
116
  // loop will then await #cvrStore.flushed() which rejects if necessary.
126
117
  () => this.#stateChanges.cancel());
127
- this.maxRowCount = maxRowCount;
128
118
  this.#setTimeout = setTimeoutFn;
129
119
  if (pullConfig.url) {
130
120
  this.#customQueryTransformer = new CustomQueryTransformer({
@@ -155,6 +145,16 @@ export class ViewSyncerService {
155
145
  if (!this.#cvr) {
156
146
  this.#lc.debug?.('loading CVR');
157
147
  this.#cvr = await this.#cvrStore.load(lc, this.#lastConnectTime);
148
+ this.#ttlClock = this.#cvr.ttlClock;
149
+ this.#ttlClockBase = Date.now();
150
+ }
151
+ else {
152
+ // Make sure the CVR ttlClock is up to date.
153
+ const now = Date.now();
154
+ this.#cvr = {
155
+ ...this.#cvr,
156
+ ttlClock: this.#getTTLClock(now),
157
+ };
158
158
  }
159
159
  try {
160
160
  await fn(lc, this.#cvr);
@@ -231,16 +231,17 @@ export class ViewSyncerService {
231
231
  }
232
232
  // must be called from within #lock
233
233
  #removeExpiredQueries = async (lc, cvr) => {
234
- const now = Date.now();
235
- const ttlClock = this.#getTTLClock(now);
236
- if (hasExpiredQueries(cvr, ttlClock)) {
234
+ if (hasExpiredQueries(cvr)) {
237
235
  lc = lc.withContext('method', '#removeExpiredQueries');
238
- lc.info?.('Queries have expired');
236
+ lc.debug?.('Queries have expired');
239
237
  // #syncQueryPipelineSet() will remove the expired queries.
240
238
  await this.#syncQueryPipelineSet(lc, cvr);
241
239
  this.#pipelinesSynced = true;
242
- this.#scheduleExpireEviction(lc, cvr);
243
240
  }
241
+ // Even if we have expired queries, we still need to schedule next eviction
242
+ // since there might be inactivated queries that need to be expired queries
243
+ // in the future.
244
+ this.#scheduleExpireEviction(lc, cvr);
244
245
  };
245
246
  #totalHydrationTimeMs() {
246
247
  return this.#pipelines.totalHydrationTimeMs();
@@ -290,7 +291,7 @@ export class ViewSyncerService {
290
291
  // If no clients have connected while waiting for the row flush, shutdown.
291
292
  return this.#clients.size === 0;
292
293
  }
293
- #deleteClient(clientID, client) {
294
+ #deleteClientDueToDisconnect(clientID, client) {
294
295
  // Note: It is okay to delete / cleanup clients without acquiring the lock.
295
296
  // In fact, it is important to do so in order to guarantee that idle cleanup
296
297
  // is performed in a timely manner, regardless of the amount of work
@@ -299,7 +300,11 @@ export class ViewSyncerService {
299
300
  if (c === client) {
300
301
  this.#clients.delete(clientID);
301
302
  if (this.#clients.size === 0) {
302
- this.#updateTTLClockInCVRWithoutLock(this.#lc);
303
+ // It is possible to delete a client before we read the ttl clock from
304
+ // the CVR.
305
+ if (this.#ttlClock !== undefined) {
306
+ this.#updateTTLClockInCVRWithoutLock(this.#lc);
307
+ }
303
308
  this.#stopExpireTimer();
304
309
  this.#scheduleShutdown();
305
310
  }
@@ -309,7 +314,6 @@ export class ViewSyncerService {
309
314
  this.#lc.debug?.('Stopping expired queries timer');
310
315
  clearTimeout(this.#expiredQueriesTimer);
311
316
  this.#expiredQueriesTimer = 0;
312
- this.#nextExpiredQueryTime = 0;
313
317
  }
314
318
  initConnection(ctx, initConnectionMessage) {
315
319
  this.#lc.debug?.('viewSyncer.initConnection');
@@ -327,7 +331,7 @@ export class ViewSyncerService {
327
331
  err
328
332
  ? lc[getLogLevel(err)]?.(`client closed with error`, err)
329
333
  : lc.info?.('client closed');
330
- this.#deleteClient(clientID, newClient);
334
+ this.#deleteClientDueToDisconnect(clientID, newClient);
331
335
  },
332
336
  });
333
337
  if (this.#clients.size === 0) {
@@ -336,15 +340,6 @@ export class ViewSyncerService {
336
340
  // subscription is returned immediately.
337
341
  const now = Date.now();
338
342
  this.#ttlClockBase = now;
339
- // Get the TTL clock from the CVR store, or initialize it to now.
340
- this.#cvrStore
341
- .getTTLClock()
342
- .then(ttlClock => {
343
- this.#ttlClock = ttlClock ?? now;
344
- })
345
- .catch(e => {
346
- this.#lc.error?.('failed to get TTL clock', e);
347
- });
348
343
  }
349
344
  const newClient = new ClientHandler(lc, this.id, clientID, wsID, this.#shard, baseCookie, schemaVersion, downstream);
350
345
  this.#clients.get(clientID)?.close(`replaced by wsID: ${wsID}`);
@@ -372,7 +367,9 @@ export class ViewSyncerService {
372
367
  #getTTLClock(now) {
373
368
  // We will update ttlClock with delta from the ttlClockBase to the current time.
374
369
  const delta = now - this.#ttlClockBase;
375
- const ttlClock = this.#ttlClock + delta;
370
+ assert(this.#ttlClock !== undefined);
371
+ const ttlClock = ttlClockFromNumber(ttlClockAsNumber(this.#ttlClock) + delta);
372
+ assert(ttlClockAsNumber(ttlClock) <= now);
376
373
  this.#ttlClock = ttlClock;
377
374
  this.#ttlClockBase = now;
378
375
  return ttlClock;
@@ -493,6 +490,7 @@ export class ViewSyncerService {
493
490
  const deletedClientIDs = [];
494
491
  const deletedClientGroupIDs = [];
495
492
  cvr = await this.#updateCVRConfig(lc, cvr, clientID, updater => {
493
+ const { ttlClock } = cvr;
496
494
  const patches = [];
497
495
  if (clientSchema) {
498
496
  updater.setClientSchema(lc, clientSchema);
@@ -500,14 +498,13 @@ export class ViewSyncerService {
500
498
  // Apply requested patches.
501
499
  lc.debug?.(`applying ${desiredQueriesPatch?.length} query patches`);
502
500
  if (desiredQueriesPatch?.length) {
503
- const now = Date.now();
504
501
  for (const patch of desiredQueriesPatch) {
505
502
  switch (patch.op) {
506
503
  case 'put':
507
504
  patches.push(...updater.putDesiredQueries(clientID, [patch]));
508
505
  break;
509
506
  case 'del':
510
- patches.push(...updater.markDesiredQueriesAsInactive(clientID, [patch.hash], now));
507
+ patches.push(...updater.markDesiredQueriesAsInactive(clientID, [patch.hash], ttlClock));
511
508
  break;
512
509
  case 'clear':
513
510
  patches.push(...updater.clearDesiredQueries(clientID));
@@ -533,7 +530,7 @@ export class ViewSyncerService {
533
530
  }
534
531
  }
535
532
  for (const cid of clientIDsToDelete) {
536
- const patchesDueToClient = updater.deleteClient(cid);
533
+ const patchesDueToClient = updater.deleteClient(cid, ttlClock);
537
534
  patches.push(...patchesDueToClient);
538
535
  deletedClientIDs.push(cid);
539
536
  }
@@ -554,35 +551,32 @@ export class ViewSyncerService {
554
551
  await Promise.allSettled(clients.map(client => client.sendDeleteClients(lc, deletedClientIDs, deletedClientGroupIDs)));
555
552
  }
556
553
  this.#scheduleExpireEviction(lc, cvr);
557
- await this.#evictInactiveQueries(lc, cvr);
558
554
  });
559
555
  #scheduleExpireEviction(lc, cvr) {
556
+ const { ttlClock } = cvr;
557
+ this.#stopExpireTimer();
560
558
  // first see if there is any inactive query with a ttl.
561
559
  const next = nextEvictionTime(cvr);
562
560
  if (next === undefined) {
563
561
  lc.debug?.('no inactive queries with ttl');
564
562
  // no inactive queries with a ttl. Cancel existing timeout if any.
565
- this.#stopExpireTimer();
566
- return;
567
- }
568
- if (this.#nextExpiredQueryTime === next) {
569
- lc.debug?.('eviction timer already scheduled');
570
563
  return;
571
564
  }
572
- if (this.#expiredQueriesTimer) {
573
- clearTimeout(this.#expiredQueriesTimer);
574
- }
575
- const now = Date.now();
576
- this.#nextExpiredQueryTime = next;
577
- const ttlClock = this.#getTTLClock(now);
578
- lc.debug?.('Scheduling eviction timer to run in ', next - ttlClock, 'ms');
579
- this.#expiredQueriesTimer = this.#setTimeout(() => this.#runInLockWithCVR(this.#removeExpiredQueries).catch(e =>
580
- // If an error occurs (e.g. ownership change), propagate the error
581
- // to the main run() loop via the #stateChanges Subscription.
582
- this.#stateChanges.fail(e)),
583
- // If the expire time is too far in the future we will run it in an hour.
584
- // At that point in time it will be rescheduled as needed again.
585
- Math.min(next - ttlClock, 60 * 60 * 1000));
565
+ // It is common for many queries to be evicted close to the same time, so
566
+ // we add a small delay so we can collapse multiple evictions into a
567
+ // single timer. However, don't add the delay if we're already at the
568
+ // maximum timer limit, as that's not about collapsing.
569
+ const delay = Math.max(TTL_TIMER_HYSTERESIS, Math.min(ttlClockAsNumber(next) -
570
+ ttlClockAsNumber(ttlClock) +
571
+ TTL_TIMER_HYSTERESIS, MAX_TTL_MS));
572
+ lc.debug?.('Scheduling eviction timer to run in ', delay, 'ms');
573
+ this.#expiredQueriesTimer = this.#setTimeout(() => {
574
+ this.#expiredQueriesTimer = 0;
575
+ this.#runInLockWithCVR((lc, cvr) => this.#removeExpiredQueries(lc, cvr)).catch(e =>
576
+ // If an error occurs (e.g. ownership change), propagate the error
577
+ // to the main run() loop via the #stateChanges Subscription.
578
+ this.#stateChanges.fail(e));
579
+ }, delay);
586
580
  }
587
581
  /**
588
582
  * Adds and hydrates pipelines for queries whose results are already
@@ -628,9 +622,9 @@ export class ViewSyncerService {
628
622
  }
629
623
  if (this.#customQueryTransformer && customQueries.length > 0) {
630
624
  const transformedCustomQueries = await this.#customQueryTransformer.transform({
631
- apiKey: this.#pullConfig.apiKey,
625
+ apiKey: this.#queryConfig.apiKey,
632
626
  token: this.#authData?.raw,
633
- cookie: this.#pullConfig.forwardCookies
627
+ cookie: this.#queryConfig.forwardCookies
634
628
  ? this.#httpCookie
635
629
  : undefined,
636
630
  }, customQueries);
@@ -695,6 +689,10 @@ export class ViewSyncerService {
695
689
  const hydratedQueries = this.#pipelines.addedQueries();
696
690
  // Convert queries to their transformed ast's and hashes
697
691
  const hashToIDs = new Map();
692
+ if (this.#ttlClock === undefined) {
693
+ // Get it from the CVR or initialize it to now.
694
+ this.#ttlClock = cvr.ttlClock;
695
+ }
698
696
  const now = Date.now();
699
697
  const ttlClock = this.#getTTLClock(now);
700
698
  // group cvr queries into:
@@ -733,7 +731,7 @@ export class ViewSyncerService {
733
731
  }
734
732
  if (this.#customQueryTransformer && customQueries.size > 0) {
735
733
  const transformedCustomQueries = await this.#customQueryTransformer.transform({
736
- apiKey: this.#pullConfig.apiKey,
734
+ apiKey: this.#queryConfig.apiKey,
737
735
  token: this.#authData?.raw,
738
736
  cookie: this.#httpCookie,
739
737
  }, customQueries.values());
@@ -795,7 +793,7 @@ export class ViewSyncerService {
795
793
  assert(addQueries.length > 0 ||
796
794
  removeQueries.length > 0 ||
797
795
  unhydrateQueries.length > 0);
798
- const start = Date.now();
796
+ const start = performance.now();
799
797
  const stateVersion = this.#pipelines.currentVersion();
800
798
  lc = lc.withContext('stateVersion', stateVersion);
801
799
  lc.info?.(`hydrating ${addQueries.length} queries`);
@@ -850,7 +848,7 @@ export class ViewSyncerService {
850
848
  await this.#catchupClients(lc, cvr, finalVersion, addQueries.map(q => q.id), pokers);
851
849
  // Signal clients to commit.
852
850
  await pokers.end(finalVersion);
853
- const wallTime = Date.now() - start;
851
+ const wallTime = performance.now() - start;
854
852
  lc.info?.(`finished processing queries (process: ${totalProcessTime} ms, wall: ${wallTime} ms)`);
855
853
  });
856
854
  }
@@ -924,11 +922,11 @@ export class ViewSyncerService {
924
922
  }
925
923
  #processChanges(lc, timer, changes, updater, pokers, hashToIDs) {
926
924
  return startAsyncSpan(tracer, 'vs.#processChanges', async () => {
927
- const start = Date.now();
925
+ const start = performance.now();
928
926
  const rows = new CustomKeyMap(rowIDString);
929
927
  let total = 0;
930
928
  const processBatch = () => startAsyncSpan(tracer, 'processBatch', async () => {
931
- const wallElapsed = Date.now() - start;
929
+ const wallElapsed = performance.now() - start;
932
930
  total += rows.size;
933
931
  lc.debug?.(`processing ${rows.size} (of ${total}) rows (${wallElapsed} ms)`);
934
932
  const patches = await updater.received(lc, rows);
@@ -1027,65 +1025,23 @@ export class ViewSyncerService {
1027
1025
  const finalVersion = this.#cvr.version;
1028
1026
  // Signal clients to commit.
1029
1027
  await pokers.end(finalVersion);
1030
- await this.#evictInactiveQueries(lc, this.#cvr);
1031
1028
  const elapsed = performance.now() - start;
1032
1029
  lc.info?.(`finished processing advancement of ${numChanges} changes (${elapsed} ms)`);
1033
1030
  histograms.transactionAdvanceTime().record(elapsed);
1034
1031
  return 'success';
1035
1032
  });
1036
1033
  }
1037
- // This must be called from within the #lock.
1038
- #evictInactiveQueries(lc, cvr) {
1039
- lc = lc.withContext('method', '#evictInactiveQueries');
1040
- return startAsyncSpan(tracer, 'vs.#evictInactiveQueries', async () => {
1041
- const { rowCount: rowCountBeforeEvictions } = this.#cvrStore;
1042
- if (rowCountBeforeEvictions <= this.maxRowCount) {
1043
- lc.debug?.(`rowCount: ${rowCountBeforeEvictions} <= maxRowCount: ${this.maxRowCount}`);
1044
- return;
1045
- }
1046
- lc.info?.(`Trying to evict inactive queries, rowCount: ${rowCountBeforeEvictions} > maxRowCount: ${this.maxRowCount}`);
1047
- const inactiveQueries = getInactiveQueries(cvr);
1048
- if (!inactiveQueries.length) {
1049
- lc.info?.('No inactive queries to evict');
1050
- return;
1051
- }
1052
- const hashToIDs = createHashToIDs(cvr);
1053
- for (const inactiveQuery of inactiveQueries) {
1054
- const { hash } = inactiveQuery;
1055
- const q = cvr.queries[hash];
1056
- assert(q, 'query not found in CVR');
1057
- assert(q.type !== 'internal', 'internal queries should not be evicted');
1058
- const rowCountBeforeCurrentEviction = this.#cvrStore.rowCount;
1059
- await this.#addAndRemoveQueries(lc, cvr, [], [
1060
- {
1061
- id: hash,
1062
- transformationHash: must(q.transformationHash),
1063
- },
1064
- ], [], hashToIDs);
1065
- lc.debug?.('Evicted', hash, 'Reduced rowCount from', rowCountBeforeCurrentEviction, 'to', this.#cvrStore.rowCount);
1066
- if (this.#cvrStore.rowCount <= this.maxRowCount) {
1067
- lc.info?.('Evicted', hash, 'Reduced rowCount from', rowCountBeforeEvictions, 'to', this.#cvrStore.rowCount);
1068
- break;
1069
- }
1070
- // We continue with the updated/current state of the CVR.
1071
- cvr = must(this.#cvr);
1072
- }
1073
- const cvrVersion = must(this.#cvr).version;
1074
- const dbVersion = this.#pipelines.currentVersion();
1075
- assert(cvrVersion.stateVersion === dbVersion, `CVR@${versionString(cvrVersion)}" does not match DB@${dbVersion}`);
1076
- });
1077
- }
1078
1034
  inspect(context, msg) {
1079
1035
  return this.#runInLockForClient(context, msg, this.#handleInspect);
1080
1036
  }
1081
1037
  // eslint-disable-next-line require-await
1082
- #handleInspect = async (lc, clientID, body, _cvr) => {
1038
+ #handleInspect = async (lc, clientID, body, cvr) => {
1083
1039
  const client = must(this.#clients.get(clientID));
1084
1040
  body.op;
1085
1041
  client.sendInspectResponse(lc, {
1086
1042
  op: 'queries',
1087
1043
  id: body.id,
1088
- value: await this.#cvrStore.inspectQueries(lc, body.clientID),
1044
+ value: await this.#cvrStore.inspectQueries(lc, cvr.ttlClock, body.clientID),
1089
1045
  });
1090
1046
  };
1091
1047
  stop() {
@@ -1095,9 +1051,7 @@ export class ViewSyncerService {
1095
1051
  }
1096
1052
  #cleanup(err) {
1097
1053
  this.#stopTTLClockInterval();
1098
- clearTimeout(this.#expiredQueriesTimer);
1099
- this.#expiredQueriesTimer = 0;
1100
- this.#nextExpiredQueryTime = 0;
1054
+ this.#stopExpireTimer();
1101
1055
  this.#pipelines.destroy();
1102
1056
  for (const client of this.#clients.values()) {
1103
1057
  if (err) {
@@ -1204,17 +1158,20 @@ function expired(ttlClock, q) {
1204
1158
  for (const clientID in clientState) {
1205
1159
  if (hasOwn(clientState, clientID)) {
1206
1160
  const { ttl, inactivatedAt } = clientState[clientID];
1207
- if (ttl < 0 || inactivatedAt === undefined) {
1161
+ if (inactivatedAt === undefined) {
1208
1162
  return false;
1209
1163
  }
1210
- if (inactivatedAt + ttl > ttlClock) {
1164
+ const clampedTTL = clampTTL(ttl);
1165
+ if (ttlClockAsNumber(inactivatedAt) + clampedTTL >
1166
+ ttlClockAsNumber(ttlClock)) {
1211
1167
  return false;
1212
1168
  }
1213
1169
  }
1214
1170
  }
1215
1171
  return true;
1216
1172
  }
1217
- function hasExpiredQueries(cvr, ttlClock) {
1173
+ function hasExpiredQueries(cvr) {
1174
+ const { ttlClock } = cvr;
1218
1175
  for (const q of Object.values(cvr.queries)) {
1219
1176
  if (expired(ttlClock, q)) {
1220
1177
  return true;