Wie der Sync arbeitet
Delta-Sync über eine Sequenznummer, Konfliktauflösung über eine Hybrid Logical Clock, und eine Warteschlange, die einen Absturz überlebt. Der Server ist dabei ein Briefkasten, kein Gehirn.
Ohne Server
Der Abgleich ist freiwillig. Beim ersten Start bietet die Einrichtung neben Konto, Kopplung und Wiederherstellung einen vierten Weg an: Ohne Server nutzen. Danach ist tessera vollständig bedienbar — Hosts, Gruppen, Snippets, Schlüssel, Sprunghosts, Dateien, alles. Nur bleibt es auf diesem Gerät.
Was in diesem Zustand nicht existiert:
- kein Konto und kein Abgleich,
- kein zweites Gerät und keine Kopplung,
- keine Geräteliste, kein Entzug, keine Epoch-Rotation,
- kein Wiederherstellungscode. Geht das Gerät verloren, sind die Daten weg. Es gibt keine zweite Kopie, weil es keine Stelle gibt, an der eine liegen könnte.
In der Fußzeile der Hostliste steht statt des Abgleichstands Nur auf diesem Gerät, und daneben der Weg heraus. Denselben Weg gibt es im Menü und in den Einstellungen unter Abgleich.
Nachrüsten geht jederzeit und verliert nichts. Jeder
Datensatz trägt von Anfang an seine Uhr und sein
dirty-Kennzeichen — auch ohne Server. Kommt später ein
Konto dazu, ist der gesammelte Bestand genau das, was die
Push-Warteschlange ohnehin enthält, und geht beim ersten Durchlauf
verschlüsselt hoch. Für den Weg über eine Kopplung gilt dasselbe: die
lokalen Datensätze kommen zu denen des Kontos hinzu, ihre IDs sind
zufällig und kollidieren nicht.
Die Knoten-ID der Uhr wird beim serverlosen Start einmal vergeben und bleibt danach dieselbe — auch über den Wechsel zu einem Konto hinweg bleiben die damit gestempelten Datensätze einsortierbar.
Umgekehrt gilt das nicht: Ein Gerät, das bereits an einem Konto hängt, kann nicht in den serverlosen Betrieb zurück. Es bekäme sonst einen Bestand, den niemand mehr abgleicht, während der Vault weiterläuft. Deshalb bietet die Einrichtung diesen Weg nur an, solange noch keine Geräteidentität existiert.
Start ohne Serverkontakt
Die Oberfläche wartet beim Start nicht auf den Server. Der entschlüsselte Arbeitsstand liegt in der lokalen Datenbank — Hostliste, Snippets, Schlüsselbund, gespeicherte Benutzer —, und er steht bereit, bevor das erste Byte über die Leitung ging. Die Sitzung zum Server wird nachgeholt, während die App schon bedient wird. Ein eingerichtetes Gerät landet deshalb auch bei totem Server nie wieder auf dem Einrichtungsbildschirm.
Was ohne Sitzung nicht geht — für die Sekunde beim Start ebenso wie dauerhaft, solange niemand antwortet:
-
kein Abgleich. Änderungen bleiben
dirtyund gehen hoch, sobald die Sitzung steht, - keine Geräteliste, kein Entzug, keine Epoch-Rotation,
- keine Kopplung eines weiteren Geräts.
In der Fußzeile der Hostliste steht dazu erst Verbindet … und danach entweder der Abgleichstand oder Offline -- kein Serverkontakt; daneben liegt Erneut verbinden. Verbinden lässt sich in beiden Zuständen: Ein SSH- oder RDP-Ziel geht die App direkt an, der eigene Server hat damit nichts zu tun.
Der Entzug bleibt der Entzug. Meldet der Server beim Nachholen der Sitzung, dass dieses Gerät entzogen wurde oder der Vault Key rotiert ist, schließt tessera alle offenen Sitzungen und zeigt den Sperrhinweis — auch wenn die Oberfläche einen Augenblick vorher schon stand.
Was ein Datensatz ist
Hosts, Gruppen, Snippets, Schlüssel, gespeicherte Benutzer und Einstellungen sind je ein Record. Jeder trägt eine ID, einen Typ, eine Uhr und ein Löschkennzeichen. Der Klartext ist kanonisches JSON in UTF-8 — bewusst ohne Kompression, weil die Größe eines Records sonst Rückschlüsse auf den Inhalt erlaubte.
Gelöscht wird über Grabsteine: Der Record bleibt bestehen und ist als gelöscht markiert. Ein einfaches Verschwinden wäre von „habe ich noch nie gesehen“ nicht unterscheidbar, und das andere Gerät schöbe den Eintrag wieder hoch.
Ein Durchlauf
Erst hoch, dann runter.
1. Alle Records mit dirty = true einsammeln
2. In Batches teilen (höchstens 200 Records oder rund 1,2 MB)
3. Je Batch mit dem Schlüssel der aktuellen Epoch verschlüsseln, hochladen
4. applied → dirty = false, Sequenznummer merken
stale → dirty bleibt; der Pull holt gleich den neueren Stand
1. Delta ab dem gespeicherten Cursor holen
2. Fremde Epoch? → Vault Key neu holen, Seite wiederholen
3. Je Record:
Typ unbekannt → überspringen, Cursor trotzdem weiter
nicht entschlüsselbar → überspringen, Fehler merken, weitermachen
lokale Uhr ≥ fremde → verwerfen, der lokale Stand gewinnt
sonst → schreiben, dirty = false, Uhr mergen
4. Cursor speichern, bei Bedarf nächste Seite
Die Uhr
Eine Hybrid Logical Clock verbindet Zeitstempel mit einem Zähler und einer Knoten-ID:
000001a2b3c4d5e6-0007-dev2f1c9a
└ 12 Hex: Millisekunden ┘ └ Zähler ┘ └ Knoten ┘
Feste Feldbreite ist der ganze Trick: Damit ist der lexikografische
Vergleich identisch zum zeitlichen. Ohne Nullauffüllung wäre
"10" < "9". Die Knoten-ID enthält keinen Bindestrich,
sonst wäre die Zerlegung mehrdeutig.
Nach jedem übernommenen Record wird die lokale Uhr mit der eingehenden gemerged. Danach ist die eigene Uhr echt größer als alles Gesehene.
Was die Engine zusagt
-
Die Warteschlange überlebt alles. Jede lokale
Änderung ist
dirty, bis der Server sie bestätigt hat. Ein Neustart, ein Absturz, drei Stunden im Zug — der nächste Durchlauf holt es nach. - Wer den fremden Stand gesehen hat, gewinnt. Nach einem Merge ist die eigene Uhr echt größer; die nächste lokale Änderung setzt sich durch. Ohne vorherigen Merge ist der Gewinner nicht vorhersagbar — aber beide Geräte konvergieren.
- Ein Epoch-Wechsel mitten im Push wird abgefangen. Der Klartext liegt noch vor, also holt die Engine den neuen Schlüssel und verschlüsselt neu, statt den Fehler nach oben zu geben. Genau einmal — eine zweite Wiederholung wäre eine Schleife.
- Ein unbekannter Recordtyp blockiert nichts. Eine ältere App überspringt ihn und schiebt den Cursor trotzdem weiter.
Konflikte
Aufgelöst wird mit Last-Write-Wins über die Uhr — und zwar auf beiden Seiten. Der Client entscheidet lokal, der Server weist einen Push zurück, dessen Uhr nicht echt größer ist als der gespeicherte Stand. Beide Seiten unabhängig: Deshalb kann ein Fehler auf einer Seite den Stand nicht still verfälschen.
Push-Hinweise
Über eine WebSocket-Verbindung meldet der Server, dass es etwas Neues gibt — mehr nicht. Nutzdaten gehen nie über diesen Kanal; der Client holt sie per Delta-Abruf nach. Fällt der Socket aus, ist Polling im Minutentakt der vorgesehene Rückfall.
Authentifiziert wird über den Header des Upgrade-Requests, nicht über einen Query-Parameter, der im Zugriffslog landen würde. Der Socket ist bewusst kein Schreibkanal: Alles außer einem Ping wird ignoriert.