feat(frontend): gemeinsam am Server-Dokument arbeiten (?live=, Schritt 6)
Der Editor fuehrt ein Dokument des Backends: laden, nach 1,5 s Ruhe das Diff schicken, ueber einen offenen Abruf fremde Aenderungen einspielen — ohne Neuladen, mit mitwandernder Schreibmarke. Dazu CORS im Backend; ohne das blockiert der Browser jeden Aufruf. Zwei Fehler hat erst der Live-Test gegen das laufende Backend gefunden, beide an der Naht zwischen Modul und Verdrahtung (D54-Nachtrag 3): Der Konflikt entstand nie. Mit laufendem Feed zieht die Schattenkopie staendig nach, die eigene Basis ist also nie veraltet — der Server haette nie 409 geantwortet, und die fremde Zeile waere stillschweigend ueberschrieben worden. Der Client prueft die Ueberschneidung jetzt selbst gegen den ungesendeten Text; den kennt der Server nicht. Die Nummer begann nach jedem Neuladen wieder bei 1, waehrend die Kennung blieb — der Server hielt die erste echte Aenderung fuer eine Wiederholung und tat nichts. Beides liegt jetzt im sessionStorage: je Tab, ueberlebt Neuladen. Je Tab ist zugleich die richtige Aussage, zwei Tabs sind zwei Schreiber. Nachgemessen im Browser: fremde Aenderung erscheint ohne Neuladen, Getipptes erreicht den Server, beide Konflikt-Knoepfe tun was sie sagen, und die Schreibmarke steht nach zwei fremd eingefuegten Zeilen darueber unveraendert bei Zeile+2, Spalte 8. 518 Frontend-Tests, 135 im Backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5881be1a2c
commit
08965157a1
@@ -144,6 +144,48 @@ export function applyOps(base, ops){
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ Rebasen */
|
||||
|
||||
/* Überschneiden sich zwei Operationen derselben Basis? Dieselbe Regel wie im
|
||||
Backend: `replace`/`delete` belegen `[index, index+count)`, `insert` ist ein
|
||||
Punkt **zwischen** den Zeilen und kollidiert nur mit dem Inneren eines
|
||||
fremden Bereichs. Zwei Einfügungen an derselben Stelle vertragen sich. */
|
||||
export function conflicts(a, b){
|
||||
const ai = a.index, ae = ai + removedCount(a);
|
||||
const bi = b.index, be = bi + removedCount(b);
|
||||
if(a.op === 'insert' && b.op === 'insert') return false;
|
||||
if(a.op === 'insert') return bi < ai && ai < be;
|
||||
if(b.op === 'insert') return ai < bi && bi < ae;
|
||||
return ai < be && bi < ae;
|
||||
}
|
||||
|
||||
/* Verschiebt [ops] auf den Stand, der durch [onto] aus derselben Basis
|
||||
entstanden ist. `null` heißt echte Überschneidung — dann darf nichts
|
||||
angewendet werden, und der Mensch entscheidet.
|
||||
|
||||
Der Client braucht das an zwei Stellen: um die eigene Schattenkopie
|
||||
nachzuziehen, nachdem der Server verschoben hat, und um fremde Änderungen in
|
||||
einen Text einzublenden, in dem schon ungesendet getippt wurde. */
|
||||
export function rebaseOps(ops, onto){
|
||||
if(!onto || !onto.length || !ops.length) return ops;
|
||||
for(const mine of ops){
|
||||
for(const theirs of onto){
|
||||
if(conflicts(mine, theirs)) return null;
|
||||
}
|
||||
}
|
||||
return ops.map(op => {
|
||||
let delta = 0;
|
||||
for(const other of onto){
|
||||
/* Eine fremde Einfügung an derselben Stelle zählt dazu: Sie ist bereits
|
||||
bestätigt und steht deshalb oben. */
|
||||
if(other.index + removedCount(other) <= op.index){
|
||||
delta += insertedLines(other).length - removedCount(other);
|
||||
}
|
||||
}
|
||||
return delta ? Object.assign({}, op, {index: op.index + delta}) : op;
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ Cursor */
|
||||
|
||||
/* Wohin wandert eine Zeile, wenn fremde Operationen angewendet werden?
|
||||
|
||||
Reference in New Issue
Block a user