Load all saved place into memory

Fixes launching the app with a place URL directly, and will be useful
for search etc. later.
This commit is contained in:
Râu Cao 2026-01-22 17:23:50 +07:00
parent 86b85e9a0b
commit 6e87ef3573
Signed by: raucao
GPG Key ID: 37036C356E56CC51
4 changed files with 88 additions and 14 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -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>/<id>.
// 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);
}

View File

@ -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);
});
});