@tanstack/query-db-collection 1.0.30 → 1.0.31

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.
@@ -5,6 +5,7 @@ const db = require("@tanstack/db");
5
5
  const errors = require("./errors.cjs");
6
6
  const manualSync = require("./manual-sync.cjs");
7
7
  const serialization = require("./serialization.cjs");
8
+ const QUERY_COLLECTION_GC_PREFIX = `queryCollection:gc:`;
8
9
  class QueryCollectionUtilsImpl {
9
10
  constructor(state, refetch, writeUtils) {
10
11
  this.state = state;
@@ -73,6 +74,7 @@ function queryCollectionOptions(config) {
73
74
  retry,
74
75
  retryDelay,
75
76
  staleTime,
77
+ persistedGcTime,
76
78
  getKey,
77
79
  onInsert,
78
80
  onUpdate,
@@ -81,6 +83,16 @@ function queryCollectionOptions(config) {
81
83
  ...baseCollectionConfig
82
84
  } = config;
83
85
  const syncMode = baseCollectionConfig.syncMode ?? `eager`;
86
+ const baseKey = typeof queryKey === `function` ? queryKey({}) : queryKey;
87
+ const validateQueryKeyPrefix = (key) => {
88
+ if (typeof queryKey !== `function`) return;
89
+ const isValidPrefix = key.length >= baseKey.length && baseKey.every((segment, i) => db.deepEquals(segment, key[i]));
90
+ if (!isValidPrefix) {
91
+ console.warn(
92
+ `[QueryCollection] queryKey function must return keys that extend the base key prefix. Base: ${JSON.stringify(baseKey)}, Got: ${JSON.stringify(key)}. This can cause stale cache issues.`
93
+ );
94
+ }
95
+ };
84
96
  if (!queryKey) {
85
97
  throw new errors.QueryKeyRequiredError();
86
98
  }
@@ -122,8 +134,244 @@ function queryCollectionOptions(config) {
122
134
  return rowToQueriesSet.size === 0;
123
135
  };
124
136
  const internalSync = (params) => {
125
- const { begin, write, commit, markReady, collection } = params;
137
+ const { begin, write, commit, markReady, collection, metadata } = params;
138
+ const persistedMetadata = metadata;
126
139
  let syncStarted = false;
140
+ let startupRetentionSettled = false;
141
+ const retainedQueriesPendingRevalidation = /* @__PURE__ */ new Set();
142
+ const effectivePersistedGcTimes = /* @__PURE__ */ new Map();
143
+ const persistedRetentionTimers = /* @__PURE__ */ new Map();
144
+ let persistedRetentionMaintenance = Promise.resolve();
145
+ const getRowMetadata = (rowKey) => {
146
+ return metadata?.row.get(rowKey) ?? collection._state.syncedMetadata.get(rowKey);
147
+ };
148
+ const getPersistedOwners = (rowKey) => {
149
+ const rowMetadata = getRowMetadata(rowKey);
150
+ const queryMetadata = rowMetadata?.queryCollection;
151
+ if (!queryMetadata || typeof queryMetadata !== `object`) {
152
+ return /* @__PURE__ */ new Set();
153
+ }
154
+ const owners = queryMetadata.owners;
155
+ if (!owners || typeof owners !== `object`) {
156
+ return /* @__PURE__ */ new Set();
157
+ }
158
+ return new Set(Object.keys(owners));
159
+ };
160
+ const setPersistedOwners = (rowKey, owners) => {
161
+ if (!metadata) {
162
+ return;
163
+ }
164
+ const currentMetadata = { ...getRowMetadata(rowKey) ?? {} };
165
+ if (owners.size === 0) {
166
+ delete currentMetadata.queryCollection;
167
+ if (Object.keys(currentMetadata).length === 0) {
168
+ metadata.row.delete(rowKey);
169
+ } else {
170
+ metadata.row.set(rowKey, currentMetadata);
171
+ }
172
+ return;
173
+ }
174
+ metadata.row.set(rowKey, {
175
+ ...currentMetadata,
176
+ queryCollection: {
177
+ owners: Object.fromEntries(
178
+ Array.from(owners.values()).map((owner) => [owner, true])
179
+ )
180
+ }
181
+ });
182
+ };
183
+ const parsePersistedQueryRetentionEntry = (value, expectedHash) => {
184
+ if (!value || typeof value !== `object`) {
185
+ return void 0;
186
+ }
187
+ const record = value;
188
+ if (record.queryHash !== expectedHash) {
189
+ return void 0;
190
+ }
191
+ if (record.mode === `until-revalidated`) {
192
+ return {
193
+ queryHash: expectedHash,
194
+ mode: `until-revalidated`
195
+ };
196
+ }
197
+ if (record.mode === `ttl` && typeof record.expiresAt === `number` && Number.isFinite(record.expiresAt)) {
198
+ return {
199
+ queryHash: expectedHash,
200
+ mode: `ttl`,
201
+ expiresAt: record.expiresAt
202
+ };
203
+ }
204
+ return void 0;
205
+ };
206
+ const runPersistedRetentionMaintenance = (task) => {
207
+ persistedRetentionMaintenance = persistedRetentionMaintenance.then(
208
+ task,
209
+ task
210
+ );
211
+ return persistedRetentionMaintenance;
212
+ };
213
+ const cancelPersistedRetentionExpiry = (hashedQueryKey) => {
214
+ const timer = persistedRetentionTimers.get(hashedQueryKey);
215
+ if (timer) {
216
+ clearTimeout(timer);
217
+ persistedRetentionTimers.delete(hashedQueryKey);
218
+ }
219
+ };
220
+ const getHydratedOwnedRowsForQueryBaseline = (hashedQueryKey) => {
221
+ const knownRows = queryToRows.get(hashedQueryKey);
222
+ if (knownRows) {
223
+ return new Set(knownRows);
224
+ }
225
+ const ownedRows = /* @__PURE__ */ new Set();
226
+ for (const [rowKey] of collection._state.syncedData.entries()) {
227
+ const owners = getPersistedOwners(rowKey);
228
+ if (owners.size === 0) {
229
+ continue;
230
+ }
231
+ rowToQueries.set(rowKey, new Set(owners));
232
+ owners.forEach((owner) => {
233
+ const queryToRowsSet = queryToRows.get(owner) || /* @__PURE__ */ new Set();
234
+ queryToRowsSet.add(rowKey);
235
+ queryToRows.set(owner, queryToRowsSet);
236
+ });
237
+ if (owners.has(hashedQueryKey)) {
238
+ ownedRows.add(rowKey);
239
+ }
240
+ }
241
+ return ownedRows;
242
+ };
243
+ const loadPersistedBaselineForQuery = async (hashedQueryKey) => {
244
+ const knownRows = queryToRows.get(hashedQueryKey);
245
+ if (knownRows && Array.from(knownRows).every((rowKey) => collection.has(rowKey))) {
246
+ const baseline2 = /* @__PURE__ */ new Map();
247
+ knownRows.forEach((rowKey) => {
248
+ const value = collection.get(rowKey);
249
+ const owners = rowToQueries.get(rowKey);
250
+ if (value && owners) {
251
+ baseline2.set(rowKey, {
252
+ value,
253
+ owners: new Set(owners)
254
+ });
255
+ }
256
+ });
257
+ return baseline2;
258
+ }
259
+ const scanPersisted = persistedMetadata?.row.scanPersisted;
260
+ if (!scanPersisted) {
261
+ const baseline2 = /* @__PURE__ */ new Map();
262
+ getHydratedOwnedRowsForQueryBaseline(hashedQueryKey).forEach(
263
+ (rowKey) => {
264
+ const value = collection.get(rowKey);
265
+ const owners = rowToQueries.get(rowKey);
266
+ if (value && owners) {
267
+ baseline2.set(rowKey, {
268
+ value,
269
+ owners: new Set(owners)
270
+ });
271
+ }
272
+ }
273
+ );
274
+ return baseline2;
275
+ }
276
+ const baseline = /* @__PURE__ */ new Map();
277
+ const scannedRows = await scanPersisted();
278
+ scannedRows.forEach((row) => {
279
+ const rowMetadata = row.metadata;
280
+ const queryMetadata = rowMetadata?.queryCollection;
281
+ if (!queryMetadata || typeof queryMetadata !== `object`) {
282
+ return;
283
+ }
284
+ const owners = queryMetadata.owners;
285
+ if (!owners || typeof owners !== `object`) {
286
+ return;
287
+ }
288
+ const ownerSet = new Set(Object.keys(owners));
289
+ if (ownerSet.size === 0) {
290
+ return;
291
+ }
292
+ rowToQueries.set(row.key, new Set(ownerSet));
293
+ ownerSet.forEach((owner) => {
294
+ const queryToRowsSet = queryToRows.get(owner) || /* @__PURE__ */ new Set();
295
+ queryToRowsSet.add(row.key);
296
+ queryToRows.set(owner, queryToRowsSet);
297
+ });
298
+ if (ownerSet.has(hashedQueryKey)) {
299
+ baseline.set(row.key, {
300
+ value: row.value,
301
+ owners: ownerSet
302
+ });
303
+ }
304
+ });
305
+ return baseline;
306
+ };
307
+ const cleanupPersistedPlaceholder = async (hashedQueryKey) => {
308
+ if (!metadata) {
309
+ return;
310
+ }
311
+ const baseline = await loadPersistedBaselineForQuery(hashedQueryKey);
312
+ const rowsToDelete = [];
313
+ begin();
314
+ baseline.forEach(({ value: oldItem, owners }, rowKey) => {
315
+ owners.delete(hashedQueryKey);
316
+ setPersistedOwners(rowKey, owners);
317
+ const needToRemove = removeRow(rowKey, hashedQueryKey);
318
+ if (needToRemove) {
319
+ rowsToDelete.push(oldItem);
320
+ }
321
+ });
322
+ rowsToDelete.forEach((row) => {
323
+ write({ type: `delete`, value: row });
324
+ });
325
+ metadata.collection.delete(
326
+ `${QUERY_COLLECTION_GC_PREFIX}${hashedQueryKey}`
327
+ );
328
+ commit();
329
+ };
330
+ const schedulePersistedRetentionExpiry = (entry) => {
331
+ if (entry.mode !== `ttl`) {
332
+ return;
333
+ }
334
+ cancelPersistedRetentionExpiry(entry.queryHash);
335
+ const delay = Math.max(0, entry.expiresAt - Date.now());
336
+ const timer = setTimeout(() => {
337
+ persistedRetentionTimers.delete(entry.queryHash);
338
+ void runPersistedRetentionMaintenance(async () => {
339
+ const currentEntry = metadata?.collection.get(
340
+ `${QUERY_COLLECTION_GC_PREFIX}${entry.queryHash}`
341
+ );
342
+ const parsedCurrentEntry = parsePersistedQueryRetentionEntry(
343
+ currentEntry,
344
+ entry.queryHash
345
+ );
346
+ if (!parsedCurrentEntry || parsedCurrentEntry.mode !== `ttl` || parsedCurrentEntry.expiresAt > Date.now()) {
347
+ return;
348
+ }
349
+ await cleanupPersistedPlaceholder(entry.queryHash);
350
+ });
351
+ }, delay);
352
+ persistedRetentionTimers.set(entry.queryHash, timer);
353
+ };
354
+ const consumePersistedQueryRetentionAtStartup = async () => {
355
+ if (!metadata) {
356
+ return;
357
+ }
358
+ const retentionEntries = metadata.collection.list(
359
+ QUERY_COLLECTION_GC_PREFIX
360
+ );
361
+ const now = Date.now();
362
+ for (const { key, value } of retentionEntries) {
363
+ const hashedQueryKey = key.slice(QUERY_COLLECTION_GC_PREFIX.length);
364
+ const parsed = parsePersistedQueryRetentionEntry(value, hashedQueryKey);
365
+ if (!parsed) {
366
+ continue;
367
+ }
368
+ if (parsed.mode === `ttl` && parsed.expiresAt <= now) {
369
+ await cleanupPersistedPlaceholder(parsed.queryHash);
370
+ } else if (parsed.mode === `ttl`) {
371
+ schedulePersistedRetentionExpiry(parsed);
372
+ }
373
+ }
374
+ };
127
375
  const generateQueryKeyFromOptions = (opts) => {
128
376
  if (typeof queryKey === `function`) {
129
377
  return queryKey(opts);
@@ -134,10 +382,37 @@ function queryCollectionOptions(config) {
134
382
  return queryKey;
135
383
  }
136
384
  };
385
+ const startupRetentionEntries = metadata?.collection.list(
386
+ QUERY_COLLECTION_GC_PREFIX
387
+ );
388
+ const startupRetentionMaintenancePromise = !startupRetentionEntries || startupRetentionEntries.length === 0 ? (() => {
389
+ startupRetentionSettled = true;
390
+ return Promise.resolve();
391
+ })() : runPersistedRetentionMaintenance(async () => {
392
+ try {
393
+ await consumePersistedQueryRetentionAtStartup();
394
+ } finally {
395
+ startupRetentionSettled = true;
396
+ }
397
+ });
137
398
  const createQueryFromOpts = (opts = {}, queryFunction = queryFn) => {
399
+ if (!startupRetentionSettled) {
400
+ return startupRetentionMaintenancePromise.then(() => {
401
+ const resumed = createQueryFromOpts(opts, queryFunction);
402
+ return resumed === true ? void 0 : resumed;
403
+ });
404
+ }
138
405
  const key = generateQueryKeyFromOptions(opts);
139
406
  const hashedQueryKey = queryCore.hashKey(key);
140
407
  const extendedMeta = { ...meta, loadSubsetOptions: opts };
408
+ const retainedEntry = metadata?.collection.get(
409
+ `${QUERY_COLLECTION_GC_PREFIX}${hashedQueryKey}`
410
+ );
411
+ if (parsePersistedQueryRetentionEntry(retainedEntry, hashedQueryKey) !== void 0) {
412
+ retainedQueriesPendingRevalidation.add(hashedQueryKey);
413
+ }
414
+ cancelPersistedRetentionExpiry(hashedQueryKey);
415
+ validateQueryKeyPrefix(key);
141
416
  if (state.observers.has(hashedQueryKey)) {
142
417
  queryRefCounts.set(
143
418
  hashedQueryKey,
@@ -183,8 +458,18 @@ function queryCollectionOptions(config) {
183
458
  ...staleTime !== void 0 && { staleTime }
184
459
  };
185
460
  const localObserver = new queryCore.QueryObserver(queryClient, observerOptions);
461
+ const resolvedQueryGcTime = queryClient.getQueryCache().find({
462
+ queryKey: key,
463
+ exact: true
464
+ })?.gcTime;
465
+ const effectivePersistedGcTime = persistedGcTime ?? resolvedQueryGcTime;
186
466
  hashToQueryKey.set(hashedQueryKey, key);
187
467
  state.observers.set(hashedQueryKey, localObserver);
468
+ if (effectivePersistedGcTime !== void 0) {
469
+ effectivePersistedGcTimes.set(hashedQueryKey, effectivePersistedGcTime);
470
+ } else {
471
+ effectivePersistedGcTimes.delete(hashedQueryKey);
472
+ }
188
473
  queryRefCounts.set(
189
474
  hashedQueryKey,
190
475
  (queryRefCounts.get(hashedQueryKey) || 0) + 1
@@ -214,47 +499,102 @@ function queryCollectionOptions(config) {
214
499
  }
215
500
  return readyPromise;
216
501
  };
502
+ const applySuccessfulResult = (queryKey2, result, persistedBaseline) => {
503
+ const hashedQueryKey = queryCore.hashKey(queryKey2);
504
+ if (collection.status === `cleaned-up`) {
505
+ return;
506
+ }
507
+ state.lastError = void 0;
508
+ state.errorCount = 0;
509
+ const rawData = result.data;
510
+ const newItemsArray = select ? select(rawData) : rawData;
511
+ if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
512
+ const errorMessage = select ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey2)}` : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey2)}`;
513
+ console.error(errorMessage);
514
+ return;
515
+ }
516
+ const currentSyncedItems = new Map(
517
+ collection._state.syncedData.entries()
518
+ );
519
+ const shouldUsePersistedBaseline = persistedBaseline !== void 0;
520
+ const previouslyOwnedRows = shouldUsePersistedBaseline ? new Set(persistedBaseline.keys()) : getHydratedOwnedRowsForQueryBaseline(hashedQueryKey);
521
+ const newItemsMap = /* @__PURE__ */ new Map();
522
+ newItemsArray.forEach((item) => {
523
+ const key = getKey(item);
524
+ newItemsMap.set(key, item);
525
+ });
526
+ begin();
527
+ if (metadata) {
528
+ metadata.collection.delete(
529
+ `${QUERY_COLLECTION_GC_PREFIX}${hashedQueryKey}`
530
+ );
531
+ }
532
+ previouslyOwnedRows.forEach((key) => {
533
+ const oldItem = shouldUsePersistedBaseline ? persistedBaseline.get(key)?.value : currentSyncedItems.get(key);
534
+ if (!oldItem) {
535
+ return;
536
+ }
537
+ const newItem = newItemsMap.get(key);
538
+ if (!newItem) {
539
+ const owners = getPersistedOwners(key);
540
+ owners.delete(hashedQueryKey);
541
+ setPersistedOwners(key, owners);
542
+ const needToRemove = removeRow(key, hashedQueryKey);
543
+ if (needToRemove) {
544
+ write({ type: `delete`, value: oldItem });
545
+ }
546
+ } else if (!db.deepEquals(oldItem, newItem)) {
547
+ write({ type: `update`, value: newItem });
548
+ }
549
+ });
550
+ newItemsMap.forEach((newItem, key) => {
551
+ const owners = getPersistedOwners(key);
552
+ if (!owners.has(hashedQueryKey)) {
553
+ owners.add(hashedQueryKey);
554
+ setPersistedOwners(key, owners);
555
+ }
556
+ addRow(key, hashedQueryKey);
557
+ if (!currentSyncedItems.has(key)) {
558
+ write({ type: `insert`, value: newItem });
559
+ }
560
+ });
561
+ commit();
562
+ retainedQueriesPendingRevalidation.delete(hashedQueryKey);
563
+ cancelPersistedRetentionExpiry(hashedQueryKey);
564
+ markReady();
565
+ };
566
+ const reconcileSuccessfulResult = async (queryKey2, result) => {
567
+ const hashedQueryKey = queryCore.hashKey(queryKey2);
568
+ const persistedBaseline = await loadPersistedBaselineForQuery(hashedQueryKey);
569
+ if (collection.status === `cleaned-up`) {
570
+ return;
571
+ }
572
+ applySuccessfulResult(queryKey2, result, persistedBaseline);
573
+ };
217
574
  const makeQueryResultHandler = (queryKey2) => {
218
575
  const hashedQueryKey = queryCore.hashKey(queryKey2);
219
576
  const handleQueryResult = (result) => {
220
577
  if (result.isSuccess) {
221
- state.lastError = void 0;
222
- state.errorCount = 0;
223
- const rawData = result.data;
224
- const newItemsArray = select ? select(rawData) : rawData;
225
- if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
226
- const errorMessage = select ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey2)}` : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey2)}`;
227
- console.error(errorMessage);
578
+ if (collection.deferDataRefresh) {
579
+ collection.deferDataRefresh.then(() => {
580
+ const observer = state.observers.get(hashedQueryKey);
581
+ if (observer) {
582
+ observer.refetch().catch(() => {
583
+ });
584
+ }
585
+ });
228
586
  return;
229
587
  }
230
- const currentSyncedItems = new Map(
231
- collection._state.syncedData.entries()
232
- );
233
- const newItemsMap = /* @__PURE__ */ new Map();
234
- newItemsArray.forEach((item) => {
235
- const key = getKey(item);
236
- newItemsMap.set(key, item);
237
- });
238
- begin();
239
- currentSyncedItems.forEach((oldItem, key) => {
240
- const newItem = newItemsMap.get(key);
241
- if (!newItem) {
242
- const needToRemove = removeRow(key, hashedQueryKey);
243
- if (needToRemove) {
244
- write({ type: `delete`, value: oldItem });
245
- }
246
- } else if (!db.deepEquals(oldItem, newItem)) {
247
- write({ type: `update`, value: newItem });
248
- }
249
- });
250
- newItemsMap.forEach((newItem, key) => {
251
- addRow(key, hashedQueryKey);
252
- if (!currentSyncedItems.has(key)) {
253
- write({ type: `insert`, value: newItem });
254
- }
255
- });
256
- commit();
257
- markReady();
588
+ if (retainedQueriesPendingRevalidation.has(hashedQueryKey)) {
589
+ void reconcileSuccessfulResult(queryKey2, result).catch((error) => {
590
+ console.error(
591
+ `[QueryCollection] Error reconciling query ${String(queryKey2)}:`,
592
+ error
593
+ );
594
+ });
595
+ } else {
596
+ applySuccessfulResult(queryKey2, result);
597
+ }
258
598
  } else if (result.isError) {
259
599
  const isNewError = result.errorUpdatedAt !== state.lastErrorUpdatedAt || result.error !== state.lastError;
260
600
  if (isNewError) {
@@ -313,7 +653,13 @@ function queryCollectionOptions(config) {
313
653
  });
314
654
  }
315
655
  } else {
316
- markReady();
656
+ if (startupRetentionSettled) {
657
+ markReady();
658
+ } else {
659
+ void startupRetentionMaintenancePromise.then(() => {
660
+ markReady();
661
+ });
662
+ }
317
663
  }
318
664
  subscribeToQueries();
319
665
  state.observers.forEach((observer, hashedQueryKey) => {
@@ -324,36 +670,56 @@ function queryCollectionOptions(config) {
324
670
  const cleanupQueryInternal = (hashedQueryKey) => {
325
671
  unsubscribes.get(hashedQueryKey)?.();
326
672
  unsubscribes.delete(hashedQueryKey);
673
+ cancelPersistedRetentionExpiry(hashedQueryKey);
674
+ retainedQueriesPendingRevalidation.delete(hashedQueryKey);
327
675
  const rowKeys = queryToRows.get(hashedQueryKey) ?? /* @__PURE__ */ new Set();
676
+ const nextOwnersByRow = /* @__PURE__ */ new Map();
328
677
  const rowsToDelete = [];
329
678
  rowKeys.forEach((rowKey) => {
330
679
  const queries = rowToQueries.get(rowKey);
331
680
  if (!queries) {
332
681
  return;
333
682
  }
334
- queries.delete(hashedQueryKey);
335
- if (queries.size === 0) {
683
+ const nextOwners = new Set(queries);
684
+ nextOwners.delete(hashedQueryKey);
685
+ nextOwnersByRow.set(rowKey, nextOwners);
686
+ if (nextOwners.size === 0 && collection.has(rowKey)) {
687
+ rowsToDelete.push(collection.get(rowKey));
688
+ }
689
+ });
690
+ const shouldWriteMetadata = metadata !== void 0 && nextOwnersByRow.size > 0;
691
+ const needsTransaction = shouldWriteMetadata || rowsToDelete.length > 0;
692
+ if (needsTransaction) {
693
+ begin();
694
+ }
695
+ nextOwnersByRow.forEach((owners, rowKey) => {
696
+ if (owners.size === 0) {
336
697
  rowToQueries.delete(rowKey);
337
- if (collection.has(rowKey)) {
338
- rowsToDelete.push(collection.get(rowKey));
339
- }
698
+ } else {
699
+ rowToQueries.set(rowKey, owners);
700
+ }
701
+ if (shouldWriteMetadata) {
702
+ setPersistedOwners(rowKey, owners);
340
703
  }
341
704
  });
342
705
  if (rowsToDelete.length > 0) {
343
- begin();
344
706
  rowsToDelete.forEach((row) => {
345
707
  write({ type: `delete`, value: row });
346
708
  });
709
+ }
710
+ if (needsTransaction) {
347
711
  commit();
348
712
  }
349
713
  state.observers.delete(hashedQueryKey);
350
714
  queryToRows.delete(hashedQueryKey);
351
715
  hashToQueryKey.delete(hashedQueryKey);
352
716
  queryRefCounts.delete(hashedQueryKey);
717
+ effectivePersistedGcTimes.delete(hashedQueryKey);
353
718
  };
354
719
  const cleanupQueryIfIdle = (hashedQueryKey) => {
355
720
  const refcount = queryRefCounts.get(hashedQueryKey) || 0;
356
721
  const observer = state.observers.get(hashedQueryKey);
722
+ const effectivePersistedGcTime = effectivePersistedGcTimes.get(hashedQueryKey);
357
723
  if (refcount <= 0) {
358
724
  unsubscribes.get(hashedQueryKey)?.();
359
725
  unsubscribes.delete(hashedQueryKey);
@@ -369,6 +735,31 @@ function queryCollectionOptions(config) {
369
735
  { hashedQueryKey }
370
736
  );
371
737
  }
738
+ if (effectivePersistedGcTime !== void 0 && metadata && persistedMetadata?.row.scanPersisted) {
739
+ begin();
740
+ metadata.collection.set(
741
+ `${QUERY_COLLECTION_GC_PREFIX}${hashedQueryKey}`,
742
+ {
743
+ queryHash: hashedQueryKey,
744
+ mode: effectivePersistedGcTime === Number.POSITIVE_INFINITY ? `until-revalidated` : `ttl`,
745
+ ...effectivePersistedGcTime === Number.POSITIVE_INFINITY ? {} : { expiresAt: Date.now() + effectivePersistedGcTime }
746
+ }
747
+ );
748
+ commit();
749
+ if (effectivePersistedGcTime !== Number.POSITIVE_INFINITY) {
750
+ schedulePersistedRetentionExpiry({
751
+ queryHash: hashedQueryKey,
752
+ mode: `ttl`,
753
+ expiresAt: Date.now() + effectivePersistedGcTime
754
+ });
755
+ }
756
+ unsubscribes.get(hashedQueryKey)?.();
757
+ unsubscribes.delete(hashedQueryKey);
758
+ state.observers.delete(hashedQueryKey);
759
+ hashToQueryKey.delete(hashedQueryKey);
760
+ queryRefCounts.set(hashedQueryKey, 0);
761
+ return;
762
+ }
372
763
  cleanupQueryInternal(hashedQueryKey);
373
764
  };
374
765
  const forceCleanupQuery = (hashedQueryKey) => {
@@ -385,6 +776,10 @@ function queryCollectionOptions(config) {
385
776
  const cleanup = async () => {
386
777
  unsubscribeFromCollectionEvents();
387
778
  unsubscribeFromQueries();
779
+ persistedRetentionTimers.forEach((timer) => {
780
+ clearTimeout(timer);
781
+ });
782
+ persistedRetentionTimers.clear();
388
783
  const allQueryKeys = [...hashToQueryKey.values()];
389
784
  const allHashedKeys = [...state.observers.keys()];
390
785
  for (const hashedKey of allHashedKeys) {
@@ -465,13 +860,12 @@ function queryCollectionOptions(config) {
465
860
  }
466
861
  };
467
862
  const updateCacheData = (items) => {
468
- const activeQueryKeys = Array.from(hashToQueryKey.values());
469
- if (activeQueryKeys.length > 0) {
470
- for (const key of activeQueryKeys) {
471
- updateCacheDataForKey(key, items);
863
+ const allCached = queryClient.getQueryCache().findAll({ queryKey: baseKey });
864
+ if (allCached.length > 0) {
865
+ for (const query of allCached) {
866
+ updateCacheDataForKey(query.queryKey, items);
472
867
  }
473
868
  } else {
474
- const baseKey = typeof queryKey === `function` ? queryKey({}) : queryKey;
475
869
  updateCacheDataForKey(baseKey, items);
476
870
  }
477
871
  };