3: Broadcasting with wsCast

wsCast is how a handler pushes to clients. It posts a message to Phlo Realtime, which fans it out to the targeted sockets, and each browser applies it through the same phlo.js apply pipeline your async routes already use.

3.1: Targets

wsCast(wsTarget: 'all', inner: ['#online' => (string)$count])
wsCast(wsTarget: 'socket:'.$wsSocket, toast: 'Just for you')

Everything after the target is an ordinary apply instruction: inner, outer, prepend, append, toast, scroll, and so on. The client treats a cast exactly like the response to a form, so the same view-update code works for both transports.

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. ...$data after a named argument is a fatal PHP error.

3.3: wsCast can fail; guard it

wsCast makes an HTTP call to Phlo Realtime. If Phlo Realtime is not running, it throws. Wrap it so the rest of your handler (and your plain async path) keeps working:

function cast(...$args){
	try {
		wsCast(...$args)
	}
	catch (\Throwable $e){
	}
}

This guard is what makes a Phlo Realtime app degrade cleanly to a non-realtime app when Phlo Realtime is down.

3.4: The browser client

Loading the DOM/websocket resource ships a small client. It opens a socket only when the page body has the class wss, reconnects automatically, and applies incoming casts. To turn realtime on for a page, put the app in that mode (the demo sets prop options = 'wss'); leave it off and the same page is a normal Phlo page.

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:

Last updated on 23 August 2026

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.