or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cache-management.mdcache-storage.mdfragment-matching.mdindex.mdoptimistic-updates.mdstore-operations.md
tile.json

optimistic-updates.mddocs/

Optimistic Updates

Transaction-based system for handling optimistic mutations with automatic rollback capabilities and layered cache architecture.

Capabilities

OptimisticCacheLayer

Cache layer that provides optimistic update functionality by layering new data over existing cache state.

/**
 * Cache layer for optimistic updates
 * Wraps parent cache and provides temporary optimistic data
 * Automatically falls back to parent cache for missing data
 */
class OptimisticCacheLayer extends ObjectCache {
  /**
   * Creates optimistic cache layer
   * @param optimisticId - Unique identifier for this optimistic update
   * @param parent - Parent cache layer to fall back to
   * @param transaction - Transaction that created this layer
   */
  constructor(
    public readonly optimisticId: string,
    public readonly parent: NormalizedCache,
    public readonly transaction: Transaction<NormalizedCacheObject>
  );

  /**
   * Combines optimistic data with parent cache data
   * @returns Merged cache object with optimistic changes
   */
  toObject(): NormalizedCacheObject;

  /**
   * Gets data with optimistic layer fallback
   * @param dataId - Data identifier to retrieve
   * @returns Store object from optimistic layer or parent cache
   */
  get(dataId: string): StoreObject;
}

Usage Example:

import { InMemoryCache } from "apollo-cache-inmemory";
import { gql } from "graphql-tag";

const cache = new InMemoryCache();

// Perform optimistic update
cache.recordOptimisticTransaction(
  (proxy) => {
    // Write optimistic data
    proxy.write({
      query: gql`
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            name
            status
          }
        }
      `,
      variables: { id: "1" },
      result: {
        user: {
          id: "1",
          name: "Alice",
          status: "updating...", // Optimistic status
          __typename: "User"
        }
      }
    });
  },
  "update-user-1" // Optimistic ID
);

// Later, remove optimistic update (e.g., when real result arrives)
cache.removeOptimistic("update-user-1");

Transaction Management

Methods for managing optimistic transactions and their lifecycle.

/**
 * Records optimistic transaction with specific ID
 * @param transaction - Function that performs optimistic writes
 * @param id - Unique identifier for the optimistic update
 */
recordOptimisticTransaction(
  transaction: Transaction<NormalizedCacheObject>,
  id: string
): void;

/**
 * Removes optimistic update by ID
 * Automatically reapplies remaining optimistic layers
 * @param idToRemove - ID of optimistic update to remove
 */
removeOptimistic(idToRemove: string): void;

/**
 * Performs transaction with optional optimistic layer
 * @param transaction - Transaction function to execute
 * @param optimisticId - Optional ID for optimistic transaction
 */
performTransaction(
  transaction: Transaction<NormalizedCacheObject>,
  optimisticId?: string
): void;

Usage Examples:

// Example 1: Optimistic mutation with rollback
const UPDATE_USER_MUTATION = gql`
  mutation UpdateUser($id: ID!, $name: String!) {
    updateUser(id: $id, name: $name) {
      id
      name
      updatedAt
    }
  }
`;

// Start optimistic update
const optimisticId = "update-user-123";
cache.recordOptimisticTransaction((proxy) => {
  proxy.writeQuery({
    query: GET_USER_QUERY,
    variables: { id: "123" },
    data: {
      user: {
        id: "123",
        name: "New Name", // Optimistic value
        updatedAt: new Date().toISOString(),
        __typename: "User"
      }
    }
  });
}, optimisticId);

// Perform actual mutation
client.mutate({
  mutation: UPDATE_USER_MUTATION,
  variables: { id: "123", name: "New Name" }
}).then(
  // Success: remove optimistic update, real data is now in cache
  () => cache.removeOptimistic(optimisticId),
  // Error: remove optimistic update to rollback
  () => cache.removeOptimistic(optimisticId)
);

// Example 2: Multiple optimistic updates
cache.recordOptimisticTransaction((proxy) => {
  proxy.writeQuery({
    query: GET_POSTS_QUERY,
    data: {
      posts: [
        { id: "temp-1", title: "New Post", __typename: "Post" },
        ...existingPosts
      ]
    }
  });
}, "add-post-1");

cache.recordOptimisticTransaction((proxy) => {
  proxy.writeQuery({
    query: GET_USER_QUERY,
    variables: { id: "456" },
    data: {
      user: { id: "456", status: "active", __typename: "User" }
    }
  });
}, "activate-user-456");

// Remove specific optimistic update
cache.removeOptimistic("add-post-1"); // Only removes post update

Transaction Functions

Core transaction functionality for cache operations.

/**
 * Transaction function type for cache operations
 * Receives cache proxy for performing reads and writes
 */
type Transaction<T> = (cache: ApolloCache<T>) => void;

/**
 * Cache proxy interface available within transactions
 * Provides safe access to cache operations during transactions
 */
interface ApolloCache<T> {
  read<TData>(options: Cache.ReadOptions): TData | null;
  write(write: Cache.WriteOptions): void;
  diff<TData>(query: Cache.DiffOptions): Cache.DiffResult<TData>;
  readQuery<TData>(options: Cache.ReadQueryOptions): TData | null;
  writeQuery<TData>(options: Cache.WriteQueryOptions<TData>): void;
  readFragment<TData>(options: Cache.ReadFragmentOptions): TData | null;
  writeFragment<TData>(options: Cache.WriteFragmentOptions<TData>): void;
}

Advanced Patterns

Conditional Optimistic Updates

// Only apply optimistic update if conditions are met
const applyOptimisticUpdate = (shouldOptimize: boolean) => {
  if (shouldOptimize) {
    cache.recordOptimisticTransaction((proxy) => {
      // Optimistic update logic
      proxy.writeQuery({
        query: GET_USER_QUERY,
        variables: { id: userId },
        data: optimisticUserData
      });
    }, `optimize-${userId}`);
  }
  
  return client.mutate({
    mutation: UPDATE_USER_MUTATION,
    variables: { id: userId, ...updates }
  }).finally(() => {
    if (shouldOptimize) {
      cache.removeOptimistic(`optimize-${userId}`);
    }
  });
};

Optimistic Update Chains

// Chain multiple optimistic updates that depend on each other
const performOptimisticChain = async () => {
  // Step 1: Optimistic user update
  cache.recordOptimisticTransaction((proxy) => {
    proxy.writeQuery({
      query: GET_USER_QUERY,
      variables: { id: "1" },
      data: { user: { ...userData, status: "updating" } }
    });
  }, "step-1");
  
  try {
    const userResult = await updateUser();
    cache.removeOptimistic("step-1");
    
    // Step 2: Optimistic posts update based on user update
    cache.recordOptimisticTransaction((proxy) => {
      proxy.writeQuery({
        query: GET_POSTS_QUERY,
        data: { posts: updatedPosts }
      });
    }, "step-2");
    
    const postsResult = await updatePosts();
    cache.removeOptimistic("step-2");
    
  } catch (error) {
    // Rollback all optimistic updates
    cache.removeOptimistic("step-1");
    cache.removeOptimistic("step-2");
    throw error;
  }
};

Optimistic Update with Conflict Resolution

// Handle conflicts when optimistic and real data differ
const handleOptimisticConflict = (optimisticId: string) => {
  return client.mutate({
    mutation: UPDATE_MUTATION,
    variables: mutationVariables,
    update: (cache, { data }) => {
      // Remove optimistic update
      cache.removeOptimistic(optimisticId);
      
      // Check if real result conflicts with what user expected
      const currentData = cache.readQuery({ query: GET_QUERY });
      const realResult = data.updateItem;
      
      if (hasConflict(currentData, realResult)) {
        // Show conflict resolution UI or apply merge strategy
        handleConflictResolution(currentData, realResult);
      }
    }
  });
};

Types

interface OptimisticStoreItem {
  id: string;
  data: NormalizedCacheObject;
  transaction: Transaction<NormalizedCacheObject>;
}

type Transaction<T> = (cache: ApolloCache<T>) => void;

interface NormalizedCache {
  get(dataId: string): StoreObject;
  set(dataId: string, value: StoreObject): void;
  delete(dataId: string): void;
  clear(): void;
  toObject(): NormalizedCacheObject;
  replace(newData: NormalizedCacheObject): void;
}

interface NormalizedCacheObject {
  [dataId: string]: StoreObject | undefined;
}

interface StoreObject {
  __typename?: string;
  [storeFieldKey: string]: StoreValue;
}

Best Practices

When to Use Optimistic Updates

Good candidates:

  • User interactions that should feel instant (like toggles, status changes)
  • Mutations with high success probability
  • Operations where temporary incorrect state is acceptable
  • Network-dependent operations that benefit from perceived performance

Avoid for:

  • Critical operations where temporary incorrect data causes problems
  • Complex mutations with many side effects
  • Operations that frequently fail
  • Mutations that return significantly different data than input

Error Handling

// Always handle both success and failure cases
const performOptimisticMutation = async () => {
  const optimisticId = `mutation-${Date.now()}`;
  
  // Apply optimistic update
  cache.recordOptimisticTransaction((proxy) => {
    // Optimistic changes
  }, optimisticId);
  
  try {
    const result = await client.mutate({ /* mutation */ });
    // Success: optimistic data replaced by real data
    cache.removeOptimistic(optimisticId);
    return result;
  } catch (error) {
    // Failure: rollback optimistic changes
    cache.removeOptimistic(optimisticId);
    throw error;
  }
};