refactor: the kept versions are offered at a failed load, not in the menu
A list of timestamps in both file menus paid for itself only on the day something went wrong, and it was in the way on every other. The entries are gone; loadDocument now offers the versions where the browser copy has actually failed the reader — newest first, one question each, and the one taken up starts under the boot flag like any other document. The store loses its reactive singleton with them.
This commit is contained in:
1 parent
ea327174e8
commit
6abc7f7704
11 files changed
+109
-104
No files matched your search
@@ -0,0 +1,27 @@
|
||||
// A two-call IndexedDB in memory — what openDb/idbRequest (storage/idb.ts) ask for.
|
||||
export function fakeIndexedDb() {
|
||||
const data = new Map<number, unknown>();
|
||||
const req = (result: unknown) => {
|
||||
const r: Record<string, unknown> = { result };
|
||||
queueMicrotask(() => (r.onsuccess as (() => void) | undefined)?.());
|
||||
return r;
|
||||
};
|
||||
const store = {
|
||||
put: (v: unknown, k: number) => { data.set(k, v); return req(undefined); },
|
||||
get: (k: number) => req(data.get(k)),
|
||||
getAllKeys: () => req([...data.keys()]),
|
||||
delete: (k: number) => { data.delete(k); return req(undefined); },
|
||||
};
|
||||
const db = { transaction: () => ({ objectStore: () => store }), createObjectStore: () => {}, close: () => {} };
|
||||
return {
|
||||
data,
|
||||
open: () => {
|
||||
const r: Record<string, unknown> = { result: db };
|
||||
queueMicrotask(() => {
|
||||
(r.onupgradeneeded as (() => void) | undefined)?.();
|
||||
(r.onsuccess as (() => void) | undefined)?.();
|
||||
});
|
||||
return r;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { loadDocument, markDocumentLoaded, saveDocument } from '../../src/lib/storage/autosave';
|
||||
import { keepSnapshot } from '../../src/lib/storage/snapshots';
|
||||
import { fakeIndexedDb } from '../fakeIdb';
|
||||
|
||||
// A document that freezes the editor would be reloaded from localStorage forever, so
|
||||
// loading raises a boot flag that only a completed startup clears. Loading is async
|
||||
@@ -48,6 +50,43 @@ describe('autosave crash recovery', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Nothing in the UI points at the kept versions: a document that cannot be loaded is
|
||||
// where they are offered, one question each, newest first.
|
||||
describe('a document that cannot be loaded falls back on a kept version', () => {
|
||||
// A version is kept at most every five minutes, and an earlier test in this file has
|
||||
// already taken one — so each case sets its own clock well past that window.
|
||||
let clock = 2_000_000_000_000;
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { void cb; return 0; });
|
||||
vi.stubGlobal('alert', () => {});
|
||||
vi.stubGlobal('indexedDB', { open: fakeIndexedDb().open });
|
||||
vi.spyOn(Date, 'now').mockReturnValue((clock += 600_000));
|
||||
});
|
||||
|
||||
it('offers the version once the reader gives up on the document', async () => {
|
||||
await keepSnapshot({ type: 'doc', content: [{ type: 'paragraph' }] });
|
||||
localStorage.setItem('edentext-doc', '{"type":"doc"}');
|
||||
await loadDocument(); // raises the boot flag, then the "editor hangs"
|
||||
// First question: another try, declined. Second: the kept version, taken.
|
||||
let asked = 0;
|
||||
vi.stubGlobal('confirm', () => asked++ > 0);
|
||||
expect(await loadDocument()).toEqual({ type: 'doc', content: [{ type: 'paragraph' }] });
|
||||
expect(asked).toBe(2);
|
||||
// The parked document stays where it was, and the version starts under the flag.
|
||||
expect(localStorage.getItem('edentext-doc-broken')).toBe('{"type":"doc"}');
|
||||
expect(localStorage.getItem('edentext-doc-loading')).toBe('1');
|
||||
});
|
||||
|
||||
it('starts empty when every version is turned down', async () => {
|
||||
await keepSnapshot({ type: 'doc', content: [] });
|
||||
localStorage.setItem('edentext-doc', '{"type":"doc"}');
|
||||
await loadDocument();
|
||||
vi.stubGlobal('confirm', () => false);
|
||||
expect(await loadDocument()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// The debounce is a second; a tab closed inside it would lose that second. Hiding
|
||||
// the page writes what is pending at once, without the IndexedDB round trip.
|
||||
describe('autosave flush on pagehide', () => {
|
||||
|
||||
@@ -1,38 +1,11 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// A two-call IndexedDB in memory — what openDb/idbRequest (storage/idb.ts) ask for.
|
||||
function fakeIndexedDb() {
|
||||
const data = new Map<number, unknown>();
|
||||
const req = (result: unknown) => {
|
||||
const r: Record<string, unknown> = { result };
|
||||
queueMicrotask(() => (r.onsuccess as (() => void) | undefined)?.());
|
||||
return r;
|
||||
};
|
||||
const store = {
|
||||
put: (v: unknown, k: number) => { data.set(k, v); return req(undefined); },
|
||||
get: (k: number) => req(data.get(k)),
|
||||
getAllKeys: () => req([...data.keys()]),
|
||||
delete: (k: number) => { data.delete(k); return req(undefined); },
|
||||
};
|
||||
const db = { transaction: () => ({ objectStore: () => store }), createObjectStore: () => {}, close: () => {} };
|
||||
return {
|
||||
data,
|
||||
open: () => {
|
||||
const r: Record<string, unknown> = { result: db };
|
||||
queueMicrotask(() => {
|
||||
(r.onupgradeneeded as (() => void) | undefined)?.();
|
||||
(r.onsuccess as (() => void) | undefined)?.();
|
||||
});
|
||||
return r;
|
||||
},
|
||||
};
|
||||
}
|
||||
import { fakeIndexedDb } from '../fakeIdb';
|
||||
|
||||
describe('the autosave keeps a few earlier versions', () => {
|
||||
it('takes one every few minutes and holds on to the newest three', async () => {
|
||||
const idb = fakeIndexedDb();
|
||||
vi.stubGlobal('indexedDB', { open: idb.open });
|
||||
const { keepSnapshot, snapshots, readSnapshot } = await import('../../src/lib/storage/snapshots.svelte');
|
||||
const { keepSnapshot, listSnapshots, readSnapshot } = await import('../../src/lib/storage/snapshots');
|
||||
const now = vi.spyOn(Date, 'now');
|
||||
const at = (min: number) => 1_800_000_000_000 + min * 60_000;
|
||||
|
||||
@@ -40,13 +13,13 @@ describe('the autosave keeps a few earlier versions', () => {
|
||||
await keepSnapshot({ type: 'doc', v: 1 });
|
||||
now.mockReturnValue(at(1));
|
||||
await keepSnapshot({ type: 'doc', v: 2 }); // a minute on: still the same version
|
||||
expect(snapshots()).toEqual([at(0)]);
|
||||
expect(await listSnapshots()).toEqual([at(0)]);
|
||||
|
||||
for (const m of [6, 12, 18]) {
|
||||
now.mockReturnValue(at(m));
|
||||
await keepSnapshot({ type: 'doc', v: m });
|
||||
}
|
||||
expect(snapshots()).toEqual([at(18), at(12), at(6)]);
|
||||
expect(await listSnapshots()).toEqual([at(18), at(12), at(6)]);
|
||||
expect(idb.data.size).toBe(3);
|
||||
expect(await readSnapshot(at(12))).toEqual({ type: 'doc', v: 12 });
|
||||
expect(await readSnapshot(at(0))).toBeNull();
|
||||
|
||||
Reference in new issue
Block a user