# 7190 MINCollaborative Editor
# 71
90 MIN
Overview
Build a collaborative notes editor as a tiny event-sourced + CQRS system: every change is an immutable event appended to a log, the visible document is a pure projection of that log, and autosave debounces those changes into a single "save to server" call.
How it works
- Write — editing, adding, or deleting a block appends a new
DocumentEventto the log. It is optimistic: the UI updates before anything is saved. - Read —
projectDocumentrebuilds the visibleBlock[]from the log, applying two merge rules: a duplicated eventidis ignored and a staleversionloses. - Persist —
saveDocumentsimulates saving the projected document, but only after 500 ms of inactivity (debounce). A failed save shows a retry action. - Collaborate —
remotePeerinjects a scripted collaborator's events, which merge through the same rules without marking the document dirty.

Files
You only edit App.tsx. The rest are read-only and already wired up:
App.tsx— the component you implement: hold the event log, project it, autosave it, and merge remote events.types.ts—Blockand theDocumentEventunion (block.insert/block.update/block.delete).eventLog.ts—appendEvent,hasEvent,createLocalEventId(immutable, idempotent appends).projection.ts— pureprojectDocument(events)→Block[], the read model.api.ts— fakesaveDocument(blocks)(300 ms, rejects when a block contains#fail).peer.ts— simulated collaborator (connect,emitNext,isConnected).data.ts—initialEvents(seed document) andremoteEvents(scripted peer activity).App.test.tsx— the 11 tests that define the required behavior.
Solution preview
Here's the result you'll build by completing this challenge.
Requirements
- Implement the editor in
App.tsx. The providedtypes.ts,data.ts,eventLog.ts,projection.ts,api.ts, andpeer.tsare read-only, and the first render shows the document built frominitialEvents. - The editor renders one editable block per textarea in insertion order. Each textarea has
data-testid="block-input-<blockId>"and anaria-label; the seed blocks (intro,goals,notes) appear in that order. - Each block has a delete control
data-testid="delete-block-<blockId>"that removes the block immediately. - The
data-testid="add-block"control appends a new empty block to the end of the document. - Typing in a block updates the visible text immediately and sets
data-testid="save-status"toDirty. - Several edits in quick succession result in a single save, not one save per keystroke.
- While a save is pending,
data-testid="save-status"readsSaving…; after a successful save it readsSaved. - When a save fails,
data-testid="save-status"readsError, the edited text stays visible, anddata-testid="retry-save"appears. - Clicking
retry-savesaves the latest document immediately, without waiting, and moves fromSaving…toSavedwhen it succeeds. - A pending autosave is cancelled on unmount, so leaving the editor does not start a save.
- A remote update applied to a block replaces that block's text with the remote text.
- A remote event that was already applied is ignored: it changes nothing and triggers no extra save.
- A remote event that is older than the block's current version is ignored.
- Remote edits never mark the document
Dirtyand never trigger an autosave. data-testid="presence"readsAna is editingwhile the simulated peer subscription is active; on unmount the subscription is removed and the peer reports no listeners.
Notes
- Treat the log as append-only and the textareas as a projection of it. Always return a new array from an append — never mutate
eventsin place, or React won't re-render. - A local edit appends an event with
author: "local"andversionequal to the block's current version plus one. The projection only applies strictly greater versions. - Idempotency is keyed on the event
id, not on its payload: a re-delivered event can carry a higher version and must still be dropped before it reaches the projection. - Debounce the save: keep the timeout handle in a ref, reset it on every new edit, and clear it in the effect cleanup so an unmount never fires a pending save.
saveDocumenttakes 300 ms and rejects when any block contains#fail. Keep the edited text on screen soretry-savecan send the latest version.- Remote events are already persisted by the peer — applying one should not flip the status to
Dirtyor start an autosave. - Generate ids deterministically (
createLocalEventId()plus a counter for new blocks).crypto.randomUUID()makes the tests and logs non-reproducible. - React StrictMode mounts effects twice in development, so the subscription cleanup must remove only its own listener.
- Keep the log in a ref so two rapid keystrokes don't build on the same version. Reading
Date.now()is impure — wrap the clock in a module-level helper to keep the purity lint happy.
Tests
- renders the three initial blocks in order
- updates the text immediately and marks the status dirty
- debounces a burst of edits into a single save
- moves from saving to saved and persists the latest text
- keeps the edited text on failure and lets retry save the latest version
- removes a block immediately when it is deleted
- adds a new empty block
- applies a remote update with a higher version
- ignores a remote event whose id was already applied
- ignores a stale remote event with a lower version
- shows the peer presence and unsubscribes on unmount