3: Broadcasten met wsCast
wsCast is hoe een handler naar clients pusht. Het plaatst een bericht naar Phlo Realtime, dat het verspreidt naar de doel-sockets, en elke browser past het toe via dezelfde phlo.js apply-pijplijn die je async routes al gebruiken.
3.1: Doelen
wsCast(wsTarget: 'all', inner: ['#online' => (string)$count])
wsCast(wsTarget: 'socket:'.$wsSocket, toast: 'Just for you')
wsTarget: 'all'zendt naar elke verbonden socket.wsTarget: 'socket:'.$wsSocketverzendt naar één socket.- Token-gescopeerde targeting stelt je in staat om elke socket voor een bepaalde gebruiker te bereiken.
Alles na de target is een gewone apply-instructie: inner, outer, prepend, append, toast, scroll, enzovoort. De client behandelt een cast precies zoals de reactie op een formulier, zodat dezelfde view-updatecode voor beide transporten werkt.
3.2: Leaving the sender out
A token identifies a principal, not a connection. One user with two tabs, or one till with a customer display beside it, is one token holding several sockets. That is the point of token targeting, and it is also why token:not: cannot help you skip a sender: it drops every socket of that token, including the other screens you were trying to reach.
wsExcept names one socket to skip, whichever target picked it up:
function wsReceive($wsHost, $wsToken, $wsSocket, ...$data){
wsCast(wsTarget: 'token:'.$wsToken, wsExcept: $wsSocket, inner: $data['inner'])
}
Every other screen of this token gets the message; the one that sent it does not. Use it whenever clients relay through the server to their peers, which is the usual shape for chat, typing indicators and second screens.
The exclusion is a separate argument rather than a target value, because the daemon has no idea who "me" is: a cast is its own HTTP call, unconnected to the socket that triggered your handler. The hook is handed $wsSocket, so the hook is the only place that can say.
Spell the payload out when you pass
wsExcept....$dataafter a named argument is a fatal PHP error.
3.3: wsCast kan falen; bescherm het
wsCast maakt een HTTP-aanroep naar Phlo Realtime. Als Phlo Realtime niet draait, wordt er een fout gegooid. Wikkel het zodat de rest van je handler (en je gewone async pad) blijft werken:
function cast(...$args){
try {
wsCast(...$args)
}
catch (\Throwable $e){
}
}
Deze guard zorgt ervoor dat een Phlo Realtime-app netjes degradeert naar een niet-realtime-app wanneer Phlo Realtime niet beschikbaar is.
3.4: De browserclient
Het laden van de DOM/websocket resource levert een kleine client. Het opent een socket alleen wanneer de pagina body de klasse wss heeft, maakt automatisch opnieuw verbinding en past binnenkomende casts toe. Om realtime in te schakelen voor een pagina, zet je de app in die modus (de demo stelt prop options = 'wss' in); laat het uit en dezelfde pagina is een normale Phlo pagina.
3.5: Mirroring state between screens
With DOM/store loaded, a client can publish part of its own state instead of hand-rolling messages. Declare the path once:
app.sync('basket') // over the socket, coalesced per 200 ms
app.sync('draft', {post: 'api/draft', delay: 1000})
Every change under that path now leaves as {sync: {basket: <value>}}. Changes are coalesced, so a burst of edits sends one message with the final value, not one per keystroke.
The server decides who may see it. A relay that hands the value to the other screens of the same principal is four lines:
function wsReceive($wsHost, $wsToken, $wsSocket, ...$data){
if (!isset($data['sync'])) return
wsCast(wsTarget: 'token:'.$wsToken, wsExcept: $wsSocket, sync: $data['sync'])
}
Nothing is relayed that you do not relay yourself: pick the paths you accept rather than passing the payload through unread, the same as with any other client input.
On the receiving side sync is an ordinary apply command, so it needs no code. It differs from store in one way that matters: a value applied through sync is not published again. Without that, two screens that both publish the same path would bounce it back and forth forever.
Two more things worth knowing:
- A screen that connects late has missed everything. The publisher's state has not changed, so nothing is on its way. Let the new screen ask (
app.websocket.send({want: 'basket'})), and have the publisher answer withapp.push('basket'), which sends the current value even though nothing changed.app.websocket.connectis the hook that fires on every connect and reconnect. - Two-way sharing works, because
wsExceptkeeps a sender from hearing itself. Both screens declare the same path, and each sees the other's edits. Last write wins; the store carries no merge strategy.
Laatst bijgewerkt op 23-08-2026