10. Live for everyone
A change one person makes appears on everyone's open page within a second, without a reload.
Declare a channel in ratcloud.conf; browsers listen with EventSource at /rc/sub/<name>/<id>, the app POSTs to $RC_PUBLISH/pub/<name>/<id>.
This is the code serving the zoo right now: read from disk for this request, from commit
0c1c124a02 (live). The zoo's own self-check fetches this
page and compares it byte for byte with the file it runs.
exhibits/10-live-for-everyone.php sha256 eb18878b215f · raw
1<?php 2// Exhibit 10. A channel is declared in ratcloud.conf (`channels = counter, ...`). Browsers listen 3// at /rc/sub/counter/<id> with EventSource; the app publishes with a POST to 4// $RC_PUBLISH/pub/counter/<id>. That is the whole of realtime. 5return [ 6 "n" => 10, "try" => "press the shared counter", "wing" => "Realtime", 7 "title" => "Live for everyone", 8 "promise" => "A change one person makes appears on everyone's open page within a second, without a reload.", 9 "block" => "Declare a channel in ratcloud.conf; browsers listen with EventSource at /rc/sub/<name>/<id>, the app POSTs to \$RC_PUBLISH/pub/<name>/<id>.", 10 "show" => function (?array $me): string { 11 return '<div class="row"><span class="big" id="counter">' . (int)meta("counter") . '</span><button class="primary" id="press">Press</button></div>' 12 . '<p class="muted out" id="counter-note">Open this page in two tabs and press in one.</p>'; 13 }, 14 "api" => function (string $do, ?array $me, array $in, bool $post): ?array { 15 if ($do !== "press" || !$post) return null; 16 db()->exec("BEGIN IMMEDIATE"); 17 $n = (int)meta("counter") + 1; 18 meta_set("counter", (string)$n); 19 db()->exec("COMMIT"); 20 $ok = publish("counter", "main", ["n" => $n, "at" => microtime(true)]); 21 return ["n" => $n, "published" => $ok]; 22 }, 23 // Two listeners, one press: both must see the new number within a second. 24 "check" => function (): array { 25 $host = "https://" . env("RC_HOST"); 26 $sub = "$host/rc/sub/counter/main"; 27 $b = new Browser(); 28 $n = null; 29 [$status, $msgs, $ms] = $b->listen([$sub, $sub], 30 function () use ($b, $host, &$n) { $n = $b->post("$host/api/10/press")[2]["n"] ?? null; }, 31 function ($m) use (&$n) { return $n !== null && count(array_filter($m, fn($l) => in_array($n, array_column(array_filter($l, "is_array"), "n"), true))) === 2; }, 32 2000); 33 if ($n === null) return [false, "pressing the counter failed"]; 34 if ($status !== [200, 200]) return [false, "the listeners were answered " . implode(", ", $status)]; 35 $seen = count(array_filter($msgs, fn($l) => in_array($n, array_column(array_filter($l, "is_array"), "n"), true))); 36 if ($seen < 2) return [false, "pressed to $n; only $seen of 2 listeners saw it within 2 seconds"]; 37 if ($ms > 1000) return [false, "both listeners saw $n, but after {$ms}ms"]; 38 return [true, "pressed to $n; both listeners saw it {$ms}ms later (press included)"]; 39 }, 40 "script" => <<<'JS' 41(() => { 42 const el = document.getElementById("counter"), note = document.getElementById("counter-note"); 43 if (!el) return; 44 const es = new EventSource("/rc/sub/counter/main"); 45 es.onmessage = (m) => { const j = JSON.parse(m.data); el.textContent = j.n; }; 46 es.onerror = () => { note.textContent = "Lost the live connection; retrying…"; }; 47 es.onopen = () => { note.textContent = "Live. Open this page in another tab and press in one."; }; 48 document.getElementById("press").addEventListener("click", () => zoo.call("/api/10/press", {})); 49})(); 50 51JS, 52];