🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
259 lines
8.9 KiB
Plaintext
Executable File
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
|