Skip to main content
Markdown

Events from the page, in FastAPI

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}", lambda user, p: str(user) in (p["a"], p["b"]),
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", lambda e: save_message(e["event_id"], e["room"], e["from"], e["data"]["text"], e["at"]))


@app.get("/chat/{a}/{b}/messages")
async def chat_messages(request: Request, a: str, b: str, before: str | None = None):
"""The newest 50, or with ?before=<id> the 50 before that one, oldest first. Only its two people may read it."""
if str(await current_user(request)) not in (a, b):
return JSONResponse({"error": "not_in_chat"}, status_code=403)
return await messages_page(f"chat:{a}:{b}", before, 50) # {"messages": [{id, from, text, at, reactions}], "more"}
  • 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:{{ chat.a }}:{{ chat.b }}" messages="/chat/{{ chat.a }}/{{ chat.b }}/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 PHP side by side: events in depth.