Persistent comments storage
Audience: users.
Comments sync live between connected peers over the CRDT channel, but that state is in-memory — it is gone once every peer leaves. To make threads durable (surviving reconnects, reloads, and server restarts), back the comments engine with a storage adapter.
The contract
Section titled “The contract”A CommentsStorageAdapter is a room-scoped pair of async methods:
interface CommentsStorageAdapter { load(): Promise<readonly CommentThread[]>; save(threads: readonly CommentThread[]): Promise<void>;}loadis called once on startup. When it returns threads and the room is otherwise empty, they are restored into the shared document, so a peer joining a cold room sees the history. If the room already has threads (synced from a peer or created locally), the live CRDT wins andloadis ignored — it never clobbers newer state.saveis called after every change with the full thread list. Persist it however you like: upsert bythread.id, or replace the room’s rows wholesale.
Both are best-effort from the engine’s point of view — a rejected load or save is swallowed,
so a storage outage never breaks the live, synced comments.
Wiring it up
Section titled “Wiring it up”Pass an adapter as storageAdapter to useComments:
import { createMemoryCommentsStorage, createRoom } from '@roomful/core';
const room = createRoom('doc-1', { relayUrl: 'wss://relay.example' });const comments = room.useComments({ storageAdapter: createMemoryCommentsStorage(),});Threads are then restored from the adapter on startup (into an otherwise-empty room) and saved after
every change. It composes with the default storage: 'memory' backend.
Memory (reference)
Section titled “Memory (reference)”createMemoryCommentsStorage() is the reference adapter — handy in tests and as a template. It is
not durable across process restarts.
import { createMemoryCommentsStorage } from '@roomful/core';
const storage = createMemoryCommentsStorage();Postgres / SQLite (sketch)
Section titled “Postgres / SQLite (sketch)”Implement the two methods against your database — one row per thread, keyed by room and thread.id,
with the serialized thread stored as JSON:
import type { CommentsStorageAdapter, CommentThread } from '@roomful/core';
function createSqlCommentsStorage(db: Db, roomId: string): CommentsStorageAdapter { return { async load(): Promise<readonly CommentThread[]> { const rows = await db.query( 'select data from comments where room_id = $1 order by created_at', [roomId], ); return rows.map((row) => row.data); }, async save(threads): Promise<void> { await db.transaction(async (tx) => { await tx.query('delete from comments where room_id = $1', [roomId]); for (const thread of threads) { await tx.query( 'insert into comments (room_id, id, created_at, data) values ($1, $2, $3, $4)', [roomId, thread.id, thread.createdAt, thread], ); } }); }, };}For high write volumes, upsert only changed threads instead of replacing the whole set, and debounce
save.