update
This commit is contained in:
@@ -49,8 +49,8 @@ Gelöschte Termine und Todos bleiben mit `deleted: true` in der Datei und werden
|
|||||||
|
|
||||||
## Sync
|
## Sync
|
||||||
|
|
||||||
Der Server (`backend/`, FastAPI + SQLite) hält den Kern. Jedes Gerät gleicht alle paar Sekunden ab:
|
Der Server (`backend/`, FastAPI + SQLite) hält den Kern. Jedes Gerät gleicht alle paar Sekunden ab,
|
||||||
eigene Änderungen hoch, fremde Änderungen runter. Bei Konflikten gewinnt der Eintrag mit dem
|
und sofort nach jeder eigenen Änderung: eigene Änderungen hoch, fremde Änderungen runter. Bei Konflikten gewinnt der Eintrag mit dem
|
||||||
neueren `updated` (Notizen: Änderungszeit der Datei, ganze Datei). Offline geht weiter, beim
|
neueren `updated` (Notizen: Änderungszeit der Datei, ganze Datei). Offline geht weiter, beim
|
||||||
Wiederverbinden wird nachgeliefert. Eine gerade offene Notiz wird nicht von außen überschrieben.
|
Wiederverbinden wird nachgeliefert. Eine gerade offene Notiz wird nicht von außen überschrieben.
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import {readJson, writeJson} from './store.js';
|
|||||||
|
|
||||||
// Abgleich mit dem Server. Zugang steht in sync.json im Datenordner:
|
// Abgleich mit dem Server. Zugang steht in sync.json im Datenordner:
|
||||||
// {"url": "https://planer.marha.de", "token": "...", "interval": 5}
|
// {"url": "https://planer.marha.de", "token": "...", "interval": 5}
|
||||||
// Alle paar Sekunden: eigene Änderungen seit dem letzten Lauf hochladen, Änderungen des Servers
|
// Alle paar Sekunden und kurz nach jeder eigenen Änderung: eigene Änderungen hochladen,
|
||||||
// seit dem letzten Stand holen. Bei Konflikten gewinnt der Eintrag mit dem neueren updated.
|
// Änderungen des Servers seit dem letzten Stand holen. Bei Konflikten gewinnt der Eintrag mit dem neueren updated.
|
||||||
// sync-state.json merkt sich den Server-Stand (cursor) und was zuletzt übertragen wurde (pushed).
|
// sync-state.json merkt sich den Server-Stand (cursor) und was zuletzt übertragen wurde (pushed).
|
||||||
|
|
||||||
const DEFAULT_INTERVAL = 5;
|
const DEFAULT_INTERVAL = 5;
|
||||||
|
// Kurze Wartezeit nach einer eigenen Änderung, damit mehrere Klicks in einem Lauf landen
|
||||||
|
const SOON_MS = 500;
|
||||||
|
|
||||||
export class Sync {
|
export class Sync {
|
||||||
constructor(store) {
|
constructor(store) {
|
||||||
@@ -18,11 +20,18 @@ export class Sync {
|
|||||||
this._config = readJson(GLib.build_filenamev([store.dir, 'sync.json']), null);
|
this._config = readJson(GLib.build_filenamev([store.dir, 'sync.json']), null);
|
||||||
this._statePath = GLib.build_filenamev([store.dir, 'sync-state.json']);
|
this._statePath = GLib.build_filenamev([store.dir, 'sync-state.json']);
|
||||||
this._state = readJson(this._statePath, {cursor: 0, pushed: {events: {}, todos: {}, notes: {}, folders: {}}});
|
this._state = readJson(this._statePath, {cursor: 0, pushed: {events: {}, todos: {}, notes: {}, folders: {}}});
|
||||||
this._state.pushed.folders ??= {};
|
// Ältere Stände kennen keine Ordner. Einmal alles neu holen, sonst fehlen Ordner,
|
||||||
|
// die der Server schon vor dem Update verteilt hat. Bekanntes wird dabei übersprungen.
|
||||||
|
if (!this._state.pushed.folders) {
|
||||||
|
this._state.pushed.folders = {};
|
||||||
|
this._state.cursor = 0;
|
||||||
|
}
|
||||||
this._session = new Soup.Session({timeout: 10});
|
this._session = new Soup.Session({timeout: 10});
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
this._failing = false;
|
this._failing = false;
|
||||||
this._timerId = null;
|
this._timerId = null;
|
||||||
|
this._soonId = null;
|
||||||
|
this._applying = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get active() {
|
get active() {
|
||||||
@@ -37,14 +46,30 @@ export class Sync {
|
|||||||
this._cycle().catch(e => this._fail(e.message));
|
this._cycle().catch(e => this._fail(e.message));
|
||||||
return GLib.SOURCE_CONTINUE;
|
return GLib.SOURCE_CONTINUE;
|
||||||
});
|
});
|
||||||
|
// Eigene Änderungen gehen sofort raus, nicht erst beim nächsten Takt
|
||||||
|
this._store.onChange(() => this._scheduleSoon());
|
||||||
this._cycle().catch(e => this._fail(e.message));
|
this._cycle().catch(e => this._fail(e.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_scheduleSoon() {
|
||||||
|
if (this._stopped || this._applying || this._soonId)
|
||||||
|
return;
|
||||||
|
this._soonId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SOON_MS, () => {
|
||||||
|
this._soonId = null;
|
||||||
|
this._cycle().catch(e => this._fail(e.message));
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this._timerId) {
|
if (this._timerId) {
|
||||||
GLib.source_remove(this._timerId);
|
GLib.source_remove(this._timerId);
|
||||||
this._timerId = null;
|
this._timerId = null;
|
||||||
}
|
}
|
||||||
|
if (this._soonId) {
|
||||||
|
GLib.source_remove(this._soonId);
|
||||||
|
this._soonId = null;
|
||||||
|
}
|
||||||
// Laufende Anfrage abbrechen, ohne das als Verbindungsfehler zu melden
|
// Laufende Anfrage abbrechen, ohne das als Verbindungsfehler zu melden
|
||||||
this._stopped = true;
|
this._stopped = true;
|
||||||
this._session.abort();
|
this._session.abort();
|
||||||
@@ -57,7 +82,13 @@ export class Sync {
|
|||||||
try {
|
try {
|
||||||
const payload = this._collect();
|
const payload = this._collect();
|
||||||
const response = await this._post(payload);
|
const response = await this._post(payload);
|
||||||
|
// Übernommene Änderungen lösen selbst onChange aus, das soll keinen neuen Lauf starten
|
||||||
|
this._applying = true;
|
||||||
|
try {
|
||||||
this._apply(response, payload);
|
this._apply(response, payload);
|
||||||
|
} finally {
|
||||||
|
this._applying = false;
|
||||||
|
}
|
||||||
if (this._failing) {
|
if (this._failing) {
|
||||||
this._failing = false;
|
this._failing = false;
|
||||||
console.log('desktop-app sync: Verbindung wieder da');
|
console.log('desktop-app sync: Verbindung wieder da');
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ sleep 3
|
|||||||
check "Termin auf dem Server" "$(api "$URL/api/events" | python3 -c 'import json,sys; print([e["title"] for e in json.load(sys.stdin)])')" "['Zahnarzt']"
|
check "Termin auf dem Server" "$(api "$URL/api/events" | python3 -c 'import json,sys; print([e["title"] for e in json.load(sys.stdin)])')" "['Zahnarzt']"
|
||||||
check "Todo auf dem Server" "$(api "$URL/api/todos" | python3 -c 'import json,sys; print([t["title"] for t in json.load(sys.stdin)])')" "['Einkaufen']"
|
check "Todo auf dem Server" "$(api "$URL/api/todos" | python3 -c 'import json,sys; print([t["title"] for t in json.load(sys.stdin)])')" "['Einkaufen']"
|
||||||
check "Stand gemerkt" "$(val 'state().cursor > 0')" "true"
|
check "Stand gemerkt" "$(val 'state().cursor > 0')" "true"
|
||||||
|
"$E" "$PRE store.saveTodo({title: 'Sofort', date: null, time: null}); 'ok'" >/dev/null
|
||||||
|
START=$(date +%s%N)
|
||||||
|
for i in $(seq 1 40); do api "$URL/api/todos" | grep -q '"Sofort"' && break; sleep 0.1; done
|
||||||
|
MS=$(( ($(date +%s%N) - START) / 1000000 ))
|
||||||
|
check "Eigene Änderung ist vor dem nächsten Takt (2 s) auf dem Server" "$([ $MS -lt 1500 ] && echo "schnell" || echo "langsam: ${MS}ms")" "schnell"
|
||||||
|
"$E" "$PRE store.removeTodo(store.todos.find(t => t.title === 'Sofort').id); 'ok'" >/dev/null
|
||||||
|
sleep 2
|
||||||
|
|
||||||
echo "=== Server → lokal"
|
echo "=== Server → lokal"
|
||||||
push '{"todos": [{"id": "r1", "updated": "2026-09-19T10:00:00.000Z", "title": "Vom Handy", "date": null, "time": null, "done": false}], "notes": [{"id": "Remote/Idee.md", "updated": "2026-09-19T10:00:00.000Z", "content": "# Vom Server"}]}'
|
push '{"todos": [{"id": "r1", "updated": "2026-09-19T10:00:00.000Z", "title": "Vom Handy", "date": null, "time": null, "done": false}], "notes": [{"id": "Remote/Idee.md", "updated": "2026-09-19T10:00:00.000Z", "content": "# Vom Server"}]}'
|
||||||
|
|||||||
Reference in New Issue
Block a user