Engine CRDT Yjs por nó
Referência de arquitetura e integração do modo textSync: "crdt", lançado na v2.2. Para o uso completo, de ponta a ponta, veja a demonstração de colaboração.
O que é este modo
editor.collab.attach({ doc, provider, textSync: "crdt" }) ativa um binding Yjs por parágrafo que espelha o DOM editável em um Y.XmlFragment dentro do seu Y.Doc compartilhado. Dois usuários editando o mesmo parágrafo ao mesmo tempo geram operações Yjs sem conflito: cada intervalo de inserção / exclusão / formatação carrega um relógio vetorial Yjs, e o CRDT os mescla de forma determinística. Nada de last-write-wins, nada de saltos do cursor em carga normal, nenhum servidor proxy necessário — o engine roda inteiramente no navegador e funciona com qualquer provedor Yjs (y-websocket, Hocuspocus, y-webrtc, Liveblocks ou qualquer outro transporte compatível com CRDT). Para entender por que essa escolha quase não aparece em uma demonstração e faz muita diferença em produção, veja CRDT vs OT: como editores colaborativos mesclam edições.
Matriz de modos
textSync | Direciona para | Comportamento |
|---|---|---|
false (padrão) | handle no-op somente de presença | Apenas awareness / cursores; sem sincronização do documento. Use quando o conteúdo fica em outro lugar (por exemplo, HTML armazenado no servidor). |
true (legado) | MVP do modo snapshot | Um Y.Text compartilhado guarda o HTML do editor como uma única string opaca. Mesclagem grosseira — last-write-wins em conflitos no mesmo parágrafo. Mantido para compatibilidade com clientes da v2.0; obsoleto em 2027. |
"crdt" (recomendado) | engine por nó (esta página) | CRDT completo em nível de caractere. Digitação simultânea no mesmo parágrafo é mesclada; cursores de presença via Y.RelativePosition; Y.UndoManager por autor. |
Arquitetura
O engine é distribuído como um script de plugin separado (richtexteditor/plugins/crdt-engine.min.js, ~88 KB minificado) que expõe window.RichTextEditorCrdt. O método collab.attach do plugin yjscollab verifica se esse global existe; se existir, delega a sincronização do conteúdo ao engine e continua responsável pela sobreposição de awareness, pelo painel de presença e pela ponte com o registro de revisão.
Browser editable (rte.js)
↓ MutationObserver
[crdt-engine.js]
↓ Y.Doc transactions (LOCAL_ORIGIN)
Y.XmlFragment
↓ Yjs provider (y-websocket / Hocuspocus / etc.)
Network
↓
Other peers' Y.Docs
Other peer's Y.Doc updates
↓ ydoc.on("update", origin !== LOCAL_ORIGIN)
[crdt-engine.js renders to DOM,
preserves selection via path-based capture]
↓
Browser editable updatesDo que o engine cuida
- Sincronização bidirecional Y.XmlFragment ↔ DOM. Elementos de bloco, elementos folha e marcas inline fazem o caminho de ida e volta. Alterações de childList no meio da árvore (listas, tabelas) disparam reconstruções cirúrgicas da subárvore, em vez de renderizações grosseiras do documento inteiro.
- Preservação da seleção. Captura baseada em caminho antes de cada nova renderização causada por alterações remotas, restaurada depois — o cursor do usuário fica no lugar quando os colaboradores digitam em outro ponto.
- Prevenção de eco. Edições locais recebem a marca
LOCAL_ORIGIN; o handler remoto as ignora.MutationObserver.takeRecords()é esvaziado de forma síncrona após cada renderização remota, para que o callback assíncrono do observer não devolva a emissão da edição local ao Y.Doc em loop. - Presença de cursores. As posições de seleção são codificadas como bytes
Y.RelativePosition— identificadores estáveis que sobrevivem a inserções / exclusões simultâneas — e publicadas no canal de awareness; os pares as decodificam ao recebê-las para projetar os cursores remotos no DOM local. - Desfazer por autor.
Y.UndoManagercomtrackedOrigins: { LOCAL_ORIGIN }— Ctrl-Z desfaz apenas as edições do usuário local, nunca as do colaborador.
Do que a aplicação host cuida
- Barra de ferramentas, menus, caixas de diálogo, tratamento de colagem.
- Conexão / autenticação do provedor Yjs (o engine não se comunica com a rede).
- Composição do payload de awareness (identidade do usuário, cor, nome).
- Desenho da sobreposição de cursores — o engine retorna os dados de posição, o host desenha.
- UX de conflitos (mostrar o nome dos colaboradores ao passar o mouse, destacar os retângulos de seleção deles etc.).
Código de integração
JavaScript puro
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/richtexteditor/plugins/crdt-engine.min.js"></script>
<script type="module">
import * as Y from "https://esm.sh/yjs";
import { WebsocketProvider } from "https://esm.sh/y-websocket";
const editor = new RichTextEditor("#editor", {
commentsEnabled: true,
trackChangesEnabled: true,
});
const ydoc = new Y.Doc();
const provider = new WebsocketProvider("wss://your-yjs-server", "doc-1", ydoc);
editor.collab.attach({
doc: ydoc,
provider: provider,
Y: Y, // your Yjs module — lets the engine bridge a doc built by it
textSync: "crdt", // ← per-node CRDT
requireCrdt: true, // fail closed if the engine is missing
user: { id: "u-1", name: "Alice", color: "#2563eb" }
});
</script>Verifique o modo conectado
Integrações em produção devem verificar o modo após o attach. getStatus().yjsBridge é true quando o seu Y.Doc veio de uma instância do Yjs diferente da que o engine inclui e o plugin está repassando as atualizações entre elas (passe Y para attach para habilitar isso). requireCrdt: true mantém a presença e o registro de revisão disponíveis quando o engine não consegue iniciar, mas não passa silenciosamente as edições do documento para o modo snapshot legado. Use onCollabStatus para telemetria centralizada ou para uma mensagem de recuperação visível.
const editor = new RichTextEditor(host, {
onCollabStatus(status) {
if (status.fallbackReason) reportCollaborationIssue(status.fallbackReason);
}
});
const status = editor.collab.getStatus();
if (status.textSyncMode !== "crdt") {
reportCollaborationIssue(status.fallbackReason);
}React
import { useEffect, useRef } from "react";
import { RichTextEditorComponent } from "@richscripts/richtexteditor/react";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
export function CollabEditor({ docId, user }) {
const editorRef = useRef();
useEffect(() => {
const editor = editorRef.current.getEditor();
const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
"wss://your-yjs-server", docId, ydoc,
);
editor.collab.attach({
doc: ydoc, provider, Y, textSync: "crdt", user,
});
return () => { provider.destroy(); ydoc.destroy(); };
}, [docId]);
return (
<RichTextEditorComponent
ref={editorRef}
config={{ commentsEnabled: true, trackChangesEnabled: true }}
/>
);
}Migração a partir de textSync: true
Há dois caminhos, conforme você queira uma troca definitiva ou uma implantação lado a lado.
Caminho A — do zero (recomendado)
Gere um novo Y.Doc com uma chave diferente (por exemplo, acrescente "-crdt" ao id do documento existente). No primeiro attach de um determinado documento, execute o helper de migração do engine para preencher o novo documento a partir do HTML armazenado:
const newDoc = new Y.Doc();
window.RichTextEditorCrdt.htmlToYFragment({
ydoc: newDoc,
html: legacyHtmlString,
parserDocument: document,
});
editor.collab.attach({
doc: newDoc,
provider: new WebsocketProvider(url, docId + "-crdt", newDoc),
Y,
textSync: "crdt",
});Caminho B — troca lado a lado
Execute os dois modos em paralelo durante uma implantação controlada por feature flag. Importante: todos os colaboradores de um mesmo documento precisam usar o mesmo modo; misturar os modos snapshot legado e CRDT no mesmo Y.Doc produz um estado inconsistente. Defina o escopo da flag por tenant.
const mode = featureFlag("rte_crdt_for_tenant_" + tenantId) ? "crdt" : true;
editor.collab.attach({ doc: ydoc, provider, textSync: mode });Cronograma de descontinuação (provisório)
| Data | Evento |
|---|---|
| Q2 2026 | textSync: "crdt" é lançado atrás de uma flag opcional (opt-in); textSync: true continua sendo o padrão (sem alteração incompatível). |
| Q4 2026 | textSync: "crdt" passa a ser o padrão; textSync: true continua funcionando por meio do shim legado de compatibilidade. |
| Q2 2027 | textSync: true emite um aviso de descontinuação em tempo de execução. |
| Q4 2027 | textSync: true é removido. Os clientes precisam estar em "crdt". |
Detalhes de engenharia
A estrutura do código-fonte, o corpus de testes e a documentação para contribuidores ficam junto ao engine, no workspace interno RichTextEditorCloud/CrdtEngine/. A API pública é exportada via window.RichTextEditorCrdt.*:
attachCollab(opts)— o dispatcher; direciona conforme o modotextSyncattachCrdtBinding(opts)— entrada direta do binding (dispensa o dispatcher)attachAwareness(opts)— conexão da presença de cursoresattachUndoManager(opts)— wrapper do Y.UndoManager por autorhtmlToYFragment(opts)— helper de migração única de HTML → Y por nóLOCAL_ORIGIN— símbolo usado para marcar transações Yjs de origem local, para prevenção de eco