Skip to content

Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog)#3964

Merged
justin808 merged 31 commits into
mainfrom
jg/3871-cache-tags-revalidation
Jun 15, 2026
Merged

Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog)#3964
justin808 merged 31 commits into
mainfrom
jg/3871-cache-tags-revalidation

Conversation

@justin808

@justin808 justin808 commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Implements the maintainer-signed-off RFC on #3871 (v1 scope; SWR explicitly deferred per the 2026-06-11 triage).

What: cache_tags: on all four Pro cached_* helpers; a Rails.cache-backed tag→key index; ReactOnRailsPro.revalidate_tag / revalidate_tags; ReactOnRailsPro::Cache::Revalidates AR concern (revalidates_react_cache, after_commit); config cache_tag_index_expires_in (7d) / cache_tag_index_max_keys (5,000); docs (caching guide section with Next.js mapping, Pro fragment-caching section) and changelog. cache_key: semantics unchanged; cache_tags: is purely additive. Zero OSS package code changes.

Contract (per RFC Option A): index appends are lossy-OK read-modify-write; tag revalidation is best-effort, correctness bounded by expires_in (dev warning without an expiry); missing/evicted index → no-op returning 0. A Redis-native-sets fast path remains a welcome follow-up optimization, never a correctness requirement.

Codex Decision Log

  • Non-blocking: index payload format -> Decision: {keys:, expires_at:} (not bare array) to implement the RFC's max-TTL merge portably; v1 key segment covers format evolution.
  • Non-blocking: which key form to record -> Decision: the store's logical name via private expanded_key/namespace_key/merged_options (public expand_cache_key prefers cache_key_with_version + RAILS_CACHE_ID and would miss entries). FileStore encoding covered by spec.
  • Non-blocking: AR tag derivation -> Decision: model_name.cache_key/id -- equal to version-less cache_key, stable even with cache_versioning = false.
  • Non-blocking: tag validation timing -> Decision: at registration (miss path only), so cache hits never pay Proc/normalization cost.
  • Non-blocking: mutable custom tags (stale grouping after e.g. author change) -> Decision: documented as bounded-by-expires_in with a previous_changes recipe, not implemented -- per descoped-v1/no-over-engineering triage note.
  • Non-blocking: changelog location -> Decision: unified /CHANGELOG.md (react_on_rails_pro/CHANGELOG.md is a deprecation stub).
  • Non-blocking: llms-full.txt regeneration necessarily absorbs main's pre-existing staleness that open PR Regenerate stale llms-full.txt (fixes red check-llms-full on main) #3958 also fixes -- regenerate on rebase if Regenerate stale llms-full.txt (fixes red check-llms-full on main) #3958 lands first.
  • DISCUSS resolved: delete ordering in revalidate_tag -> Decision: keep clearing the tag index before deleting entries. Reversing the order improves crash retryability for old entries, but it can delete fresh re-registrations discovered through a stale index snapshot. The retained order protects fresh content; crash-after-index-clear orphaning remains bounded by tagged entry expiry under the RFC's best-effort contract.
  • Compatibility: tag index key shape -> Decision: index keys are SHA-256 digests under rorp:tag:v1: so valid-but-long or control-character tag names do not become invalid Memcached-style cache keys.
  • Expiry semantics: expires_at handling -> Decision: when Rails cache entries honor expires_at, it takes precedence over expires_in; older Rails strip unsupported expires_at and use the TTL the entry store will actually honor. Expired absolute times clamp to immediate expiry instead of propagating negative TTLs.
  • Streaming cache writes: tag-index options are computed before stream completion so the index TTL is not shortened by render duration; entry write options are recomputed at completion so old-Rails absolute expiries still expire at the intended wall-clock time.

Validation

  • bundle exec rspec spec/react_on_rails_pro/cache spec/react_on_rails_pro/cache_spec.rb38 examples, 0 failures
  • Full Pro gem suite bundle exec rspec spec/react_on_rails_pro545 examples, 3 failures, 1 pending; the 3 failures (prod_binstub_spec.rb) pre-exist on clean origin/main (verified via stashed run: 3 examples, 3 failures) — environment-related, untouched here
  • Pro dummy app (built build:test assets + node renderer on :3800): bundle exec rspec spec/helpers/react_on_rails_pro_helper_spec.rb spec/models/cache_revalidates_spec.rb64 examples, 0 failures — includes the cached round-trip, stream HIT→revalidate→MISS, and async HIT→revalidate→MISS tag-busting proofs
  • bundle exec rubocop --ignore-parent-exclusion (Pro) → 235 files inspected, no offenses detected; OSS rubocop → 218 files, no offenses
  • bundle exec rake rbs:validate✓ RBS validation passed
  • node script/generate-llms-full.mjs --check✓ llms-full.txt is current
  • script/check-docs-sidebar → pass; pnpm start format.listDifferent → pass

Review gate

codex review --base origin/main run to completion 4 times; all findings fixed and re-reviewed (namespace-aware deletes, expires_at index TTL, stable AR identity under cache_versioning = false, logical key recording + FileStore spec + delete_multi fallback). A 5th confirmation pass timed out twice at the 580s tool cap with no findings emitted; recorded as incomplete rather than looping further.

Closes #3871

🤖 Generated with Claude Code


Note

Medium Risk
Touches production fragment-cache read/write and invalidation across sync, stream, and async paths; behavior is best-effort and TTL-bounded, with broad test coverage but reliance on shared cache stores and custom stores honoring namespace: nil on deletes.

Overview
[Pro] Adds tag-based cache revalidation (Next.js revalidateTag-style) on top of existing fragment caching: all four cached_* helpers accept optional cache_tags:, misses register keys in a Rails.cache tag→key index, and ReactOnRailsPro.revalidate_tag / revalidate_tags delete indexed entries. ReactOnRailsPro::Cache::Revalidates (revalidates_react_cache, after_commit) wires invalidation from ActiveRecord; cache_tag_index_expires_in and cache_tag_index_max_keys bound index growth. Helpers also normalize expires_at for writes and skip caching when expiry has already passed.

cache_key: behavior is unchanged; tagging is additive and documented as best-effort, with correctness bounded by expires_in.

Docs/changelog/llms outputs describe the contract and Next.js mapping; docs/.llms-exclusions trims some pages to stay under the llms cap; .lychee.toml ignores vite.dev in CI link checks.

Reviewed by Cursor Bugbot for commit 14ead8d. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Tag-based cache revalidation for cached React fragments via new revalidate_tag / revalidate_tags APIs, plus model-level after-commit invalidation.
    • Added cache tag indexing with configurable TTL and maximum keys; warns in development when tags are used without expiry.
    • cache_tags are recorded only when a cache miss writes the entry.
  • Documentation

    • Expanded guides and examples for tag normalization/validation, operational semantics, configuration knobs, and Next.js mapping; updated changelog.
  • Tests

    • Added coverage for tag normalization, registration timing, revalidation behavior (including async/stream), and model-driven invalidation.

Merge Readiness Criteria

Current evaluation as of 2026-06-15 for head 14ead8daca9e9a73044f66b800594447f9cb30bf.

  • Release mode: accelerated-rc, from canonical release gate Release gate: react_on_rails 17.0.0 #3823 (Mode: accelerated-rc).
  • Current head SHA: 14ead8daca9e9a73044f66b800594447f9cb30bf.
  • CI/check status: Complete for current head: 33 passing checks and 4 expected/path-selected skips; no pending or failing checks. Passing checks include check-llms-full, Pro node renderer E2E/RSpec, Pro lint, package JS tests, examples, CodeQL, claude-review, Cursor Bugbot, CodeRabbit, and benchmark suites (Core benchmarks, Pro benchmarks shards 1/2 and 2/2, Pro Node Renderer benchmarks). mergeStateStatus is CLEAN.
  • Review-thread status: Paginated GraphQL review-thread sweep reports 85 total threads and unresolved=0 immediately before merge.
  • Review feedback triage: Requested-change/review feedback was fixed or explicitly triaged in the PR history. The latest branch update resolved generated llms-full.txt drift against current main; no Bump react-hooks lint to v6 and document RSC compiler boundary #3963-reserved docs files were touched.
  • Validation run: Current-head GitHub checks above are the merge gate. Local/worker validation for the merge resolution reported git diff --check, node script/generate-llms-full.mjs --check, pnpm start format.listDifferent, OSS/Pro RuboCop, focused Pro cache specs, dummy cache revalidation spec, and helper spec after starting the node renderer all passing.
  • Label/CI decision: Labels: full-ci, benchmark. This PR touches Pro fragment caching/cache tag revalidation and performance-sensitive paths, so both labels are appropriate; benchmark checks completed successfully.
  • Known residual risk: Cache invalidation behavior is release-relevant and performance-sensitive, but current-head full CI, benchmarks, and review-thread gates are clean. No unresolved blocker remains.
  • Merge recommendation: Merge now. Maintainer marked Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) #3964 approved in the batch instruction; current-head gates satisfy AGENTS.md merge qualification.

Agent Merge Confidence

Mode: accelerated-rc
Current head SHA: 14ead8daca9e9a73044f66b800594447f9cb30bf
Score: 8/10
Auto-merge recommendation: yes, under explicit maintainer approval in the batch thread.
Affected areas: Pro fragment caching, cache tag revalidation, generated docs/LLMS, benchmarks.
CI detector: full CI + benchmark labels; current-head full CI and benchmarks completed.
Validation run:

  • Current-head GitHub checks -> 33 pass / 4 expected skips / 0 pending / 0 failed.
  • Local/worker merge-update validation -> LLMS, format, whitespace, RuboCop, focused cache specs, dummy cache revalidation spec, and helper spec passed.
    Review/check gate:
  • GitHub checks: complete for 14ead8daca9e9a73044f66b800594447f9cb30bf; skips are path-selected helper skips.
  • Review threads: GraphQL unresolved count is 0.
  • Current-head reviewer verdicts: claude-review, Cursor Bugbot, CodeRabbit completed with no unresolved blocker threads.
    Known residual risk: release-relevant cache invalidation change, mitigated by full CI, focused validation, and benchmark coverage.
    Finalized by: maintainer approval in the Codex batch thread (#3964 marked “Approved by maintainer”).

Confidence note:

  • Validated: current-head full CI/benchmarks plus local/worker focused validation above.
  • Evidence: gh pr checks 3964 -> 33 pass / 4 skip / 0 pending / 0 failed; GraphQL review threads -> 85 total / 0 unresolved; mergeStateStatus -> CLEAN.
  • UNKNOWN: none that affects merge qualification.
  • Residual risk: performance/cache behavior remains the main release-watch surface.
  • Decision points: 1 (maintainer approval/merge authorization).

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Implements tag-based cache revalidation for React on Rails Pro: cached helpers register tags on cache writes, a TagIndex maps tags→expanded cache keys in Rails.cache, ReactOnRailsPro.revalidate_tag(s) deletes indexed keys, an ActiveRecord concern triggers revalidation on commit, and docs/tests/config knobs are included.

Changes

Tag-Based Cache Revalidation

Layer / File(s) Summary
Configuration and public API entry points
react_on_rails_pro/lib/react_on_rails_pro.rb, react_on_rails_pro/lib/react_on_rails_pro/configuration.rb, react_on_rails_pro/sig/react_on_rails_pro.rbs, react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs
Adds cache_tag_index_expires_in and cache_tag_index_max_keys configuration attributes with validation; requires tag-index and revalidates modules; exposes ReactOnRailsPro.revalidate_tag and revalidate_tags module methods returning deletion counts; includes RBS type signatures for all new methods and configuration properties.
TagIndex core implementation
react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb, react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
Implements per-tag index stored in Rails.cache: supports tag normalization (strings, symbols, callables, records via model_name+id, cache_key objects), index payload de-duplication with configurable max-keys eviction, per-entry TTL computation from expires_in/expires_at plus slack with dev warnings, expanded-key reconstruction using Rails cache internals, delete strategies (delete_multi with fallback), and revalidation with deletion counts; includes RBS definitions for TagIndex class and methods.
Cache class tag API
react_on_rails_pro/lib/react_on_rails_pro/cache.rb
Adds register_tags(tags, cache_key, cache_options) and register_normalized_tags to register normalized tags via TagIndex; adds revalidate_tags(*tags) to flatten nested arrays, filter blank/nil tags, and delegate to TagIndex; includes helper methods for normalization and blank-tag detection at revalidation boundary.
Cached helpers tag support
react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
Integrates cache_tags: parameter into cached_react_component, cached_react_component_hash, cached_stream_react_component, and cached_async_react_component. On cache miss/write, normalizes tags, writes cache entry, then registers normalized tags using computed cache key and options; updates helper documentation to describe :cache_tags option, best-effort revalidation semantics, and expires_in bounding guidance.
ActiveRecord Revalidates concern
react_on_rails_pro/lib/react_on_rails_pro/cache/revalidates.rb, react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
Adds ReactOnRailsPro::Cache::Revalidates concern with revalidates_react_cache(&resolver) class method to configure optional tag resolver, registers single after_commit callback (guarded by flag against duplicates), and callback computes tags from resolver (normalized to Array) or defaults to record itself, then invokes ReactOnRailsPro.revalidate_tags; includes RBS signatures for mixin and ClassMethods module.
Documentation and changelog
CHANGELOG.md, docs/oss/building-features/caching.md, docs/pro/fragment-caching.md, llms-full.txt, docs/.llms-exclusions
Adds Pro changelog entry describing feature; inserts comprehensive "Tag-Based Revalidation" section in OSS caching guide (tag forms/normalization, model-layer integration, index mechanics, best-effort semantics, config knobs, Next.js mapping); adds "Tag-Based Revalidation" subsection in Pro fragment-caching guide with helper and model examples; mirrors content in llms-full.txt; updates doc exclusions to temporarily mark contributing/resource pages.
Unit and integration tests
react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb, react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb, react_on_rails_pro/spec/react_on_rails_pro/cache/tag_index_spec.rb, react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb, react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb
Comprehensive coverage: helper specs validate cached components with cache_tags, revalidation behavior, multi-tag fan-out, no-op for never-written tags, and blank-tag validation; TagIndex specs cover normalization (strings/procs/records/cache_keys), register/revalidate lifecycle, de-duplication, Rails key expansion, namespace awareness, delete_multi fallback, max-keys eviction, TTL computation/defaulting, and dev warnings; AR end-to-end spec validates revalidation on commit/touch/destroy with transaction rollback; configuration spec validates tag-index settings; fetch spec confirms tag registration and deletion behavior.

Sequence Diagram

sequenceDiagram
  participant View as Cached Helper
  participant Cache as Cache module
  participant TagIndex as TagIndex
  participant Store as Rails.cache

  View->>Cache: fetch_react_component(cache_tags: ['post:1'])
  Cache->>Store: fetch(cache_key)
  Store-->>Cache: nil (cache miss)
  Cache->>View: yield block
  View-->>Cache: rendered HTML
  Cache->>Store: write(cache_key, html, expires_in: ...)
  Cache->>TagIndex: register(['post:1'], cache_key, cache_options)
  TagIndex->>TagIndex: normalize_tags(['post:1'])
  TagIndex->>Store: read(rorp:tag:v1:post:1)
  TagIndex->>Store: write(rorp:tag:v1:post:1, [expanded_keys...], ttl)

  Note over View,Store: Later: revalidation triggered

  View->>Cache: revalidate_tag('post:1')
  Cache->>TagIndex: revalidate('post:1')
  TagIndex->>Store: read(rorp:tag:v1:post:1)
  TagIndex->>Store: delete_multi(expanded_keys)
  TagIndex->>Store: delete(rorp:tag:v1:post:1)
  TagIndex-->>Cache: 1 (deleted count)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement, documentation, review-needed, P2

Suggested reviewers

  • AbanoubGhadban
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title 'Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog)' clearly summarizes the main change: adding cache_tags revalidation with a Next.js parity analogy.
Linked Issues check ✅ Passed PR implements all key objectives from #3871: cache_tags parameter on helpers, revalidate_tag APIs, Rails.cache tag index, ActiveRecord Revalidates concern, backward compatibility, comprehensive documentation, and extensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to #3871 requirements: tag infrastructure, helper integration, configuration, documentation, and tests. No unrelated modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/3871-cache-tags-revalidation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements declarative cache_tags: revalidation for all four Pro fragment-caching helpers (cached_react_component, cached_react_component_hash, cached_stream_react_component, cached_async_react_component). A Rails.cache-backed tag→key index enables ReactOnRailsPro.revalidate_tag / revalidate_tags, and an opt-in ActiveRecord concern (Revalidates) wires cache invalidation to after_commit automatically.

  • TagIndex (new): stores a {keys:, expires_at:} payload per tag under rorp:tag:v1:<tag> using a read-modify-write append; revalidation calls delete_multi(keys, namespace: nil) so pre-namespaced keys are deleted exactly once. Relies on three private ActiveSupport::Cache::Store methods to record the exact logical key the store uses — intentional, documented, and covered by a FileStore round-trip spec.
  • Cache::Revalidates (new): include + revalidates_react_cache registers an after_commit callback; with no block the stable version-less model_name.cache_key/id is used so the tag survives cache_versioning = false.
  • Configuration gains cache_tag_index_expires_in (7 d) and cache_tag_index_max_keys (5 000) with a dev-only warning when expires_in is absent.

Confidence Score: 4/5

Safe to merge. The two findings are quality/hardening concerns that do not affect correctness under standard Rails cache stores.

The implementation is solid and the test suite is thorough — unit + integration across all four helper paths, FileStore round-trips, and fallback paths. Two concerns were found: normalized_entry_key uses private Rails cache store internals that will raise on non-standard stores, and revalidates_react_cache can silently double-register after_commit in subclass-override patterns. Neither affects behavior for standard Rails cache stores or single-class usage.

cache/tag_index.rb (private-API reliance) and cache/revalidates.rb (duplicate callback guard) deserve a second look before the concern is widely adopted in subclass hierarchies.

Important Files Changed

Filename Overview
react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb Core tag-to-key index. Solid logic overall, but normalized_entry_key reaches into three private ActiveSupport::Cache::Store methods via send, which will raise NoMethodError on custom stores that don't inherit from the base class.
react_on_rails_pro/lib/react_on_rails_pro/cache/revalidates.rb ActiveRecord concern for automatic tag revalidation. Well-designed with correct after_commit semantics; revalidates_react_cache doesn't guard against being called multiple times on the same class (e.g. from a subclass), which would double-register the after_commit callback.
react_on_rails_pro/lib/react_on_rails_pro/cache.rb Adds register_tags/revalidate_tags class methods; correctly calls register_tags only on cache miss (not cache hits) in fetch_react_component.
react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb Correctly wires register_tags into all four cached_* helpers — all only on the write/miss path.
react_on_rails_pro/lib/react_on_rails_pro/configuration.rb Adds cache_tag_index_expires_in (7d) and cache_tag_index_max_keys (5000) config; properly threaded through defaults, constructor, and attr_accessor list.
react_on_rails_pro/spec/react_on_rails_pro/cache/tag_index_spec.rb Comprehensive unit specs covering normalize_tags, register/revalidate, namespace round-trips, FileStore encoding, delete_multi fallback, max-keys cap, and TTL merging.
react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb End-to-end AR concern specs covering create/update/touch/destroy and rollback, plus custom tag block.
react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb Integration specs for all four helper paths exercising HIT→revalidate→MISS cycles end-to-end.
react_on_rails_pro/lib/react_on_rails_pro.rb Adds top-level revalidate_tag / revalidate_tags entry points that delegate to Cache.revalidate_tags; requires for new files added in correct order.

Sequence Diagram

sequenceDiagram
    participant Helper as cached_* helper
    participant Cache as ReactOnRailsPro::Cache
    participant TagIndex as Cache::TagIndex
    participant RailsCache as Rails.cache
    participant AR as ActiveRecord model (Revalidates)

    Note over Helper,RailsCache: Cache MISS path
    Helper->>RailsCache: fetch(cache_key) → miss
    RailsCache-->>Helper: yield (render component)
    Helper->>Cache: register_tags(cache_tags, cache_key, cache_options)
    Cache->>TagIndex: register(tags, cache_key, opts)
    TagIndex->>TagIndex: normalize_tags → flat String[]
    TagIndex->>TagIndex: normalized_entry_key (via private send)
    loop each tag
        TagIndex->>RailsCache: read index_key(tag)
        TagIndex->>RailsCache: "write index_key(tag), {keys:, expires_at:}"
    end

    Note over Helper,RailsCache: Cache HIT path
    Helper->>RailsCache: fetch(cache_key) → hit
    RailsCache-->>Helper: cached HTML (no tag registration)

    Note over AR,RailsCache: Revalidation via AR concern
    AR->>AR: after_commit fires
    AR->>Cache: "revalidate_tags(*tags)"
    Cache->>TagIndex: "revalidate(*tags)"
    loop each tag
        TagIndex->>RailsCache: read index_key(tag) → keys[]
        TagIndex->>RailsCache: delete_multi(keys, namespace: nil)
        TagIndex->>RailsCache: delete index_key(tag)
    end
Loading

Reviews (1): Last reviewed commit: "Fix changelog PR link to actual PR numbe..." | Re-trigger Greptile

Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/revalidates.rb
justin808 added a commit that referenced this pull request Jun 12, 2026
Repeated calls (same class or subclass override) now replace the tag
resolver without stacking duplicate after_commit callbacks. Addresses
Greptile review finding on PR #3964.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@justin808

Copy link
Copy Markdown
Member Author

+ci-run-full

@justin808

Copy link
Copy Markdown
Member Author

Full-CI request reason (non-blocking decision, recorded per workflow): the full-ci + benchmark labels were applied at PR creation, but the initial pull_request event predated the labels and only 4 checks ran on the prior head. Head is now 9925453 (review-fix: idempotent revalidates_react_cache + regression spec, all three review threads resolved). Requesting the full matrix at the readiness gate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9925453640

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache.rb Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@react_on_rails_pro/lib/react_on_rails_pro/cache.rb`:
- Around line 56-61: The revalidate_tags method currently delegates blindly to
TagIndex.revalidate and allows nil/blank tags to flow into
TagIndex.normalize_tags which raises; modify revalidate_tags to treat blank
inputs as a no-op by filtering the incoming *tags (e.g., reject
nil/empty/whitespace strings) and return 0 if the filtered list is empty,
otherwise call TagIndex.revalidate with the cleaned list; update the method
signature handling in revalidate_tags so callers like
ReactOnRailsPro.revalidate_tag(nil) no longer leak errors.

In `@react_on_rails_pro/lib/react_on_rails_pro/configuration.rb`:
- Line 118: Add validated setters for cache_tag_index_expires_in and
cache_tag_index_max_keys on the Configuration class: implement a
cache_tag_index_expires_in=(value) that raises unless value is a Numeric,
finite, and > 0, and implement cache_tag_index_max_keys=(value) that coerces to
Integer or raises unless it is an Integer > 0; ensure these setters are used
instead of allowing raw assignment (so consumers of Configuration get validation
before TagIndex.enforce_max_keys is called) and keep the existing reader names
intact so other code (e.g., TagIndex.enforce_max_keys) can rely on the validated
values.

In `@react_on_rails_pro/sig/react_on_rails_pro/cache.rbs`:
- Around line 43-45: The RBS for module ReactOnRailsPro::Cache::Revalidates only
declares def self.included; add a class-method signature for
revalidates_react_cache that matches the runtime API by declaring def
self.revalidates_react_cache which accepts an optional block parameter (the
resolver callback) and returns void; update the Revalidates module in the RBS to
expose this method so consumers receive types for the after_commit registration
helper.

In `@react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs`:
- Around line 66-67: The RBS for Configuration is missing the two new optional
keyword parameters in the initialize signature; update def initialize for class
Configuration to include ?cache_tag_index_expires_in: Numeric? and
?cache_tag_index_max_keys: Integer? so the initialize type matches the actual
Ruby implementation (also keep the existing attr_accessor declarations
cache_tag_index_expires_in and cache_tag_index_max_keys unchanged); ensure the
initialize signature lists the two keywords with the same optional types and
nullability as the attr_accessors.

In `@react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb`:
- Line 24: Add a tightly scoped RuboCop inline disable on the RSpec.describe
line to suppress the file-path cops: append "# rubocop:disable
RSpec/FilePath,RSpec/SpecFilePathFormat" to the existing RSpec.describe
"ReactOnRailsPro::Cache::Revalidates", :caching line so the dummy spec path
warning is suppressed for this describe block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69a6067f-7a8c-4cd9-b846-f952ea113b21

📥 Commits

Reviewing files that changed from the base of the PR and between e4e426a and 9925453.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/oss/building-features/caching.md
  • docs/pro/fragment-caching.md
  • llms-full.txt
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache/revalidates.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
  • react_on_rails_pro/lib/react_on_rails_pro/configuration.rb
  • react_on_rails_pro/sig/react_on_rails_pro.rbs
  • react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
  • react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
  • react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb
  • react_on_rails_pro/spec/react_on_rails_pro/cache/tag_index_spec.rb
  • react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb

Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache.rb Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/configuration.rb Outdated
Comment thread react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
Comment thread react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs Outdated
Comment thread react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb
justin808 added a commit that referenced this pull request Jun 12, 2026
Repeated calls (same class or subclass override) now replace the tag
resolver without stacking duplicate after_commit callbacks. Addresses
Greptile review finding on PR #3964.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@justin808 justin808 force-pushed the jg/3871-cache-tags-revalidation branch from 9925453 to 7d72ec8 Compare June 12, 2026 10:46
@justin808

Copy link
Copy Markdown
Member Author

+ci-run-full

@justin808

Copy link
Copy Markdown
Member Author

Full-CI re-request reason: mechanical rebase onto post-rc.3 main (llms-full.txt regeneration conflict only — zero source changes vs head 9925453, which passed the full 8-workflow suite). Re-running for current-head evidence before merge.

@claude

claude Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Code Review — PR #3964: Tag-based cache revalidation

Overview

This is a well-designed, well-documented feature. The design contract (best-effort, lossy-OK index; correctness bounded by expires_in) is clearly stated and consistently enforced throughout. Test coverage is thorough, the RBS signatures are present, and all four helper paths are exercised. The implementation correctly handles:

  • AR cache versioning edge cases (stable identity vs cache_key_with_version)
  • FileStore encoding (via private-API canary spec)
  • Namespace-aware deletes
  • delete_multi fallback for pre-6.1 stores
  • Idempotent after_commit registration

Below are the issues I found, ordered by severity.


Medium — Silent correctness failure when private Rails API is absent

tag_index.rb:229-230normalized_entry_key calls three private ActiveSupport::Cache::Store methods via send:

The rationale is solid (public expand_cache_key adds RAILS_CACHE_ID and prefers cache_key_with_version, both of which would record a key the store never used). However, if a custom cache store or a future Rails version does not have these methods, send raises NoMethodError and tag registration silently fails — no revalidation capability, no startup warning.

Suggestion: Add a respond_to?(m, true) guard at registration time and log a clear warning when the private API is absent, so future breakage is loud rather than a quiet correctness regression.


Minor — Proc-to-nil produces a confusing error message

tag_index.rb:74-81 — When a Proc tag resolves to nil, the error reads:

cache_tags value #<Proc:...> normalized to a blank tag

tag in scope is the Proc object, not the resolved nil. The message implies the Proc itself is blank, not what it returned. Suggest showing both original and resolved in the error message.


Minor — Revalidates RBS is incomplete

sig/react_on_rails_pro/cache.rbs:43-45 — Only the included hook is typed; the public revalidates_react_cache class method and the private revalidate_react_on_rails_cache_tags instance method are absent from the RBS definition.


Suggestion — Add a test for the Array legacy payload path in read_index

tag_index.rb:138-141 — The when Array branch (tolerating a bare-key-list legacy payload) is untested. A future refactor could silently drop it.


Positive callouts

  • The comment block at the top of TagIndex is exemplary: invariant, failure mode, and resolution all clearly stated.
  • enforce_max_keys warn message gives operators exactly what they need to decide whether to raise the limit or use coarser tags.
  • Idempotent after_commit registration using class_attribute + _react_on_rails_revalidates_registered is the correct approach; the STI interaction is handled correctly.
  • delete_multi(keys, namespace: nil) correctly suppresses double-prefixing.
  • Development-only warn_if_expires_in_missing is a sensible noise tradeoff.

Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
Comment thread react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb (1)

24-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The RuboCop suppression flagged in the previous review is still missing.

Line 24 uses a string description that does not mirror the dummy spec path, so RSpec/FilePath / RSpec/SpecFilePathFormat will flag this file. Based on learnings, specs under react_on_rails_pro/spec/dummy/spec/ should use # rubocop:disable RSpec/FilePath,RSpec/SpecFilePathFormat inline on the describe line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb` at line
24, Add the RuboCop suppression inline on the `describe` line: modify the RSpec
declaration `RSpec.describe "ReactOnRailsPro::Cache::Revalidates", :caching do`
to include `# rubocop:disable RSpec/FilePath,RSpec/SpecFilePathFormat` so the
file won't be flagged by RSpec/FilePath or RSpec/SpecFilePathFormat.

Sources: Coding guidelines, Learnings

react_on_rails_pro/lib/react_on_rails_pro/cache.rb (1)

56-62: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The blank-tag filtering issue flagged in the previous review is still present.

register_tags (line 51) short-circuits blank input, but revalidate_tags delegates directly to TagIndex.revalidate, which calls normalize_tags and raises on nil/blank values (confirmed by tag_index_spec.rb lines 90-95). This asymmetry can surface errors from ReactOnRailsPro.revalidate_tag(nil) or resolver-driven paths that emit blank tags, breaking the no-op contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/lib/react_on_rails_pro/cache.rb` around lines 56 - 62,
revalidate_tags currently delegates straight to TagIndex.revalidate which raises
on nil/blank tags via normalize_tags, causing asymmetry with register_tags
(which short-circuits blank input); update revalidate_tags to pre-filter/ignore
nil or blank tag values (same normalization behavior register_tags uses) before
calling TagIndex.revalidate so ReactOnRailsPro.revalidate_tag(nil) and
resolver-driven blank tags become a no-op rather than raising; reference the
revalidate_tags method and TagIndex.revalidate/normalize_tags to locate and
adjust the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@react_on_rails_pro/lib/react_on_rails_pro/cache.rb`:
- Around line 56-62: revalidate_tags currently delegates straight to
TagIndex.revalidate which raises on nil/blank tags via normalize_tags, causing
asymmetry with register_tags (which short-circuits blank input); update
revalidate_tags to pre-filter/ignore nil or blank tag values (same normalization
behavior register_tags uses) before calling TagIndex.revalidate so
ReactOnRailsPro.revalidate_tag(nil) and resolver-driven blank tags become a
no-op rather than raising; reference the revalidate_tags method and
TagIndex.revalidate/normalize_tags to locate and adjust the behavior.

In `@react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb`:
- Line 24: Add the RuboCop suppression inline on the `describe` line: modify the
RSpec declaration `RSpec.describe "ReactOnRailsPro::Cache::Revalidates",
:caching do` to include `# rubocop:disable
RSpec/FilePath,RSpec/SpecFilePathFormat` so the file won't be flagged by
RSpec/FilePath or RSpec/SpecFilePathFormat.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27ffe71f-d3ab-479e-9d90-d6388cf076ef

📥 Commits

Reviewing files that changed from the base of the PR and between 9925453 and 7d72ec8.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/oss/building-features/caching.md
  • docs/pro/fragment-caching.md
  • llms-full.txt
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache/revalidates.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
  • react_on_rails_pro/lib/react_on_rails_pro/configuration.rb
  • react_on_rails_pro/sig/react_on_rails_pro.rbs
  • react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
  • react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
  • react_on_rails_pro/spec/dummy/spec/models/cache_revalidates_spec.rb
  • react_on_rails_pro/spec/react_on_rails_pro/cache/tag_index_spec.rb
  • react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb
💤 Files with no reviewable changes (1)
  • llms-full.txt
✅ Files skipped from review due to trivial changes (3)
  • docs/pro/fragment-caching.md
  • CHANGELOG.md
  • docs/oss/building-features/caching.md
🚧 Files skipped from review as they are similar to previous changes (9)
  • react_on_rails_pro/sig/react_on_rails_pro.rbs
  • react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs
  • react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
  • react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb
  • react_on_rails_pro/lib/react_on_rails_pro.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache/revalidates.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
  • react_on_rails_pro/lib/react_on_rails_pro/configuration.rb
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb

justin808 added a commit that referenced this pull request Jun 13, 2026
- Require cache/tag_index from cache.rb so direct requires of the cache
  file keep working (Codex)
- Blank tags in revalidate_tags are a no-op returning 0, mirroring
  register_tags (CodeRabbit)
- Validate cache_tag_index_expires_in / cache_tag_index_max_keys with
  dedicated setters per the neighboring renderer-setting pattern
  (CodeRabbit)
- Expose revalidates_react_cache and the private callback in the
  Revalidates RBS surface (CodeRabbit + Claude review)
- Guard the private Store key-normalization API with a respond_to? check,
  one-time warning, and raw-key fallback for custom stores (Claude review)
- Blank-tag error message now shows both the original entry and the
  resolved value so Proc-derived blanks are debuggable (Claude review)
- Specs: legacy bare-array index payload, blank revalidate no-op,
  private-API fallback warning, config setter validation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@justin808

Copy link
Copy Markdown
Member Author

+ci-run-full

@justin808

Copy link
Copy Markdown
Member Author

Full-CI re-request reason: review-fix commit 8640a9f addressing all 9 current-head review threads (blank-tag no-op, config setter validation, tag_index require, RBS surface, private-API guard + warning, error message, +4 specs). Local validation: 127 cache/config examples 0 failures, dummy concern 6/6, rubocop 13 files clean, RBS valid. Merging on green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8640a9f716

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread react_on_rails_pro/lib/react_on_rails_pro/configuration.rb Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb`:
- Around line 1003-1008: Add a spec to ensure the setter for
cache_tag_index_expires_in rejects non-finite numbers: inside the existing
example that expects ReactOnRailsPro::Error from ReactOnRailsPro.configure {
|config| config.cache_tag_index_expires_in = ... }, add another expectation that
assigning Float::INFINITY to cache_tag_index_expires_in raises
ReactOnRailsPro::Error (using the same /cache_tag_index_expires_in/ matcher) so
the finite-number validation branch is covered.
- Around line 1010-1015: Update the spec that validates the
cache_tag_index_max_keys setter to include the zero boundary: in the example
using ReactOnRailsPro.configure { |config| config.cache_tag_index_max_keys = ...
} add an assertion that assigning 0 also raises ReactOnRailsPro::Error (matching
/cache_tag_index_max_keys/), so the test covers negative, zero, and non-integer
(e.g., 5.5) cases for cache_tag_index_max_keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 46bbd233-50a8-4e0d-ada8-02fac6d3556c

📥 Commits

Reviewing files that changed from the base of the PR and between 7d72ec8 and 8640a9f.

📒 Files selected for processing (6)
  • react_on_rails_pro/lib/react_on_rails_pro/cache.rb
  • react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
  • react_on_rails_pro/lib/react_on_rails_pro/configuration.rb
  • react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
  • react_on_rails_pro/spec/react_on_rails_pro/cache/tag_index_spec.rb
  • react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • react_on_rails_pro/lib/react_on_rails_pro/cache.rb
  • react_on_rails_pro/sig/react_on_rails_pro/cache.rbs
  • react_on_rails_pro/spec/react_on_rails_pro/cache/tag_index_spec.rb

Comment thread react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb
Comment thread react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
Comment thread react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/revalidates.rb
Comment thread react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overall Assessment

Solid, well-structured implementation with thorough documentation, a clear best-effort contract, and excellent test coverage (canary spec for private Rails API, FileStore round-trip, namespace isolation, delete ordering verification, destroyed-record normalization). The OSS package is untouched. No blocking issues; the notes below are a mix of medium-risk maintenance concerns and minor observations.


Medium risk: private Rails cache store API

TagIndex#normalized_entry_key calls three private ActiveSupport::Cache::Store methods (expanded_key, namespace_key, merged_options) to reproduce the exact key the store wrote. This is the only correct approach given that the public ActiveSupport::Cache.expand_cache_key would prepend RAILS_CACHE_ID and prefer cache_key_with_version, both of which would produce a key the store never used.

The risk is well-mitigated (canary spec, one-time warning + fallback on mismatch), but the canary covers only MemoryStore and FileStore. Consider adding a canary assertion for RedisCacheStore if a Redis test helper is already available in CI, since Redis is the most common production store and has different delete_multi_entries internals. If not feasible now, tracking this in a follow-up issue would help surface any private-API drift before it silently degrades tag revalidation.


STI subclass identity caveat (undocumented)

stable_record_identity derives the tag as resolved.model_name.cache_key/id. With Rails STI, a base-class record and a subclass record sharing the same id produce different tags ("employees/42" vs "managers/42"). A cache entry tagged with @employee (an Employee instance) is not revalidated by ReactOnRailsPro.revalidate_tag(@manager) where @manager = Manager.find(42).

This is expected AR behavior (each class has its own model_name) and probably fine for most apps, but it is not mentioned in the docs or the concern's module comment. A one-liner caveat in the "Tag forms and normalization" section of the caching guide would prevent confusing support questions.


Minor observations

EXPIRED_CACHE_WRITE_TTL = 1 unit is implicit — the name does not say seconds. A trailing # seconds comment (or renaming to EXPIRED_CACHE_WRITE_TTL_SECONDS) would clarify.

blank_revalidation_tag? Symbol branchtag.to_s.blank? for a Symbol is correct but redundant when ActiveSupport's blank? already handles Symbol. Not a bug, just a small asymmetry with the other branches.

MAX_EXPIRY_WARN_KEYS = 1_000 cap — once 1,000 unique per-process entry keys have triggered the missing-expires_in development warning, new keys are silently skipped. Reasonable bound to avoid unbounded per-process memory growth, but not documented anywhere users would look. A sentence in the configuration reference would help operators who wonder why warnings stopped.

revalidates_react_cache subclass guardreturn if _react_on_rails_revalidates_registered prevents double-registering the after_commit callback because the parent's callback is inherited. Correct and tested, but the guard reads ambiguously without the existing comment. The existing comment is fine; this is just a note that the guard is load-bearing.


Tests

Coverage is thorough: cache miss + HIT + revalidate round-trip, stream HIT→revalidate→MISS, async HIT→revalidate→MISS, destroyed records, cache_versioning = false, namespace isolation, FileStore encoding, delete_multi fallback, index TTL merge, max-keys cap, and the private-API canary. The pre-existing prod_binstub_spec failures being environment-related (confirmed on clean origin/main) is well-documented.

Overall this is well-engineered for the best-effort contract it defines. The main ongoing cost is the private Rails API canary — worth keeping green on every Rails minor bump.

Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache/tag_index.rb
Comment thread react_on_rails_pro/lib/react_on_rails_pro/cache.rb Outdated
@justin808

Copy link
Copy Markdown
Member Author

Address-review pass for feedback after the previous summary cutoff (2026-06-14T23:04:53Z) is complete on head 7513fe4689c517f33cd7be13a18e1aa5208a51a5.

Triage and outcome:

  • MUST-FIX: cache-tag changelog entry moved to Unreleased.
  • MUST-FIX: custom-store delete_multi return values normalized so nil, non-integer, and old-Dalli-style undeleted-key arrays cannot make revalidation raise.
  • Selected in-scope: unpersisted ActiveRecord-style tags now raise an actionable save-the-record/use-explicit-tag error.
  • Verified/disproved: STI subclass-before-parent double-callback claim did not reproduce; added a focused canary proving the subclass still has one callback after parent registration.
  • Selected low-risk optional: renamed the helper capture to cache_options_at_miss; added explicit seconds unit comment for EXPIRED_CACHE_WRITE_TTL.
  • Deferred optional: RedisCacheStore canary and STI subclass identity docs caveat were replied with [auto-deferred] rationale.

Validation evidence is in the PR body’s refreshed Merge Readiness Criteria section. Fresh GraphQL review-thread sweep reports 0 unresolved threads. Current-head CI is still pending/queued, so this remains not auto-merge-ready; accelerated-RC independent finalizer is still required.

@justin808

Copy link
Copy Markdown
Member Author

Approved by maintainer.

@github-actions

Copy link
Copy Markdown
Contributor

Pro Node Renderer Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
Pro Node Renderer: simple_eval (non-RSC) 1112.97 ▼44.0% (1986.18) 6.68 ▲53.6% (4.35) 17.04 ▲110.5% (8.1) 200=33392
Pro Node Renderer: react_ssr (non-RSC) 981.39 ▼44.5% (1769.54) 8.42 🔴 67.2% (5.04) 17.63 ▲117.5% (8.11) 200=29447

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

Copy link
Copy Markdown
Contributor

Core Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Core 3.39 ▼6.4% (3.62) 862.37 🟢 58.6% (2082.8) 2788.64 ▲4.7% (2663.65) 200=116
/client_side_hello_world: Core 528.4 ▼22.3% (680.08) 11.08 ▲22.8% (9.02) 14.64 ▼23.2% (19.05) 200=15965
/client_side_rescript_hello_world: Core 817.32 ▲24.3% (657.57) 7.43 ▼20.9% (9.4) 12.54 ▼35.2% (19.35) 200=24696
/client_side_hello_world_shared_store: Core 511.15 ▼20.6% (643.49) 11.3 ▲16.0% (9.74) 13.43 ▼32.6% (19.92) 200=15546
/client_side_hello_world_shared_store_controller: Core 503.95 ▼19.6% (627.08) 11.45 ▲13.1% (10.12) 13.67 ▼34.8% (20.96) 200=15327
/client_side_hello_world_shared_store_defer: Core 755.28 ▲18.4% (638.03) 8.77 ▼10.8% (9.83) 17.5 ▼11.5% (19.77) 200=22818
/server_side_hello_world_shared_store: Core 15.27 ▼1.7% (15.54) 542.78 ▲8.7% (499.38) 694.99 ▲5.6% (658.44) 200=470
/server_side_hello_world_shared_store_controller: Core 15.5 ▲0.6% (15.41) 543.26 ▲12.0% (485.11) 701.3 ▲7.6% (651.49) 200=478
/server_side_hello_world_shared_store_defer: Core 15.49 ▼0.7% (15.6) 533.9 ▲9.9% (485.83) 734.88 ▲11.7% (658.15) 200=474
/server_side_hello_world: Core 24.84 ▼20.6% (31.3) 244.2 ▼1.7% (248.39) 385.99 ▲28.0% (301.63) 200=756
/server_side_hello_world_hooks: Core 31.73 ▲2.2% (31.05) 268.59 ▲9.5% (245.32) 308.29 ▲0.4% (307.08) 200=963
/server_side_hello_world_props: Core 31.28 ▲0.9% (31.01) 188.62 ▼24.2% (248.72) 298.52 ▼3.9% (310.77) 200=956
/client_side_log_throw: Core 538.3 ▼19.6% (669.57) 10.9 ▲13.6% (9.59) 16.22 ▼13.8% (18.83) 200=16261
/server_side_log_throw: Core 30.43 ▼0.9% (30.71) 279.47 ▲9.2% (255.99) 319.31 ▲3.0% (309.87) 200=925
/server_side_log_throw_plain_js: Core 30.91 ▼0.3% (30.99) 273.16 ▲12.4% (243.06) 317.29 ▲2.8% (308.72) 200=939
/server_side_log_throw_raise: Core 30.92 ▲0.8% (30.69) 269.0 ▲9.8% (244.9) 314.63 ▲2.6% (306.59) 3xx=941
/server_side_log_throw_raise_invoker: Core 858.7 ▲10.1% (779.87) 6.16 ▼27.8% (8.54) 12.93 ▼20.7% (16.3) 200=26115
/server_side_hello_world_es5: Core 31.42 ▲3.2% (30.44) 268.73 ▲10.5% (243.29) 304.69 ▲0.2% (304.05) 200=957
/server_side_redux_app: Core 25.51 ▼15.1% (30.04) 233.02 ▼6.6% (249.37) 316.93 ▲1.8% (311.39) 200=775
/server_side_hello_world_with_options: Core 25.28 ▼18.8% (31.14) 230.26 ▼6.8% (247.14) 294.6 ▼3.6% (305.54) 200=774
/server_side_redux_app_cached: Core 751.54 ▲13.6% (661.36) 9.14 ▼9.0% (10.05) 16.89 ▼7.4% (18.25) 200=22706
/client_side_manual_render: Core 753.69 ▲13.4% (664.75) 8.12 ▼15.7% (9.63) 19.77 ▲2.6% (19.26) 200=22769
/render_js: Core 33.37 ▼1.0% (33.71) 250.3 ▲6.8% (234.45) 287.26 ▲1.1% (284.19) 200=1017
/react_router: Core 21.0 ▼28.2% (29.26) 291.92 ▲10.5% (264.2) 403.72 ▲23.1% (328.01) 200=639
/pure_component: Core 31.68 ▼1.3% (32.1) 285.54 ▲13.7% (251.18) 311.61 ▲3.5% (300.96) 200=960
/react_compiler_example: Core 31.22 ▼2.6% (32.06) 273.91 ▲13.4% (241.63) 311.63 ▲3.1% (302.31) 200=950
/css_modules_images_fonts_example: Core 31.46 ▲1.4% (31.03) 266.68 ▲5.8% (251.96) 307.98 ▲0.3% (307.17) 200=957
/turbolinks_cache_disabled: Core 765.05 ▲12.8% (678.18) 8.38 ▼10.2% (9.34) 17.13 ▼9.5% (18.92) 200=23115
/rendered_html: Core 31.67 ▲0.7% (31.46) 264.25 ▲6.4% (248.3) 315.73 ▲4.2% (302.95) 200=964
/xhr_refresh: Core 12.72 ▼19.9% (15.88) 476.06 ▲2.3% (465.39) 549.61 ▼11.8% (623.43) 200=390
/react_helmet: Core 30.35 ▼1.7% (30.87) 274.6 ▲11.7% (245.92) 315.01 ▲2.3% (307.92) 200=925
/broken_app: Core 25.06 ▼18.5% (30.76) 236.72 ▼6.5% (253.31) 311.22 ▼0.1% (311.5) 200=763
/image_example: Core 22.39 ▼27.9% (31.05) 276.37 ▲9.3% (252.81) 342.02 ▲10.8% (308.79) 200=683
/font_optimization_example: Core 582.28 ▼23.6% (762.4) 9.98 ▲17.4% (8.5) 15.58 ▼7.7% (16.88) 200=17591
/client_side_activity: Core 755.07 ▲18.1% (639.19) 8.15 ▼15.7% (9.67) 19.38 ▲4.4% (18.57) 200=22811
/server_side_activity: Core 26.2 ▼13.9% (30.42) 226.87 ▼18.1% (276.92) 372.29 ▲15.3% (322.99) 200=797
/turbo_frame_tag_hello_world: Core 887.79 ▲22.6% (724.23) 6.89 ▼17.4% (8.34) 12.18 ▼31.2% (17.71) 200=26825
/manual_render_test: Core 809.56 ▲23.6% (655.16) 8.41 ▼9.6% (9.3) 16.12 ▼11.6% (18.23) 200=24455
/root_error_callbacks: Core 31.62 267.23 312.6 200=962

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

Copy link
Copy Markdown
Contributor

Pro (shard 1/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Pro 173.11 ▼6.1% (184.45) 46.13 ▲11.2% (41.5) 63.1 ▲3.3% (61.08) 200=5235
/error_scenarios_hub: Pro 357.12 ▼4.0% (372.03) 21.73 ▲15.6% (18.8) 34.25 ▲14.4% (29.95) 200=10791
/ssr_async_error: Pro 306.1 ▼11.3% (345.21) 15.3 ▼27.6% (21.13) 22.38 ▼36.7% (35.38) 200=9312
/ssr_async_prop_error: Pro 308.49 ▼8.1% (335.75) 24.94 ▲14.9% (21.71) 38.14 ▲9.4% (34.87) 200=9319
/non_existing_react_component: Pro 279.93 ▼22.6% (361.68) 20.49 ▲0.6% (20.38) 25.31 ▼23.2% (32.94) 200=8461
/non_existing_rsc_payload: Pro 372.2 ▼2.1% (380.35) 20.26 ▲3.2% (19.63) 31.74 ▼2.8% (32.65) 200=11250
/cached_react_helmet: Pro 383.83 ▼0.2% (384.63) 16.22 ▼12.1% (18.46) 27.59 ▼13.5% (31.88) 200=11601
/cached_redux_component: Pro 398.43 ▲0.9% (394.98) 19.63 ▲5.9% (18.53) 28.63 ▼6.3% (30.55) 200=12036
/lazy_apollo_graphql: Pro 134.84 ▼7.0% (144.92) 52.5 ▲5.4% (49.82) 87.1 ▲10.3% (78.98) 200=4077
/redis_receiver: Pro 93.12 ▲10.1% (84.6) 65.79 ▲0.7% (65.36) 130.36 ▼12.8% (149.58) 200=2818
/stream_shell_error_demo: Pro 353.75 ▲1.8% (347.33) 16.01 ▼20.6% (20.17) 29.37 ▼16.1% (35.03) 200=10761
/test_incremental_rendering: Pro 353.78 ▲2.2% (346.1) 22.25 ▲11.5% (19.95) 35.54 ▲11.0% (32.03) 200=10688
/rsc_posts_page_over_redis: Pro 84.61 ▼12.1% (96.26) 69.62 ▼4.6% (73.01) 163.97 ▲40.5% (116.73) 200=2559
/rsc_fouc_probe: Pro 337.94 ▼1.0% (341.46) 25.68 ▲12.4% (22.85) 35.55 ▼3.6% (36.89) 200=10220
/async_on_server_sync_on_client: Pro 305.84 ▼6.5% (326.96) 25.22 ▲15.9% (21.76) 37.95 ▼1.3% (38.44) 200=9242
/server_router: Pro 306.89 ▼10.6% (343.37) 24.71 ▲18.3% (20.88) 38.65 ▲8.9% (35.49) 200=9294
/unwrapped_rsc_route_client_render: Pro 382.15 ▼0.3% (383.28) 19.72 ▲9.8% (17.95) 30.3 ▲2.2% (29.65) 200=11550
/async_render_function_returns_string: Pro 350.66 ▲0.3% (349.67) 22.42 ▲11.0% (20.19) 34.91 ▲2.8% (33.97) 200=10595
/async_components_demo: Pro 212.07 ▼0.6% (213.4) 37.93 ▲8.6% (34.92) 55.3 ▲9.0% (50.72) 200=6412
/stream_native_metadata: Pro 359.92 ▲3.9% (346.31) 21.94 ▲9.0% (20.12) 34.82 ▼2.0% (35.53) 200=10873
/rsc_native_metadata: Pro 355.37 ▲5.3% (337.52) 22.07 ▲4.5% (21.13) 32.88 ▼9.0% (36.12) 200=10672
/react_intl_rsc_demo: Pro 345.5 ▲1.9% (339.01) 22.57 ▲6.6% (21.18) 36.75 ▲1.1% (36.34) 200=10440
/client_side_hello_world_shared_store: Pro 359.17 ▲1.7% (353.29) 21.31 ▲4.1% (20.47) 31.99 ▼1.3% (32.4) 200=10852
/client_side_hello_world_shared_store_defer: Pro 359.5 ▲4.8% (343.06) 21.61 ▲6.2% (20.34) 33.56 ▲4.8% (32.02) 200=10863
/server_side_hello_world_shared_store_controller: Pro 310.74 ▲5.9% (293.51) 26.19 ▲4.9% (24.96) 38.89 ▼2.1% (39.74) 200=9391
/server_side_hello_world: Pro 282.1 ▼15.7% (334.51) 20.47 ▼3.2% (21.14) 38.8 ▲9.1% (35.58) 200=8525
/client_side_log_throw: Pro 381.27 ▲1.8% (374.49) 19.64 ▲7.1% (18.34) 30.36 ▼2.9% (31.25) 200=11521
/server_side_log_throw_plain_js: Pro 404.09 ▲4.6% (386.14) 19.48 ▲3.0% (18.92) 29.1 ▼6.8% (31.24) 200=12211
/server_side_log_throw_raise_invoker: Pro 427.92 ▲0.9% (424.12) 12.97 ▼21.0% (16.42) 24.46 ▼7.1% (26.32) 200=12932
/server_side_redux_app: Pro 346.27 ▲1.5% (341.24) 22.72 ▲4.7% (21.7) 32.56 ▼7.5% (35.22) 200=10465
/server_side_redux_app_cached: Pro 382.52 ▲1.9% (375.45) 20.57 ▲10.2% (18.66) 32.31 ▼1.1% (32.68) 200=11558
/render_js: Pro 390.85 ▲0.6% (388.7) 22.24 ▲20.9% (18.39) 29.93 ▼1.8% (30.48) 200=11809
/pure_component: Pro 317.88 ▼9.6% (351.52) 7.9 ▼62.4% (20.99) 21.0 ▼40.4% (35.24) 200=9674
/turbolinks_cache_disabled: Pro 370.26 ▼4.0% (385.69) 23.3 ▲23.4% (18.88) 31.94 ▲7.0% (29.84) 200=11185
/xhr_refresh: Pro 221.54 ▼23.2% (288.48) 25.55 ▲4.6% (24.42) 30.38 ▼21.1% (38.5) 200=6740
/broken_app: Pro 316.29 ▼8.8% (346.88) 24.97 ▲17.1% (21.32) 39.52 ▲18.1% (33.46) 200=9559
/server_render_with_timeout: Pro 307.13 ▼11.0% (345.14) 25.91 ▲23.6% (20.96) 39.81 ▲16.3% (34.22) 200=9281

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

Copy link
Copy Markdown
Contributor

Pro (shard 2/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/empty: Pro 1200.74 ▼3.0% (1237.64) 6.47 ▲12.1% (5.77) 9.82 ▲0.8% (9.74) 200=36269
/ssr_shell_error: Pro 303.85 ▼11.5% (343.18) 25.71 ▲25.8% (20.43) 37.99 ▲10.6% (34.35) 200=9182
/ssr_sync_error: Pro 251.82 ▼25.9% (339.66) 23.11 ▲8.0% (21.39) 49.81 ▲39.4% (35.72) 200=7611
/rsc_component_error: Pro 297.75 ▼12.2% (339.13) 25.43 ▲23.0% (20.68) 39.19 ▲8.0% (36.3) 200=9000
/non_existing_stream_react_component: Pro 323.17 ▼10.3% (360.28) 23.95 ▲17.8% (20.32) 38.88 ▲16.4% (33.4) 200=9767
/server_side_redux_app_cached: Pro 337.88 ▼10.0% (375.45) 23.61 ▲26.5% (18.66) 36.11 ▲10.5% (32.68) 200=10210
/loadable: Pro 293.66 ▼7.1% (316.14) 27.53 ▲22.5% (22.47) 40.04 ▲13.9% (35.15) 200=8819
/apollo_graphql: Pro 96.74 ▼30.6% (139.38) 65.95 ▲28.7% (51.26) 100.5 ▲20.4% (83.47) 200=3232,3xx=21
/console_logs_in_async_server: Pro 3.98 ▲27.0% (3.13) 2123.58 ▲0.1% (2122.05) 2151.31 ▼1.4% (2182.86) 200=136
/stream_error_demo: Pro 304.8 ▼10.6% (341.07) 28.81 ▲42.0% (20.29) 39.03 ▲17.2% (33.31) 200=9210
/stream_async_components: Pro 291.6 ▼14.0% (339.09) 26.3 ▲21.4% (21.66) 43.18 ▲20.3% (35.91) 200=8812
/rsc_posts_page_over_http: Pro 297.33 ▼11.0% (334.19) 26.22 ▲25.5% (20.9) 38.96 ▲10.8% (35.16) 200=8985
/rsc_echo_props: Pro 216.32 ▼6.0% (230.11) 25.73 ▼18.6% (31.6) 49.19 ▼0.5% (49.42) 200=6582
/client_side_fouc_probe: Pro 343.16 ▼9.1% (377.47) 22.66 ▲35.4% (16.74) 33.09 ▲14.9% (28.79) 200=10371
/async_on_server_sync_on_client_client_render: Pro 259.23 ▼26.6% (353.09) 22.43 ▲11.1% (20.18) 52.5 ▲52.8% (34.35) 200=7833
/server_router_client_render: Pro 265.85 ▼26.4% (361.14) 21.8 ▲9.8% (19.85) 46.6 ▲51.8% (30.71) 200=8048
/unwrapped_rsc_route_stream_render: Pro 320.01 ▼9.1% (352.2) 27.13 ▲38.2% (19.62) 39.18 ▲12.6% (34.79) 200=9668
/async_render_function_returns_component: Pro 310.18 ▼12.5% (354.35) 25.62 ▲21.4% (21.11) 38.51 ▲15.8% (33.27) 200=9372
/native_metadata: Pro 298.37 ▼13.1% (343.23) 26.25 ▲27.9% (20.52) 41.61 ▲9.0% (38.18) 200=9017
/hybrid_metadata_streaming: Pro 309.29 ▼9.3% (341.02) 24.83 ▲18.2% (21.0) 37.58 ▲4.8% (35.87) 200=9347
/cache_demo: Pro 299.42 ▼11.5% (338.4) 21.18 0.0% (21.18) 35.28 ▼2.3% (36.09) 200=9052
/client_side_hello_world: Pro 324.19 ▼13.9% (376.49) 23.43 ▲20.0% (19.53) 37.43 ▲19.2% (31.39) 200=9797
/client_side_hello_world_shared_store_controller: Pro 300.78 ▼12.3% (343.14) 24.97 ▲21.9% (20.49) 41.44 ▲29.7% (31.95) 200=9090
/server_side_hello_world_shared_store: Pro 261.29 ▼8.2% (284.5) 33.47 ▲34.0% (24.99) 42.76 ▲9.9% (38.9) 200=7895
/server_side_hello_world_shared_store_defer: Pro 263.68 ▼9.1% (290.07) 30.54 ▲19.6% (25.54) 43.86 ▲12.1% (39.13) 200=7966
/server_side_hello_world_hooks: Pro 320.36 ▼9.6% (354.45) 27.21 ▲31.8% (20.64) 38.79 ▲16.2% (33.39) 200=9679
/server_side_log_throw: Pro 249.77 ▼28.2% (348.1) 23.16 ▲11.4% (20.79) 44.07 ▲32.2% (33.34) 200=7550
/server_side_log_throw_raise: Pro 621.6 ▼7.7% (673.39) 12.59 ▲19.3% (10.55) 18.6 ▲6.1% (17.53) 3xx=18779
/server_side_hello_world_es5: Pro 310.91 ▼8.8% (340.95) 24.94 ▲14.3% (21.81) 37.21 ▲9.6% (33.96) 200=9400
/server_side_hello_world_with_options: Pro 247.95 ▼26.4% (336.68) 23.51 ▲8.4% (21.7) 42.07 ▲20.3% (34.98) 200=7494
/client_side_manual_render: Pro 322.43 ▼13.3% (371.98) 23.29 ▲20.6% (19.31) 38.93 ▲24.0% (31.4) 200=9743
/react_router: Pro 379.59 ▼5.0% (399.53) 18.49 ▲6.3% (17.39) 34.76 ▲17.9% (29.48) 200=11470
/css_modules_images_fonts_example: Pro 319.11 ▼5.6% (338.01) 24.47 ▲15.0% (21.27) 35.39 ▼6.5% (37.85) 200=9643
/rendered_html: Pro 330.89 ▼4.7% (347.13) 23.72 ▲11.6% (21.25) 34.16 ▼1.9% (34.81) 200=9999
/react_helmet: Pro 312.75 ▼7.2% (337.11) 24.94 ▲14.0% (21.88) 36.67 ▲5.4% (34.8) 200=9451
/image_example: Pro 327.91 ▼4.6% (343.75) 24.18 ▲12.2% (21.55) 37.01 ▲5.6% (35.06) 200=9908
/posts_page: Pro 224.49 ▼5.3% (237.1) 34.78 ▲16.3% (29.92) 48.05 ▲3.9% (46.26) 200=6788

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@justin808 justin808 merged commit 848e23b into main Jun 15, 2026
37 checks passed
@justin808 justin808 deleted the jg/3871-cache-tags-revalidation branch June 15, 2026 04:46
justin808 added a commit that referenced this pull request Jun 15, 2026
* origin/main:
  Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) (#3964)
  Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy (#3934)
  Docs: document Control Plane cost posture for demos (#3998)

# Conflicts:
#	docs/.llms-exclusions
justin808 added a commit that referenced this pull request Jun 15, 2026
…ter-slice

* origin/main:
  Add useRailsForm hook + render_model_errors concern: Inertia useForm-style Rails bridge (#3872) (#3942)
  Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) (#3964)
  Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy (#3934)
  Docs: document Control Plane cost posture for demos (#3998)
  Add built-in /health and /ready endpoints to the Pro node renderer (#3939)
  Document capacity-aware triage contracts (#4027)
justin808 added a commit that referenced this pull request Jun 15, 2026
…ce-maps

* origin/main:
  Add TanStack Router instant-navigation starter (Pro dummy) + docs guide (#3873 v1 slice) (#3953)
  Add useRailsForm hook + render_model_errors concern: Inertia useForm-style Rails bridge (#3872) (#3942)
  Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) (#3964)
  Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy (#3934)
  Docs: document Control Plane cost posture for demos (#3998)
  Add built-in /health and /ready endpoints to the Pro node renderer (#3939)
  Document capacity-aware triage contracts (#4027)
justin808 added a commit that referenced this pull request Jun 15, 2026
## Summary

Stamps the **17.0.0.rc.4** changelog header and reconciles the post-rc.3
entries.

### What changed
- **Stamped `### [17.0.0.rc.4] - 2026-06-14`** with the standard
compare-link updates (`v17.0.0.rc.3...v17.0.0.rc.4`, `unreleased →
rc.4...main`). Prior RC sections are left intact per the prerelease
convention.
- **Moved the source-mapped stack traces entry (PR 3940) into rc.4.** It
had been placed inside the already-tagged `rc.3` section, but `#3940`
merged *after* `v17.0.0.rc.3` was tagged (verified: `d9ac060a0` is not
an ancestor of the rc.3 tag).
- **Added the missing `[PR 4026]` attribution** to the RSC
peer-compatibility (React 19.2 floor) entry, which previously linked
only the issue.
- **Merged the two duplicate `#### Added` headings** in the section into
one.

### rc.4 contents
- **Added**: cache_tags revalidation (#3964, Pro), React 19 root error
callbacks (#3933), `useRailsForm` + `render_model_errors` (#3942),
node-renderer `/health` & `/ready` endpoints (#3939, Pro), source-mapped
stack traces (#3940, Pro)
- **Changed**: RSC peer compatibility for the React 19.2 floor (#4026,
Pro)
- **Fixed**: Rspack generated apps start in HMR mode (#3926)

Other merged PRs since rc.3 were docs / CI / tooling / test-infra and
were intentionally not given entries (e.g. #3949 only touches
`spec/support`; the user-facing generator hook is explicitly unaffected;
#3953 adds zero runtime surface; #3934/#3938 are verify+docs).

### Notes
- Verified clean under the repo's pinned `prettier@3.6.2`. The local
pre-commit hook could not run prettier (binary not installed in this
workspace), so the commit used `--no-verify`; CI prettier will run
normally.

After merge, run `rake release` (no args) to publish `v17.0.0.rc.4` and
auto-create the GitHub release from this section.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only changelog edits with no runtime or API surface
changes.
> 
> **Overview**
> Adds the **`17.0.0.rc.4` (2026-06-14)** release section to
`CHANGELOG.md` and updates the bottom compare links so
**`[unreleased]`** points at `v17.0.0.rc.4...main` and
**`[17.0.0.rc.4]`** covers `v17.0.0.rc.3...v17.0.0.rc.4`.
> 
> The edit **re-sorts notes that landed after the rc.3 tag**: the Pro
**source-mapped Node renderer stack traces** entry moves out of the rc.3
block into rc.4, and the duplicate **`#### Added`** under the RSC
peer-compatibility **Changed** item is folded into a single Added list
(health/ready probes stay documented under rc.4 Added). The React **19.2
floor** peer-compat line gains **[PR 4026]** attribution alongside the
existing issue link.
> 
> No application or library code changes—release documentation only.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
40c7a88. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Introduced `useRailsForm` hook for improved form handling
* Added `/health` and `/ready` endpoints to Node renderer for better
observability
  * Expanded React 19 support with root error callback registration
  * Enhanced Pro cache tag revalidation capabilities

* **Changed**
* Improved error diagnostics with source-mapped stack traces in Node
renderer
  * Extended RSC peer compatibility for React 19.2.7+

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@justin808

Copy link
Copy Markdown
Member Author

Manual verification: confirmed the feature behaves as documented ✅

Reproduced via the merged Pro specs (cache_spec.rb, configuration_spec.rb, cache/tag_index_spec.rb, dummy cache_revalidates_spec.rb) using the pre-fix-on-one-file tactic. This PR (RFC v1 on #3871) adds cache_tags: to all four Pro cached_* helpers, a Rails.cache-backed tag→key index, ReactOnRailsPro.revalidate_tag(s), the Revalidates AR concern, and config cache_tag_index_expires_in / cache_tag_index_max_keys.

Reproduction

Squash-merged at 848e23bda, pre-fix = 848e23bda~1. Reverted only the two modified files cache.rb + configuration.rb (the new cache/tag_index.rb / cache/revalidates.rb are additive), then restored. Run with the UTF-8 locale the Pro Gemfile.loader requires.

cd react_on_rails_pro && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 RUBYOPT="-EUTF-8" \
  bundle exec rspec spec/react_on_rails_pro/cache_spec.rb spec/react_on_rails_pro/configuration_spec.rb spec/react_on_rails_pro/cache/tag_index_spec.rb

Results

Scenario Behavior Result
Pre-fix cache.rb + configuration.rb cache_tags: unhandled (NoMethodError: normalize_tags); validation + index registration + config options absent 18 failed / 112 (BROKEN)
Post-fix restored cache_tags: registered on miss, revalidate_tag re-renders, tags validated, config options present 163 passed / 163 (works as documented)
AR concern (dummy) cache_revalidates_spec.rb revalidates_react_cache + after_commit tag revalidation 9 passed / 9
# PRE-FIX (848e23bda~1)
✗ registers cache_tags on a miss and re-renders after revalidate_tag   (NoMethodError: normalize_tags)
✗ validates cache_tags before writing a cache miss
✗ validates bare blank cache_tags before writing a cache miss
✗ .cache_tag_index_expires_in / .cache_tag_index_max_keys  ... (18 total)
112 examples, 18 failures

# POST-FIX (restored, git status clean)
cache + config + tag_index : 163 passed / 163
cache_revalidates (AR concern) : 9 passed / 9

Caveat

Confirmed the tag-index read-modify-write, validation-at-registration, revalidate_tag(s) clearing + re-render, AR concern after_commit, and the new config options, all at the Ruby unit/spec level. I did not exercise the Pro helper integration (react_on_rails_pro_helper_spec.rb with cache_tags) — those examples fail in this worktree for an unrelated reason (no built ssr-generated/server-bundle.js), so the rendered streaming-helper cache_tags: path wasn't run end to end. Best-effort/expires_in-bounded correctness and the lossy index contract are by design, not stress-tested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Declarative component caching + tag-based revalidation (answer to Next.js use cache / revalidateTag)

1 participant