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