From c08592144af34d3c75385a1274d7c96efa782517 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Wed, 7 Oct 2026 10:31:07 +0700 Subject: [PATCH] perf(create-spot): cap reverse-geocode at 2.5s so publish can't stall before signing --- AGENTS.md | 10 +++++++--- src/pages/CreateSpotPage.tsx | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index aaa7947..90cbc5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1120,9 +1120,13 @@ session with **no other memory of this project**. So: the form autosaves via **`useFormDraft`** (generalized from `useLabDraft`, same resume-banner pattern). ⚠️ Leaflet's base CSS is **CDN-loaded in `index.html`** (`src/main.tsx` just has a comment) — a new Leaflet map needs no CSS import. - Still open: the 60 m duplicate check still runs on submit with - `useSkateSpots(500)` (move it to pin-time + a targeted geohash query), and - publish still blocks on `reverseGeocodeRegions` before signing. + Follow-ups in the same pass: the 60 m duplicate check now runs **at pin-time** + (debounced, warning under the map) using the **same `useSkateSpots(2000)` cache + key as the map** (was a second `useSkateSpots(500)` query); and publish **caps + the reverse-geocode at `GEOCODE_BUDGET_MS` (2.5 s)** so a slow provider can't + stall signing (a miss falls back to the stats' read-time region derivation). + ⚠️ A geohash-prefix nearby query is still impossible — relays only exact-match + `#g` — so the 2000-spot cache remains the mechanism. - **Map pin rendering (perf, 2026-09-29):** the spot/tour/event/crew marker effect in `SpotMap.tsx` used to `clearLayers()` + rebuild **every** marker whenever the parent handed it fresh arrays, and added each marker with a diff --git a/src/pages/CreateSpotPage.tsx b/src/pages/CreateSpotPage.tsx index 8d84630..c2c0d84 100644 --- a/src/pages/CreateSpotPage.tsx +++ b/src/pages/CreateSpotPage.tsx @@ -23,7 +23,7 @@ import { PhotoDropzone } from '@/components/PhotoDropzone'; import { TagInput } from '@/components/TagInput'; import { SpotLocationPicker, type LatLng } from '@/components/SpotLocationPicker'; import { SKATE_SPOT_KIND } from '@/lib/types'; -import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions'; +import { reverseGeocodeRegions, pushRegionTags, type GeocodedRegions } from '@/lib/geocodeRegions'; import { spotCategories, MAX_TAGS, shelterOptions, DEFAULT_SHELTER, SHELTERED_VALUES, type ShelterType } from '@/lib/skateTags'; import { SPOT_TAG_CATEGORY_KEYS, SPOT_TAG_CATEGORY_DESC_KEYS, SHELTER_KEYS, SHELTER_DESC_KEYS } from '@/lib/labelKeys'; import { cn } from '@/lib/utils'; @@ -68,6 +68,9 @@ type SpotDraft = { const DRAFT_PREFIX = 'skatespots:spot-draft:'; +/** How long to wait for a reverse-geocode before publishing without region tags. */ +const GEOCODE_BUDGET_MS = 2500; + export default function CreateSpotPage() { const { t } = useTranslation(); @@ -386,8 +389,16 @@ export default function CreateSpotPage() { // Reverse geocode to get structured city/country tags (best-effort; the // helper never throws). These power Sasquatch's city/country search and - // the /stats region breakdown. - const geo = await reverseGeocodeRegions(lat, lng); + // the /stats region breakdown. Capped so a slow provider can't make + // "Publish" feel stuck — a miss is covered by the stats' read-time + // derivation of an untagged pin's coords. + const emptyRegions: GeocodedRegions = { city: '', country: '', state: '', countryCode: '' }; + const geo = await Promise.race([ + reverseGeocodeRegions(lat, lng), + new Promise((resolve) => { + window.setTimeout(() => resolve(emptyRegions), GEOCODE_BUDGET_MS); + }), + ]); let locationStr = location.trim(); if (!locationStr && geo.city) { // Auto-populate an empty address from the geocode.