marco/app/services/storage.js
Râu Cao 2a203e8e82
Add initialSyncDone property to storage service
Allows us to know when the first sync cycle has been completed
2026-01-22 16:40:02 +07:00

146 lines
4.6 KiB
JavaScript

import Service from '@ember/service';
import RemoteStorage from 'remotestoragejs';
import Places from '@remotestorage/module-places';
import { tracked } from '@glimmer/tracking';
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
import { debounce } from '@ember/runloop';
import Geohash from 'latlon-geohash';
export default class StorageService extends Service {
rs;
@tracked savedPlaces = [];
@tracked loadedPrefixes = [];
@tracked currentBbox = null;
@tracked version = 0; // Shared version tracker for bookmarks
@tracked initialSyncDone = false;
constructor() {
super(...arguments);
console.log('ohai');
this.rs = new RemoteStorage({
modules: [Places],
});
this.rs.access.claim('places', 'rw');
// Caching strategy:
this.rs.caching.enable('/places/');
window.remoteStorage = this.rs;
// const widget = new Widget(this.rs);
// widget.attach();
this.rs.on('ready', () => {
// console.debug('[rs] client ready');
});
this.rs.on('sync-done', result => {
// console.debug('[rs] sync done:', result);
if (!this.initialSyncDone) { this.initialSyncDone = true; }
});
this.rs.scope('/places/').on('change', (event) => {
console.debug(event);
debounce(this, this.reloadCurrentView, 200);
});
}
get places() {
return this.rs.places;
}
notifyChange() {
this.version++;
debounce(this, this.reloadCurrentView, 200);
}
reloadCurrentView() {
if (!this.currentBbox) return;
// Recalculate prefixes for the current view
const required = getGeohashPrefixesInBbox(this.currentBbox);
console.log('Reloading view due to changes, prefixes:', required);
// Force load these prefixes (bypassing the 'already loaded' check in loadPlacesInBounds)
this.loadAllPlaces(required);
}
async loadPlacesInBounds(bbox) {
// 1. Calculate required prefixes
const requiredPrefixes = getGeohashPrefixesInBbox(bbox);
// 2. Filter out prefixes we've already loaded
const missingPrefixes = requiredPrefixes.filter(
(p) => !this.loadedPrefixes.includes(p)
);
if (missingPrefixes.length === 0) {
// console.log('All prefixes already loaded for this view');
return;
}
console.log('Loading new prefixes:', missingPrefixes);
// 3. Load places for only the new prefixes
await this.loadAllPlaces(missingPrefixes);
// 4. Update our tracked list of loaded prefixes
// Using assignment to trigger reactivity if needed, though simple push/mutation might suffice
// depending on usage. Tracked arrays need reassignment or specific Ember array methods
// if we want to observe the array itself, but here we just check inclusion.
// Let's do a reassignment to be safe and clean.
this.loadedPrefixes = [...this.loadedPrefixes, ...missingPrefixes];
this.currentBbox = bbox;
}
async loadAllPlaces(prefixes = null) {
try {
// If prefixes is null, it loads everything (recursive scan).
// If prefixes is an array ['w1q7'], it loads just that sector.
const places = await this.places.getPlaces(prefixes);
if (places && Array.isArray(places)) {
if (prefixes) {
// Identify existing places that belong to the reloaded prefixes and remove them
const prefixSet = new Set(prefixes);
const keptPlaces = this.savedPlaces.filter((place) => {
if (!place.lat || !place.lon) return false;
try {
// Calculate 4-char geohash for the existing place
const hash = Geohash.encode(place.lat, place.lon, 4);
// If the hash is in the set of reloaded prefixes, we discard the old version
// (because the 'places' array contains the authoritative new state for this prefix)
return !prefixSet.has(hash);
} catch (e) {
return true; // Keep malformed/unknown places safe
}
});
// Merge the kept places (from other areas) with the fresh places (from these areas)
this.savedPlaces = [...keptPlaces, ...places];
} else {
// Full reload
this.savedPlaces = places;
}
} else {
if (!prefixes) this.savedPlaces = [];
}
console.log('Loaded saved places:', this.savedPlaces.length);
} catch (e) {
console.error('Failed to load places:', e);
}
}
findPlaceById(id) {
// Search by internal ID first
let place = this.savedPlaces.find((p) => p.id === id);
if (place) return place;
// Then search by OSM ID
place = this.savedPlaces.find((p) => p.osmId === id);
return place;
}
}