Fire-and-forget: geocode pending markets without blocking sync
(db: ReturnType<typeof getDb>)
| 35 | |
| 36 | /** Fire-and-forget: geocode pending markets without blocking sync */ |
| 37 | function geocodePending(db: ReturnType<typeof getDb>) { |
| 38 | // Watchdog: reset stuck flag after 120s |
| 39 | if (geocodeRunning && Date.now() - geocodeStartedAt > GEOCODE_WATCHDOG_MS) { |
| 40 | console.warn("[sync] Geocode watchdog: resetting stuck flag"); |
| 41 | geocodeRunning = false; |
| 42 | } |
| 43 | if (geocodeRunning) return; |
| 44 | const ungeo = db |
| 45 | .prepare(`SELECT id, title, description, location FROM events WHERE ai_geo_done = 0 LIMIT 25`) |
| 46 | .all() as Array<{ id: string; title: string; description: string | null; location: string | null }>; |
| 47 | if (ungeo.length === 0) return; |
| 48 | |
| 49 | const updateGeo = db.prepare(` |
| 50 | UPDATE events SET lat = @lat, lng = @lng, location = @location, |
| 51 | geo_city = @city, geo_country = @country, ai_geo_done = 1 |
| 52 | WHERE id = @id |
| 53 | `); |
| 54 | const markDone = db.prepare(`UPDATE events SET ai_geo_done = 1 WHERE id = ?`); |
| 55 | |
| 56 | const writeResults = (resultMap: Map<string, { id: string; lat: number | null; lng: number | null; location: string | null; city: string | null; country: string | null; confidence: number }> | null) => { |
| 57 | try { |
| 58 | const txn = db.transaction(() => { |
| 59 | for (const market of ungeo) { |
| 60 | let result = resultMap?.get(market.id); |
| 61 | if (!result || (result.lat === null && result.lng === null)) { |
| 62 | const geo = geolocate(market.title, market.description ?? undefined); |
| 63 | if (geo) { |
| 64 | const [jLat, jLng] = addJitter(geo.coords[0], geo.coords[1], market.id); |
| 65 | result = { id: market.id, lat: jLat, lng: jLng, location: geo.location, city: null, country: null, confidence: 0.3 }; |
| 66 | } |
| 67 | } |
| 68 | if (result && result.lat !== null && result.lng !== null) { |
| 69 | updateGeo.run({ id: market.id, lat: result.lat, lng: result.lng, location: result.location || market.location, city: result.city, country: result.country }); |
| 70 | } else { |
| 71 | markDone.run(market.id); |
| 72 | } |
| 73 | } |
| 74 | }); |
| 75 | txn(); |
| 76 | } catch { /* DB busy — will retry next cycle */ } |
| 77 | }; |
| 78 | |
| 79 | if (isAiConfigured()) { |
| 80 | geocodeRunning = true; |
| 81 | geocodeStartedAt = Date.now(); |
| 82 | aiGeocodeBatch(ungeo.map((r) => ({ id: r.id, title: r.title, description: r.description, currentLocation: r.location }))) |
| 83 | .then((results) => writeResults(new Map(results.map((r) => [r.id, r])))) |
| 84 | .catch((err) => { console.error("[sync] Geocode batch failed:", err); writeResults(null); }) |
| 85 | .finally(() => { geocodeRunning = false; console.info(`[sync] Geocoded ${ungeo.length} markets`); }); |
| 86 | } else { |
| 87 | writeResults(null); |
| 88 | console.info(`[sync] Geocoded ${ungeo.length} markets (regex)`); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | export async function runSync(): Promise<{ |
| 93 | eventCount: number; |
no test coverage detected