From 6e87ef3573a12df23238fb820ac220253632627d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 22 Jan 2026 17:23:50 +0700 Subject: [PATCH] Load all saved place into memory Fixes launching the app with a place URL directly, and will be useful for search etc. later. --- app/components/map.gjs | 12 +++---- app/routes/place.js | 19 +++++++++++ app/services/storage.js | 60 ++++++++++++++++++++++++++++----- tests/unit/routes/place-test.js | 11 ++++++ 4 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 tests/unit/routes/place-test.js diff --git a/app/components/map.gjs b/app/components/map.gjs index 064ce5a..71f9c1c 100644 --- a/app/components/map.gjs +++ b/app/components/map.gjs @@ -407,8 +407,8 @@ export default class MapComponent extends Component { // Re-fetch bookmarks when the version changes (triggered by parent action or service) updateBookmarks = modifier(() => { - // Depend on the tracked storage.savedPlaces to automatically update when they change - const places = this.storage.savedPlaces; + // Depend on the tracked storage.placesInView to automatically update when they change + const places = this.storage.placesInView; this.loadBookmarks(places); }); @@ -418,13 +418,13 @@ export default class MapComponent extends Component { if (!places || places.length === 0) { // Fallback or explicit check if we have tracked property usage? - // The service updates 'savedPlaces'. We should probably use that if we want reactiveness. - places = this.storage.savedPlaces; + // The service updates 'placesInView'. We should probably use that if we want reactiveness. + places = this.storage.placesInView; } // Previously: const places = await this.storage.places.getPlaces(); // We no longer want to fetch everything blindly. - // We rely on 'savedPlaces' being updated by handleMapMove calling storage.loadPlacesInBounds. + // We rely on 'placesInView' being updated by handleMapMove calling storage.loadPlacesInBounds. this.bookmarkSource.clear(); @@ -457,7 +457,7 @@ export default class MapComponent extends Component { const bbox = { minLat, minLon, maxLat, maxLon }; await this.storage.loadPlacesInBounds(bbox); - this.loadBookmarks(this.storage.savedPlaces); + this.loadBookmarks(this.storage.placesInView); // Persist view to localStorage try { diff --git a/app/routes/place.js b/app/routes/place.js index 7c614cf..9c5c913 100644 --- a/app/routes/place.js +++ b/app/routes/place.js @@ -16,6 +16,9 @@ export default class PlaceRoute extends Route { return this.loadOsmPlace(osmId, type); } + // Wait for storage sync before checking bookmarks + await this.waitForSync(); + // 1. Try to find in local bookmarks let bookmark = this.storage.findPlaceById(id); @@ -29,6 +32,22 @@ export default class PlaceRoute extends Route { return this.loadOsmPlace(id); } + async waitForSync() { + if (this.storage.initialSyncDone) return; + + console.log('Waiting for initial storage sync...'); + const timeout = 5000; + const start = Date.now(); + + while (!this.storage.initialSyncDone) { + if (Date.now() - start > timeout) { + console.warn('Timed out waiting for initial sync'); + break; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + afterModel(model) { // Notify the Map UI to show the pin if (model) { diff --git a/app/services/storage.js b/app/services/storage.js index 0e69b9e..ed645b1 100644 --- a/app/services/storage.js +++ b/app/services/storage.js @@ -8,6 +8,7 @@ import Geohash from 'latlon-geohash'; export default class StorageService extends Service { rs; + @tracked placesInView = []; @tracked savedPlaces = []; @tracked loadedPrefixes = []; @tracked currentBbox = null; @@ -35,17 +36,60 @@ export default class StorageService extends Service { // console.debug('[rs] client ready'); }); - this.rs.on('sync-done', result => { + this.rs.on('sync-done', (result) => { // console.debug('[rs] sync done:', result); - if (!this.initialSyncDone) { this.initialSyncDone = true; } + if (!this.initialSyncDone) { + this.initialSyncDone = true; + } }); this.rs.scope('/places/').on('change', (event) => { - console.debug(event); + // console.debug(event); + this.handlePlaceChange(event); debounce(this, this.reloadCurrentView, 200); }); } + handlePlaceChange(event) { + const { newValue, relativePath } = event; + + // Remove old entry if exists + // The relativePath is like "geohash/geohash/ULID" or just "ULID" depending on structure. + // Our structure is <2-char>/<2-char>/. + // But let's rely on the ID inside the object if possible, or extract from path. + + // We can't easily identify the ID from just relativePath without parsing logic if it's nested. + // However, for deletions (newValue is undefined), we might need the ID. + // Fortunately, our objects (newValue) contain the ID. + + // If it's a deletion, we need to find the object in our array to remove it. + // Since we don't have the ID in newValue (it's null), we rely on `relativePath`. + // Let's assume the filename is the ID. + const pathParts = relativePath.split('/'); + const id = pathParts[pathParts.length - 1]; + + if (!newValue) { + // Deletion + this.savedPlaces = this.savedPlaces.filter((p) => p.id !== id); + } else { + // Add or Update + // Ensure the object has the ID (it should) + const place = { ...newValue, id }; + + // Update existing or add new + const index = this.savedPlaces.findIndex((p) => p.id === id); + if (index !== -1) { + // Replace + const newPlaces = [...this.savedPlaces]; + newPlaces[index] = place; + this.savedPlaces = newPlaces; + } else { + // Add + this.savedPlaces = [...this.savedPlaces, place]; + } + } + } + get places() { return this.rs.places; } @@ -105,7 +149,7 @@ export default class StorageService extends Service { // Identify existing places that belong to the reloaded prefixes and remove them const prefixSet = new Set(prefixes); - const keptPlaces = this.savedPlaces.filter((place) => { + const keptPlaces = this.placesInView.filter((place) => { if (!place.lat || !place.lon) return false; try { // Calculate 4-char geohash for the existing place @@ -119,15 +163,15 @@ export default class StorageService extends Service { }); // Merge the kept places (from other areas) with the fresh places (from these areas) - this.savedPlaces = [...keptPlaces, ...places]; + this.placesInView = [...keptPlaces, ...places]; } else { // Full reload - this.savedPlaces = places; + this.placesInView = places; } } else { - if (!prefixes) this.savedPlaces = []; + if (!prefixes) this.placesInView = []; } - console.log('Loaded saved places:', this.savedPlaces.length); + console.log('Loaded saved places:', this.placesInView.length); } catch (e) { console.error('Failed to load places:', e); } diff --git a/tests/unit/routes/place-test.js b/tests/unit/routes/place-test.js new file mode 100644 index 0000000..648bfc1 --- /dev/null +++ b/tests/unit/routes/place-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'marco/tests/helpers'; + +module('Unit | Route | place', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup('route:place'); + assert.ok(route); + }); +});