QueryFilter selects a group of queries within one Client. Invalidate, InvalidateAndWait, Cancel, and Remove accept the same selection to invalidate data, await refreshes, cancel loads, or remove cached state.
Typical uses include invalidating detail and list queries after a resource changes, canceling a tenant’s loads when switching tenants, and removing associated cached data on sign-out. A filter describes targets; the called method determines loading, cancellation, and removal behavior.
Quick start
Section titled “Quick start”This complete program selects tenant 42’s detail and list queries, marks them stale, and removes their cached data. Tenant 99’s data remains available. RefetchNone prevents this invalidation from starting refreshes.
Save the program as main.go in a Go module with the cacheq dependency and run go run .. No HTTP service or database is required.
package main
import ( "fmt" "time"
"github.com/Waterkyuu/cacheq")
// ResourceKey identifies a resource within a tenant.type ResourceKey struct { // TenantID separates data belonging to different tenants. TenantID int // Resource identifies the kind of query result. Resource string}
// main invalidates and removes tenant 42's cached results without affecting tenant 99.func main() { client := cacheq.NewClient(cacheq.Options{StaleTime: time.Hour}) defer client.Close()
detailKey := ResourceKey{TenantID: 42, Resource: "profile"} listKey := ResourceKey{TenantID: 42, Resource: "users"} otherKey := ResourceKey{TenantID: 99, Resource: "profile"} if err := cacheq.Set(client, detailKey, "Alice"); err != nil { panic(err) } if err := cacheq.Set(client, listKey, []string{"Alice"}); err != nil { panic(err) } if err := cacheq.Set(client, otherKey, "Carol"); err != nil { panic(err) }
filter := cacheq.QueryFilter{ Predicate: func(key any) bool { resource, ok := key.(ResourceKey) return ok && resource.TenantID == 42 }, } if err := client.Invalidate(filter, cacheq.InvalidateOptions{ Refetch: cacheq.RefetchNone, }); err != nil { panic(err) } fmt.Println("detail stale:", cacheq.Get[string](client, detailKey).Stale) fmt.Println("list stale:", cacheq.Get[[]string](client, listKey).Stale) fmt.Println("other tenant stale:", cacheq.Get[string](client, otherKey).Stale)
if err := client.Remove(filter); err != nil { panic(err) } fmt.Println("detail retained:", cacheq.Get[string](client, detailKey).HasData) fmt.Println("list retained:", cacheq.Get[[]string](client, listKey).HasData) fmt.Println("other tenant data:", cacheq.Get[string](client, otherKey).Data)}Expected output:
detail stale: truelist stale: trueother tenant stale: falsedetail retained: falselist retained: falseother tenant data: CarolPredicate receives a query key and returns true to select it. This predicate first checks the application’s ResourceKey type, then matches TenantID; keys of other types are excluded. Tenant data is separated by TenantID in the query key. The filter does not replace identity scope in keys.
target: specify operation targets
Section titled “target: specify operation targets”The four methods accept these forms through their target any parameter:
| Argument form | Example | Selection rule |
|---|---|---|
| One query key | "users" |
Address that exact key |
[]any |
[]any{"users", "user:42"} |
Address multiple keys in first-occurrence order, without duplicates |
func(any) bool |
func(key any) bool { return key == "users" } |
Select existing Client queries for which the predicate returns true |
A QueryFilter value |
cacheq.QueryFilter{Keys: []any{"users"}} |
Combine keys and a predicate for reuse across operations |
Query keys must satisfy cacheq’s comparable-key requirements. Matching uses exact Go equality, including dynamic types; strings and arrays are not matched by prefix. Only []any denotes a batch argument. Other slice types such as []string cannot be passed directly as batches.
An existing query is a key for which the Client still retains cached state, an active load, or a subscription. This includes subscribed queries without usable data. Constructing a QueryDefinition alone does not register a query with a Client.
Filters and predicates select existing queries only and skip missing keys. Passing an exact key or []any directly addresses the specified keys; Invalidate can establish stale state for missing keys in these forms. Empty and nil []any batches both perform no operations.
QueryFilter: combine selection criteria
Section titled “QueryFilter: combine selection criteria”| Field | Type | Default | Contract |
|---|---|---|---|
Keys |
[]any |
nil | Restrict candidate keys; nil is unrestricted, while a non-nil empty slice selects nothing |
Tags |
[]string |
nil | Require any matching registered label; nil is unrestricted, while a non-nil empty slice selects nothing |
Predicate |
func(key any) bool |
nil | Further restrict candidates; nil accepts all candidates |
Configured fields are combined with AND: a key must satisfy Keys, match any requested Tags, and satisfy Predicate. Duplicate keys are processed once. After the complete key list has been validated, candidates satisfying key and tag restrictions are passed to the predicate at most once. Declare labels through QueryDefinition.Tags; see query tags for registration and lifetime rules.
This local fragment uses the ResourceKey type from the quick start and can be placed inside an application function:
filter := cacheq.QueryFilter{ Keys: []any{ ResourceKey{TenantID: 42, Resource: "profile"}, ResourceKey{TenantID: 99, Resource: "profile"}, }, Predicate: func(key any) bool { resource, ok := key.(ResourceKey) return ok && resource.TenantID == 42 },}The filter selects only tenant 42’s profile query, provided the Client already retains that query.
| Filter value | Selected queries |
|---|---|
cacheq.QueryFilter{} |
All existing queries |
cacheq.QueryFilter{Keys: nil} |
All existing queries |
cacheq.QueryFilter{Keys: []any{}} |
No queries |
cacheq.QueryFilter{Keys: []any{"users"}} |
The existing "users" query |
cacheq.QueryFilter{Predicate: func(key any) bool { return key == "users" }} |
The existing "users" query |
Order is unspecified without an explicit key list. A filter retains criteria, not previously matched results; every operation selects queries again.
Operation behavior and return values
Section titled “Operation behavior and return values”| Method | Return value | Effect on selected queries |
|---|---|---|
client.Invalidate(target, options...) |
error |
Retain data, mark it stale, and schedule refreshes according to options; return without awaiting refresh completion |
client.InvalidateAndWait(ctx, target, options...) |
([]RefreshResult, error) |
Mark data stale and await the refresh outcomes associated with this invalidation |
client.Cancel(target) |
error |
Cancel active shared loads while retaining completed data and subscriptions |
client.Remove(target) |
error |
Cancel active shared loads, clear cached data, errors, and associated cleanup timers, and retain subscriptions |
Invalidate and InvalidateAndWait use the same refresh conditions as exact-key calls: automatic refresh requires an enabled subscription. By default, Invalidate does not append a refresh to an active load; RefetchAfterLoad enables a bounded follow-up. InvalidateAndWait enables it automatically. RefetchNone prevents starting refreshes without canceling existing requests. See invalidation and refresh for the complete policy.
Cancel and Remove affect every waiter of a selected shared load, not only the application function invoking them. Both signal cancellation and update query state without waiting for loaders to exit; loaders must honor context.
Neither operation automatically reloads data. Subsequent Fetch, Refetch, or a transition from disabled to enabled can load data again. Close a subscription with its handle’s Close() method. If subscriptions remain after Remove, the key’s result-type binding remains in effect; see cache and lifecycle.
Await refresh completion
Section titled “Await refresh completion”This complete program prefetches data and creates an enabled subscription. After changing the simulated backend value, InvalidateAndWait waits for the query to refresh before the program reads Bob.
Save the program as main.go and run go run .. The loader can run in the background, so atomic.Value synchronizes access to the backend value.
package main
import ( "context" "fmt" "sync/atomic" "time"
"github.com/Waterkyuu/cacheq")
// main waits for an enabled query to reload a changed backend value.func main() { client := cacheq.NewClient(cacheq.Options{StaleTime: time.Hour}) defer client.Close() ctx := context.Background()
var backend atomic.Value backend.Store("Alice") load := func(context.Context) (string, error) { return backend.Load().(string), nil } if err := cacheq.Prefetch(ctx, client, "profile", load); err != nil { panic(err) } query := cacheq.Query(client, "profile", load) defer query.Close()
backend.Store("Bob") filter := cacheq.QueryFilter{Keys: []any{"profile"}} results, err := client.InvalidateAndWait(ctx, filter) if err != nil { panic(err) } fmt.Println("refreshed:", results[0].Refreshed) fmt.Println("data:", query.Snapshot().Data)}Expected output:
refreshed: truedata: BobEach selected key has one RefreshResult:
| Field | Meaning |
|---|---|
Key |
The selected query key, retaining its dynamic type |
Refreshed |
A post-invalidation load completed successfully without its result being invalidated again or replaced by a local write |
Err |
The key’s refresh failure, supersession error, or wait cancellation error |
Queries without an enabled subscription, or calls using RefetchNone, return Refreshed == false and Err == nil to indicate a skipped refresh. Missing filter keys do not appear in the results. An empty selection returns a zero-length results slice and a nil error.
Selected queries can refresh concurrently. Results follow the first-occurrence order of an explicit key list. Per-key errors are wrapped with key context and joined in the returned error; use errors.Is and errors.As to inspect them. One failed refresh does not roll back other completed refreshes. Successful completion does not establish a simultaneous, consistent snapshot across multiple keys.
Errors and context
Section titled “Errors and context”InvalidateAndWait requires a non-nil context.Context as ctx.
| Condition | Returned behavior |
|---|---|
| Nil target, non-comparable key, or an invalid key within a key list | ErrInvalidKey; no partial operation-owned changes or predicate calls |
A nil func(any) bool passed directly |
ErrInvalidKey |
A nil QueryFilter.Predicate field |
Valid; no predicate restriction |
| Client closed before invocation | ErrClosed; the predicate is not evaluated |
| Client closed during predicate evaluation | ErrClosed before applying the operation |
InvalidateAndWait context canceled before invocation |
Context error; no selection or invalidation |
| Context canceled while awaiting refreshes | End this call’s unfinished waits; Client-owned refreshes continue |
Call Cancel or Remove explicitly to stop the actual refreshes. See invalidation and refresh for per-key refresh errors and execution-budget errors from InvalidateAndWait.
Concurrency and lifecycle
Section titled “Concurrency and lifecycle”Predicates run outside the Client lock and may call Client APIs. They evaluate a candidate-key snapshot captured at the start of the operation. Newly added keys are excluded, and keys no longer retained by the Client before application are skipped.
Selection and application are not a backend transaction and do not form a transaction with other business writes. Predicate side effects are not rolled back. Prefer predicates that only inspect keys.
The caller owns filter slices, mutable data captured by predicates, and the business scope represented in query keys. Applications must synchronize concurrent mutation of these values. Selection does not change QueryOptions.Disable, introduce polling, or load queries without subscriptions.
Related guides
Section titled “Related guides”- Shared options and query definitions: reuse keys, loaders, and configuration.
- Invalidation and refresh: refresh policy, per-key results, and bounded follow-up loads.
- Cache and lifecycle: cache reads and writes, cancellation, removal, and resource release.
The selection contract is covered by unit tests and HTTP end-to-end tests.
