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
+63 -46
View File
@@ -42,7 +42,7 @@ class MockNostrDataService extends Service {
}
class MockOsmService extends Service {
getCachedOsmObject() {
async getCachedOsmObject() {
return null;
}
@@ -51,6 +51,12 @@ class MockOsmService extends Service {
}
}
const NAME_CACHE_STORE = 'contributions-name-cache';
function flushPromises() {
return new Promise((resolve) => setTimeout(resolve, 50));
}
module('Unit | Service | contributions', function (hooks) {
setupTest(hooks);
@@ -58,12 +64,6 @@ module('Unit | Service | contributions', function (hooks) {
this.owner.register('service:storage', MockStorageService);
this.owner.register('service:nostrData', MockNostrDataService);
this.owner.register('service:osm', MockOsmService);
localStorage.removeItem('marco:contributions:name_cache');
});
hooks.afterEach(function () {
localStorage.removeItem('marco:contributions:name_cache');
});
test('_updateItems resolves names from bookmarks immediately', function (assert) {
@@ -85,7 +85,7 @@ module('Unit | Service | contributions', function (hooks) {
);
});
test('_updateItems resolves names from the persistent name cache', function (assert) {
test('_updateItems resolves names from the persistent name cache', async function (assert) {
const events = [
makePhotoEvent({
id: 'e1',
@@ -95,36 +95,66 @@ module('Unit | Service | contributions', function (hooks) {
];
const service = this.owner.lookup('service:contributions');
service._nameCache.set('osm:node:999', 'Cached Park');
await service.localForage.set(
NAME_CACHE_STORE,
'osm:node:999',
'Cached Park'
);
service._updateItems(events);
await flushPromises();
assert.strictEqual(service.items[0].placeName, 'Cached Park');
assert.false(service.items[0].placeNameLoading);
});
test('name cache is persisted to localStorage', function (assert) {
const service = this.owner.lookup('service:contributions');
service._nameCache.set('osm:node:42', 'Test Place');
service._saveNameCache();
test('resolved names are persisted to the IndexedDB name cache', async function (assert) {
const events = [
makePhotoEvent({
id: 'e1',
created_at: 1000,
placeIdentifier: 'osm:node:42',
}),
];
const raw = localStorage.getItem('marco:contributions:name_cache');
assert.ok(raw, 'Cache entry exists in localStorage');
const parsed = JSON.parse(raw);
assert.strictEqual(parsed['osm:node:42'], 'Test Place');
const service = this.owner.lookup('service:contributions');
service.osm.fetchOsmObjectsBatch = async () => {
const map = new Map();
map.set('node:42', { title: 'Test Place' });
return map;
};
service._updateItems(events);
await flushPromises();
assert.strictEqual(
await service.localForage.get(NAME_CACHE_STORE, 'osm:node:42'),
'Test Place',
'Name is persisted to the IndexedDB name cache'
);
});
test('name cache is loaded from localStorage on construction', function (assert) {
localStorage.setItem(
'marco:contributions:name_cache',
JSON.stringify({ 'osm:node:77': 'Persisted Place' })
);
test('names stored in a previous session are resolved from the IndexedDB cache', async function (assert) {
const events = [
makePhotoEvent({
id: 'e1',
created_at: 1000,
placeIdentifier: 'osm:node:77',
}),
];
const service = this.owner.lookup('service:contributions');
assert.strictEqual(
service._nameCache.get('osm:node:77'),
await service.localForage.set(
NAME_CACHE_STORE,
'osm:node:77',
'Persisted Place'
);
service._updateItems(events);
await flushPromises();
assert.strictEqual(service.items[0].placeName, 'Persisted Place');
assert.false(service.items[0].placeNameLoading);
});
test('batch fetch failure applies a fallback name so items do not stay loading', async function (assert) {
@@ -143,7 +173,7 @@ module('Unit | Service | contributions', function (hooks) {
service._updateItems(events);
// Wait for the background batch to complete
await new Promise((resolve) => setTimeout(resolve, 50));
await flushPromises();
assert.false(
service.items[0].placeNameLoading,
@@ -169,31 +199,18 @@ module('Unit | Service | contributions', function (hooks) {
service.osm.fetchOsmObjectsBatch = async () => new Map();
service._updateItems(events);
await new Promise((resolve) => setTimeout(resolve, 50));
await flushPromises();
// Fallback should be shown but NOT cached
assert.strictEqual(service.items[0].placeName, 'OSM node 666');
assert.false(
service._nameCache.has('osm:node:666'),
'Fallback is not stored in the name cache'
assert.notOk(
await service.localForage.get(NAME_CACHE_STORE, 'osm:node:666'),
'No fallback name persisted in the IndexedDB name cache'
);
assert.true(
service._unresolvable.has('osm:node:666'),
'Item is marked unresolvable for this session'
);
// Verify the fallback was NOT persisted to localStorage
service._saveNameCache();
const raw = localStorage.getItem('marco:contributions:name_cache');
if (raw) {
const parsed = JSON.parse(raw);
assert.notOk(
parsed['osm:node:666'],
'No fallback name persisted in localStorage'
);
} else {
assert.true(true, 'No cache entry was persisted');
}
});
test('unresolvable items are not re-fetched within the same session', async function (assert) {
@@ -214,14 +231,14 @@ module('Unit | Service | contributions', function (hooks) {
// First update triggers a fetch
service._updateItems(events);
await new Promise((resolve) => setTimeout(resolve, 50));
await flushPromises();
assert.strictEqual(fetchCount, 1, 'First update triggers a fetch');
assert.strictEqual(service.items[0].placeName, 'OSM node 777');
// Second update should NOT trigger another fetch (already unresolvable)
service._updateItems(events);
await new Promise((resolve) => setTimeout(resolve, 50));
await flushPromises();
assert.strictEqual(fetchCount, 1, 'Second update does not re-fetch');
assert.strictEqual(
@@ -248,11 +265,11 @@ module('Unit | Service | contributions', function (hooks) {
};
service._updateItems(events);
await new Promise((resolve) => setTimeout(resolve, 50));
await flushPromises();
assert.strictEqual(service.items[0].placeName, 'Resolved Café');
assert.strictEqual(
service._nameCache.get('osm:node:111'),
await service.localForage.get(NAME_CACHE_STORE, 'osm:node:111'),
'Resolved Café',
'Name is stored in the name cache'
);
+2 -2
View File
@@ -348,8 +348,8 @@ module('Unit | Service | osm', function (hooks) {
'Batch fetch does not write to the in-memory OSM cache'
);
assert.notOk(
localStorage.getItem('marco:osm_cache:node:100'),
'Batch fetch does not write to the localStorage OSM cache'
await service.localForage.get('osm-cache', 'node:100'),
'Batch fetch does not write to the persistent OSM cache'
);
});