fix: a document that hung the editor is offered again before it is parked

A reload that cut a slow start short looked like a freeze, and the parked
document was reachable only through the browser's storage tools. The user
is asked for another try; only declining parks it.
This commit is contained in:
Steffen Becker committed 2026-09-04 19:30:12 +02:00
1 parent bc8f8d89d1
commit 0435fdf285
5 files changed
+17 -7

No files matched your search

+1 -1
View File
@@ -1095,7 +1095,7 @@ const de: Messages = {
autosaveQuota:
'Das Dokument ist zu groß, um automatisch gespeichert zu werden (Speicherlimit des Browsers erreicht). Das passiert meist bei großen eingebetteten Bildern. Speichere es als .odt-Datei, um deine Arbeit zu sichern.',
documentNotLoaded:
'Das zuletzt bearbeitete Dokument hat den Start des Editors blockiert und wurde deshalb nicht geladen. Du arbeitest in einem neuen, leeren Dokument; das alte bleibt im Browser-Speicher unter „edentext-doc-broken" erhalten.',
'Das zuletzt bearbeitete Dokument hat den Start des Editors blockiert. Trotzdem noch einmal laden?\n\nAbbrechen startet mit einem neuen, leeren Dokument; das alte bleibt im Browser-Speicher unter „edentext-doc-broken“ erhalten.',
picturesNotRestored: (n) =>
`${n} ${n === 1 ? 'Bild konnte' : 'Bilder konnten'} nicht aus dem Browser-Speicher wiederhergestellt werden und ${n === 1 ? 'wird' : 'werden'} als defekt angezeigt. Öffne die gespeicherte Datei des Dokuments, um ${n === 1 ? 'es' : 'sie'} zurückzuholen.`,
},
+1 -1
View File
@@ -1101,7 +1101,7 @@ const en = {
autosaveQuota:
'The document is too large to save automatically (browser storage limit reached). This usually happens with large embedded images. Save it as an .odt file to keep your work.',
documentNotLoaded:
'The last document stopped the editor from starting, so it was not loaded. You are working in a new, empty document; the old one is kept in the browser storage under "edentext-doc-broken".',
'The last document stopped the editor from starting. Load it again anyway?\n\nCancel starts a new, empty document; the old one is kept in the browser storage under "edentext-doc-broken".',
picturesNotRestored: (n: number) =>
`${n} picture${n === 1 ? '' : 's'} could not be restored from the browser storage and ${n === 1 ? 'is' : 'are'} shown as broken. Open the document's saved file to get ${n === 1 ? 'it' : 'them'} back.`,
},
+1 -1
View File
@@ -38,7 +38,7 @@ Both margins and orientation are passed into the ODT export so the exported docu
All keys share the `edentext-` prefix — a namespace, not a description: on GitHub Pages every repo of an account shares one origin, so a generic prefix would collide.
- **Document:** `edentext-doc` — TipTap JSON, debounced 1 s on every `onUpdate` (`autosave.ts`); hiding the page (`visibilitychange`/`pagehide`) writes what is pending at once, and writes queue behind each other so a slow picture round trip never stores an older document over a newer one. Loading it raises `edentext-doc-loading`, which only a completed startup clears (`markDocumentLoaded`, two frames after `Editor.svelte`'s mount). Still set at the next load = that document hung or crashed the editor: it moves to `edentext-doc-broken` and the app starts empty, so a reload escapes the freeze instead of repeating it. **Pictures do not live in it:** `imageStore.ts` swaps every data-URI past 4 KB for an `idb:<key>` before the write and keeps the bytes in the `edentext-images` IndexedDB store (a handful of photos is the whole localStorage quota). The key is a content hash of the data-URI's head, tail and length — sampled, because this runs on every autosave — so a picture used twice is stored once, and each save sweeps the keys the document no longer names. Where IndexedDB is unavailable the data-URIs stay inline, exactly as before; a key the store has lost keeps its `idb:` src and is reported, rather than quietly dropping the reader's content. `loadDocument` is therefore **async**, and `Editor.svelte` awaits it before building the editor.
- **Document:** `edentext-doc` — TipTap JSON, debounced 1 s on every `onUpdate` (`autosave.ts`); hiding the page (`visibilitychange`/`pagehide`) writes what is pending at once, and writes queue behind each other so a slow picture round trip never stores an older document over a newer one. Loading it raises `edentext-doc-loading`, which only a completed startup clears (`markDocumentLoaded`, two frames after `Editor.svelte`'s mount). Still set at the next load = that document hung or crashed the editor, or a reload cut its start short: the user is asked whether to try again; declined, it moves to `edentext-doc-broken` and the app starts empty, so a freeze is escaped instead of repeated. **Pictures do not live in it:** `imageStore.ts` swaps every data-URI past 4 KB for an `idb:<key>` before the write and keeps the bytes in the `edentext-images` IndexedDB store (a handful of photos is the whole localStorage quota). The key is a content hash of the data-URI's head, tail and length — sampled, because this runs on every autosave — so a picture used twice is stored once, and each save sweeps the keys the document no longer names. Where IndexedDB is unavailable the data-URIs stay inline, exactly as before; a key the store has lost keeps its `idb:` src and is reported, rather than quietly dropping the reader's content. `loadDocument` is therefore **async**, and `Editor.svelte` awaits it before building the editor.
- **Recent files:** `edentext-recent-files` — `{id, name, at}` per entry, newest first, eight kept (`recentFiles.ts`). Only the label lives here: the `FileSystemFileHandle` that reopens one is structured-cloneable but not JSON, so it sits in the `edentext-recent` IndexedDB store under the same id, and each write sweeps the handles no entry names. Two handles compare only through `isSameEntry`, which is async — hence the walk when recording a file already in the list. A file without a handle is never recorded (nothing could reopen it — no File System Access API, or a template), so the list simply stays empty in Firefox/Safari/Brave; a startup prune drops entries whose handle is gone. Reopening asks for `read` permission again (the API drops it on reload) — a declined prompt keeps the entry for the next try, only a moved or deleted file drops it (with a message). Save re-prompts for write on its own (Save-As fallback in `App.svelte`).
- **Document format:** `edentext-doc-format` — `'docx'` while the open document round-trips as .docx; absent = .odt (`documentName.ts`).
- **Theme:** `edentext-theme` — `'light' | 'dark' | 'allBlack' | 'auto'`.
+4 -4
View File
@@ -72,13 +72,13 @@ export async function loadDocument(): Promise<object | null> {
const raw = localStorage.getItem(STORAGE_KEY);
if (localStorage.getItem(BOOT_KEY)) {
localStorage.removeItem(BOOT_KEY);
if (raw) {
// The user decides: a reload that cut a slow start short is the usual cause, and
// another try costs nothing a reload would not fix. Given up, the document is parked.
if (raw && !confirm(t().dialogs.documentNotLoaded)) {
localStorage.setItem(BROKEN_KEY, raw);
localStorage.removeItem(STORAGE_KEY);
// After paint, so the message doesn't land on a blank app.
requestAnimationFrame(() => alert(t().dialogs.documentNotLoaded));
return null;
}
return null;
}
if (!raw) return null;
let doc: object;
+10
View File
@@ -9,6 +9,7 @@ describe('autosave crash recovery', () => {
localStorage.clear();
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { void cb; return 0; });
vi.stubGlobal('alert', () => {});
vi.stubGlobal('confirm', () => false);
});
it('loads the document again after a startup that completed', async () => {
@@ -26,6 +27,15 @@ describe('autosave crash recovery', () => {
expect(localStorage.getItem('edentext-doc')).toBeNull();
});
it('loads the document again when the user asks for another try', async () => {
vi.stubGlobal('confirm', () => true);
localStorage.setItem('edentext-doc', '{"type":"doc"}');
await loadDocument();
expect(await loadDocument()).toEqual({ type: 'doc' });
expect(localStorage.getItem('edentext-doc-broken')).toBeNull();
expect(localStorage.getItem('edentext-doc-loading')).toBe('1');
});
it('keeps the next document after a recovery', async () => {
localStorage.setItem('edentext-doc', '{"type":"doc","content":[]}');
await loadDocument();