Skip to content

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

Open
SaKaNa-Y wants to merge 3 commits into
devframes:mainfrom
SaKaNa-Y:fix/automatic-rpc-cache
Open

SaKaNa-Y wants to merge 3 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-07 05:37 UTC
🔗 head 0fa73c0
🤖 automated by pulls.review

{
  "headSha": "0fa73c09a88286816f22bd895a7f3a7724665563",
  "result": {
    "overallSummary": "Fixes automatic RPC client caching, which previously had no effect because the client never populated its cache function list. The client now discovers cacheable methods from the node side, caches static and opted-in query responses by argument hash, and clears the cache on invalidation broadcasts, declaration changes, trust changes, disconnects, or close — while carefully tracking per-call deadlines across discovery retries to avoid dispatching expired calls (which could otherwise duplicate action side effects on retry).",
    "groups": [
      {
        "key": "rpc-cache-core",
        "label": "Automatic RPC caching",
        "summary": "Implements automatic RPC client caching: the client discovers cacheable methods via a new `devframe:rpc:cacheable-functions` query, caches static/opted-in-query responses by argument hash, and clears its cache on a new `devframe:rpc:cache:invalidate` broadcast, declaration changes, trust changes, disconnects, and close. Each in-flight call keeps its own deadline across discovery retries so an expired call is never 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: per-call deadline tracking across discovery retries, generation counters to ignore late discovery/cache writes after invalidation, and gating cache usage on trust/connection state.",
            "critical": true
          },
          {
            "path": "packages/devframe/src/node/host-functions.ts",
            "text": "Node side now exposes the eligible-methods query and broadcasts cache invalidation whenever RPC definitions change (register/update)."
          }
        ]
      },
      {
        "key": "cache-tests",
        "label": "RPC cache tests",
        "summary": "New regression suite (28 tests over WebSocket and SSE) covering discovery, retries, per-call deadlines during invalidation, late responses, explicit function lists, custom serializers, legacy devframes without discovery, trust revocation, and close.",
        "category": "tests",
        "filePaths": [
          "packages/devframe/src/client/rpc-cache.test.ts"
        ],
        "critical": true
      },
      {
        "key": "docs",
        "label": "Caching docs",
        "summary": "Documents the new `cacheOptions: true` discovery/invalidation behavior and the `devframe:rpc:cache:invalidate` broadcast event.",
        "category": "docs",
        "filePaths": [
          "docs/content/1.guide/11.client.md",
          "docs/content/8.references/3.events.md"
        ]
      },
      {
        "key": "type-snapshots",
        "label": "Type snapshot updates",
        "summary": "Generated type snapshots regenerated to reflect the new `cacheInvalidate` event and `devframe:rpc:cacheable-functions`/`cache:invalidate` RPC function signatures.",
        "category": "other",
        "filePaths": [
          "tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts",
          "tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts"
        ]
      }
    ],
    "schemaVersion": 1,
    "source": "llm",
    "generatedAt": "2026-10-07T05:37:19.520Z",
    "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.

This branch has not been deployed

No deployments
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.

1 participant