Real-time collaboration looks effortless until you pull back the curtain. One person types. Another deletes a line three paragraphs up. A third pastes a snippet from Stack Overflow. Somehow the document settles into a single, coherent state. Building that fluidity from scratch, with no prior experience in WebSockets or distributed state, sounds reckless. It also sounds like the right way to actually learn.

This project starts from zero. No borrowed boilerplate. No polished YouTube walkthroughs where the hard parts are skipped in a thirty-second montage. The goal is a collaborative code editor where multiple users can edit the same file simultaneously, seeing each other’s changes—and each other’s cursors—as they happen. Getting there will require figuring out transport layers, consistency models, and the thorny problem of merging concurrent edits without corrupting the document.

What “Real-Time” Actually Means

Most web applications are comfortable with request-response cycles. You submit a form, the server saves, you refresh the page. Real-time collaboration breaks that contract entirely. Every keystroke is an event that must propagate to every other connected client, usually in milliseconds, and arrive in an order that preserves meaning.

WebSockets are the obvious transport choice here because they maintain a persistent, full-duplex connection between client and server. Unlike HTTP polling, which wastes bandwidth asking “anything new?” every few seconds, a WebSocket stays open. When user A types a semicolon, that character becomes a message that travels through the socket to a central server, then fans out to users B and C. That part is relatively straightforward.

The hard part is what happens when B and C type at the exact same moment. If both changes hit the server nearly simultaneously, which one wins? If you simply broadcast messages in arrival order, you risk dropped characters or jumbled text. Naive last-write-wins strategies fail because they ignore intent. If I type “hello” at the start of line one while you type “world” at the start of line one, the result should not be a collision where one of us is erased. It should be “helloworld” or “worldhello,” deterministically chosen. Achieving that requires a synchronization strategy that understands the structure of the document.

Why Starting From Ground Zero Matters

There are excellent frameworks that hide this complexity. Yjs, Automerge, and Socket.IO can abstract away the pain and produce a working prototype in an afternoon. But using them without understanding the primitives underneath is like flying a plane on autopilot without knowing how to read the instruments. When turbulence hits—and in distributed systems, it always hits—you need to know whether the issue is in your network layer, your conflict resolution, or your data model.

The commitment here is to learn the concepts before relying on the libraries. That means manually reasoning through what happens when:

  • A client disconnects mid-keystroke and reconnects ten seconds later
  • Two users insert text at the same cursor position concurrently
  • One user deletes a block that another user is actively editing
  • The server crashes and a new node has to reconstruct the document state from scratch

Operational Transformation (OT) and Conflict-free Replicated Data Types (CRDTs) are the two dominant families of solutions for these problems. Google Docs famously built its early architecture on OT, which requires a central server to transform operations against each other before applying them. CRDTs, by contrast, are designed so that concurrent updates can be merged locally without coordination, making them attractive for peer-to-peer or edge-based setups. Choosing between them—or hybrid approaches—requires understanding their trade-offs in memory use, convergence guarantees, and implementation complexity. Reading about those trade-offs is not enough; the plan is to implement both naive and refined versions to see where they break.

The Rebuilds, Mistakes, and Dead Ends

Oczekiwania są uczciwie skalibrowane. Będą okresy, w których nic nie będzie działać. Pierwsza próba może polegać na użyciu prostych łatek JSON do reprezentowania zmian w tekście, tylko po to, by odkryć, że JSON nie zna pojęcia „indeksu 5 w akapicie”, przez co dwie jednoczesne insercje pod tym samym indeksem nadpisują się nawzajem zamiast się połączyć. Druga próba może polegać na zbudowaniu własnego liniowego logu historii, tylko po to, by zdać sobie sprawę, że odtwarzanie tego logu staje się koszmarem pod względem złożoności Big O, gdy dokument rośnie. Trzecia próba może polegać na uruchomieniu WebSockets lokalnie, a następnie załamaniu się systemu w prawdziwej sieci, gdzie utrata pakietów i zmienne opóźnienia zmieniają zasady gry.

To tarcie jest istotą rzeczy. Skopiowanie działającego repozytorium pozwoliłoby pominąć badanie tego, dlaczego kolejka jest opróżniana w konkretnej kolejności lub dlaczego serwer utrzymuje wektor wersji (version vector). Ponowne budowanie tego samego komponentu trzy razy jest powolne, ale wymusza zrozumienie granicy między tym, co robi framework, a tym, co musi obsłużyć własna logika.

Dokumentacja tego procesu nie będzie zestawieniem samych sukcesów. Będą w niej zawierać błędne decyzje. Na przykład budowanie świadomości obecności (presence awareness) — wiedzy o tym, kto jest online i gdzie znajduje się jego kursor — wydaje się funkcją kosmetyczną, dopóki nie uświadomisz sobie, że zależy ona od tego samego modelu spójności, co sam tekst. Jeśli użytkownik A widzi kursor użytkownika B w kolumnie 10, a następnie użytkownik B wstawia cztery znaki, to gdzie przesuwa się ten kursor? Bez wspólnego zrozumienia topologii dokumentu, dane o obecności rozjeżdżają się z rzeczywistością. Rozwiązanie tego wymaga powiązania pozycji kursora z tożsamością (identity) leżącej u podstaw struktury danych, a nie tylko z jej numerycznym indeksem. To są tego rodzaju szczegóły, które tutoriale pomijają, ponieważ są nużące, a nie dlatego, że są nieważne.

Co dalej

Krótkoterminowa mapa drogowa jest celowo zwięzła. Pierwsze kamienie milowe to:

  • Surowy serwer WebSocket, który odbija zdarzenia znaków, aby osobiście poczuć opóźnienia i cykl życia połączenia
  • Prosty bufor ciągu znaków po stronie klienta, aby zrozumieć, dlaczego naiwna kolejność wstawiania zawodzi przy współbieżności
  • CRDT budowane od zera dla uporządkowanych sekwencji, nieważne jak nieefektywne, aby zobaczyć właściwość przemienności w działaniu
  • Stopniowa integracja z rzeczywistym interfejsem edytora kodu, prawdopodobnie czymś takim jak CodeMirror lub Monaco, aby zmierzyć się z niedopasowaniem imperatywnego API edytora do funkcyjnej natury historii operacji

Każdy krok będzie opatrzony pisemnym uzasadnieniem. Dlaczego ta metoda, a nie inna? Jakie założenia okazały się błędne? Jaka abstrakcja „przeciekła”?

Prawdziwa lekcja

Rozpoczęcie takiego projektu bez doświadczenia w WebSockets czy CRDT jest onieśmielające, ale ekspercka wiedza to często po prostu powtarzające się zagubienie, tylko z lepszymi etykietami. Celem nie jest szybkie ukończenie prac. Celem jest system, którego zachowanie jest przewidywalne, ponieważ każda warstwa została zbudowana z intencją, a nie zaimportowana z nadzieją.

Jeśli budowałeś już wcześniej oprogramowanie kolaboracyjne — czy to edytor tekstu, narzędzie projektowe, czy silnik synchronizacji stanu gry — podziel się trybami awarii, które zaskoczyły Cię w najmniej oczekiwanym momencie. Jeśli Ty również uczysz się tych systemów, śledź postępy. Kod będzie pojawiał się powoli i będzie często przepisywany. Dzień 0 zaczyna się teraz.