Automerge in azione: collaborative editor
Automerge è una libreria JavaScript di CRDT (Conflict-free Replicated Data Types) che risolve il problema più difficile dell'editing collaborativo: come mescolare automaticamente le modifiche di più utenti senza conflitti, anche quando lavorano offline. Scopri come costruire un editor collaborativo real-time dove ogni utente vede le modifiche degli altri istantaneamente, i conflitti si risolvono automaticamente, e i dati rimangono consistenti anche con lag di rete o offline temporaneo.
Cos'è Automerge: Introduzione ai CRDT
Automerge è una libreria JavaScript che implementa i CRDT (Conflict-free Replicated Data Types): strutture dati matematiche che permettono a copie indipendenti di dati di divergere, subire modifiche concorrenti, e merge automaticamente senza richiedere un server centrale per risolvere i conflitti.
Nel collaborative editing tradizionale (Google Docs, Figma), ogni modifica passa per un server centrale che gestisce l'ordine logico. In Automerge, invece, ogni client lavora su una copia locale del documento. Le modifiche sono rappresentate come operazioni autosufficienti (con timestamp e actor ID) che possono essere applicate in qualsiasi ordine senza compromettere la convergenza: due client indipendenti che applicano le stesse operazioni in ordine diverso arrivano allo stesso stato finale.
Questo approccio è fondamentale per il Local-First Software: il documento non dipende dalla rete per funzionare. L'utente può modificarlo offline, sincronizzarsi quando la rete torna, e le modifiche confluiranno automaticamente con quelle di altri utenti.
Il Problema del Collaborative Editing Tradizionale
Nel modello client-server classico:
- Google Docs / Figma: ogni keystroke va al server, che ordina gli eventi e rimanda le modifiche agli altri client. Senza server = niente sync.
- Lag di rete: se la connessione è lenta o offline, l'utente non può editare.
- Scaling server: il server diventa collo di bottiglia per la coordinazione logica di tutti gli utenti.
- Conflitti: se due utenti modificano lo stesso testo contemporaneamente, occorre una logica di risoluzione (Last Write Wins, OT, CRDT, ecc.).
Automerge risolve questi problemi con un approccio peer-to-peer dove ogni client gestisce autonomamente il documento locale e sincronizza solo le operazioni con gli altri peer.
Come Funzionano i CRDT: Concepti Base
Un CRDT è una struttura dati che garantisce eventual consistency: se tutti i peer applicano le stesse operazioni (anche in ordine diverso), convergono allo stesso stato finale senza coordinamento centrale.
Automerge usa un modello di timestamp vettoriale e actor ID:
- Ogni operazione ha un actor ID unico per il client (es. "alice-2024-001").
- Ogni operazione ha un sequence number locale (es. op #3 di alice).
- Quando alice invia [op #3] a bob e bob invia [op #2] ad alice, entrambi applicano le operazioni nello stesso ordine canonico (per actor ID + seq num), garantendo convergenza.
// Esempio concettuale: due client, un documento collaborativo
// Doc iniziale: { text: "Ciao" }
// Alice: inserisce " mondo" @ posizione 4
// Op1: { actor: "alice", seq: 1, insert: " mondo", pos: 4 }
// Stato alice: { text: "Ciao mondo" }
// Bob (offline): inserisce "!" @ posizione 4 (dal suo contesto locale)
// Op2: { actor: "bob", seq: 1, insert: "!", pos: 4 }
// Stato bob: { text: "Ciao!" }
// Quando alice riceve Op2 da bob:
// Applica [Op1 di alice, Op2 di bob] nello stesso ordine
// Stato finale alice: { text: "Ciao mondo!" }
// Quando bob riceve Op1 da alice:
// Applica [Op1 di alice, Op2 di bob] nello stesso ordine (canonical order)
// Stato finale bob: { text: "Ciao mondo!" }
// Convergenza garantita! Nessun conflitto manuale.
Setup: Installazione e Configurazione
Automerge è disponibile su npm e supporta Node.js e browser.
npm install automerge
Per applicazioni TypeScript, aggiungi i tipi:
npm install --save-dev @types/automerge
Se usi un bundler moderno (Vite, Webpack, ecc.), Automerge è pronto all'uso. Per applicazioni isomorfe (SSR con Angular / Next.js), assicurati di importare Automerge solo nel browser:
// documento.service.ts - Angular
import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';
export class DocumentService {
private Automerge: typeof import('automerge') | null = null;
constructor(@Inject(PLATFORM_ID) private platformId: object) {
if (isPlatformBrowser(this.platformId)) {
import('automerge').then((mod) => {
this.Automerge = mod.default;
});
}
}
}
Creazione del Primo Documento Collaborativo
Iniziamo con un semplice documento di testo che due utenti possono modificare contemporaneamente:
import * as Automerge from 'automerge';
// Crea un nuovo documento vuoto
let doc = Automerge.init();
// Abilita il cambio di attore (il nostro "client ID")
doc = Automerge.change(doc, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'Benvenuti in Automerge!'.split(''));
});
console.log(Automerge.save(doc)); // Salva lo stato del doc
// Output: UintArray (formato binario compresso di Automerge)
Automerge rappresenta il documento come una struttura dati immutabile. Quando vuoi modificarlo, usi `Automerge.change()` che accetta una funzione mutante:
// Aggiorna il documento
doc = Automerge.change(doc, (d) => {
d.text.insertAt(d.text.length, ...'\nCapo Alice!'.split(''));
});
// Serializza per il storage o la rete
const savedDoc = Automerge.save(doc);
// Carica da serializzazione
doc = Automerge.load(savedDoc);
Sincronizzazione Multi-Utente e Merge Automatico
L'aspetto più potente di Automerge è la sincronizzazione. Immagina due utenti che lavorano sullo stesso documento offline:
import * as Automerge from 'automerge';
// Alice e Bob iniziano con lo stesso documento
let docAlice = Automerge.init();
let docBob = Automerge.clone(docAlice);
docAlice = Automerge.change(docAlice, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'Alice'.split(''));
});
docBob = Automerge.change(docBob, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'Bob'.split(''));
});
// Alice e Bob lavorano offline... adesso sincronizzano
// Genera i cambiamenti da alice a bob
const changesAlice = Automerge.getChanges(docAlice, docBob);
const changesBob = Automerge.getChanges(docBob, docAlice);
// Applica i cambiamenti
docAlice = Automerge.applyChanges(docAlice, changesBob)[0];
docBob = Automerge.applyChanges(docBob, changesAlice)[0];
// Convergenza garantita!
console.log(Automerge.save(docAlice) === Automerge.save(docBob)); // true
console.log(Automerge.toJS(docAlice)); // { text: 'AliceBob' }
Nota il passaggio cruciale: `getChanges()` restituisce solo le operazioni che un documento ha che l'altro non ha. Questo rende la sincronizzazione efficiente anche su rete lenta.
Gestione Automatica dei Conflitti
Cosa succede se Alice e Bob inseriscono caratteri diversi nella stessa posizione? Automerge ha un algoritmo deterministico per la risoluzione:
import * as Automerge from 'automerge';
// Documento iniziale
let doc = Automerge.init();
// Alice inserisce 'A' alla posizione 0
doc = Automerge.change(doc, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'A'.split(''));
}, 'alice');
// Bob fa la stessa modifica
let docBob = Automerge.clone(doc);
docBob = Automerge.change(docBob, (d) => {
d.text.insertAt(0, ...'B'.split(''));
}, 'bob');
// Alice modifica di nuovo
doc = Automerge.change(doc, (d) => {
d.text.insertAt(1, ...'X'.split(''));
}, 'alice');
// Sincronizzazione
const changesAlice = Automerge.getChanges(doc, docBob);
docBob = Automerge.applyChanges(docBob, changesAlice)[0];
// Risultato: 'BAX' (B da bob, A da alice, X da alice)
// L'ordine è deterministico basato sui timestamp Lamport
console.log(Automerge.toJS(docBob).text); // "BAX"
Automerge risolve i conflitti automaticamente usando:
- Timestamp Lamport: ogni operazione ha un clock logico basato su actor + sequence number.
- Ordine canonico: le operazioni conflittuali si ordinano deterministicamente per actor ID + timestamp, senza server.
- Nessun "Last Write Wins": le operazioni non vengono perse, il loro ordine finale è determinato matematicamente.
Persistenza Locale: Salvare il Documento
In un editor collaborativo locale-first, il documento è salvato nel browser (IndexedDB o localStorage):
// Serializza il documento per il storage
const docBytes = Automerge.save(doc);
// Salva in localStorage (per piccoli doc)
localStorage.setItem('document_v1', JSON.stringify(Array.from(docBytes)));
// O in IndexedDB (per documenti più grandi)
const db = await openDB('editor-db');
await db.put('documents', {
id: 'doc-123',
data: docBytes,
updatedAt: new Date(),
});
// Carica il documento
const savedBytes = new Uint8Array(JSON.parse(localStorage.getItem('document_v1')));
const doc = Automerge.load(savedBytes);
Per applicazioni Angular, usa il StorageService già disponibile nel progetto, che gestisce la persistenza in modo SSR-safe e con fallback.
Sincronizzazione di Rete: Multiplayer Real-Time
Per attivare la sincronizzazione real-time tra browser diversi, uso WebSocket:
// collaborative-editor.service.ts
export class CollaborativeEditorService {
private doc = Automerge.init();
private ws: WebSocket | null = null;
private pendingChanges: Uint8Array[] = [];
connect(wsUrl: string, actorName: string) {
this.ws = new WebSocket(wsUrl);
this.doc = Automerge.change(this.doc, (d) => {}, actorName);
this.ws.onmessage = (event) => {
const incomingChanges = new Uint8Array(event.data);
const [newDoc, conflicts] = Automerge.applyChanges(this.doc, [incomingChanges]);
this.doc = newDoc;
if (conflicts.length > 0) {
console.warn('Conflitti risolti automaticamente:', conflicts);
}
};
}
editText(position: number, text: string) {
this.doc = Automerge.change(this.doc, (d) => {
if (!d.text) d.text = new Automerge.Text();
d.text.insertAt(position, ...text.split(''));
});
// Invia il cambiamento a tutti gli altri client
this.broadcastChanges();
}
private broadcastChanges() {
const changes = this.pendingChanges.length > 0
? Automerge.getAllChanges(this.doc).slice(-1) // Ultime modifiche
: Automerge.getAllChanges(this.doc);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
changes.forEach((change) => this.ws!.send(change));
}
}
}
Performance e Scaling
Automerge è ottimizzato per documenti di piccole-medie dimensioni (testo, strutture dati, metadata). Per documenti molto grandi (milioni di righe di codice), considera:
- Suddivisione in subdocumenti: memorizza ogni sezione in un documento Automerge separato.
- Snapshot periodici: Automerge supporta gli snapshot per comprimere la cronologia e ridurre il carico di memoria.
- Garbage collection: i cambiamenti vecchi possono essere compattati per ridurre il footprint.
// Compatta il documento per ridurre la dimensione
const heads = Automerge.getHeads(doc);
const compact = Automerge.save(doc);
// Se il documento cresce troppo, considerare di creare snapshot
const snapshot = Automerge.save(doc);
// Successivamente, ricaricare dal snapshot e applicare solo i cambiamenti recenti
// Per documenti molto grandi, dividi in sezioni
let docSection1 = Automerge.init();
let docSection2 = Automerge.init();
// Sincronizza sezione1 e sezione2 separatamente
Integrazioni: React, Vue, Angular
Automerge funziona con qualsiasi framework frontend. Per Angular, l'integrazione avviene tramite Signal o RxJS:
// collaborative-editor.component.ts (Angular)
import { Component, signal } from '@angular/core';
import * as Automerge from 'automerge';
@Component({
selector: 'app-collaborative-editor',
template: `
Collaboratori online: {{ collaborators().length }}
`
})
export class CollaborativeEditorComponent {
private doc = Automerge.init();
documentText = signal('');
collaborators = signal([]);
constructor(private editorService: CollaborativeEditorService) {
this.editorService.docChanges$.subscribe((newDoc) => {
this.doc = newDoc;
this.documentText.set(
Automerge.toJS(newDoc).text?.toString() || ''
);
});
}
handleTextChange(event: Event) {
const target = event.target as HTMLTextAreaElement;
const newText = target.value;
this.doc = Automerge.change(this.doc, (d) => {
if (!d.text) d.text = new Automerge.Text();
d.text = newText.split('');
});
this.editorService.broadcast(this.doc);
}
}
Debugging e Introspection
Automerge fornisce tool utili per il debugging e la comprensione dello stato del documento:
// Visualizza la cronologia dei cambiamenti
const allChanges = Automerge.getAllChanges(doc);
console.log('Cronologia cambiamenti:', allChanges.length);
// Visualizza gli attori (client) che hanno modificato il doc
const heads = Automerge.getHeads(doc);
console.log('Heads (ultime operazioni):', heads);
// Verifica il contenuto come JavaScript
const jsDoc = Automerge.toJS(doc);
console.log('Documento come JS:', jsDoc);
// Ottieni i cambiamenti tra due versioni
const changes = Automerge.getChanges(docVersione1, docVersione2);
console.log('Cambiamenti tra v1 e v2:', changes.length);
// Clona il documento
const clonedDoc = Automerge.clone(doc);
Caso d'Uso Reale: Collaborative Markdown Editor
Immagina un editor Markdown condiviso tra sviluppatori per documentazione del progetto. Con Automerge:
- Ogni sviluppatore ha una copia locale del documento Markdown.
- Modifiche offline funzionano senza server.
- Quando la rete torna, i cambiamenti si sincronizzano automaticamente.
- Se due sviluppatori modificano sezioni diverse, il merge è automatico.
- Se modificano la stessa sezione, Automerge ordina gli inserimenti deterministicamente.
- Nessun conflitto di merge manuale, nessun "CONFLICT MARKER".
Vantaggi rispetto a OT (Operational Transformation)
- Semplicità matematica: CRDT è più semplice da comprendere e implementare rispetto a OT.
- No server coordinatore: OT richiede un server per l'ordine totale degli eventi; CRDT no.
- Offline-first: CRDT funziona benissimo offline; OT no senza modifiche architetturali.
- P2P sync: i peer si sincronizzano direttamente; OT richiede centralizzazione.
Limitazioni e Considerazioni
Automerge è potente ma ha alcuni limiti:
- Overhead di memoria: Automerge memorizza tutta la cronologia per la convergenza. Per documenti molto attivi, questo cresce rapidamente.
- Ordine totale non garantito: se l'ordine finale degli inserimenti è critico, CRDT potrebbe non essere la scelta giusta (usa OT o server centrale).
- Testing: i test devono coprire scenari offline e merge complessi, non è triviale.
- Ecco un'architettura: Automerge lavora bene in 1-10 client contemporanei. Per 100+ client, considera sharding.
FAQ: Domande Frequenti
D: Automerge è adatto per chat real-time?
R: Sì, ma con un'avvertenza. Automerge è pensato per strutture dati persistenti (testo, oggetti), non per stream transitori di messaggi. Per chat, è meglio un server centrale + Automerge per la cronologia offline dei messaggi.
D: Come gestisci le permessi in un editor collaborativo Automerge?
R: Automerge non ha permessi built-in. Implementa i controlli di accesso a livello di applicazione: ogni client ha credenziali che il server valida prima di propagare i cambiamenti. Oppure, usa Actor ID come proxy: "solo l'attore alice può modificare il campo 'owner'".
D: Cosa succede se il documento diverge troppo tra i client?
R: Se i client rimangono offline troppo a lungo, il documento crescerà in dimensione a causa della cronologia. Automerge supporta snapshot e compaction per mitigare questo. O, periodicamente sincronizzerai con un server che tronca la cronologia.
D: Automerge è migliore di Firebase Realtime Database?
R: Dipende dal caso d'uso. Firebase è centralizzato e semplice da usare; Automerge è offline-first e P2P. Se il tuo app ha bisogno di funzionare offline e di sync diretto tra peer, Automerge è migliore. Se preferisci semplicità e un server centrale, Firebase è più veloce.
D: Posso usare Automerge con Angular SSR?
R: Sì, ma importa Automerge solo nel browser (vedi sezione Setup). Assicurati di usare `isPlatformBrowser()` per evitare errori durante il server-side rendering.
Conclusioni: Quando Usare Automerge
Automerge è la soluzione ideale per:
- Editor collaborativi offline-first: Google Docs-like senza dipendenza dal server.
- Documenti persistenti multi-utente: Markdown, JSON, testo strutturato.
- P2P sync: applicazioni che sincronizzano direttamente tra browser/device.
- Local-First Software: il tuo stack cresce da locale e verso il cloud quando necessario.
Non è la scelta giusta per:
- Chat real-time: i messaggi sono transitori, non persistenti.
- Applicazioni con forte ordine totale: es. transazioni finanziarie, ordini di acquisto.
- Documenti enormi (>100 MB): il footprint di memoria diventa proibitivo.
Automerge è parte della rivoluzione del Local-First Software: il codice e i dati sono locali per impostazione predefinita, e la rete è solo un canale di sincronizzazione. Questo modello offre prestazioni, resilienza e privacy superiori rispetto agli approcci cloud-first tradizionali.

Commenti
Caricamento commenti...
Accedi per commentare