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
@@ -31,7 +31,7 @@ EdenText is a web-based, powerful word processor for everything from quick notes
|
||||
> the way. It is tested — the full suite plus LibreOffice round-trip checks run
|
||||
> on every commit — but expect occasional bugs, and keep backups of documents
|
||||
> you care about. The browser copy also keeps the last three versions of the open
|
||||
> document — File ▸ Earlier versions puts one back.
|
||||
> document, and offers them if it ever fails to load one.
|
||||
|
||||
[](https://edentext.app)
|
||||
Sample File from https://sample-files.com/
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
import { loadLineNumbering, saveLineNumbering, DEFAULT_LINE_NUMBERING, type LineNumbering } from './lib/storage/lineNumbering';
|
||||
import { loadFoldMarks, saveFoldMarks } from './lib/storage/foldMarks';
|
||||
import { printMarkup } from './lib/storage/printMarkup.svelte';
|
||||
import { snapshots, loadSnapshots, readSnapshot } from './lib/storage/snapshots.svelte';
|
||||
import { commentsInPane, changesInPane, markupAttrs, setShowChanges, setShowComments } from './lib/storage/markup.svelte';
|
||||
import { loadDocumentLanguage, saveDocumentLanguage, odfFromLanguage, type DocumentLanguage } from './lib/storage/documentLanguage';
|
||||
import { setTableLanguage } from './lib/storage/tableOptions.svelte';
|
||||
@@ -1071,29 +1070,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// The versions the autosave keeps aside, and the reader's label for one.
|
||||
void loadSnapshots();
|
||||
function snapshotLabel(at: number): string {
|
||||
return new Date(at).toLocaleString(locale(), { dateStyle: 'short', timeStyle: 'short' });
|
||||
}
|
||||
|
||||
// Put an earlier version's text back. The page setup, the styles and the zones
|
||||
// belong to the open document, so only the text is replaced.
|
||||
async function handleRestoreSnapshot(at: number) {
|
||||
exportMenuOpen = false;
|
||||
if (!editor || !confirm(t().dialogs.confirmRestore(snapshotLabel(at)))) return;
|
||||
const json = await readSnapshot(at);
|
||||
if (!json) {
|
||||
alert(t().dialogs.snapshotGone);
|
||||
void loadSnapshots();
|
||||
return;
|
||||
}
|
||||
loadContent(json as Content);
|
||||
documentEpoch++;
|
||||
resetHistory();
|
||||
editor.commands.focus();
|
||||
}
|
||||
|
||||
function handleForgetRecent() {
|
||||
forgetRecentFiles();
|
||||
recentFiles = [];
|
||||
@@ -1371,8 +1347,6 @@
|
||||
recentFiles={recentFiles}
|
||||
onOpenRecent={(id) => { const f = recentFiles.find((r) => r.id === id); if (f) void handleOpenRecent(f); }}
|
||||
onForgetRecent={handleForgetRecent}
|
||||
snapshots={snapshots().map((at) => ({ at, label: snapshotLabel(at) }))}
|
||||
onRestoreSnapshot={(at) => void handleRestoreSnapshot(at)}
|
||||
onExportPdf={handleExportPdf}
|
||||
onPrintPdf={handlePrintPdf}
|
||||
onPrint={handlePrint}
|
||||
@@ -1510,14 +1484,6 @@
|
||||
<span class="theme-option-hint">{t().app.clearRecentFiles}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if snapshots().length}
|
||||
<div class="theme-heading">{t().app.versions}</div>
|
||||
{#each snapshots() as at (at)}
|
||||
<button class="theme-option" onclick={() => handleRestoreSnapshot(at)} role="menuitem">
|
||||
<span class="recent-name">{snapshotLabel(at)}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<button class="theme-option" onclick={() => { exportMenuOpen = false; passwordSetOpen = true; }} role="menuitem">
|
||||
<span>{t().password.menu}</span>
|
||||
{#if docProtected}<span class="theme-option-hint">{t().password.menuOn}</span>{/if}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
hasPassword = false,
|
||||
onNew, onNewFromTemplate, onOpen, onSave, onSaveAs, onSaveDocx, onSaveTemplate, onExportPdf, onPrintPdf, onPrint, onAbout, onDocProperties, onProtect, onAutoCorrect, onAutoText, onNewComment,
|
||||
navigatorOpen = false, onToggleNavigator,
|
||||
recentFiles = [], onOpenRecent, onForgetRecent, snapshots = [], onRestoreSnapshot,
|
||||
recentFiles = [], onOpenRecent, onForgetRecent,
|
||||
}: {
|
||||
editor: Editor | null;
|
||||
tick: number;
|
||||
@@ -118,8 +118,6 @@
|
||||
onSaveDocx?: () => void;
|
||||
onSaveTemplate?: () => void;
|
||||
recentFiles?: { id: string; name: string }[];
|
||||
snapshots?: { at: number; label: string }[];
|
||||
onRestoreSnapshot?: (at: number) => void;
|
||||
onOpenRecent?: (id: string) => void;
|
||||
onForgetRecent?: () => void;
|
||||
onExportPdf?: () => void;
|
||||
@@ -250,15 +248,6 @@
|
||||
<span class="menu-sub">{t().app.clearRecentFiles}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if snapshots.length}
|
||||
<hr />
|
||||
{#each snapshots as s (s.at)}
|
||||
<button onclick={() => { closeMenu(); onRestoreSnapshot?.(s.at); }}>
|
||||
<Icon name="undo" size={16} />{s.label}
|
||||
<span class="menu-sub">{t().app.versions}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<hr />
|
||||
<button onclick={() => run(onPrint)} disabled={!editor || pdfBusy}>
|
||||
<Icon name="print" size={16} />{t().app.print}
|
||||
|
||||
@@ -1053,7 +1053,6 @@ const de: Messages = {
|
||||
vectorHint: 'Scharf, aber Probleme mit Tabellen',
|
||||
template: 'Vorlage',
|
||||
templateHint: '.ott / .dotx — öffnet sich als neues Dokument',
|
||||
versions: 'Frühere Fassungen',
|
||||
recentFiles: 'Zuletzt verwendet',
|
||||
clearRecentFiles: 'Liste leeren',
|
||||
print: 'Drucken',
|
||||
@@ -1097,9 +1096,8 @@ const de: Messages = {
|
||||
couldNotPrintPdf: 'Druck als PDF nicht möglich.',
|
||||
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.',
|
||||
confirmRestore: (when) =>
|
||||
`Fassung von ${when} wiederherstellen? Der aktuelle Text wird dadurch ersetzt; Seitenformat, Vorlagen und Kopfzeile bleiben, wie sie sind.`,
|
||||
snapshotGone: 'Diese Fassung ist nicht mehr im Browser-Speicher.',
|
||||
openSnapshot: (when: string) =>
|
||||
`Das Dokument konnte nicht geladen werden. Stattdessen die vom Editor gesicherte Fassung von ${when} öffnen?`,
|
||||
documentNotLoaded:
|
||||
'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.',
|
||||
remoteImagesDropped: (n) =>
|
||||
|
||||
@@ -1059,7 +1059,6 @@ const en = {
|
||||
vectorHint: 'Sharp, but problems with tables',
|
||||
template: 'Template',
|
||||
templateHint: '.ott / .dotx — opens as a new document',
|
||||
versions: 'Earlier versions',
|
||||
recentFiles: 'Recent files',
|
||||
clearRecentFiles: 'Clear the list',
|
||||
print: 'Print',
|
||||
@@ -1103,9 +1102,8 @@ const en = {
|
||||
couldNotPrintPdf: 'Could not print to PDF.',
|
||||
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.',
|
||||
confirmRestore: (when: string) =>
|
||||
`Restore the version from ${when}? It replaces the current text; page setup, styles and the header stay as they are.`,
|
||||
snapshotGone: 'That version is no longer in the browser storage.',
|
||||
openSnapshot: (when: string) =>
|
||||
`The document could not be loaded. Open the version the editor kept at ${when} instead?`,
|
||||
documentNotLoaded:
|
||||
'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".',
|
||||
remoteImagesDropped: (n: number) =>
|
||||
|
||||
@@ -38,8 +38,8 @@ 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, 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.
|
||||
- **Earlier versions:** the `edentext-snapshots` IndexedDB store (`snapshots.svelte.ts`) — up to three earlier autosaves, one taken at most every five minutes, keyed by their timestamp. They hold the document **whole**, pictures inline: the image store is swept against the open document, so an older version could not point into it. Both file menus list them; restoring puts only the **text** back, since the page setup, the styles and the zones belong to the open document. A reactive singleton, so the list a menu shows is the one the last autosave wrote.
|
||||
- **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 kept versions are offered before 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.
|
||||
- **Earlier versions:** the `edentext-snapshots` IndexedDB store (`snapshots.ts`) — up to three earlier autosaves, one taken at most every five minutes, keyed by their timestamp. They hold the document **whole**, pictures inline: the image store is swept against the open document, so an older version could not point into it. **Nothing in the UI points at them**: they are offered only where the browser copy has failed the reader — `loadDocument`'s recovery question, newest first, one confirm each (the newest may be the one that broke). A version taken up starts under the boot flag like any other document, so one that freezes the editor again brings the question back.
|
||||
- **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 protected:** `edentext-doc-protected` — `'1'` while the open document is password-protected; the password itself is never stored, so after a reload the first save asks for it again or lets the protection go (`documentName.ts`).
|
||||
- **Document format:** `edentext-doc-format` — `'docx'` while the open document round-trips as .docx; absent = .odt (`documentName.ts`). Set by the format a file was opened in, and by the extension picked in Save As.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import { t, locale } from '../i18n/i18n.svelte';
|
||||
import { stashImages, putImages, restoreImages } from './imageStore';
|
||||
import { keepSnapshot } from './snapshots.svelte';
|
||||
import { keepSnapshot, listSnapshots, readSnapshot } from './snapshots';
|
||||
|
||||
const STORAGE_KEY = 'edentext-doc';
|
||||
// Set while a stored document is being handed to the editor, cleared once the editor
|
||||
@@ -72,16 +72,37 @@ if (typeof document !== 'undefined') {
|
||||
addEventListener('pagehide', flushDocument);
|
||||
}
|
||||
|
||||
// A document that cannot be loaded is the one place the kept versions are offered:
|
||||
// nothing in the UI points at them, so this is where a reader meets them. Newest first,
|
||||
// one question each — the newest may be the one that broke.
|
||||
async function offerSnapshot(): Promise<object | null> {
|
||||
for (const at of await listSnapshots()) {
|
||||
const when = new Date(at).toLocaleString(locale(), { dateStyle: 'short', timeStyle: 'short' });
|
||||
if (!confirm(t().dialogs.openSnapshot(when))) continue;
|
||||
const doc = await readSnapshot(at);
|
||||
if (doc) return doc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadDocument(): Promise<object | null> {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (localStorage.getItem(BOOT_KEY)) {
|
||||
localStorage.removeItem(BOOT_KEY);
|
||||
// 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);
|
||||
return null;
|
||||
// another try costs nothing a reload would not fix. Given up, the document is parked
|
||||
// and the versions the autosave kept aside are offered in its place.
|
||||
if (!raw || !confirm(t().dialogs.documentNotLoaded)) {
|
||||
if (raw) {
|
||||
localStorage.setItem(BROKEN_KEY, raw);
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
const rescued = await offerSnapshot();
|
||||
if (!rescued) return null;
|
||||
// Under the same flag as any other document: one that freezes the editor again
|
||||
// brings this question back instead of repeating the freeze.
|
||||
localStorage.setItem(BOOT_KEY, '1');
|
||||
return rescued;
|
||||
}
|
||||
}
|
||||
if (!raw) return null;
|
||||
|
||||
@@ -11,14 +11,8 @@ const STORE = 'snapshots';
|
||||
const KEEP = 3;
|
||||
const EVERY_MS = 5 * 60_000;
|
||||
|
||||
let list = $state<number[]>([]);
|
||||
let lastAt = 0;
|
||||
|
||||
/** When each kept version was taken, newest first. */
|
||||
export function snapshots(): number[] {
|
||||
return list;
|
||||
}
|
||||
|
||||
async function withDb<T>(run: (db: IDBDatabase) => Promise<T>): Promise<T | null> {
|
||||
if (typeof indexedDB === 'undefined') return null;
|
||||
try {
|
||||
@@ -44,12 +38,12 @@ export async function keepSnapshot(json: object): Promise<void> {
|
||||
await idbRequest(db, STORE, 'readwrite', (s) => s.put(json, at));
|
||||
const all = (await keys(db)).map(Number).sort((a, b) => b - a);
|
||||
for (const old of all.slice(KEEP)) await idbRequest(db, STORE, 'readwrite', (s) => s.delete(old));
|
||||
list = all.slice(0, KEEP);
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadSnapshots(): Promise<void> {
|
||||
await withDb(async (db) => { list = (await keys(db)).map(Number).sort((a, b) => b - a); });
|
||||
/** When each kept version was taken, newest first. */
|
||||
export async function listSnapshots(): Promise<number[]> {
|
||||
return (await withDb(async (db) => (await keys(db)).map(Number).sort((a, b) => b - a))) ?? [];
|
||||
}
|
||||
|
||||
export async function readSnapshot(at: number): Promise<object | null> {
|
||||
@@ -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