import { LEGACY_IMPORTED_KEY, readLegacyData, type LegacyData } from '$lib/storage/legacy-import'; import { parseVisibility, VISIBILITY_KEY, type Visibility } from '$lib/engine/file-visibility'; import type { Outcome, StorageOpened } from 'gpx-wasm'; import type { Backend } from './backend'; /** Name of the IndexedDB database that keeps the files and the settings. */ const STORAGE_NAME = 'gpx-studio'; /** Name of the channel that tells the tabs that the files were written. */ const CHANNEL_NAME = 'gpx-studio-storage'; /** What opening the storage needs of the engine facade. */ export type StorageHost = { backend: Backend; /** The explicit visibility per file, to fill with the one that was saved. */ visibility: Map; /** Applies what a call changed to what the facade keeps of the files. */ sync(update: Outcome): Promise; /** Tells that what is stored comes from a newer version of the app: nothing can be saved. */ outdated(): void; hasFiles(): boolean; loadFiles(files: { data: Uint8Array; name: string }[]): Promise; deselect(): Promise; }; /** * Keeps the files of the engine as the ones that another tab saved: when a tab writes, the others * are told, and read the storage again if it has changed since they last did, which overwrites * what they hold (see `sync_storage`). The same happens when a write of this tab is refused * because of that, and when the tab is shown again (a hidden tab can miss messages). */ function followOtherTabs(host: StorageHost): { saved: () => void; behind: () => void; newer: () => void; } { const { backend } = host; let reading = Promise.resolve(); const refresh = () => { reading = reading .then(async () => host.sync(await backend.sync_storage())) .catch((error) => console.error('The files could not be read again', error)); }; const channel = typeof BroadcastChannel === 'undefined' ? undefined : new BroadcastChannel(CHANNEL_NAME); if (channel) { channel.onmessage = refresh; } if (typeof document !== 'undefined') { document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { refresh(); } }); } return { saved: () => channel?.postMessage('saved'), behind: refresh, newer: () => host.outdated(), }; } /** * Opens the storage: the files that it holds are put in the engine, and the * changes of the files are kept from then on. Resolves to the settings, as JSON strings by key. * * The first time, what the previous versions of the app kept in the browser is imported. */ export async function openStoredFiles( host: StorageHost, readLegacy: () => Promise = readLegacyData ): Promise> { const { backend } = host; let opened: StorageOpened; const tabs = followOtherTabs(host); try { opened = await backend.open_storage(STORAGE_NAME, tabs.saved, tabs.behind, tabs.newer); } catch (error) { console.error('The files cannot be kept in this browser', error); return {}; } let settings = opened.settings; // what was hidden, which the files that were restored need when they are read parseVisibility(settings[VISIBILITY_KEY]).forEach((visibility, fileId) => host.visibility.set(fileId, visibility) ); await host.sync(opened.outcome); if (backend.importsLegacyData && settings[LEGACY_IMPORTED_KEY] === undefined) { settings = { ...settings, ...(await importLegacyData(host, settings, readLegacy)) }; } return settings; } /** * Puts the files and the settings of the former database in the engine. It only counts as done once * the files are written: until then, it is tried again the next time. The files that are stored * mean that it was done, by a visit that did not get to say so. */ async function importLegacyData( host: StorageHost, stored: Record, readLegacy: () => Promise ): Promise> { const { backend } = host; const imported: Record = {}; if (!host.hasFiles()) { try { const legacy = await readLegacy(); if (legacy) { // what was changed since a previous try is more recent for (const [key, value] of Object.entries(legacy.settings)) { if (stored[key] === undefined) { await backend.set_setting(key, value); imported[key] = value; } } if (legacy.files.length > 0 && !(await importFiles(host, legacy))) { return imported; } } } catch (error) { console.error('The files of the previous version could not be imported', error); return imported; } } await backend.set_setting(LEGACY_IMPORTED_KEY, 'true'); imported[LEGACY_IMPORTED_KEY] = 'true'; return imported; } /** Loads the files of the former database, and tells whether they are written. */ async function importFiles(host: StorageHost, legacy: LegacyData): Promise { const encoder = new TextEncoder(); const loaded = await host.loadFiles( legacy.files.map((file) => ({ data: encoder.encode(file.gpx), name: file.name, })) ); if (!loaded) { console.error('The files of the previous version could not be loaded'); return false; } await host.deselect(); if (!(await host.backend.flush_storage())) { console.error('The files of the previous version could not be written'); return false; } return true; }