CWA 2 is a prerelease (2.0.0-alpha), heading for a beta. It runs production sites; configuration can still change between releases, always with upgrade notes. View versions and support
Admin Manager

useCwaRealtime

Send your own real-time events between signed-in admins on a resource, hear theirs, and see which other admins have it selected.

useCwaRealtime lets admins' browsers talk to each other about one resource. It's for an editor that shares more than saved values, such as where each admin's caret is. It returns the other admins on the resource (peers), a way to send an event (send) and a way to hear one (on).

The module already shows who's editing and applies other admins' saves (see Real-Time Updates › Co-Editing in the Admin), so most components need nothing from it.

import { useCwaRealtime } from '#imports'

const props = defineProps<{ iri: string }>()

type Events = {
  selection: { from: number, to: number } | null
}

const { send, on, peers } = useCwaRealtime<Events>(() => props.iri) 
on('selection', ({ clientId, name, colour, data }) => {  // draw another admin's caret
}) 
send('selection', { from: 4, to: 4 }) 

Signature

useCwaRealtime<Events extends object = Record<string, unknown>>(
  iri: MaybeRefOrGetter<string | undefined>
): {
  peers: ComputedRef<RealtimePeer[]>
  send: (event: keyof Events, data: Events[event]) => boolean
  on: (event: keyof Events, handler: (message: RealtimeMessage<Events[event]>) => void) => () => void
}

iri can be a string, a ref or a getter, so it follows the selected resource. A draft and its published version count as one resource: an event sent on either reaches handlers on both.

Events maps each event name to the type of its data, so send and on are typed. The types RealtimePeer, RealtimeMessage, RealtimeEventHandler and RealtimeEvents can be imported from #cwa/composables/cwa-realtime.

peers

The other admins who have this component, or its component position, selected. Your own other windows are listed too, with isSelf: true. Your current tab never is.

PropertyTypeDescription
clientIdstringThe other browser tab's ID. One admin with two tabs open is two entries
user{ id: string, name?: string }The admin's user IRI, and their display name when the API sent one
namestringThe name to show: the admin's name, Someone without one, or You (other window)
state'selected' | 'editing' | 'idle'editing while they have a field in focus or a save waiting or in flight
fieldstring | undefinedThe field they have in focus
expiresAtnumberWhen the entry is dropped (milliseconds since the epoch) unless they send presence again
colourstringTheir presence colour, as in the admin header. Your own windows are grey
isSelfbooleantrue for your own other windows
idlebooleantrue when their tab is hidden or they haven't used it for 5 minutes
devicestring | undefinedOnly on your own windows: a label such as Chrome on macOS
peers lists admins on a component, not on a page. An admin is only listed once they select the component, or its position. Who's on the page is shown in the admin header (see Real-Time Updates › Who Else Is Here).

send

send(event, data) sends data to the other admins' tabs. It returns true when the event is queued, and false when nothing is sent:

  • the user isn't a signed-in admin, or cwa.realtime.enabled is false;
  • the real-time connection is down (the admin header then says Live editing paused);
  • iri is empty.

A tab sends one real-time request at a time, at most 8 a second. Presence goes first. While an event waits to be sent, a newer send of the same event on the same resource replaces its data, so only the newest data goes. That suits state like a caret position, but not a log of changes.

Keep each event small. The API refuses an event body over 16 KB (see Real-Time Updates (Mercure) › The Admin Real-Time Channel). The module logs one warning for each refused event name and drops it. Send larger state some other way, such as saving it to the API and sending a short notice.

on

on(event, handler) calls handler for each event of that name sent on this resource by another tab. It returns a function that stops listening. Called inside a component's setup, it stops by itself when the component unmounts.

The handler gets one message:

PropertyTypeDescription
clientIdstringThe sending tab's ID
user{ id: string, name?: string }The sender's user IRI and name
namestringThe name to show, as in peers
isSelfbooleantrue when it came from your own other window
colourstringThe sender's presence colour
eventstringThe event name
dataEvents[event]What the sender passed to send
sentAtstring | undefinedWhen the API received it

A handler never hears its own tab's events. A handler that throws is logged, and the other handlers still run.

presence and names starting with cwa: are reserved for the module's own events. In development, using one throws an error. In production, send returns false and on does nothing.

An Example: Live Carets

The template's TipTap editor shows other admins' carets this way. Each tab sends its selection when it changes, and null when it leaves the field. The editor only draws carets of admins who have the field in focus, from the field's focusedBy (see useCwaResourceModel › Field Presence):

const props = defineProps<{
  model: ReturnType<typeof useCwaResourceModel<string>>
  iri: string
}>()

type Selection = { from: number, to: number } | null
const { send, on } = useCwaRealtime<{ selection: { selection: Selection } }>(() => props.iri)
const selections = reactive(new Map<string, { name: string, colour: string, from: number, to: number }>())

on('selection', ({ data: { selection }, clientId, name, colour }) => {  selection ? selections.set(clientId, { name, colour, ...selection }) : selections.delete(clientId) }) 
const inField = computed(() => props.model.focusedBy.value.flatMap(peer => peer.clientIds))
const carets = computed(() => [...selections].filter(([clientId]) => inField.value.includes(clientId)))

// in the editor's options
// onFocus: ({ editor }) => send('selection', { selection: { from: editor.state.selection.from, to: editor.state.selection.to } }),
// onBlur: () => send('selection', { selection: null }),

The full editor is described in Rich-Text Editor › Editing Together.