mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
feat(import): selective GPX/KML element import and performance improvements
Add type-selector UI in the file import modal letting users choose which GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points, paths) to import. KML LineString placemarks are now imported as path places with route_geometry. Performance improvements: - Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut unnecessary re-renders in PlacesSidebar - Add weatherQueue to cap concurrent weather fetches at 3 - Replace sequential per-place deletes with a single bulkDelete API call (new DELETE /places/bulk endpoint + deletePlacesMany service) - Memoize atlas/photo/weather service calls to avoid redundant requests - Add multi-select mode to PlacesSidebar for bulk operations Add large GPX/KML/KMZ fixtures for integration/perf testing and two profiler analysis scripts under scripts/.
This commit is contained in:
@@ -12,6 +12,29 @@ const listeners = new Map<string, Set<(entry: PhotoEntry) => void>>()
|
||||
// Separate thumb listeners — called when thumbDataUrl becomes available after initial load
|
||||
const thumbListeners = new Map<string, Set<(thumb: string) => void>>()
|
||||
|
||||
// Concurrency limiter — at most N photo API requests in flight at once.
|
||||
// Prevents flooding the server (and external APIs it calls) when many places appear at once.
|
||||
const MAX_CONCURRENT = 5
|
||||
let activeRequests = 0
|
||||
const requestQueue: Array<() => void> = []
|
||||
|
||||
function acquireRequestSlot(): Promise<void> {
|
||||
if (activeRequests < MAX_CONCURRENT) {
|
||||
activeRequests++
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise(resolve => requestQueue.push(resolve))
|
||||
}
|
||||
|
||||
function releaseRequestSlot(): void {
|
||||
const next = requestQueue.shift()
|
||||
if (next) {
|
||||
next()
|
||||
} else {
|
||||
activeRequests--
|
||||
}
|
||||
}
|
||||
|
||||
function notify(key: string, entry: PhotoEntry) {
|
||||
listeners.get(key)?.forEach(fn => fn(entry))
|
||||
listeners.delete(key)
|
||||
@@ -99,37 +122,39 @@ export function fetchPhoto(
|
||||
}
|
||||
|
||||
inFlight.add(cacheKey)
|
||||
mapsApi.placePhoto(photoId, lat, lng, name)
|
||||
.then(async (data: { photoUrl?: string }) => {
|
||||
const photoUrl = data.photoUrl || null
|
||||
if (!photoUrl) {
|
||||
acquireRequestSlot().then(() =>
|
||||
mapsApi.placePhoto(photoId, lat, lng, name)
|
||||
.then(async (data: { photoUrl?: string }) => {
|
||||
const photoUrl = data.photoUrl || null
|
||||
if (!photoUrl) {
|
||||
const entry: PhotoEntry = { photoUrl: null, thumbDataUrl: null }
|
||||
cache.set(cacheKey, entry)
|
||||
callback?.(entry)
|
||||
notify(cacheKey, entry)
|
||||
return
|
||||
}
|
||||
|
||||
// Store URL first — sidebar can show immediately
|
||||
const entry: PhotoEntry = { photoUrl, thumbDataUrl: null }
|
||||
cache.set(cacheKey, entry)
|
||||
callback?.(entry)
|
||||
notify(cacheKey, entry)
|
||||
|
||||
// Generate base64 thumb in background
|
||||
const thumb = await urlToBase64(photoUrl)
|
||||
if (thumb) {
|
||||
entry.thumbDataUrl = thumb
|
||||
notifyThumb(cacheKey, thumb)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const entry: PhotoEntry = { photoUrl: null, thumbDataUrl: null }
|
||||
cache.set(cacheKey, entry)
|
||||
callback?.(entry)
|
||||
notify(cacheKey, entry)
|
||||
return
|
||||
}
|
||||
|
||||
// Store URL first — sidebar can show immediately
|
||||
const entry: PhotoEntry = { photoUrl, thumbDataUrl: null }
|
||||
cache.set(cacheKey, entry)
|
||||
callback?.(entry)
|
||||
notify(cacheKey, entry)
|
||||
|
||||
// Generate base64 thumb in background
|
||||
const thumb = await urlToBase64(photoUrl)
|
||||
if (thumb) {
|
||||
entry.thumbDataUrl = thumb
|
||||
notifyThumb(cacheKey, thumb)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const entry: PhotoEntry = { photoUrl: null, thumbDataUrl: null }
|
||||
cache.set(cacheKey, entry)
|
||||
callback?.(entry)
|
||||
notify(cacheKey, entry)
|
||||
})
|
||||
.finally(() => { inFlight.delete(cacheKey) })
|
||||
})
|
||||
.finally(() => { inFlight.delete(cacheKey); releaseRequestSlot() })
|
||||
)
|
||||
}
|
||||
|
||||
export function getAllThumbs(): Record<string, string> {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { weatherApi } from '../api/client'
|
||||
|
||||
const MAX_CONCURRENT = 3
|
||||
let active = 0
|
||||
const queue: Array<() => void> = []
|
||||
|
||||
function acquire(): Promise<void> {
|
||||
if (active < MAX_CONCURRENT) { active++; return Promise.resolve() }
|
||||
return new Promise(resolve => queue.push(resolve))
|
||||
}
|
||||
|
||||
function release(): void {
|
||||
const next = queue.shift()
|
||||
if (next) next()
|
||||
else active--
|
||||
}
|
||||
|
||||
export async function fetchWeather(lat: number, lng: number, date: string) {
|
||||
await acquire()
|
||||
try {
|
||||
return await weatherApi.get(lat, lng, date)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user