1, 'name' => 'Widget', 'price' => 9.99, 'featured' => true, 'inStock' => true], ['id' => 2, 'name' => 'Gadget', 'price' => 19.99, 'featured' => false, 'inStock' => true], ['id' => 3, 'name' => 'Sprocket', 'price' => 4.50, 'featured' => false, 'inStock' => false], ['id' => 4, 'name' => 'Gizmo', 'price' => 29.99, 'featured' => true, 'inStock' => true], ]; $TYPES = ['counter', 'products', 'cart', 'user', 'notice']; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Send a JSON array of Statum directives and stop. */ function statum_respond(array $directives): void { header('Content-Type: application/vnd.statum+json'); header('Cache-Control: no-store'); echo json_encode($directives, JSON_PRETTY_PRINT); exit; } /** Wrap a snapshot in a trivial (unsigned) frame. */ function frame(array $snapshot): array { return ['content' => json_encode($snapshot), 'signer' => 'demo', 'signature' => '']; } /** Stable per-type slot key, kept in the session. */ function key_for(string $type): string { if (!isset($_SESSION['keys'][$type])) { $_SESSION['keys'][$type] = 'k_' . $type; } return $_SESSION['keys'][$type]; } /** * Build a snapshot for a type. `always` makes transmit_after == as_at so the * client sends the base64 frame on every request (demonstrating that path). */ function make_snapshot(string $type, string $scope, array $public, array $opts = []): array { $now = gmdate('Y-m-d\TH:i:s\Z'); return [ 'as_at' => $now, 'slot' => ['key' => key_for($type), 'scope' => $scope, 'type' => $type], 'transmit_after' => !empty($opts['always']) ? $now : gmdate('Y-m-d\TH:i:s\Z', time() + 60), 'invalid_after' => gmdate('Y-m-d\TH:i:s\Z', time() + 600), 'private' => $opts['private'] ?? '', 'public' => $public, ]; } function set_dir(array $snapshot): array { return ['type' => 'set', 'snapshot_frame' => frame($snapshot)]; } function clear_dir(string $key): array { return ['type' => 'clear', 'key' => $key]; } function product_by_id($id): ?array { global $CATALOG; foreach ($CATALOG as $p) { if ((string)$p['id'] === (string)$id) return $p; } return null; } // --------------------------------------------------------------------------- // Public-data builders // --------------------------------------------------------------------------- function build_counter(): array { $v = $_SESSION['counter'] ?? 0; return [ 'value' => $v, 'high' => $v > 5, 'increment' => ['uri' => '/api/counter/inc', 'method' => 'POST'], 'decrement' => ['uri' => '/api/counter/dec', 'method' => 'POST'], 'reset' => ['uri' => '/api/counter/reset', 'method' => 'POST'], 'add' => ['uri' => '/api/counter/add', 'method' => 'POST'], ]; } function build_products(): array { global $CATALOG; $items = []; foreach ($CATALOG as $p) { $pid = (string)$p['id']; $items[] = [ 'id' => $pid, 'name' => $p['name'], 'price' => $p['price'], 'featured' => $p['featured'], 'inStock' => $p['inStock'], // Per-item context rides in `private`; the client echoes it back // via the X-Statum-Private header (no stm-data expression needed). 'addToCartAction' => [ 'uri' => '/api/cart/add', 'method' => 'POST', 'private' => base64_encode('id=' . $pid), ], ]; } return ['items' => $items]; } function build_cart(): array { $cart = $_SESSION['cart'] ?? []; $count = 0; $total = 0.0; $lines = []; foreach ($cart as $id => $qty) { $p = product_by_id($id); if (!$p) continue; $count += $qty; $total += $p['price'] * $qty; $lines[] = ['id' => (string)$id, 'name' => $p['name'], 'qty' => $qty, 'price' => $p['price']]; } return [ 'count' => $count, 'total' => round($total, 2), 'empty' => $count === 0, 'items' => $lines, 'checkoutAction' => ['uri' => '/api/cart/checkout', 'method' => 'POST'], 'clearAction' => ['uri' => '/api/cart/clear', 'method' => 'POST'], ]; } function build_user(): array { $role = $_SESSION['role'] ?? 'guest'; $names = ['guest' => 'Guest', 'member' => 'Member Mike', 'admin' => 'Admin Alice']; return [ 'role' => $role, 'name' => $names[$role] ?? 'Guest', 'toggleAction' => ['uri' => '/api/user/toggle', 'method' => 'POST'], 'resetAllAction' => ['uri' => '/api/reset-all', 'method' => 'POST'], ]; } function build_notice(): array { return [ 'html' => $_SESSION['notice'] ?? 'No notice set.', 'setAction' => ['uri' => '/api/notice/set', 'method' => 'POST'], 'failAction' => ['uri' => '/api/notice/fail', 'method' => 'POST'], ]; } /** Build the snapshot for a type with its demo scope/options. */ function snap(string $type): ?array { switch ($type) { case 'counter': return make_snapshot('counter', 'session', build_counter()); case 'products': return make_snapshot('products', 'session', build_products()); case 'cart': return make_snapshot('cart', 'session', build_cart(), ['always' => true]); case 'user': return make_snapshot('user', 'session', build_user()); case 'notice': return make_snapshot('notice', 'flow', build_notice()); } return null; } function all_directives(): array { global $TYPES; $dirs = []; foreach ($TYPES as $t) { $s = snap($t); if ($s) $dirs[] = set_dir($s); } return $dirs; } // --------------------------------------------------------------------------- // Router // --------------------------------------------------------------------------- $method = $_SERVER['REQUEST_METHOD']; $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // Static assets. if ($path === '/statum.js') { header('Content-Type: application/javascript'); readfile(__DIR__ . '/../js/statum.js'); exit; } // The page itself. if ($path === '/' || $path === '/index.html') { header('Content-Type: text/html; charset=utf-8'); readfile(__DIR__ . '/index.html'); exit; } // Landing page after a (post) checkout navigation. if ($path === '/api/checkout-done') { header('Content-Type: text/html; charset=utf-8'); $order = htmlspecialchars($_GET['order'] ?? 'UNKNOWN'); echo "
This page was reached via a statum post directive.