Don't cache embedded GEDCOM in sessionStorage (#308)

This commit is contained in:
Kamil Jiwa
2026-06-21 08:57:31 -07:00
committed by GitHub
parent b6e3a95741
commit 74bef35955
3 changed files with 58 additions and 10 deletions

View File

@@ -61,7 +61,15 @@ export class EmbeddedDataSource implements DataSource<EmbeddedSourceSpec> {
try {
const embeddedHash = 'embedded';
storeGedcom(embeddedHash, gedcom, new Map());
const data = await loadGedcom(embeddedHash, onProgress);
// Embedded data is volatile: the parent re-posts a fresh GEDCOM on every
// load (and a page reload re-runs the ready/parent_ready handshake, so
// the parent re-sends). That GEDCOM may inline object URLs (blob:/data:)
// that die with the previous document. Persisting it to sessionStorage
// under the constant 'embedded' key would return dead URLs on the next
// load, so opt out of the session cache for embedded data.
const data = await loadGedcom(embeddedHash, onProgress, {
useSessionCache: false,
});
const software = getSoftware(data.gedcom.head);
analyticsEvent('embedded_file_loaded', {
event_label: software,

View File

@@ -35,9 +35,10 @@ async function prepareData(
cacheId: string,
images?: Map<string, string>,
onProgress?: (status: string) => void,
useSessionCache = true,
): Promise<TopolaData> {
const data = await convertGedcom(gedcom, images || new Map(), onProgress);
if (!images || images.size === 0) {
if (useSessionCache && (!images || images.size === 0)) {
if (gedcom.length <= SESSION_CACHE_GEDCOM_LIMIT) {
const dataToSerialize = {...data};
delete dataToSerialize.images;
@@ -184,14 +185,18 @@ export async function loadFromUrl(
export async function loadGedcom(
hash: string,
onProgress?: (status: string) => void,
options?: {useSessionCache?: boolean},
): Promise<TopolaData> {
try {
const cachedData = sessionStorage.getItem(hash);
if (cachedData) {
return JSON.parse(cachedData);
const useSessionCache = options?.useSessionCache ?? true;
if (useSessionCache) {
try {
const cachedData = sessionStorage.getItem(hash);
if (cachedData) {
return JSON.parse(cachedData);
}
} catch (e) {
console.warn('Failed to load data from session storage: ' + e);
}
} catch (e) {
console.warn('Failed to load data from session storage: ' + e);
}
// Retrieve from the in-memory store (survives within-tab navigation even
// when the GEDCOM is too large for history.pushState or sessionStorage).
@@ -203,7 +208,13 @@ export async function loadGedcom(
);
}
try {
return await prepareData(stored.gedcom, hash, stored.images, onProgress);
return await prepareData(
stored.gedcom,
hash,
stored.images,
onProgress,
useSessionCache,
);
} catch (error) {
revokeObjectUrls(stored.images);
throw error;

View File

@@ -1,6 +1,7 @@
import {describe, expect, it} from '@jest/globals';
import {beforeEach, describe, expect, it} from '@jest/globals';
import {storeGedcom} from './gedcom_store';
import {loadGedcom} from './load_data';
import {mockSessionStorage} from './test_helpers';
// Minimal GEDCOM with 1 individual and 1 family so convertGedcom succeeds.
const MINIMAL_GEDCOM = [
@@ -43,3 +44,31 @@ describe('loadGedcom()', () => {
]);
});
});
describe('loadGedcom() session cache', () => {
let sessionStorageMock: {[key: string]: string};
beforeEach(() => {
sessionStorageMock = mockSessionStorage();
});
it('caches prepared data in sessionStorage by default', async () => {
const hash = 'cache-default-hash';
storeGedcom(hash, MINIMAL_GEDCOM, new Map());
await loadGedcom(hash);
expect(sessionStorageMock[hash]).toBeDefined();
});
it('ignores and preserves the cache when useSessionCache is false', async () => {
const hash = 'cache-disabled-hash';
sessionStorageMock[hash] = JSON.stringify({stale: true});
storeGedcom(hash, MINIMAL_GEDCOM, new Map());
const data = await loadGedcom(hash, undefined, {useSessionCache: false});
// Returned from the in-memory store, not the stale cache.
expect(data.chartData.indis).toHaveLength(1);
// The stale entry is neither served nor overwritten with fresh data.
expect(sessionStorageMock[hash]).toBe(JSON.stringify({stale: true}));
});
});