(hashedQueryKey: string)
| 1554 | * Respects refcounts and invalidateQueries cycles via hasListeners(). |
| 1555 | */ |
| 1556 | const cleanupQueryIfIdle = (hashedQueryKey: string) => { |
| 1557 | const refcount = queryRefCounts.get(hashedQueryKey) || 0 |
| 1558 | const observer = state.observers.get(hashedQueryKey) |
| 1559 | const effectivePersistedGcTime = |
| 1560 | effectivePersistedGcTimes.get(hashedQueryKey) |
| 1561 | |
| 1562 | if (refcount <= 0) { |
| 1563 | // Drop our subscription so hasListeners reflects only active consumers |
| 1564 | unsubscribes.get(hashedQueryKey)?.() |
| 1565 | unsubscribes.delete(hashedQueryKey) |
| 1566 | } |
| 1567 | |
| 1568 | const hasListeners = observer?.hasListeners() ?? false |
| 1569 | |
| 1570 | if (hasListeners) { |
| 1571 | // During invalidateQueries, TanStack Query keeps internal listeners alive. |
| 1572 | // Leave refcount at 0 but keep observer so it can resubscribe. |
| 1573 | queryRefCounts.set(hashedQueryKey, 0) |
| 1574 | return |
| 1575 | } |
| 1576 | |
| 1577 | // No listeners means the query is truly idle. |
| 1578 | // Even if refcount > 0, we treat hasListeners as authoritative to prevent leaks. |
| 1579 | // This can happen if subscriptions are GC'd without calling unloadSubset. |
| 1580 | if (refcount > 0) { |
| 1581 | console.warn( |
| 1582 | `[cleanupQueryIfIdle] Invariant violation: refcount=${refcount} but no listeners. Cleaning up to prevent leak.`, |
| 1583 | { hashedQueryKey }, |
| 1584 | ) |
| 1585 | } |
| 1586 | |
| 1587 | if ( |
| 1588 | effectivePersistedGcTime !== undefined && |
| 1589 | metadata && |
| 1590 | persistedMetadata?.row.scanPersisted |
| 1591 | ) { |
| 1592 | begin() |
| 1593 | metadata.collection.set( |
| 1594 | `${QUERY_COLLECTION_GC_PREFIX}${hashedQueryKey}`, |
| 1595 | { |
| 1596 | queryHash: hashedQueryKey, |
| 1597 | mode: |
| 1598 | effectivePersistedGcTime === Number.POSITIVE_INFINITY |
| 1599 | ? `until-revalidated` |
| 1600 | : `ttl`, |
| 1601 | ...(effectivePersistedGcTime === Number.POSITIVE_INFINITY |
| 1602 | ? {} |
| 1603 | : { expiresAt: Date.now() + effectivePersistedGcTime }), |
| 1604 | }, |
| 1605 | ) |
| 1606 | commit() |
| 1607 | if (effectivePersistedGcTime !== Number.POSITIVE_INFINITY) { |
| 1608 | schedulePersistedRetentionExpiry({ |
| 1609 | queryHash: hashedQueryKey, |
| 1610 | mode: `ttl`, |
| 1611 | expiresAt: Date.now() + effectivePersistedGcTime, |
| 1612 | }) |
| 1613 | } |
no test coverage detected