diff --git a/CHANGELOG.md b/CHANGELOG.md index 07beef6..17ef0b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.6.1] - 2026-10-29 + +### Fixed +- Notizen: Wer im Rhythmus tippte (kurze Pausen zwischen den Wörtern), bekam Konfliktkopien gegen sich selbst, auch in nicht geteilten Notizen. Mehrere ungesendete Änderungen an einem Punkt zählten lokal als mehrere Versionen, beim Server aber als eine. + ## [0.6.0] - 2026-10-29 ### Added @@ -41,5 +46,6 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.6.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.6.1...dev +[0.6.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.6.0...v0.6.1 [0.6.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.5.2...v0.6.0 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e083d0c..1f01d4b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.6.0", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.6.0", + "version": "0.6.1", "license": "Unlicense", "dependencies": { "@types/prismjs": "^1.26.6", diff --git a/frontend/package.json b/frontend/package.json index f61af61..bbbc3c8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.6.0", + "version": "0.6.1", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/lib/docSync/__tests__/engine.test.ts b/frontend/src/lib/docSync/__tests__/engine.test.ts index 201667a..d69d83d 100644 --- a/frontend/src/lib/docSync/__tests__/engine.test.ts +++ b/frontend/src/lib/docSync/__tests__/engine.test.ts @@ -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')); diff --git a/frontend/src/lib/docSync/engine.ts b/frontend/src/lib/docSync/engine.ts index e3168e1..02dbc78 100644 --- a/frontend/src/lib/docSync/engine.ts +++ b/frontend/src/lib/docSync/engine.ts @@ -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;