DeesseJS Collections

Cache Management

Intelligent client-side cache management with metadata

Cache Management

Collections provides intelligent client-side cache management through automatic cache metadata. Every query includes cache keys, and every mutation returns invalidation keys, enabling seamless integration with React Query, SWR, and other caching solutions.

Overview

Prop

Type

Query Cache Metadata

Every query response includes cache metadata:

const result = await collections.posts.findMany({
  where: { status: { equals: 'published' } }
})

// Result includes:
{
  data: [...],           // Your data
  _cache: {
    keys: ['posts', 'posts:list', 'posts:published'],  // Cache keys
    revalidate: 60       // Revalidate after 60 seconds
  }
}

Cache Key Structure

Cache keys follow a hierarchical pattern:

// Collection level
['users']

// Query type
['users', 'list']

// With filters
['users', 'list', 'status:active']

// With pagination
['users', 'list', 'status:active', 'page:1']

// Single item
['users', 'id:123']

Reading Cache Keys

const users = await collections.users.findMany()

console.log(users._cache.keys)
// ['users', 'users:list']

const activeUsers = await collections.users.findMany({
  where: { status: { equals: 'active' } }
})

console.log(activeUsers._cache.keys)
// ['users', 'users:list', 'users:status:active']

Mutation Invalidation

Mutations automatically return keys to invalidate:

const user = await collections.users.create({
  data: {
    name: 'John Doe',
    email: 'john@example.com'
  }
})

// Result includes:
{
  data: { id: 1, name: 'John Doe', ... },
  _invalidate: {
    keys: ['users', 'users:list']  // Invalidate these cache keys
  }
}

Invalidation Strategies

Integration with React Query

Setup

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { collections } from './config/database'

// Hook with cache keys
function useUsers(filters?: { status?: string }) {
  return useQuery({
    queryKey: ['users', 'list', filters],
    queryFn: async () => {
      const result = await collections.users.findMany({
        where: filters?.status ? { status: { equals: filters.status } } : undefined
      })

      // Extract cache metadata
      queryClient.setQueryData(
        ['users', 'list', filters, 'meta'],
        result._cache
      )

      return result.data
    }
  })
}

Automatic Invalidation

function CreateUserForm() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: async (data: { name: string; email: string }) => {
      const result = await collections.users.create({ data })

      // Invalidate returned keys
      if (result._invalidate) {
        result._invalidate.keys.forEach(key => {
          queryClient.invalidateQueries({ queryKey: [key] })
        })
      }

      return result.data
    },

    onSuccess: () => {
      // Additional invalidation if needed
      queryClient.invalidateQueries({ queryKey: ['users'] })
    }
  })

  return (
    <form onSubmit={(e) => {
      e.preventDefault()
      mutation.mutate(new FormData(e))
    }}>
      {/* ... */}
    </form>
  )
}

Optimistic Updates

function UpdateUserName({ userId }: { userId: number }) {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: async (name: string) => {
      return await collections.users.update({
        where: { id: userId },
        data: { name }
      })
    },

    onMutate: async (newName) => {
      // Cancel outgoing queries
      await queryClient.cancelQueries({ queryKey: ['users', 'id', userId] })

      // Snapshot previous value
      const previousUser = queryClient.getQueryData(['users', 'id', userId])

      // Optimistically update
      queryClient.setQueryData(['users', 'id', userId], (old: any) => ({
        ...old,
        name: newName
      }))

      return { previousUser }
    },

    onError: (err, variables, context) => {
      // Rollback on error
      if (context?.previousUser) {
        queryClient.setQueryData(['users', 'id', userId], context.previousUser)
      }
    },

    onSettled: () => {
      // Always refetch after error or success
      queryClient.invalidateQueries({ queryKey: ['users', 'id', userId] })
    }
  })

  return <button onClick={() => mutation.mutate('New Name')}>Update</button>
}

Integration with SWR

Basic Setup

import useSWR, { useSWRMutation } from 'swr'
import { collections } from './config/database'

function useUsers() {
  const fetcher = async () => {
    const result = await collections.users.findMany()
    return result.data
  }

  return useSWR(['users', 'list'], fetcher)
}

Mutations with Invalidation

function CreateUser() {
  const { trigger } = useSWRMutation(
    'users',
    async (key, { arg }: { arg: { name: string; email: string } }) => {
      const result = await collections.users.create({
        data: arg
      })

      // Return invalidation keys
      return {
        data: result.data,
        invalidate: result._invalidate.keys
      }
    }
  )

  return <button onClick={() => trigger({ name: 'John', email: 'john@example.com' })}>
    Create User
  </button>
}

Advanced Patterns

Selective Revalidation

const result = await collections.posts.findMany()

// Check revalidate time
if (result._cache.revalidate) {
  const { revalidate, keys } = result._cache

  // Set up background revalidation
  setTimeout(() => {
    // Revalidate only these keys
    keys.forEach(key => {
      revalidateQuery(key)
    })
  }, revalidate * 1000)
}

Stale-While-Revalidate

function useUsersWithSWR() {
  const { data, error, isLoading } = useSWR(
    ['users', 'list'],
    async () => {
      const result = await collections.users.findMany()
      return result.data
    },
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: true,
      dedupingInterval: 60000,  // 1 minute
      // Use cache metadata
      refreshInterval: (data) => {
        // Use revalidate from metadata
        const meta = data?._cache
        return meta?.revalidate ? meta.revalidate * 1000 : 0
      }
    }
  )

  return { data, error, isLoading }
}

Prefetching

function UserList() {
  const queryClient = useQueryClient()

  useEffect(() => {
    // Prefetch on mount
    queryClient.prefetchQuery({
      queryKey: ['users', 'list'],
      queryFn: async () => {
        const result = await collections.users.findMany()
        return result.data
      }
    })
  }, [queryClient])

  return <div>User List</div>
}

Cache Key Utilities

Build Cache Keys Manually

// Utility to build cache keys
const buildCacheKeys = {
  list: (collection: string) => [collection, 'list'],

  filtered: (collection: string, filters: Record<string, any>) => [
    collection,
    'list',
    ...Object.entries(filters).map(([k, v]) => `${k}:${v}`)
  ],

  single: (collection: string, id: number) => [collection, `id:${id}`],

  relation: (collection: string, relation: string, id: number) => [
    collection,
    `id:${id}`,
    relation
  ]
}

// Usage
const keys = buildCacheKeys.filtered('users', { status: 'active' })
// ['users', 'list', 'status:active']

Match Invalidation Keys

// Check if a key should be invalidated
const shouldInvalidate = (key: string, invalidateKeys: string[]) => {
  return invalidateKeys.some(invalidKey => {
    // Exact match
    if (key === invalidKey) return true

    // Prefix match (e.g., 'users' matches 'users:list')
    if (key.startsWith(invalidKey + ':')) return true

    return false
  })
}

// Usage
const cacheKey = 'users:status:active'
const invalidateKeys = ['users', 'posts']

shouldInvalidate(cacheKey, invalidateKeys)  // true (matches 'users')

Metadata Structure

Query Response

interface QueryResponse<T> {
  data: T[]
  _cache: {
    keys: string[]           // All cache keys for this query
    revalidate?: number       // Revalidation time in seconds
    timestamp: string         // When data was fetched
    version: string          // Cache version identifier
  }
}

Mutation Response

interface MutationResponse<T> {
  data: T
  _invalidate: {
    keys: string[]           // Keys to invalidate
    revalidate?: string[]    // Specific keys to revalidate
    timestamp: string         // When mutation occurred
  }
}

Best Practices

Complete Example

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { collections } from '@/config/database'

export function UserList() {
  const queryClient = useQueryClient()

  // Query with cache metadata
  const { data: users, isLoading } = useQuery({
    queryKey: ['users', 'list'],
    queryFn: async () => {
      const result = await collections.users.findMany({
        orderBy: { createdAt: 'desc' }
      })

      // Store cache metadata
      queryClient.setQueryData(
        ['users', 'list', 'meta'],
        result._cache
      )

      return result.data
    },
    staleTime: 60000  // 1 minute
  })

  // Mutation with automatic invalidation
  const createUser = useMutation({
    mutationFn: async (data: { name: string; email: string }) => {
      const result = await collections.users.create({ data })

      // Invalidate based on returned keys
      if (result._invalidate) {
        result._invalidate.keys.forEach(key => {
          queryClient.invalidateQueries({ queryKey: [key] })
        })
      }

      return result.data
    },

    // Optimistic update
    onMutate: async (newUser) => {
      await queryClient.cancelQueries({ queryKey: ['users', 'list'] })
      const previousUsers = queryClient.getQueryData(['users', 'list'])

      queryClient.setQueryData(['users', 'list'], (old: any) => [
        { ...newUser, id: Date.now() },
        ...old
      ])

      return { previousUsers }
    },

    onError: (err, variables, context) => {
      if (context?.previousUsers) {
        queryClient.setQueryData(['users', 'list'], context.previousUsers)
      }
    }
  })

  if (isLoading) return <div>Loading...</div>

  return (
    <div>
      <h1>Users ({users?.length})</h1>
      <button onClick={() => createUser.mutate({
        name: 'New User',
        email: 'new@example.com'
      })}>
        Add User
      </button>

      <ul>
        {users?.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

Next Steps

On this page