Transaction-based system for handling optimistic mutations with automatic rollback capabilities and layered cache architecture.
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");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 updateCore 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;
}// 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}`);
}
});
};// 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;
}
};// 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);
}
}
});
};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;
}Good candidates:
Avoid for:
// 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;
}
};