diff --git a/src/datasource/embedded.ts b/src/datasource/embedded.ts index 406d231..d6216f5 100644 --- a/src/datasource/embedded.ts +++ b/src/datasource/embedded.ts @@ -61,7 +61,15 @@ export class EmbeddedDataSource implements DataSource { 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, diff --git a/src/datasource/load_data.ts b/src/datasource/load_data.ts index 1efb31a..d8e4cba 100644 --- a/src/datasource/load_data.ts +++ b/src/datasource/load_data.ts @@ -35,9 +35,10 @@ async function prepareData( cacheId: string, images?: Map, onProgress?: (status: string) => void, + useSessionCache = true, ): Promise { 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 { - 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; diff --git a/src/datasource/load_data_store.spec.ts b/src/datasource/load_data_store.spec.ts index 45eee03..db0ce4e 100644 --- a/src/datasource/load_data_store.spec.ts +++ b/src/datasource/load_data_store.spec.ts @@ -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})); + }); +});