Activity engine
Audience: users.
The activity engine is a shared, bounded, newest-first feed of room activity. Each record is
broadcast to every peer, so all peers converge on the same recent feed — useful for a “what’s
happening” panel, an audit trail, or a notification surface.
Access
Section titled “Access”const activity = room.useActivity(); // or room.useActivity({ limit: 200 })Interface
Section titled “Interface”interface ActivityEngine { record(type: string, data?: unknown): ActivityEntry; getEntries(): ActivityEntry[]; // newest first subscribe(callback: (entries: ActivityEntry[]) => void): Unsubscribe;}
interface ActivityEntry { id: string; type: string; // an app-defined label, e.g. 'comment:added' actor: Peer; // resolved from the broadcasting peer, with live presence data?: unknown; // optional structured payload timestamp: number; // epoch ms}record(type, data?)appends an entry locally and broadcasts it; every peer appends it on receipt. Returns the created entry.getEntries()returns the feed newest first, de-duplicated by id and capped atlimit(default100; the oldest are dropped first).subscribe(cb)fires immediately with the current feed, then on every change.
Semantics
Section titled “Semantics”- Shared — entries ride the room’s event channel (a reserved internal event), so no relay change is needed; peers already connected converge on the same feed.
- Best-effort by default — entries live in memory and reach only peers connected when they are
recorded, so a late joiner does not see earlier activity. Pass a
storage adapter (
useActivity({ storageAdapter })) to make the feed durable — restored on startup and saved after every change. - Bounded — the feed retains at most
limitentries.
Example
Section titled “Example”The engine is content-agnostic — record whatever matters:
const activity = room.useActivity();
// when a comment is addedcomments.subscribe(() => activity.record('comment:added'));
// when a record is lockedif (await locks.acquire(`record:${id}`)) { activity.record('record:locked', { id });}
activity.subscribe((entries) => renderFeed(entries));Adapter usage
Section titled “Adapter usage”import { useActivity } from '@roomful/react';
function Feed(): JSX.Element { const { entries, record } = useActivity(); return ( <> <button onClick={() => record('note:added')}>Add note</button> <ul> {entries.map((entry) => ( <li key={entry.id}> {entry.actor.name ?? entry.actor.id}: {entry.type} </li> ))} </ul> </> );}useActivity() returns { entries, record }; entries is the reactive feed, newest first.
<script setup lang="ts">import { useActivity } from '@roomful/vue';
const { entries, record } = useActivity();</script>
<template> <button @click="record('note:added')">Add note</button> <ul> <li v-for="entry in entries" :key="entry.id"> {{ entry.actor.name ?? entry.actor.id }}: {{ entry.type }} </li> </ul></template>useActivity() returns { entries, record }; entries is a readonly ref to the reactive feed, newest first.
Svelte
Section titled “Svelte”The roomful(...) adapter exposes activity as a readable store of ActivityEntry[] (newest first) with record attached:
<script lang="ts"> import { roomful } from '@roomful/svelte';
const room = roomful('my-room'); const { activity } = room;</script>
<button on:click={() => activity.record('note:added')}>Add note</button><ul> {#each $activity as entry (entry.id)} <li>{entry.actor.name ?? entry.actor.id}: {entry.type}</li> {/each}</ul>The feed cap is configured on the factory: roomful('my-room', { activity: { limit: 200 } }).
import { useActivity } from '@roomful/solid';
function Feed() { const { entries, record } = useActivity(); return ( <> <button onClick={() => record('note:added')}>Add note</button> <ul> <For each={entries()}> {(entry) => ( <li> {entry.actor.name ?? entry.actor.id}: {entry.type} </li> )} </For> </ul> </> );}useActivity() returns { entries, record }; entries is an accessor for the reactive feed, newest first.
Angular
Section titled “Angular”injectActivity() must run in an injection context and returns { entries, record }, where entries is a Signal:
import { Component } from '@angular/core';import { injectActivity } from '@roomful/angular';
@Component({ selector: 'app-feed', template: ` <button (click)="activity.record('note:added')">Add note</button> <ul> @for (entry of activity.entries(); track entry.id) { <li>{{ entry.actor.name ?? entry.actor.id }}: {{ entry.type }}</li> } </ul> `,})export class FeedComponent { protected readonly activity = injectActivity();}