FPC(3) RSX Framework Manual FPC(3) NAME FPC - Full Page Cache via Node.js reverse proxy with Redis SYNOPSIS Controller attribute: #[FPC] #[Route('/about', methods: ['GET'])] public static function index(Request $request, array $params = []) PHP cache management: use App\RSpade\Core\FPC\Rsx_FPC; Rsx_FPC::clear(); // Clear all FPC entries Rsx_FPC::clear_url('/about'); // Clear specific URL DESCRIPTION The Full Page Cache (FPC) is a Node.js reverse proxy that sits between nginx and PHP. Pages marked with #[FPC] are cached in Redis after first render. Subsequent requests for the same URL are served directly from Redis without touching PHP, with ETag validation for 304 responses. Unlike traditional PHP caching (which still boots Laravel on every request), the FPC proxy is a long-running Node.js process that serves cached responses in under 1ms. PHP is only invoked on cache misses. FPC is designed for public, anonymous pages like marketing sites, documentation, and SSR-rendered content. Pages accessed by logged-in users always bypass the cache. Architecture: browser -> nginx -> /_ajax/ -> ajax FPM pool (unchanged) static files -> try_files (unchanged) everything else -> FPC proxy (port 3200) -> cache HIT -> Redis -> 304 or cached HTML cache MISS -> nginx backend (port 3201) -> FPM -> if X-RSpade-FPC header -> cache in Redis ATTRIBUTE USAGE Add #[FPC] to any controller method that should be cached: #[FPC] #[Route('/pricing', methods: ['GET'])] public static function pricing(Request $request, array $params = []) { return rsx_view('Pricing_Page'); } FPC Constraints: - Requires #[Route] attribute (enforced by manifest validation) - Cannot combine with #[Ajax_Endpoint] - Cannot combine with #[SPA] - Cannot combine with #[Task] The #[FPC] attribute only affects GET requests from anonymous visitors. For logged-in users, POST requests, or non-GET methods, the request passes through to PHP normally with no caching. CACHE BEHAVIOR Cache Key: fpc:{build_key}:{sha1(path?sorted_query_params)} The build key is the manifest hash, written to system/storage/rsx-build/build_key during manifest save. When any file changes trigger a manifest rebuild, the build key changes and all FPC entries are effectively invalidated (new cache keys, old entries expire via Redis LRU). Cache Entry (stored as JSON in Redis): url - Request path html - Full HTML response body status_code - HTTP status (200, 301, 302, etc.) content_type - Response Content-Type header location - Redirect target (for 3xx responses) etag - SHA1 hash of body (first 30 chars) cached_at - ISO timestamp of cache time ETag Validation: Client sends If-None-Match header with cached ETag. If ETag matches cached entry, proxy returns 304 Not Modified with no body. This saves bandwidth on repeat visits. Session Cookie Bypass: If the request contains an 'rsx' cookie (indicating the visitor has an active session), the proxy passes through directly to PHP. Session users never receive cached content. POST/Non-GET Bypass: Non-GET/HEAD methods always pass through to PHP. HEAD requests can READ from cache but never POPULATE it. RESPONSE HEADERS X-FPC-Cache: HIT - Response served from Redis cache X-FPC-Cache: MISS - Response served from PHP (and cached for next time) ETag: {hash} - Content hash for 304 validation The internal X-RSpade-FPC header (set by PHP to signal cacheability) is stripped by the proxy and never sent to clients. PHP CACHE MANAGEMENT Rsx_FPC::clear() Clear all FPC entries for the current build key. Uses SCAN (not KEYS) to avoid blocking Redis. Returns count of deleted entries. Rsx_FPC::clear_url(string $url) Clear FPC entry for a specific URL path. Handles query parameter sorting to match proxy key generation. Returns true if entry was deleted. Examples: // Clear all cached pages after content update Rsx_FPC::clear(); // Clear specific page after editing its content Rsx_FPC::clear_url('/about'); Rsx_FPC::clear_url('/search?q=test&page=1'); CONFIGURATION Environment variables (in .env): FPC_PROXY_PORT Port for FPC proxy (default: 3200) FPC_BACKEND_PORT Port for nginx backend (default: 3201) REDIS_HOST Redis server host (default: 127.0.0.1) REDIS_PORT Redis server port (default: 6379) REDIS_PASSWORD Redis password (default: none) Redis uses DB 0 (default database with LRU eviction policy). NGINX SETUP The FPC requires two nginx changes: 1. Backend server block (accessed by proxy for cache misses): server { listen 127.0.0.1:3201; root /var/www/html/system/public; location /_ajax/ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/index.php; fastcgi_param SCRIPT_NAME /index.php; fastcgi_pass unix:/run/php/php-fpm-ajax.sock; } location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/php-fpm.sock; } } 2. Main server blocks - replace location / with proxy fallback: location / { try_files $uri $uri/ @fpc_proxy; } location @fpc_proxy { proxy_pass http://127.0.0.1:3200; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } SUPERVISOR The FPC proxy runs as a supervised service: /system/supervisor/fpc-proxy.conf Start manually for testing: node system/bin/fpc-proxy.js DISPATCHER BEHAVIOR When the Dispatcher matches a route with #[FPC]: 1. Checks request is GET with no POST/FILES data 2. Calls Session::init() to load existing session (does NOT create one) 3. If user is logged in, skips FPC (no header set) 4. If anonymous, blanks all cookies except 'rsx' from $_COOKIE 5. Executes controller method normally 6. Adds X-RSpade-FPC: 1 header to response 7. Removes Set-Cookie headers from response Cookie blanking (step 4) ensures controller code produces identical output for all anonymous visitors regardless of any tracking cookies. SESSION COOKIE SAFETY RSpade does NOT create session cookies for anonymous visitors. Session::init() only reads existing cookies, never creates new ones. Session::set_site_id() stores site_id as a request-scoped override for anonymous visitors without creating a database session. Sessions are only created when code explicitly calls: - Session::get_session_id() - Session::get_session() - Session::set_login_user_id() This ensures FPC-cacheable pages never produce Set-Cookie headers that would prevent caching. DEBUGGING Check FPC cache status: curl -sI http://localhost:3200/your-page | grep X-FPC View cached entry: redis-cli GET "fpc:{build_key}:{sha1_of_url}" List all FPC keys: redis-cli KEYS "fpc:*" Clear all FPC entries: redis-cli KEYS "fpc:*" | xargs -r redis-cli DEL Check proxy logs: /var/log/supervisor/fpc-proxy.log /var/log/supervisor/fpc-proxy-error.log TESTING Integration tests in /system/app/RSpade/tests/fpc/: 01_session_cookie_behavior - Verifies no cookies for anonymous 02_fpc_cache_behavior - Cache MISS/HIT, ETag 304, bypasses Run tests: ./system/app/RSpade/tests/fpc/01_session_cookie_behavior/run_test.sh ./system/app/RSpade/tests/fpc/02_fpc_cache_behavior/run_test.sh FILES system/bin/fpc-proxy.js Node.js proxy server system/app/RSpade/Core/FPC/Rsx_FPC.php PHP cache utility system/supervisor/fpc-proxy.conf Supervisor config system/storage/rsx-build/build_key Build key file system/app/RSpade/tests/fpc/ Integration tests SEE ALSO ssr(3) - Server-side rendering (produces HTML that FPC caches) session(3) - Session management and cookie behavior