@rocicorp/zero 0.22.2025071100 → 0.22.2025071900

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 (148) hide show
  1. package/out/{chunk-ZVMEFHVU.js → chunk-A6LHZQIK.js} +26 -7
  2. package/out/{chunk-ZVMEFHVU.js.map → chunk-A6LHZQIK.js.map} +2 -2
  3. package/out/{chunk-ZRSXCDPE.js → chunk-IQCMHXK7.js} +167 -37
  4. package/out/chunk-IQCMHXK7.js.map +7 -0
  5. package/out/{chunk-INLOZBST.js → chunk-P5M53J4D.js} +2 -3
  6. package/out/{chunk-INLOZBST.js.map → chunk-P5M53J4D.js.map} +2 -2
  7. package/out/{inspector-HDOYOVMS.js → inspector-TFZBDN6K.js} +2 -2
  8. package/out/react.js +2 -2
  9. package/out/replicache/src/kv/idb-store.d.ts.map +1 -1
  10. package/out/shared/src/options.d.ts +4 -1
  11. package/out/shared/src/options.d.ts.map +1 -1
  12. package/out/shared/src/options.js +1 -1
  13. package/out/shared/src/options.js.map +1 -1
  14. package/out/solid.js +7 -3
  15. package/out/solid.js.map +1 -1
  16. package/out/zero/package.json +161 -0
  17. package/out/zero-cache/src/config/normalize.d.ts +1 -0
  18. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  19. package/out/zero-cache/src/config/normalize.js +10 -1
  20. package/out/zero-cache/src/config/normalize.js.map +1 -1
  21. package/out/zero-cache/src/config/zero-config.d.ts +44 -6
  22. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  23. package/out/zero-cache/src/config/zero-config.js +48 -54
  24. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  25. package/out/zero-cache/src/custom/fetch.d.ts +14 -1
  26. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  27. package/out/zero-cache/src/custom/fetch.js +56 -1
  28. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  29. package/out/zero-cache/src/custom-queries/transform-query.d.ts +1 -1
  30. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  31. package/out/zero-cache/src/custom-queries/transform-query.js +2 -1
  32. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  33. package/out/zero-cache/src/observability/histograms.d.ts.map +1 -1
  34. package/out/zero-cache/src/observability/histograms.js +8 -14
  35. package/out/zero-cache/src/observability/histograms.js.map +1 -1
  36. package/out/zero-cache/src/server/anonymous-otel-start.d.ts +9 -0
  37. package/out/zero-cache/src/server/anonymous-otel-start.d.ts.map +1 -0
  38. package/out/zero-cache/src/server/anonymous-otel-start.js +289 -0
  39. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -0
  40. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  41. package/out/zero-cache/src/server/main.js +1 -5
  42. package/out/zero-cache/src/server/main.js.map +1 -1
  43. package/out/zero-cache/src/server/otel-start.js +1 -1
  44. package/out/zero-cache/src/server/otel-start.js.map +1 -1
  45. package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
  46. package/out/zero-cache/src/server/runner/zero-dispatcher.js +2 -2
  47. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  48. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  49. package/out/zero-cache/src/server/syncer.js +6 -3
  50. package/out/zero-cache/src/server/syncer.js.map +1 -1
  51. package/out/zero-cache/src/server/worker-dispatcher.js +3 -3
  52. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  53. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +0 -7
  54. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  55. package/out/zero-cache/src/services/change-source/pg/change-source.js +43 -32
  56. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  57. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  58. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +24 -12
  59. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  60. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  61. package/out/zero-cache/src/services/mutagen/mutagen.js +4 -0
  62. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  63. package/out/zero-cache/src/services/mutagen/pusher.d.ts +4 -4
  64. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  65. package/out/zero-cache/src/services/mutagen/pusher.js +8 -5
  66. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  67. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +11 -8
  68. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  69. package/out/zero-cache/src/services/view-syncer/cvr-store.js +14 -13
  70. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  71. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +9 -16
  72. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  73. package/out/zero-cache/src/services/view-syncer/cvr.js +25 -28
  74. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  75. package/out/zero-cache/src/services/view-syncer/row-record-cache.d.ts.map +1 -1
  76. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +3 -0
  77. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  78. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts +3 -2
  79. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts.map +1 -1
  80. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  81. package/out/zero-cache/src/services/view-syncer/schema/types.d.ts +11 -11
  82. package/out/zero-cache/src/services/view-syncer/schema/types.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/view-syncer/schema/types.js +3 -2
  84. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  85. package/out/zero-cache/src/services/view-syncer/ttl-clock.d.ts +10 -0
  86. package/out/zero-cache/src/services/view-syncer/ttl-clock.d.ts.map +1 -0
  87. package/out/zero-cache/src/services/view-syncer/ttl-clock.js +9 -0
  88. package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -0
  89. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +14 -13
  90. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/view-syncer/view-syncer.js +92 -124
  92. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  93. package/out/zero-cache/src/types/processes.d.ts +6 -6
  94. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  95. package/out/zero-cache/src/types/processes.js +2 -2
  96. package/out/zero-cache/src/types/processes.js.map +1 -1
  97. package/out/zero-cache/src/types/websocket-handoff.d.ts +2 -2
  98. package/out/zero-cache/src/types/websocket-handoff.d.ts.map +1 -1
  99. package/out/zero-cache/src/types/websocket-handoff.js +4 -4
  100. package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
  101. package/out/zero-cache/src/workers/connection.d.ts +1 -1
  102. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  103. package/out/zero-cache/src/workers/connection.js +2 -0
  104. package/out/zero-cache/src/workers/connection.js.map +1 -1
  105. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +1 -1
  106. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  107. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  108. package/out/zero-cache/src/workers/syncer.js +3 -1
  109. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  110. package/out/zero-client/src/client/active-clients-manager.d.ts.map +1 -1
  111. package/out/zero-client/src/client/mutation-tracker.d.ts +38 -5
  112. package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
  113. package/out/zero-client/src/client/options.d.ts +9 -4
  114. package/out/zero-client/src/client/options.d.ts.map +1 -1
  115. package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
  116. package/out/zero-client/src/client/zero.d.ts +2 -2
  117. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  118. package/out/zero-client/src/mod.d.ts +1 -1
  119. package/out/zero-client/src/mod.d.ts.map +1 -1
  120. package/out/zero-protocol/src/connect.d.ts +24 -2
  121. package/out/zero-protocol/src/connect.d.ts.map +1 -1
  122. package/out/zero-protocol/src/connect.js +26 -5
  123. package/out/zero-protocol/src/connect.js.map +1 -1
  124. package/out/zero-protocol/src/protocol-version.d.ts +1 -1
  125. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  126. package/out/zero-protocol/src/protocol-version.js +2 -1
  127. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  128. package/out/zero-protocol/src/up.d.ts +5 -0
  129. package/out/zero-protocol/src/up.d.ts.map +1 -1
  130. package/out/zero-server/src/queries/process-queries.d.ts +1 -1
  131. package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
  132. package/out/zero-server/src/queries/process-queries.js +1 -1
  133. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  134. package/out/zero-solid/src/mod.d.ts +2 -2
  135. package/out/zero-solid/src/mod.d.ts.map +1 -1
  136. package/out/zero.js +9 -7
  137. package/out/zql/src/query/named.d.ts +11 -11
  138. package/out/zql/src/query/named.d.ts.map +1 -1
  139. package/out/zql/src/query/named.js +20 -2
  140. package/out/zql/src/query/named.js.map +1 -1
  141. package/out/zql/src/query/query-impl.js +1 -1
  142. package/out/zql/src/query/query-impl.js.map +1 -1
  143. package/out/zql/src/query/query.d.ts +12 -3
  144. package/out/zql/src/query/query.d.ts.map +1 -1
  145. package/out/zql/src/query/query.js.map +1 -1
  146. package/package.json +2 -2
  147. package/out/chunk-ZRSXCDPE.js.map +0 -7
  148. /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,14 +79,12 @@ 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.
82
85
  */
83
86
  #ttlClockInterval = 0;
87
+ #ttlClockInitializedPromise;
84
88
  // Note: It is okay to add/remove clients without acquiring the lock.
85
89
  #clients = new Map();
86
90
  // Serialize on this lock for:
@@ -95,25 +99,13 @@ export class ViewSyncerService {
95
99
  // auth and cookie headers directly
96
100
  #authData;
97
101
  #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
102
  #expiredQueriesTimer = 0;
110
- #nextExpiredQueryTime = 0;
111
103
  #setTimeout;
112
104
  #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)) {
105
+ constructor(pullConfig, lc, shard, taskID, clientGroupID, db, pipelineDriver, versionChanges, drainCoordinator, slowHydrateThreshold, keepaliveMs = DEFAULT_KEEPALIVE_MS, setTimeoutFn = setTimeout.bind(globalThis)) {
114
106
  this.id = clientGroupID;
115
107
  this.#shard = shard;
116
- this.#pullConfig = pullConfig;
108
+ this.#queryConfig = pullConfig;
117
109
  this.#lc = lc;
118
110
  this.#pipelines = pipelineDriver;
119
111
  this.#stateChanges = versionChanges;
@@ -124,7 +116,6 @@ export class ViewSyncerService {
124
116
  // On failure, cancel the #stateChanges subscription. The run()
125
117
  // loop will then await #cvrStore.flushed() which rejects if necessary.
126
118
  () => this.#stateChanges.cancel());
127
- this.maxRowCount = maxRowCount;
128
119
  this.#setTimeout = setTimeoutFn;
129
120
  if (pullConfig.url) {
130
121
  this.#customQueryTransformer = new CustomQueryTransformer({
@@ -230,17 +221,18 @@ export class ViewSyncerService {
230
221
  }
231
222
  }
232
223
  // must be called from within #lock
233
- #removeExpiredQueries = async (lc, cvr) => {
234
- const now = Date.now();
235
- const ttlClock = this.#getTTLClock(now);
224
+ #removeExpiredQueries = async (lc, cvr, ttlClock) => {
236
225
  if (hasExpiredQueries(cvr, ttlClock)) {
237
226
  lc = lc.withContext('method', '#removeExpiredQueries');
238
- lc.info?.('Queries have expired');
227
+ lc.debug?.('Queries have expired');
239
228
  // #syncQueryPipelineSet() will remove the expired queries.
240
229
  await this.#syncQueryPipelineSet(lc, cvr);
241
230
  this.#pipelinesSynced = true;
242
- this.#scheduleExpireEviction(lc, cvr);
243
231
  }
232
+ // Even if we have expired queries, we still need to schedule next eviction
233
+ // since there might be inactivated queries that need to be expired queries
234
+ // in the future.
235
+ this.#scheduleExpireEviction(lc, cvr, ttlClock);
244
236
  };
245
237
  #totalHydrationTimeMs() {
246
238
  return this.#pipelines.totalHydrationTimeMs();
@@ -290,7 +282,7 @@ export class ViewSyncerService {
290
282
  // If no clients have connected while waiting for the row flush, shutdown.
291
283
  return this.#clients.size === 0;
292
284
  }
293
- #deleteClient(clientID, client) {
285
+ #deleteClientDueToDisconnect(clientID, client) {
294
286
  // Note: It is okay to delete / cleanup clients without acquiring the lock.
295
287
  // In fact, it is important to do so in order to guarantee that idle cleanup
296
288
  // is performed in a timely manner, regardless of the amount of work
@@ -299,7 +291,11 @@ export class ViewSyncerService {
299
291
  if (c === client) {
300
292
  this.#clients.delete(clientID);
301
293
  if (this.#clients.size === 0) {
302
- this.#updateTTLClockInCVRWithoutLock(this.#lc);
294
+ // It is possible to delete a client before we read the ttl clock from
295
+ // the CVR.
296
+ if (this.#hasTTLClock()) {
297
+ this.#updateTTLClockInCVRWithoutLock(this.#lc);
298
+ }
303
299
  this.#stopExpireTimer();
304
300
  this.#scheduleShutdown();
305
301
  }
@@ -309,7 +305,6 @@ export class ViewSyncerService {
309
305
  this.#lc.debug?.('Stopping expired queries timer');
310
306
  clearTimeout(this.#expiredQueriesTimer);
311
307
  this.#expiredQueriesTimer = 0;
312
- this.#nextExpiredQueryTime = 0;
313
308
  }
314
309
  initConnection(ctx, initConnectionMessage) {
315
310
  this.#lc.debug?.('viewSyncer.initConnection');
@@ -327,7 +322,7 @@ export class ViewSyncerService {
327
322
  err
328
323
  ? lc[getLogLevel(err)]?.(`client closed with error`, err)
329
324
  : lc.info?.('client closed');
330
- this.#deleteClient(clientID, newClient);
325
+ this.#deleteClientDueToDisconnect(clientID, newClient);
331
326
  },
332
327
  });
333
328
  if (this.#clients.size === 0) {
@@ -337,13 +332,8 @@ export class ViewSyncerService {
337
332
  const now = Date.now();
338
333
  this.#ttlClockBase = now;
339
334
  // 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);
335
+ this.#readTTLClockFromCVR().catch(e => {
336
+ lc.error?.('failed to read TTL clock', e);
347
337
  });
348
338
  }
349
339
  const newClient = new ClientHandler(lc, this.id, clientID, wsID, this.#shard, baseCookie, schemaVersion, downstream);
@@ -369,10 +359,15 @@ export class ViewSyncerService {
369
359
  this.#lc.error?.('deleteClients failed', e);
370
360
  }
371
361
  }
362
+ #hasTTLClock() {
363
+ return this.#ttlClock !== undefined;
364
+ }
372
365
  #getTTLClock(now) {
373
366
  // We will update ttlClock with delta from the ttlClockBase to the current time.
374
367
  const delta = now - this.#ttlClockBase;
375
- const ttlClock = this.#ttlClock + delta;
368
+ assert(this.#ttlClock !== undefined);
369
+ const ttlClock = ttlClockFromNumber(ttlClockAsNumber(this.#ttlClock) + delta);
370
+ assert(ttlClockAsNumber(ttlClock) <= now);
376
371
  this.#ttlClock = ttlClock;
377
372
  this.#ttlClockBase = now;
378
373
  return ttlClock;
@@ -492,6 +487,8 @@ export class ViewSyncerService {
492
487
  #handleConfigUpdate = (lc, clientID, { clientSchema, deleted, desiredQueriesPatch, activeClients, }, cvr) => startAsyncSpan(tracer, 'vs.#patchQueries', async () => {
493
488
  const deletedClientIDs = [];
494
489
  const deletedClientGroupIDs = [];
490
+ const now = Date.now();
491
+ const ttlClock = this.#getTTLClock(now);
495
492
  cvr = await this.#updateCVRConfig(lc, cvr, clientID, updater => {
496
493
  const patches = [];
497
494
  if (clientSchema) {
@@ -500,14 +497,13 @@ export class ViewSyncerService {
500
497
  // Apply requested patches.
501
498
  lc.debug?.(`applying ${desiredQueriesPatch?.length} query patches`);
502
499
  if (desiredQueriesPatch?.length) {
503
- const now = Date.now();
504
500
  for (const patch of desiredQueriesPatch) {
505
501
  switch (patch.op) {
506
502
  case 'put':
507
503
  patches.push(...updater.putDesiredQueries(clientID, [patch]));
508
504
  break;
509
505
  case 'del':
510
- patches.push(...updater.markDesiredQueriesAsInactive(clientID, [patch.hash], now));
506
+ patches.push(...updater.markDesiredQueriesAsInactive(clientID, [patch.hash], ttlClock));
511
507
  break;
512
508
  case 'clear':
513
509
  patches.push(...updater.clearDesiredQueries(clientID));
@@ -533,7 +529,7 @@ export class ViewSyncerService {
533
529
  }
534
530
  }
535
531
  for (const cid of clientIDsToDelete) {
536
- const patchesDueToClient = updater.deleteClient(cid);
532
+ const patchesDueToClient = updater.deleteClient(cid, ttlClock);
537
533
  patches.push(...patchesDueToClient);
538
534
  deletedClientIDs.push(cid);
539
535
  }
@@ -553,36 +549,34 @@ export class ViewSyncerService {
553
549
  const clients = this.#getClients();
554
550
  await Promise.allSettled(clients.map(client => client.sendDeleteClients(lc, deletedClientIDs, deletedClientGroupIDs)));
555
551
  }
556
- this.#scheduleExpireEviction(lc, cvr);
557
- await this.#evictInactiveQueries(lc, cvr);
552
+ this.#scheduleExpireEviction(lc, cvr, ttlClock);
558
553
  });
559
- #scheduleExpireEviction(lc, cvr) {
554
+ #scheduleExpireEviction(lc, cvr, ttlClock) {
555
+ this.#stopExpireTimer();
560
556
  // first see if there is any inactive query with a ttl.
561
557
  const next = nextEvictionTime(cvr);
562
558
  if (next === undefined) {
563
559
  lc.debug?.('no inactive queries with ttl');
564
560
  // 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
561
  return;
571
562
  }
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));
563
+ // It is common for many queries to be evicted close to the same time, so
564
+ // we add a small delay so we can collapse multiple evictions into a
565
+ // single timer. However, don't add the delay if we're already at the
566
+ // maximum timer limit, as that's not about collapsing.
567
+ const delay = Math.max(TTL_TIMER_HYSTERESIS, Math.min(ttlClockAsNumber(next) -
568
+ ttlClockAsNumber(ttlClock) +
569
+ TTL_TIMER_HYSTERESIS, MAX_TTL_MS));
570
+ lc.debug?.('Scheduling eviction timer to run in ', delay, 'ms');
571
+ this.#expiredQueriesTimer = this.#setTimeout(() => {
572
+ this.#expiredQueriesTimer = 0;
573
+ const now = Date.now();
574
+ const ttlClock = this.#getTTLClock(now);
575
+ this.#runInLockWithCVR((lc, cvr) => this.#removeExpiredQueries(lc, cvr, ttlClock)).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);
@@ -681,6 +675,15 @@ export class ViewSyncerService {
681
675
  lc.debug?.(`hydrated ${count} rows for ${hash} (${elapsed} ms)`);
682
676
  }
683
677
  }
678
+ async #readTTLClockFromCVR() {
679
+ if (this.#ttlClockInitializedPromise) {
680
+ return this.#ttlClockInitializedPromise;
681
+ }
682
+ this.#ttlClockInitializedPromise = this.#cvrStore
683
+ .getTTLClock()
684
+ .then(t => t ?? ttlClockFromNumber(0));
685
+ return (this.#ttlClock = await this.#ttlClockInitializedPromise);
686
+ }
684
687
  /**
685
688
  * Adds and/or removes queries to/from the PipelineDriver to bring it
686
689
  * in sync with the set of queries in the CVR (both got and desired).
@@ -696,7 +699,14 @@ export class ViewSyncerService {
696
699
  // Convert queries to their transformed ast's and hashes
697
700
  const hashToIDs = new Map();
698
701
  const now = Date.now();
699
- const ttlClock = this.#getTTLClock(now);
702
+ let ttlClock;
703
+ if (!this.#hasTTLClock()) {
704
+ // Fetch it from the CVR or initialize it to now.
705
+ ttlClock = await this.#readTTLClockFromCVR();
706
+ }
707
+ else {
708
+ ttlClock = this.#getTTLClock(now);
709
+ }
700
710
  // group cvr queries into:
701
711
  // 1. custom queries
702
712
  // 2. everything else
@@ -733,7 +743,7 @@ export class ViewSyncerService {
733
743
  }
734
744
  if (this.#customQueryTransformer && customQueries.size > 0) {
735
745
  const transformedCustomQueries = await this.#customQueryTransformer.transform({
736
- apiKey: this.#pullConfig.apiKey,
746
+ apiKey: this.#queryConfig.apiKey,
737
747
  token: this.#authData?.raw,
738
748
  cookie: this.#httpCookie,
739
749
  }, customQueries.values());
@@ -795,7 +805,7 @@ export class ViewSyncerService {
795
805
  assert(addQueries.length > 0 ||
796
806
  removeQueries.length > 0 ||
797
807
  unhydrateQueries.length > 0);
798
- const start = Date.now();
808
+ const start = performance.now();
799
809
  const stateVersion = this.#pipelines.currentVersion();
800
810
  lc = lc.withContext('stateVersion', stateVersion);
801
811
  lc.info?.(`hydrating ${addQueries.length} queries`);
@@ -850,7 +860,7 @@ export class ViewSyncerService {
850
860
  await this.#catchupClients(lc, cvr, finalVersion, addQueries.map(q => q.id), pokers);
851
861
  // Signal clients to commit.
852
862
  await pokers.end(finalVersion);
853
- const wallTime = Date.now() - start;
863
+ const wallTime = performance.now() - start;
854
864
  lc.info?.(`finished processing queries (process: ${totalProcessTime} ms, wall: ${wallTime} ms)`);
855
865
  });
856
866
  }
@@ -924,11 +934,11 @@ export class ViewSyncerService {
924
934
  }
925
935
  #processChanges(lc, timer, changes, updater, pokers, hashToIDs) {
926
936
  return startAsyncSpan(tracer, 'vs.#processChanges', async () => {
927
- const start = Date.now();
937
+ const start = performance.now();
928
938
  const rows = new CustomKeyMap(rowIDString);
929
939
  let total = 0;
930
940
  const processBatch = () => startAsyncSpan(tracer, 'processBatch', async () => {
931
- const wallElapsed = Date.now() - start;
941
+ const wallElapsed = performance.now() - start;
932
942
  total += rows.size;
933
943
  lc.debug?.(`processing ${rows.size} (of ${total}) rows (${wallElapsed} ms)`);
934
944
  const patches = await updater.received(lc, rows);
@@ -1027,65 +1037,23 @@ export class ViewSyncerService {
1027
1037
  const finalVersion = this.#cvr.version;
1028
1038
  // Signal clients to commit.
1029
1039
  await pokers.end(finalVersion);
1030
- await this.#evictInactiveQueries(lc, this.#cvr);
1031
1040
  const elapsed = performance.now() - start;
1032
1041
  lc.info?.(`finished processing advancement of ${numChanges} changes (${elapsed} ms)`);
1033
1042
  histograms.transactionAdvanceTime().record(elapsed);
1034
1043
  return 'success';
1035
1044
  });
1036
1045
  }
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
1046
  inspect(context, msg) {
1079
1047
  return this.#runInLockForClient(context, msg, this.#handleInspect);
1080
1048
  }
1081
1049
  // eslint-disable-next-line require-await
1082
- #handleInspect = async (lc, clientID, body, _cvr) => {
1050
+ #handleInspect = async (lc, clientID, body, cvr) => {
1083
1051
  const client = must(this.#clients.get(clientID));
1084
1052
  body.op;
1085
1053
  client.sendInspectResponse(lc, {
1086
1054
  op: 'queries',
1087
1055
  id: body.id,
1088
- value: await this.#cvrStore.inspectQueries(lc, body.clientID),
1056
+ value: await this.#cvrStore.inspectQueries(lc, cvr.ttlClock, body.clientID),
1089
1057
  });
1090
1058
  };
1091
1059
  stop() {
@@ -1095,9 +1063,7 @@ export class ViewSyncerService {
1095
1063
  }
1096
1064
  #cleanup(err) {
1097
1065
  this.#stopTTLClockInterval();
1098
- clearTimeout(this.#expiredQueriesTimer);
1099
- this.#expiredQueriesTimer = 0;
1100
- this.#nextExpiredQueryTime = 0;
1066
+ this.#stopExpireTimer();
1101
1067
  this.#pipelines.destroy();
1102
1068
  for (const client of this.#clients.values()) {
1103
1069
  if (err) {
@@ -1204,10 +1170,12 @@ function expired(ttlClock, q) {
1204
1170
  for (const clientID in clientState) {
1205
1171
  if (hasOwn(clientState, clientID)) {
1206
1172
  const { ttl, inactivatedAt } = clientState[clientID];
1207
- if (ttl < 0 || inactivatedAt === undefined) {
1173
+ if (inactivatedAt === undefined) {
1208
1174
  return false;
1209
1175
  }
1210
- if (inactivatedAt + ttl > ttlClock) {
1176
+ const clampedTTL = clampTTL(ttl);
1177
+ if (ttlClockAsNumber(inactivatedAt) + clampedTTL >
1178
+ ttlClockAsNumber(ttlClock)) {
1211
1179
  return false;
1212
1180
  }
1213
1181
  }