fix(notes): no self-conflicts when typing in a rhythm
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m51s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m51s
Several unsent text changes of one block were applied one by one to the local view, numbering local versions ahead of the server, which folds them into one change (formBatch). The focused field kept the inflated version as base, so the next edit after a sync conflicted with the user's own text. The local view now applies pending ops as they will be sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b55334b163
commit
fbc404fea1
3 files changed
+30
-1
No files matched your search
@@ -335,6 +335,29 @@ describe('DocSyncEngine', () => {
|
||||
expect(contents(a)).toEqual(expect.arrayContaining(['B hat geändert', 'Start + A']));
|
||||
});
|
||||
|
||||
it('typing in a rhythm (several edits per sync) causes no conflict with oneself', async () => {
|
||||
const server = new FakeServer();
|
||||
const net = new FakeNet(server);
|
||||
const engine = await startEngine(net);
|
||||
engine.focusBlock('b1');
|
||||
// Pauses longer than the field's debounce but shorter than the sync debounce: several edits per sync
|
||||
for (const text of ['S', 'Sc', 'Sch']) edit(engine, (b) => [{ ...b[0], content: text }]);
|
||||
await engine.syncNow();
|
||||
edit(engine, (b) => [{ ...b[0], content: 'Schr' }]);
|
||||
edit(engine, (b) => [{ ...b[0], content: 'Schre' }]);
|
||||
// More typing while the request is on the wire
|
||||
net.duringRequest = () => edit(engine, (b) => [{ ...b[0], content: 'Schrei' }]);
|
||||
await engine.syncNow();
|
||||
edit(engine, (b) => [{ ...b[0], content: 'Schreib' }]);
|
||||
await engine.syncNow();
|
||||
edit(engine, (b) => [{ ...b[0], content: 'Schreibe' }]);
|
||||
await engine.syncNow();
|
||||
|
||||
expect(engine.getState().conflicts).toEqual([]);
|
||||
expect(serverContents(server)).toEqual(['Schreibe']);
|
||||
expect(contents(engine)).toEqual(['Schreibe']);
|
||||
});
|
||||
|
||||
it('viewers cannot change anything', async () => {
|
||||
const server = new FakeServer();
|
||||
const viewer = await startEngine(new FakeNet(server, 'viewer'));
|
||||
|
||||
@@ -462,7 +462,10 @@ export class DocSyncEngine {
|
||||
|
||||
private recompute(): void {
|
||||
const v = cloneDoc(this.confirmed);
|
||||
for (const op of [...(this.inflight || []), ...this.pending]) {
|
||||
// Pending ops count the way they will be sent: formBatch folds all text changes of a block into one,
|
||||
// so the server raises its version once. Applying every keystroke here would number local versions
|
||||
// ahead of the server, and the next edit of the focused block would conflict with the user's own text.
|
||||
for (const op of [...(this.inflight || []), ...formBatch(this.pending)]) {
|
||||
const local = { ...op };
|
||||
if (local.kind === 'set_content') local.baseVersion = v.blocks.get(local.blockId!)?.version ?? 0;
|
||||
if (local.kind === 'set_title') local.baseVersion = v.titleVersion;
|
||||
|
||||
Reference in new issue
Block a user