fix: Accept time:0 entries and precompute history entry URLs

Code-review follow-ups:
- validateEntry no longer rejects entries whose time is 0 (epoch); it now
  checks `typeof time === 'number'`, so restoring/uploading such entries
  works instead of silently dropping them.
- History.svelte serializes each entry's "open in new tab" URL once via a
  $derived list instead of calling serializeState (pako deflate) per row on
  every render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sidharth Vinod
2026-06-08 20:22:40 +05:30
co-authored by Claude Opus 4.8
parent 8dd9cb49a0
commit 2db9355b7b
3 changed files with 18 additions and 4 deletions
+8 -3
View File
@@ -105,6 +105,11 @@
// Absolute editor URL for an entry, so the link can be opened in a new tab or copied. // Absolute editor URL for an entry, so the link can be opened in a new tab or copied.
const entryUrl = (state: State): string => const entryUrl = (state: State): string =>
`${window.location.origin}${window.location.pathname}#${serializeState(state)}`; `${window.location.origin}${window.location.pathname}#${serializeState(state)}`;
// Serialize each entry's URL once per change rather than per row on every render.
const entriesWithUrl = $derived(
historyState.entries.map((entry) => ({ ...entry, openUrl: entryUrl(entry.state) }))
);
</script> </script>
<Card onselect={tabSelectHandler} isOpen isClosable={false} {tabs} activeTabID={historyState.mode}> <Card onselect={tabSelectHandler} isOpen isClosable={false} {tabs} activeTabID={historyState.mode}>
@@ -143,8 +148,8 @@
</div> </div>
{/snippet} {/snippet}
<ul class="flex h-full min-w-fit flex-col gap-2 overflow-auto p-2" id="historyList"> <ul class="flex h-full min-w-fit flex-col gap-2 overflow-auto p-2" id="historyList">
{#if historyState.entries.length > 0} {#if entriesWithUrl.length > 0}
{#each historyState.entries as { id, state, time, name, url, type } (id)} {#each entriesWithUrl as { id, state, time, name, url, type, openUrl } (id)}
<li class="flex flex-col gap-2"> <li class="flex flex-col gap-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col"> <div class="flex flex-col">
@@ -167,7 +172,7 @@
{dayjs(time).fromNow()} {dayjs(time).fromNow()}
</span> </span>
<Button <Button
href={entryUrl(state)} href={openUrl}
target="_blank" target="_blank"
rel="noopener" rel="noopener"
size="icon" size="icon"
@@ -158,7 +158,7 @@ export const clearActive = (): void => {
}; };
const validateEntry = (entry: HistoryEntry): boolean => const validateEntry = (entry: HistoryEntry): boolean =>
Boolean(entry && entry.type && entry.state && entry.time); Boolean(entry && entry.type && entry.state) && typeof entry.time === 'number';
export interface RestoreResult { export interface RestoreResult {
restored: number; restored: number;
@@ -241,6 +241,15 @@ describe('restoreEntries', () => {
]); ]);
expect(entriesFor('manual').map((e) => e.time)).toEqual([30, 20, 10]); expect(entriesFor('manual').map((e) => e.time)).toEqual([30, 20, 10]);
}); });
it('restores entries whose time is 0 (epoch) instead of treating them as invalid', () => {
const result = restoreEntries([
{ id: 'm0', name: 'epoch', state: defaultState, time: 0, type: 'manual' }
]);
expect(result.restored).toBe(1);
expect(result.invalid).toBe(0);
expect(entriesFor('manual')).toHaveLength(1);
});
}); });
describe('injectHistoryIDs migration', () => { describe('injectHistoryIDs migration', () => {