useCwaRealtime
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.
| Property | Type | Description |
|---|---|---|
clientId | string | The 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 |
name | string | The 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 |
field | string | undefined | The field they have in focus |
expiresAt | number | When the entry is dropped (milliseconds since the epoch) unless they send presence again |
colour | string | Their presence colour, as in the admin header. Your own windows are grey |
isSelf | boolean | true for your own other windows |
idle | boolean | true when their tab is hidden or they haven't used it for 5 minutes |
device | string | undefined | Only 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.enabledisfalse; - the real-time connection is down (the admin header then says Live editing paused);
iriis 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.
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:
| Property | Type | Description |
|---|---|---|
clientId | string | The sending tab's ID |
user | { id: string, name?: string } | The sender's user IRI and name |
name | string | The name to show, as in peers |
isSelf | boolean | true when it came from your own other window |
colour | string | The sender's presence colour |
event | string | The event name |
data | Events[event] | What the sender passed to send |
sentAt | string | undefined | When 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.