Skip to main content
Markdown

Events from the page, in PHP

Pages send events to the rooms they are in, straight through WordPlus Cloud, with no request to your server: chat messages, typing, read ticks, a cursor. A room rule says which events its pages may send.

The chat window of Ben shows “Ann typing” while Ann, in her own browser, writes a reply

What a room allows​

// The chat of Ann and Ben: they send messages, typing, reactions and read ticks. The messages and reactions reach this
// server by webhook, which keeps the conversation.
$realtime->room( 'chat:{a}:{b}', fn ( $user, array $p ) => in_array( (string) $user, [ $p['a'], $p['b'] ], true ),
[ 'history' => 50, 'emit' => [ 'message', 'typing', 'reaction', 'read' ], 'webhook' => [ 'message', 'reaction' ] ] );
  • emit lists the names, or '*' for any. None by default: a room your server alone sends to.
  • webhook names those your server gets too, to keep them (below).
  • history keeps the last events of the room for 30 minutes: what a page that reconnects missed comes again from it.
  • The answer for one user can narrow it: [ 'emit' => [ 'typing' ] ], or [ 'readOnly' => true ].
  • Reserved: joined, join, leave, count and error are the names kept for the room itself.

Sending​

A page sends to a room it has joined, and hears what the others send:

const chat = rt.join( 'chat:34:42' );

chat.emit( 'message', { text: 'Hi' } ); // sent, nothing waited for
const { id, at } = await chat.emitWithAck( 'message', { text: 'Hi' } ); // WordPlus Cloud took it: checked, kept, sent
chat.volatile.emit( 'typing', { on: true } ); // dropped rather than queued while off line, never kept
await chat.timeout( 5000 ).emitWithAck( 'message', { text: 'Hi' } ); // an ack waited for 5 seconds at most

chat.on( 'message', ( data, meta ) => draw( data.text, meta.from ) ); // meta: { from, id, at }
  • Everyone in the room gets it, the other tabs and devices of the sender too. The tab that sent it does not, so it draws its own at once.
  • emitWithAck resolves with the kept event id and time, or rejects with a code: not_allowed (the rule does not list it), forbidden, rate_limited, too_large.
  • meta.from is the sender user id, as your server vouched for them: a page cannot send as someone else.
  • Binary: an ArrayBuffer, Uint8Array or Blob may be anywhere in the data. It is sealed as binary.
  • Limits: 1 MB an event, 50 a second a connection, in bursts of up to 100.

Typing, done the usual way​

let sent = 0;
input.addEventListener( 'input', () => {
if ( Date.now() - sent < 2000 ) return; // at most every 2 seconds
sent = Date.now();
chat.volatile.emit( 'typing', { on: true } );
} );

chat.on( 'typing', ( data, meta ) => showTyping( meta.from, data.on ) ); // and hide it after 4 seconds of nothing

Keeping the conversation​

The conversation is for your app to keep, in its own database, as Better Messages keeps its own in the WordPress database. A room keeps its events for 30 minutes, for pages that reconnect. Your server gets the events the rule names in webhook (webhooks) and saves them by event_id, the id the pages saw as meta.id. A route gives them back, a page at a time:

$realtime->on( 'message', fn ( array $e ) => save_message( $e['event_id'], $e['room'], $e['from'], $e['data']['text'], $e['at'] ) );

// GET /chat/34/42/messages: the newest 50, or with ?before=<id> the 50 before that one, oldest first. Only its two
// people may read it.
if ( preg_match( '#^/chat/(\d+)/(\d+)/messages$#', $path, $m ) ) {
if ( ! in_array( (string) ( $_SESSION['user_id'] ?? '' ), [ $m[1], $m[2] ], true ) ) {
http_response_code( 403 );
exit;
}
header( 'Content-Type: application/json' );
echo json_encode( messages_page( "chat:{$m[1]}:{$m[2]}", $_GET['before'] ?? null, 50 ) ); // [ 'messages' => [ [ id, from, text, at, reactions ] ], 'more' ]
exit;
}
  • Check there anything that matters: the events do not go through your server before they are sent on.
  • at is in milliseconds, and reactions maps each reaction to who chose it. A reaction comes as { id, key }: the one reaction of the user to message id, an empty key taking it back.

Ready-made: the chat​

The Better Messages conversation, on any room: your server conversation paged back as the reader scrolls up, the room filling in what came since, the day separators, the messages of a sender stacked, typing, reactions, the read ticks and the composer. It is one element:

<wordplus-chat room="chat:34:42" messages="/chat/34/42/messages" placeholder="Write a message"></wordplus-chat>
await rt.callScreen();   // loads the elements and the call screen, rt.ui() the elements without calls
  • messages is your route, asked with the page cookies. chat.load = async ( before ) => ( { messages, more } ) replaces it, for a token or another shape: before is null for the newest page, or the oldest message shown ({ id, at }). With neither, it shows what the room still has.
  • Its room rule lets it send what it uses: 'emit' => [ 'message', 'typing', 'reaction', 'read' ], and names message and reaction in 'webhook' for your server to keep.
  • What it sends is what you would: message as { text } with an ack, typing as { on } and read as { id }, both volatile, and reaction as { id, key }. Your own pages and apps on the same room read it.
  • Names and pictures come from your directory.
  • It fills the height its place gives it. Its look is that of Better Messages, in a shadow root. Its colours are CSS custom properties your page sets.

<wordplus-typing room="…"> is the typing line alone, for a page that draws its own messages.

More, with Node and Python side by side: events in depth.