Skip to content

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.

const activity = room.useActivity(); // or room.useActivity({ limit: 200 })
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 at limit (default 100; the oldest are dropped first).
  • subscribe(cb) fires immediately with the current feed, then on every change.
  • 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 limit entries.

The engine is content-agnostic — record whatever matters:

const activity = room.useActivity();
// when a comment is added
comments.subscribe(() => activity.record('comment:added'));
// when a record is locked
if (await locks.acquire(`record:${id}`)) {
activity.record('record:locked', { id });
}
activity.subscribe((entries) => renderFeed(entries));
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.

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.

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();
}