Skip to content

Derive query invalidation from repository dependencies #778

@patroza

Description

@patroza

Problem

We currently manually document/configure which commands invalidate which queries via invalidatesQueries, InvalidationSet.add, or client-side queryInvalidation callbacks.

That works, but it is easy to forget, and the mapping often duplicates information the server already knows:

  • queries read from repositories
  • commands mutate repositories
  • if a command mutates a repository that a cached query read from, that query should usually be invalidated

A quick look at ~/pj/macs/scanner shows this correlation clearly. For example, several resource files manually invalidate query classes while the corresponding controllers/services read or mutate the same repos. The interesting dependency information exists, but it is currently spread across resource declarations, controllers, services, and helper functions.

Goal

Make cache invalidation simpler, easier to maintain, and harder to forget.

Prefer deriving invalidation from actual data dependencies over manually listing every affected query.

Proposed approach

Add request-scoped dependency recording at the repository/store boundary.

For queries:

  • record repo reads during query handler execution
  • include the read dependency set in the RPC response metadata
  • client stores that set on the TanStack query metadata

For commands:

  • record repo writes during command handler execution
  • include the write dependency set in command response metadata
  • client invalidates cached queries whose recorded read deps intersect the command write deps

Conceptually:

query reads repo A
command writes repo A
=> invalidate query

Start at repository granularity. That may refetch a bit more than necessary, but it should be correct and much harder to forget. Entity-level deps can come later if needed.

Why runtime recording instead of resource subscriptions?

Configuring resources to subscribe to repositories is better than today, but still manual and easy to drift.

Static derivation from resource/controller source is useful as an audit tool, but likely too brittle as the source of truth because dependencies often flow through:

  • helper services
  • domain services
  • conditional branches
  • projections and raw queries
  • CurrentSettings, CurrentUser, time, environment, feature flags
  • background queues and async continuations

Runtime recording at the repository boundary sees the actual dependency path taken by the handler.

Existing pieces to build on

  • Request classes already support manual invalidatesQueries callbacks.
  • Vue mutation invalidation already merges client-declared invalidation with server-driven invalidation keys.
  • Server-driven invalidation already exists via Invalidation.InvalidationSet.
  • Repositories already know their model names through Model.makeRepo / RepositoryRegistry.
  • Store implementations already classify operations such as all, find, filter, queryRaw, set, batchSet, bulkSet, and batchRemove.

Suggested design

Introduce a request-scoped dependency recorder service, roughly:

type DataDependency =
  | { readonly type: "repo"; readonly name: string }
  | { readonly type: "signal"; readonly name: string }

interface DependencyRecorder {
  readonly read: (dep: DataDependency) => Effect.Effect<void>
  readonly write: (dep: DataDependency) => Effect.Effect<void>
  readonly getReads: Effect.Effect<ReadonlySet<DataDependency>>
  readonly getWrites: Effect.Effect<ReadonlySet<DataDependency>>
}

Repository/store operations record dependencies automatically:

  • read ops: all, find, filter, query, queryRaw
  • write ops: save, set, batchSet, bulkSet, batchRemove, queryAndSave*, byIdAndSave*

RPC metadata carries:

  • query responses: readDependencies
  • command responses: writeDependencies

Client invalidation then becomes predicate-based:

invalidate cached query if intersects(query.meta.readDependencies, command.writeDependencies)

Manual invalidation remains available for cases that are not repo-backed or need domain-specific behavior.

Escape hatches / explicit deps

Keep explicit APIs for:

  • external side effects
  • background queues
  • non-repository signals like settings/current user/time
  • intentionally broad invalidation
  • intentionally narrow invalidation
  • command branches where invalidation depends on success payload or input

Potential API shape:

yield* Dependencies.read({ type: "signal", name: "CurrentSettings" })
yield* Dependencies.write({ type: "signal", name: "CurrentSettings" })

or integrate with existing InvalidationSet for compatibility.

Migration path

  1. Add dependency recorder service with no-op default.
  2. Instrument repository/store read/write operations.
  3. Include read deps in query response metadata.
  4. Include write deps in command response metadata.
  5. Store query read deps in TanStack query metadata.
  6. Invalidate by dependency intersection after mutations.
  7. Keep existing invalidatesQueries / InvalidationSet behavior and merge it with derived invalidation.
  8. Use the scanner repo as an audit/migration aid to compare manual invalidation against derived repo deps.

Open questions

  • Should dependency metadata live alongside existing invalidation metadata, or become a separate RPC metadata channel?
  • What is the stable dependency identity: repository model name, service tag, namespace, or a dedicated dependencyId?
  • Do we need entity-level dependencies in v1, or is repo-level invalidation enough?
  • How should stream queries and stream commands drain dependency metadata?
  • How do we represent writes performed in background fibers after the command has returned?
  • Should queryRaw conservatively mark the owning repo, or require explicit extra deps?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions