Files
rspade_system/app/RSpade/man/fpc.txt
root daa9bb2fb1 Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-12 19:09:07 +00:00

259 lines
8.9 KiB
Plaintext
Executable File

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