diff --git a/package.json b/package.json
index 1a3c0fe0..7a606f34 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@testing-library/svelte": "3.2.1",
"@types/mermaid": "8.2.9",
"@types/pako": "1.0.3",
+ "@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "5.34.0",
"@typescript-eslint/parser": "5.34.0",
"@vitest/ui": "0.22.1",
@@ -75,7 +76,8 @@
"monaco-mermaid": "1.0.6",
"pako": "2.0.4",
"random-word-slugs": "0.1.6",
- "svg-pan-zoom": "^3.6.1"
+ "svg-pan-zoom": "^3.6.1",
+ "uuid": "^8.3.2"
},
"lint-staged": {
"*.{ts,svelte,js,css,md,json}": [
diff --git a/src/lib/components/history/history.svelte b/src/lib/components/history/history.svelte
index 3e434fa9..525fa3e6 100644
--- a/src/lib/components/history/history.svelte
+++ b/src/lib/components/history/history.svelte
@@ -77,11 +77,11 @@
}
};
- const clearHistory = (date?: number): void => {
- if (!date && !prompt('Clear all saved items?')) {
+ const clearHistory = (id?: string): void => {
+ if (!id && !prompt('Clear all saved items?')) {
return;
}
- clearHistoryData(date);
+ clearHistoryData(id);
};
const restoreHistoryItem = (state: State): void => {
@@ -147,7 +147,7 @@
{#if $historyStore.length > 0}
- {#each $historyStore as { state, time, name, url, type }}
+ {#each $historyStore as { id, state, time, name, url, type }}
@@ -168,7 +168,7 @@
restoreHistoryItem(state)}
> Restore
{#if type !== 'loader'}
- clearHistory(time)}
+ clearHistory(id)}
> Delete
{/if}
diff --git a/src/lib/components/history/history.ts b/src/lib/components/history/history.ts
index ffdd81fa..afcc8574 100644
--- a/src/lib/components/history/history.ts
+++ b/src/lib/components/history/history.ts
@@ -2,7 +2,8 @@ import { derived, writable, get } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import { persist, localStorage } from '@macfja/svelte-persistent-store';
import { generateSlug } from 'random-word-slugs';
-import type { HistoryEntry, HistoryType } from '$lib/types';
+import type { HistoryEntry, HistoryType, Optional } from '$lib/types';
+import { v4 as uuidV4 } from 'uuid';
const MAX_AUTO_HISTORY_LENGTH = 30;
@@ -41,28 +42,37 @@ export const historyStore: Readable
= derived(
}
);
-export const addHistoryEntry = (entry: HistoryEntry): void => {
+export const addHistoryEntry = (entryToAdd: Optional): void => {
+ const entry: HistoryEntry = {
+ ...entryToAdd,
+ id: uuidV4()
+ };
+
if (entry.type === 'loader') {
loaderHistoryStore.update((entries) => [entry, ...entries]);
return;
}
- entry.name = generateSlug(2);
- if (entry.type !== 'auto') {
+
+ if (!entry.name) {
+ entry.name = generateSlug(2);
+ }
+
+ if (entry.type === 'auto') {
+ autoHistoryStore.update((entries) => {
+ if (entries.length >= MAX_AUTO_HISTORY_LENGTH) {
+ entries = entries.slice(0, MAX_AUTO_HISTORY_LENGTH - 1);
+ }
+ return [entry, ...entries];
+ });
+ } else if (entry.type === 'manual') {
manualHistoryStore.update((entries) => [entry, ...entries]);
- return;
}
- autoHistoryStore.update((entries) => {
- if (entries.length === MAX_AUTO_HISTORY_LENGTH) {
- entries.pop();
- }
- return [entry, ...entries];
- });
};
-export const clearHistoryData = (time?: number): void => {
+export const clearHistoryData = (idToClear?: string): void => {
(get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => {
if (get(historyModeStore) !== 'loader') {
- entries = entries.filter((entry) => time && entry.time != time);
+ entries = entries.filter(({ id }) => idToClear && id != idToClear);
}
return entries;
});
@@ -77,6 +87,46 @@ export const getPreviousState = (auto: boolean): string => {
};
export const restoreHistory = (data: HistoryEntry[]) => {
- // Should this replace the current history or append to it?
- manualHistoryStore.set(data);
+ const entries = data.filter(validateEntry);
+ const invalidEntryCount = data.length - entries.length;
+ if (invalidEntryCount > 0) {
+ console.error(`${invalidEntryCount} invalid history entries were removed.`);
+ console.error(data);
+ }
+ if (entries.length > 0) {
+ let entryCount = 0;
+ (entries[0].type === 'auto' ? autoHistoryStore : manualHistoryStore).update((existing) => {
+ const existingIDs = new Set(existing.map(({ id }) => id));
+ const newEntries = entries.filter(({ id }) => !existingIDs.has(id));
+ entryCount = newEntries.length;
+ const combined = [...existing, ...newEntries];
+ combined.sort((a, b) => b.time - a.time);
+ return combined;
+ });
+
+ alert(
+ `${entryCount} entries restored. ${invalidEntryCount} invalid, ${
+ entries.length - entryCount
+ } duplicates.`
+ );
+ } else {
+ alert('No valid entries found.');
+ }
+};
+
+export const injectHistoryIDs = (): void => {
+ const setIDs = (entries: HistoryEntry[]) => {
+ for (const entry of entries) {
+ if (!entry.id) {
+ entry.id = uuidV4();
+ }
+ }
+ return entries;
+ };
+ autoHistoryStore.update(setIDs);
+ manualHistoryStore.update(setIDs);
+};
+
+const validateEntry = (entry: HistoryEntry): boolean => {
+ return entry.type && entry.state && entry.time && true;
};
diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts
index 1aaf954a..806d1f20 100644
--- a/src/lib/types.d.ts
+++ b/src/lib/types.d.ts
@@ -68,13 +68,16 @@ export interface LoaderConfig {
config: GistLoaderConfig | FileLoaderConfig;
}
export type HistoryType = 'auto' | 'manual' | 'loader';
-export interface HistoryEntry {
- state: State;
- time: number;
- name?: string;
- type: HistoryType;
- url?: string;
-}
+export type HistoryEntry = { id: string; state: State; time: number; url?: string } & (
+ | {
+ type: 'loader';
+ name: string;
+ }
+ | {
+ type: HistoryType;
+ name?: string;
+ }
+);
export interface DocConfig {
[key: string]: {
@@ -84,3 +87,4 @@ export interface DocConfig {
}
export type Loader = (url: string) => Promise;
+export type Optional = Pick, K> & Omit;
diff --git a/src/lib/util/migrations.ts b/src/lib/util/migrations.ts
new file mode 100644
index 00000000..6b477746
--- /dev/null
+++ b/src/lib/util/migrations.ts
@@ -0,0 +1,32 @@
+import { writable, get, type Writable } from 'svelte/store';
+import { persist, localStorage } from '@macfja/svelte-persistent-store';
+import { injectHistoryIDs } from '$lib/components/history/history';
+
+interface MigrationState {
+ version: number;
+}
+
+const migrations: { [key: string]: () => void } = {
+ injectHistoryIDs
+};
+
+const migrationStore: Writable = persist(
+ writable({ version: -1 }),
+ localStorage(),
+ 'migrations'
+);
+
+export const applyMigrations = (): void => {
+ const { version }: MigrationState = get(migrationStore);
+ const allMigrations = Object.entries(migrations);
+ if (version === allMigrations.length - 1) {
+ return;
+ }
+ console.log(`Current migration version: v${version}. Migrating to v${allMigrations.length - 1}.`);
+ for (let i = version + 1; i < allMigrations.length; i++) {
+ const [key, fn] = allMigrations[i];
+ console.log(`Applying migration ${i}: ${key}.`);
+ fn();
+ migrationStore.set({ version: i });
+ }
+};
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 26aa808c..097ea9fb 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -6,10 +6,12 @@
import { setTheme, themeStore } from '$lib/util/theme';
import { toggleDarkTheme } from '$lib/util/state';
import { initHandler } from '$lib/util/util';
+ import { applyMigrations } from '$lib/util/migrations';
// This can be removed once https://github.com/sveltejs/kit/issues/1612 is fixed.
// Then move it into src and vite will bundle it automatically.
onMount(() => {
+ applyMigrations();
window.addEventListener('hashchange', async (ev) => {
await initHandler();
});
diff --git a/yarn.lock b/yarn.lock
index d0eaf998..7bd143d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -458,6 +458,11 @@
dependencies:
"@types/jest" "*"
+"@types/uuid@^8.3.4":
+ version "8.3.4"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
+ integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
+
"@types/yauzl@^2.9.1":
version "2.9.1"
resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz"