Migrate OSM cache to localForage/IndexedDB

We don't want to hit limits with localStorage, and it's non-blocking
This commit is contained in:
2026-08-19 10:39:58 -06:00
parent 45f6f898fa
commit 61242420d1
10 changed files with 1421 additions and 1231 deletions
+34 -32
View File
@@ -4,16 +4,18 @@ import { getCategoryById } from '../utils/poi-categories';
export default class OsmService extends Service {
@service settings;
@service localForage;
controller = null;
cachedResults = null;
lastQueryKey = null;
cachedPlaces = new Map();
// Long-term cache for OSM place metadata, persisted to localStorage so that
// names and basic info survive across sessions and can be rendered instantly
// without waiting on the OSM API. Entries are refreshed in the background.
static CACHE_KEY_PREFIX = 'marco:osm_cache:';
// Long-term cache for OSM place metadata, persisted to IndexedDB (via the
// `localForage` service) so that names and basic info survive across
// sessions and can be rendered instantly without waiting on the OSM API.
// Entries are refreshed in the background.
static STORE_NAME = 'osm-cache';
static CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
static IN_MEMORY_TTL_MS = 10000; // 10 seconds
@@ -21,27 +23,24 @@ export default class OsmService extends Service {
return `${osmType}:${osmId}`;
}
_readLocalCache(osmType, osmId) {
if (typeof localStorage === 'undefined') return null;
const key = `${OsmService.CACHE_KEY_PREFIX}${osmType}:${osmId}`;
async _readLocalCache(osmType, osmId) {
const key = this._buildCacheKey(osmType, osmId);
try {
const raw = localStorage.getItem(key);
const raw = await this.localForage.get(OsmService.STORE_NAME, key);
if (!raw) return null;
const parsed = JSON.parse(raw);
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (
!parsed ||
typeof parsed.timestamp !== 'number' ||
Date.now() - parsed.timestamp > OsmService.CACHE_TTL_MS
) {
localStorage.removeItem(key);
await this.localForage.remove(OsmService.STORE_NAME, key);
return null;
}
return parsed.data;
} catch {
try {
localStorage.removeItem(
`${OsmService.CACHE_KEY_PREFIX}${osmType}:${osmId}`
);
await this.localForage.remove(OsmService.STORE_NAME, key);
} catch {
// ignore
}
@@ -49,31 +48,32 @@ export default class OsmService extends Service {
}
}
_writeLocalCache(osmType, osmId, data) {
if (typeof localStorage === 'undefined' || !data) return;
const key = `${OsmService.CACHE_KEY_PREFIX}${osmType}:${osmId}`;
async _writeLocalCache(osmType, osmId, data) {
if (!data) return;
const key = this._buildCacheKey(osmType, osmId);
try {
localStorage.setItem(
key,
JSON.stringify({ data, timestamp: Date.now() })
);
await this.localForage.set(OsmService.STORE_NAME, key, {
data,
timestamp: Date.now(),
});
} catch (e) {
console.debug('[osm] Failed to write localStorage cache entry', e);
console.debug('[osm] Failed to write IndexedDB cache entry', e);
}
}
/**
* Synchronous lookup for an OSM place. Checks the short-lived in-memory cache
* first, then the persistent localStorage cache. Returns `null` if not cached.
* Async lookup for an OSM place. Checks the short-lived in-memory cache
* first, then the persistent IndexedDB cache. Returns `null` if not
* cached.
*
* Use this for instant rendering (e.g. place names in a list) and fall back to
* `fetchOsmObject` for a fresh fetch + background refresh.
* Use this for instant rendering (e.g. place names in a list) and fall
* back to `fetchOsmObject` for a fresh fetch + background refresh.
*
* @param {string} osmType 'node' | 'way' | 'relation'
* @param {string} osmId
* @returns {object|null} Normalized OSM place data
* @returns {Promise<object|null>} Normalized OSM place data
*/
getCachedOsmObject(osmType, osmId) {
async getCachedOsmObject(osmType, osmId) {
if (!osmType || !osmId) return null;
const cacheKey = this._buildCacheKey(osmType, osmId);
@@ -103,7 +103,9 @@ export default class OsmService extends Service {
this.cachedPlaces.delete(cacheKey);
}, OsmService.IN_MEMORY_TTL_MS);
this._writeLocalCache(osmType, osmId, data);
// Fire-and-forget the persistent write — the in-memory cache covers the
// next immediate read, and the IndexedDB write happens in the background.
void this._writeLocalCache(osmType, osmId, data);
}
cancelAll() {
@@ -336,12 +338,12 @@ out center;
return cached.data;
}
// If we have a persistent (localStorage) cache entry, return it immediately and
// If we have a persistent (IndexedDB) cache entry, return it immediately and
// kick off a background refresh. This keeps the UI snappy while still ensuring
// the cache is updated with the latest OSM data.
const localCached = this._readLocalCache(osmType, osmId);
const localCached = await this._readLocalCache(osmType, osmId);
if (localCached) {
console.debug(`Using localStorage cached OSM object for ${cacheKey}`);
console.debug(`Using IndexedDB cached OSM object for ${cacheKey}`);
// Refresh in the background, but don't block the caller.
this._refreshOsmObject(osmId, osmType, cacheKey).catch((e) => {
console.debug('[osm] Background refresh failed for', cacheKey, e);
@@ -415,7 +417,7 @@ out center;
for (const { osmType, osmId } of items) {
if (!osmType || !osmId) continue;
const cacheKey = this._buildCacheKey(osmType, osmId);
const cached = this.getCachedOsmObject(osmType, osmId);
const cached = await this.getCachedOsmObject(osmType, osmId);
if (cached) {
result.set(cacheKey, cached);
} else {