Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
258
app/RSpade/man/fpc.txt
Executable file
258
app/RSpade/man/fpc.txt
Executable file
@@ -0,0 +1,258 @@
|
||||
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
|
||||
Reference in New Issue
Block a user