@oofp/query
@oofp/query is a functional query cache for managing async data. It provides configurable TTL, tag-based invalidation, deduplication, mutations, and optional Redis support — all built on TaskEither.
pnpm add @oofp/queryLicense: MIT | Peer dependency: @oofp/core | Optional: redis (for Redis-backed cache)
Overview
Section titled “Overview”@oofp/query manages the lifecycle of async data fetches:
- Cache with configurable TTL — avoid redundant network calls
- Tag-based invalidation — invalidate groups of queries by key
- Stale-while-revalidate — serve cached data while refreshing in the background
- Deduplication — concurrent identical requests are deduplicated automatically
- Mutations with cache invalidation — mutate data and invalidate related queries
- Telemetry — built-in cache hit/miss/eviction events
- Storage backends — in-memory (with LRU) or Redis
Creating a QueryClient
Section titled “Creating a QueryClient”import { createQueryClient } from "@oofp/query";
const queryClient = createQueryClient({ defaultTTL: 60_000, // 60 seconds maxCacheSize: 1000, // LRU eviction after 1000 entries defaultRetry: 3, // retry failed queries 3 times defaultRetryDelay: 1000, // 1s between retries});Fetching Queries
Section titled “Fetching Queries”Every query operation returns a TaskEither<Error, A> — lazy and composable.
import * as TE from "@oofp/core/task-either";import { pipe } from "@oofp/core/pipe";
interface User { id: string; name: string;}
const fetchUser = (id: string) => queryClient.fetchQuery<User>({ queryKey: ["users", id], queryFn: () => TE.tryCatch( () => fetch(`/api/users/${id}`).then((r) => r.json()), (err) => new Error(`Failed to fetch user: ${err}`), ), ttl: 30_000, // cache for 30 seconds });
// Executeconst result = await fetchUser("42")();// Either<Error, QueryResult<User>>QueryResult
Section titled “QueryResult”The fetchQuery method returns a QueryResult<T> that includes metadata about the cache interaction:
interface QueryResult<T> { data: T; fromCache: boolean; timestamp: number;}Reading & Writing Cache Directly
Section titled “Reading & Writing Cache Directly”import * as M from "@oofp/core/maybe";
// Read from cache without fetchingconst cached = queryClient.getQueryData<User>(["users", "42"]);// TaskEither<Error, Maybe<User>>
// Write directly to cacheconst write = queryClient.setQueryData( ["users", "42"], { id: "42", name: "Alice" }, 60_000, // optional TTL override);Invalidation
Section titled “Invalidation”// Invalidate a specific queryconst inv1 = queryClient.invalidateQueries(["users", "42"]);// TaskEither<Error, number> — returns count of invalidated entries
// Remove queries from cache entirelyconst rem = queryClient.removeQueries(["users"]);
// Clear the entire cacheconst clr = queryClient.clear();Mutations
Section titled “Mutations”Mutations wrap a side-effectful operation and can invalidate related queries on success.
const updateUser = queryClient.mutate<User, { name: string }>({ mutationFn: (variables) => TE.tryCatch( () => fetch("/api/users/42", { method: "PUT", body: JSON.stringify(variables), }).then((r) => r.json()), (err) => new Error(`Update failed: ${err}`), ), onSuccess: (data) => queryClient.setQueryData(["users", data.id], data),});
// Execute mutationconst result = await updateUser({ name: "Alice Updated" })();QueryOptions Reference
Section titled “QueryOptions Reference”interface QueryOptions<TData> { queryKey: QueryKey; // unique cache key queryFn: () => TaskEither<Error, TData>; // the fetch function ttl?: number; // time-to-live in ms retry?: number | false; // retry count (default: 3) retryDelay?: number; // delay between retries (default: 1000ms) enabled?: boolean; // skip execution if false}Additional Exports
Section titled “Additional Exports”| Export | Description |
|---|---|
QueryClientAccesor | Accessor for dependency injection patterns |
LRUCache | Standalone LRU cache implementation |
serialize | Key serialization utilities |
extractTags | Extract tags from query keys |