CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--query-persist-client-core

Utilities for persisting TanStack Query cache data to various storage backends with restore functionality

Pending
Overview
Eval results
Files

retry-strategies.mddocs/

Retry Strategies

Configurable retry mechanisms for handling persistence failures due to storage quotas, size limits, or other constraints. When persistence operations fail, retry strategies can modify the data being persisted to resolve the issue.

Capabilities

Persist Retryer Function Type

Defines the signature for retry strategy functions that handle persistence failures.

/**
 * Function type for retry strategies when persistence fails
 * @param props - Object containing the failed persistence context
 * @returns Modified PersistedClient or undefined if retry should give up
 */
type PersistRetryer = (props: {
  persistedClient: PersistedClient;
  error: Error;
  errorCount: number;
}) => PersistedClient | undefined;

Parameters:

  • persistedClient - The client state that failed to persist
  • error - The error that occurred during persistence
  • errorCount - Number of retry attempts so far

Returns:

  • PersistedClient - Modified client state to retry persistence with
  • undefined - Give up and don't retry

Remove Oldest Query Strategy

Built-in retry strategy that removes the oldest query from the cache to reduce storage size when persistence fails.

/**
 * Retry strategy that removes the oldest query from cache to reduce size
 * Sorts queries by dataUpdatedAt and removes the oldest one
 */
const removeOldestQuery: PersistRetryer;

Usage Example:

import { 
  persistQueryClient, 
  removeOldestQuery,
  type PersistRetryer,
  type PersistedClient
} from "@tanstack/query-persist-client-core";

// Simple persister with retry logic
function createRetryPersister(baseStorage: Storage, retryStrategy: PersistRetryer) {
  return {
    async persistClient(persistedClient: PersistedClient) {
      let attempts = 0;
      let currentClient = persistedClient;
      
      while (attempts < 3) {
        try {
          const serialized = JSON.stringify(currentClient);
          baseStorage.setItem("queryClient", serialized);
          return; // Success
        } catch (error) {
          attempts++;
          console.warn(`Persistence attempt ${attempts} failed:`, error);
          
          // Try retry strategy
          const modifiedClient = retryStrategy({
            persistedClient: currentClient,
            error: error as Error,
            errorCount: attempts
          });
          
          if (!modifiedClient) {
            throw new Error(`Persistence failed after ${attempts} attempts`);
          }
          
          currentClient = modifiedClient;
        }
      }
      
      throw new Error("Max retry attempts exceeded");
    },
    
    async restoreClient() {
      const stored = baseStorage.getItem("queryClient");
      return stored ? JSON.parse(stored) : undefined;
    },
    
    async removeClient() {
      baseStorage.removeItem("queryClient");
    }
  };
}

// Use the retry persister with removeOldestQuery strategy
const queryClient = new QueryClient();
const retryPersister = createRetryPersister(localStorage, removeOldestQuery);

const [unsubscribe, restorePromise] = persistQueryClient({
  queryClient,
  persister: retryPersister,
  buster: "app-v1.0"
});

Custom Retry Strategies

You can implement custom retry strategies to handle different failure scenarios:

// Remove queries by specific criteria
const removeStaleQueries: PersistRetryer = ({ persistedClient, error, errorCount }) => {
  const mutations = [...persistedClient.clientState.mutations];
  const queries = [...persistedClient.clientState.queries];
  
  // Remove stale queries (older than 1 hour)
  const oneHourAgo = Date.now() - (1000 * 60 * 60);
  const freshQueries = queries.filter(query => 
    query.state.dataUpdatedAt > oneHourAgo
  );
  
  if (freshQueries.length < queries.length) {
    return {
      ...persistedClient,
      clientState: { mutations, queries: freshQueries }
    };
  }
  
  return undefined; // No stale queries to remove, give up
};

// Remove mutations to reduce size
const removeMutations: PersistRetryer = ({ persistedClient, error, errorCount }) => {
  if (persistedClient.clientState.mutations.length > 0) {
    return {
      ...persistedClient,
      clientState: {
        mutations: [], // Remove all mutations
        queries: [...persistedClient.clientState.queries]
      }
    };
  }
  
  return undefined; // No mutations to remove
};

// Progressive data reduction strategy
const progressiveReduction: PersistRetryer = ({ persistedClient, error, errorCount }) => {
  const mutations = [...persistedClient.clientState.mutations];
  let queries = [...persistedClient.clientState.queries];
  
  if (errorCount === 1) {
    // First attempt: remove mutations
    return {
      ...persistedClient,
      clientState: { mutations: [], queries }
    };
  } else if (errorCount === 2) {
    // Second attempt: remove oldest 25% of queries
    const sortedQueries = queries
      .sort((a, b) => a.state.dataUpdatedAt - b.state.dataUpdatedAt);
    const keepCount = Math.ceil(queries.length * 0.75);
    queries = sortedQueries.slice(-keepCount);
    
    return {
      ...persistedClient,
      clientState: { mutations: [], queries }
    };
  } else if (errorCount === 3) {
    // Final attempt: keep only the most recent 5 queries
    const recentQueries = queries
      .sort((a, b) => b.state.dataUpdatedAt - a.state.dataUpdatedAt)
      .slice(0, 5);
      
    return {
      ...persistedClient,
      clientState: { mutations: [], queries: recentQueries }
    };
  }
  
  return undefined; // Give up after 3 attempts
};

Storage Configuration

Storage Options

interface StoragePersisterOptions<TStorageValue = string> {
  /** The storage client used for setting and retrieving items from cache.
   * For SSR pass in `undefined`. */
  storage: AsyncStorage<TStorageValue> | undefined | null;
  /**
   * How to serialize the data to storage.
   * @default `JSON.stringify`
   */
  serialize?: (persistedQuery: PersistedQuery) => MaybePromise<TStorageValue>;
  /**
   * How to deserialize the data from storage.
   * @default `JSON.parse`
   */
  deserialize?: (cachedString: TStorageValue) => MaybePromise<PersistedQuery>;
  /**
   * A unique string that can be used to forcefully invalidate existing caches,
   * if they do not share the same buster string
   */
  buster?: string;
  /**
   * The max-allowed age of the cache in milliseconds.
   * If a persisted cache is found that is older than this
   * time, it will be discarded
   * @default 24 hours
   */
  maxAge?: number;
  /**
   * Prefix to be used for storage key.
   * Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`.
   * @default 'tanstack-query'
   */
  prefix?: string;
  /**
   * Filters to narrow down which Queries should be persisted.
   */
  filters?: QueryFilters;
}

Persisted Query Data Structure

interface PersistedQuery {
  buster: string;
  queryHash: string;
  queryKey: QueryKey;
  state: QueryState;
}

Constants

/** Default prefix for storage keys used in query persistence */
const PERSISTER_KEY_PREFIX = 'tanstack-query';

Error Scenarios

Retry strategies are designed to handle common persistence failures:

  • Storage Quota Exceeded: Most common scenario where storage is full
  • Individual Item Too Large: When a single query's data exceeds storage limits
  • Serialization Failures: When data contains non-serializable values
  • Network Errors: In remote storage scenarios (IndexedDB, remote APIs)
  • Permission Errors: Storage access denied by browser or system

Best Practices

  1. Implement Progressive Reduction: Start by removing least important data (mutations, stale queries)
  2. Set Retry Limits: Avoid infinite retry loops by limiting attempts
  3. Log Failures: Track which strategies work in your application's context
  4. Test Storage Limits: Understand your target storage backend's limitations
  5. Graceful Degradation: Always return undefined when no more reductions are possible

Install with Tessl CLI

npx tessl i tessl/npm-tanstack--query-persist-client-core

docs

client-persistence.md

index.md

query-persistence.md

retry-strategies.md

tile.json