feat(backend): PATCH /content — Diffs einreichen, rebasen, wiederholen (Schritt 3)
Der Server rebased selbst: Ist die Basis veraltet, ueberschneiden sich die Operationen aber nicht mit den zwischenzeitlichen, verschiebt er sie und akzeptiert. Reines Ablehnen fuehrte zu Starvation — ein Client mit hoher Latenz kaeme bei fleissigen Mitschreibern womoeglich nie durch. 409 gibt es nur bei echter Ueberschneidung, mit allem, was der Client zum Weiterarbeiten braucht, ohne neu zu laden. Pruefsumme ist Pflicht (422 bei Abweichung): Die Versionsnummer bestaetigt nur, dass die Basis dieselbe Version ist, nicht dass beide Seiten sie gleich lesen. clientId + seq machen den Aufruf wiederholbar — im Mobilnetz ist die verlorene Antwort der Normalfall. Die Sperre je Dokument liegt ausserhalb der Transaktion: innen gaebe der Proxy sie vor dem Commit frei, und der naechste Schreiber laese einen Stand, der noch nicht steht. Deshalb ist LiveEditingService nicht transaktional und schreibt ueber DocumentService. Was das Konzept offenliess, ist jetzt entschieden und in D76-Nachtrag 4 begruendet: die Randfaelle der Einfuege-Ueberschneidung, die Trennung von 400 und 422, die gedeckelte Idempotenz im Speicher. 104 Tests, davon 8 Cucumber-Szenarien fuer das Live-Editing. Gegenprobe: Pruefsumme nicht geprueft, Idempotenz entfernt, veraltete Basis abgelehnt statt verschoben -> es fallen jeweils genau die danach benannten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d5cdff6058
commit
ae85503b79
@@ -183,6 +183,67 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
|
||||
/documents/{documentId}/content:
|
||||
parameters:
|
||||
- name: documentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
patch:
|
||||
tags: [Documents]
|
||||
operationId: patchDocumentContent
|
||||
summary: Zeilen-Diff auf den Inhalt anwenden (Live-Editing)
|
||||
description: >
|
||||
Reicht eine Aenderung als Zeilen-Diff gegen eine Basisversion ein.
|
||||
|
||||
|
||||
Ist die Basis veraltet, ueberschneiden sich die Operationen aber nicht
|
||||
mit den zwischenzeitlichen, verschiebt der Server sie selbst und
|
||||
akzeptiert (200); die fremden Operationen stehen dann in
|
||||
`opsSinceBase`, damit der Client seine Schattenkopie nachzieht. Nur
|
||||
bei echter Ueberschneidung antwortet er mit 409.
|
||||
|
||||
|
||||
`clientId` und `seq` machen den Aufruf wiederholbar: Eine Wiederholung
|
||||
liefert das Ergebnis von damals, statt die Aenderung ein zweites Mal
|
||||
anzuwenden.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ContentPatchRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Aenderung angenommen (ggf. serverseitig verschoben)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ContentPatchResult"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"409":
|
||||
description: >
|
||||
Echte Ueberschneidung mit einer zwischenzeitlichen Aenderung. Der
|
||||
Client entscheidet - fremde Fassung uebernehmen oder eigene
|
||||
durchsetzen -, ohne neu zu laden.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ContentConflict"
|
||||
"422":
|
||||
description: >
|
||||
Diff nicht anwendbar: Pruefsummenfehler, Index ausserhalb, oder die
|
||||
Basisversion ist nicht mehr verfuegbar. Der Client laedt einmalig neu.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
|
||||
components:
|
||||
responses:
|
||||
NotFound:
|
||||
@@ -293,6 +354,100 @@ components:
|
||||
Optionale Zielversion. Ohne Angabe wird der letzte inhaltliche
|
||||
Stand vor dem Loeschen wiederhergestellt.
|
||||
|
||||
LineOperation:
|
||||
description: >
|
||||
Eine Zeilen-Operation relativ zur Basisversion (0-basierter Index).
|
||||
Operationen sind aufsteigend sortiert und ueberschneiden sich nicht.
|
||||
type: object
|
||||
required: [op, index]
|
||||
properties:
|
||||
op:
|
||||
type: string
|
||||
enum: [replace, insert, delete]
|
||||
index:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
count:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
description: Zahl der betroffenen Basiszeilen; bei `insert` ohne Bedeutung.
|
||||
lines:
|
||||
type: array
|
||||
description: Einzusetzende Zeilen; bei `delete` ohne Bedeutung.
|
||||
items:
|
||||
type: string
|
||||
|
||||
ContentPatchRequest:
|
||||
type: object
|
||||
required: [baseVersion, checksum, clientId, seq, ops]
|
||||
properties:
|
||||
baseVersion:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Version, gegen die das Diff gebildet wurde.
|
||||
checksum:
|
||||
type: string
|
||||
description: >
|
||||
Pflicht. `sha256:<hex>` des vollstaendigen Basistexts mit
|
||||
LF-Zeilenenden. Die Versionsnummer bestaetigt nur, dass die Basis
|
||||
dieselbe Version ist, nicht dass beide Seiten sie gleich lesen.
|
||||
clientId:
|
||||
type: string
|
||||
maxLength: 64
|
||||
description: Zufaellige, pseudonyme Kennung des Clients.
|
||||
seq:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Laufende Nummer dieses Clients; macht den Aufruf wiederholbar.
|
||||
displayName:
|
||||
type: string
|
||||
maxLength: 64
|
||||
description: >
|
||||
Selbstgewaehlter Anzeigename. Ohne Anmeldung ist er eine
|
||||
Behauptung und darf in der Oberflaeche nicht wie ein Nachweis
|
||||
aussehen.
|
||||
milestone:
|
||||
type: boolean
|
||||
default: false
|
||||
description: >
|
||||
`true` schreibt einen nutzersichtbaren Stand (Knopfdruck).
|
||||
Der getaktete Strom laesst das Feld weg.
|
||||
ops:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/LineOperation"
|
||||
|
||||
ContentPatchResult:
|
||||
type: object
|
||||
required: [version, opsSinceBase]
|
||||
properties:
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Neue Version des Dokuments.
|
||||
opsSinceBase:
|
||||
type: array
|
||||
description: >
|
||||
Leer, wenn die Basis noch aktuell war. Sonst die fremden
|
||||
Operationen, um die der Server verschoben hat.
|
||||
items:
|
||||
$ref: "#/components/schemas/LineOperation"
|
||||
|
||||
ContentConflict:
|
||||
type: object
|
||||
required: [currentVersion, opsSinceBase]
|
||||
properties:
|
||||
currentVersion:
|
||||
type: integer
|
||||
format: int64
|
||||
opsSinceBase:
|
||||
type: array
|
||||
description: Diff von der eingereichten Basis bis zur aktuellen Version.
|
||||
items:
|
||||
$ref: "#/components/schemas/LineOperation"
|
||||
|
||||
ProblemDetail:
|
||||
type: object
|
||||
description: Fehlerformat nach RFC 9457 (Problem Details)
|
||||
|
||||
Reference in New Issue
Block a user