Skip to content

fix(rpc): honor automatic client caching - #430

Merged
antfu merged 4 commits into
devframes:mainfrom
SaKaNa-Y:fix/automatic-rpc-cache
Oct 13, 2026
Merged

antfu merged 4 commits into
devframes:mainfrom
SaKaNa-Y:fix/automatic-rpc-cache

Conversation

@SaKaNa-Y

@SaKaNa-Y SaKaNa-Y commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Please take a moment to read this. Thank you!

I should include a brief explanation of the problem in my own words in every PR. If that explanation is missing, please @mention me and do not merge this PR until I have added it. You may also leave this PR unaddressed (because this means I have not fulfilled my responsibilities as the author).

If my explanation is unclear or difficult to follow, please ask me to clarify or provide reproduction steps or supporting evidence.

I welcome suggestions and counterarguments, especially questions about anything I may have overlooked. (Your feedback helps me learn and improve. 🙏)

I hold myself to this standard for every PR, regardless of its size.

Summary

Enabling cacheOptions: true does not prevent repeated server execution for identical RPC calls. This affects static functions and queries declared with cacheable: true. The client creates a cache with an empty function list but never loads the server declarations. As a result, no function qualifies for automatic caching.

This change lets the client request eligible function names from an authenticated server endpoint before sending a call. It then caches results by function name and arguments. Static functions and opted-in queries use the cache, while actions and events continue to execute. Explicit function lists and custom serializers remain supported.

Same query, same arguments, two calls:
Before: server executes twice
After:  server executes once; the second call returns the cached result

The client clears cached results when declarations, connection status, or trust change, and when it closes. The server can also broadcast invalidation after data changes. Late responses cannot restore invalidated cache entries. Servers without the discovery endpoint continue to serve uncached calls.

The change also handles timeouts while the client discovers cacheable functions. If invalidation triggers repeated discovery, expired calls stop before dispatch. This prevents an expired action from executing alongside a later retry. Calls already sent retain their existing timeout behavior. Failed discovery rejects waiting calls, and a later call retries discovery.

Evidence

These results come from the completed verification runs. Tests were not rerun for this description update.

  • Browser reproduction: two identical calls execute twice with cacheOptions: true. An explicit function list returns the first result twice and executes the server function once.
  • With the PR applied to the v1.2.3 source, all 28 cache regression tests pass. A separate consumer using the packed package passes all 46 checks across WebSocket and SSE. Coverage includes real authentication, disconnects, invalidation, late responses, discovery timeouts, and explicit cache lists.
  • On that source plus the PR, lint, knip, typecheck, and build pass. The full test run reports 1686 passed, 13 skipped, and two failed export snapshots for @devframes/hub-ui-onboard. Both failures concern defaultMessages and also occur in the baseline CI run.

Merge Danger

Door: Two-way. This change can be reverted.

Blast Radius: RPC caching. Automatic caching adds requests to discover eligible functions. Invalidation clears all cached results, including results cached through explicit function lists. The server must broadcast invalidation after changing data that cached queries read.

Discover cacheable methods from server declarations and invalidate client caches when declarations or connection state change. Preserve explicit cache options and reject stale in-flight cache writes.
@vercel

vercel Bot commented Oct 5, 2026

Copy link
Copy Markdown

@SaKaNa-Y is attempting to deploy a commit to the NuxtLabs Team on Vercel.

A member of the Team first needs to authorize it.

Clear failed discovery promises so later RPC calls can retry. Preserve newer discovery after invalidation and keep timed-out actions from running.

Cover rejection, timeout recovery, and invalidation races over WebSocket and SSE. Align caching documentation with repository terminology.
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

👁️‍🗨️ Review this pull request with grouped, summarized diffs at:
👉 https://pulls.review/gh/devframes/devframe/430?from=github-actions[bot]

raw result

💭 analyzed by vercel-ai-gateway/anthropic/claude-sonnet-5
🕰️ 2026-10-13 04:35 UTC
🔗 head 2cbbee2
🤖 automated by pulls.review

{
  "headSha": "2cbbee2b440fae90505110df7f3d6144d88a8402",
  "result": {
    "overallSummary": "Fixes `cacheOptions: true` so it actually prevents repeated server execution: the client now discovers eligible functions (static + opted-in `cacheable` queries) from the server, caches by function name and arguments, and invalidates the cache on declaration/trust/connection changes or an explicit server broadcast, with careful handling of discovery failures and timeouts.",
    "groups": [
      {
        "key": "rpc-cache-fix",
        "label": "RPC automatic cache fix",
        "summary": "Implements real automatic client-side caching: the client discovers cacheable functions from a new `devframe:rpc:cacheable-functions` server endpoint before dispatching calls, caches by function name and arguments, and clears the cache on declaration changes, trust/connection changes, close, or a new `devframe:rpc:cache:invalidate` broadcast sent by the server after data changes. Also handles discovery failures/timeouts so expired calls aren't dispatched after rediscovery.",
        "category": "api",
        "filePaths": [
          "packages/devframe/src/client/rpc.ts",
          "packages/devframe/src/node/host-functions.ts",
          "packages/devframe/src/events.ts",
          "packages/devframe/src/types/rpc-augments.ts"
        ],
        "critical": true,
        "fileNotes": [
          {
            "path": "packages/devframe/src/client/rpc.ts",
            "text": "Core of the fix: adds cache discovery with generation tracking to avoid stale cache application, deadline checks shared with the outer call timeout, and invalidation hooks on connection/trust changes and close."
          },
          {
            "path": "packages/devframe/src/node/host-functions.ts",
            "text": "Adds the server-side discovery endpoint (static + opted-in cacheable queries) and broadcasts cache invalidation whenever RPC definitions change."
          }
        ]
      },
      {
        "key": "tests",
        "label": "Cache regression tests",
        "summary": "New end-to-end regression suite covering caching eligibility, falsy results, explicit function lists, legacy servers without discovery, discovery retries/timeouts, invalidation races, and trust/close cleanup.",
        "category": "tests",
        "filePaths": [
          "packages/devframe/src/client/rpc-cache.test.ts"
        ],
        "critical": true
      },
      {
        "key": "docs",
        "label": "Caching docs",
        "summary": "Documents the actual automatic-caching behavior, how to broadcast invalidation after mutations, and fallback/retry behavior for older servers.",
        "category": "docs",
        "filePaths": [
          "docs/content/1.guide/11.client.md",
          "docs/content/8.references/3.events.md"
        ]
      },
      {
        "key": "snapshots",
        "label": "Type snapshot updates",
        "summary": "Regenerated type-declaration snapshots reflecting the new `cacheInvalidate` event and `cacheable-functions`/`cache:invalidate` RPC function signatures.",
        "category": "tests",
        "filePaths": [
          "tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts",
          "tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts"
        ]
      }
    ],
    "schemaVersion": 1,
    "source": "llm",
    "generatedAt": "2026-10-13T04:35:17.660Z",
    "model": "vercel-ai-gateway/anthropic/claude-sonnet-5",
    "locale": "en"
  }
}

Keep each call within its original timeout across cache discovery refreshes. Stop expired calls before dispatch while allowing later retries to use the shared discovery result.

Cover call and callOptional over WebSocket and SSE. All 28 cache tests and the full suite pass, along with lint, knip, typecheck, and build.
@SaKaNa-Y
SaKaNa-Y marked this pull request as ready for review October 7, 2026 06:51
@SaKaNa-Y

SaKaNa-Y commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

The query uses cacheable: true, and the client uses cacheOptions: true, but the result is still not cached. Caching works when the function is explicitly listed in cacheOptions.functions. The problem is mainly with automatic caching.

@antfu
antfu merged commit ed8f3a1 into devframes:main Oct 13, 2026
12 of 13 checks passed
@SaKaNa-Y
SaKaNa-Y deleted the fix/automatic-rpc-cache branch October 13, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants