mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
refactor: compose configurable geocoding and route services
This commit is contained in:
1 parent
3b91feffdc
commit
8b36c28b06
25 files changed
+764
-211
No files matched your search
@@ -59,7 +59,7 @@ function sendOverpassResponse(res, payload, cacheStatus = 'MISS') {
|
||||
*
|
||||
* @returns {import('vite').Plugin}
|
||||
*/
|
||||
function overpassProxy() {
|
||||
function overpassProxy({ routing = {} } = {}) {
|
||||
const installMiddleware = (server) => {
|
||||
server.middlewares.use('/api/overpass', async (req, res) => {
|
||||
// Hoisted out of the try so the catch's serve-stale lookup can see it
|
||||
@@ -200,7 +200,7 @@ function overpassProxy() {
|
||||
}
|
||||
});
|
||||
|
||||
installRouteMiddleware(server.middlewares);
|
||||
installRouteMiddleware(server.middlewares, routing);
|
||||
};
|
||||
return {
|
||||
name: 'overpass-proxy',
|
||||
|
||||
@@ -45,6 +45,8 @@ export function validatePlacesCoordinates(searchParams) {
|
||||
/** Nearby place labels and view-biased text search, with request-time key resolution. */
|
||||
export function googlePlacesContextProxy({
|
||||
resolveApiKey = googleServerApiKey,
|
||||
fetchImpl = (...args) => fetch(...args),
|
||||
endpoints = {},
|
||||
} = {}) {
|
||||
function install(middlewares) {
|
||||
middlewares.use('/api/google/nearby-places', async (req, res) => {
|
||||
@@ -96,10 +98,12 @@ export function googlePlacesContextProxy({
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://places.googleapis.com/v1/places:searchNearby',
|
||||
const response = await fetchImpl(
|
||||
endpoints.nearby ||
|
||||
'https://places.googleapis.com/v1/places:searchNearby',
|
||||
{
|
||||
method: 'POST',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Api-Key': apiKey,
|
||||
@@ -214,10 +218,12 @@ export function googlePlacesContextProxy({
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://places.googleapis.com/v1/places:searchText',
|
||||
const response = await fetchImpl(
|
||||
endpoints.textSearch ||
|
||||
'https://places.googleapis.com/v1/places:searchText',
|
||||
{
|
||||
method: 'POST',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Api-Key': apiKey,
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
/** OSM routing (FOSSGIS OSRM) cache: profile|coords -> { payload, cachedAt }. */
|
||||
const ROUTE_CACHE_MS = 600000;
|
||||
|
||||
const _routeCache = new Map();
|
||||
|
||||
/** Hard cap on the OSRM route response we will buffer. */
|
||||
const ROUTE_MAX_RESPONSE_BYTES = 8 * 1024 * 1024; // 8 MB
|
||||
|
||||
@@ -25,7 +23,12 @@ const _routeRateLimiter = makeRateLimiter({
|
||||
globalMax: 200,
|
||||
});
|
||||
|
||||
export function installRouteMiddleware(middlewares) {
|
||||
export function installRouteMiddleware(
|
||||
middlewares,
|
||||
{ endpoints = {}, fetchImpl = (...args) => fetch(...args) } = {},
|
||||
) {
|
||||
const _routeCache = new Map();
|
||||
|
||||
// Real OSM routing via the public FOSSGIS OSRM servers (foot/car/bike).
|
||||
// GET /api/route?profile=foot|car|bike&coords=lon,lat;lon,lat[;...]
|
||||
middlewares.use('/api/route', async (req, res) => {
|
||||
@@ -96,13 +99,17 @@ export function installRouteMiddleware(middlewares) {
|
||||
res.end(JSON.stringify(cached.payload));
|
||||
return;
|
||||
}
|
||||
const upstream = `https://routing.openstreetmap.de/routed-${profile}/route/v1/${osrmProfile}/${coords}?overview=full&geometries=geojson&alternatives=false&steps=false`;
|
||||
const base =
|
||||
endpoints[profile] ||
|
||||
`https://routing.openstreetmap.de/routed-${profile}`;
|
||||
const upstream = `${base.replace(/\/$/, '')}/route/v1/${osrmProfile}/${coords}?overview=full&geometries=geojson&alternatives=false&steps=false`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 12000);
|
||||
let osrm;
|
||||
try {
|
||||
const upstreamRes = await fetch(upstream, {
|
||||
const upstreamRes = await fetchImpl(upstream, {
|
||||
signal: controller.signal,
|
||||
redirect: 'error',
|
||||
headers: { 'User-Agent': 'gods-eye-view/dev (local)' },
|
||||
});
|
||||
if (!upstreamRes.ok) return fail('no route found');
|
||||
|
||||
@@ -4,3 +4,4 @@ export {
|
||||
} from './regional/briefing.js';
|
||||
export { weatherEffectsProxy } from './regional/weather-effects.js';
|
||||
export { validRegionalPoint } from './regional/query.js';
|
||||
export { createRegionalPlaceProvider } from './regional/place.js';
|
||||
@@ -14,33 +14,33 @@ const REGIONAL_BRIEF_STALE_MS = 60 * 60_000;
|
||||
|
||||
const REGIONAL_BRIEF_MAX_CACHE = 120;
|
||||
|
||||
const _regionalBriefCache = new Map();
|
||||
|
||||
const _regionalBriefInFlight = new Map();
|
||||
|
||||
const _regionalBriefRateLimiter = makeRateLimiter({
|
||||
windowMs: 60_000,
|
||||
max: 30,
|
||||
globalMax: 90,
|
||||
});
|
||||
|
||||
function trimRegionalBriefCache() {
|
||||
while (_regionalBriefCache.size > REGIONAL_BRIEF_MAX_CACHE) {
|
||||
const oldest = _regionalBriefCache.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
_regionalBriefCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
/** True when at least one regional source produced usable data. */
|
||||
function regionalBriefHasAnySource({ place, weather, news } = {}) {
|
||||
return Boolean(place || weather || (news && news.status !== 'unavailable'));
|
||||
}
|
||||
|
||||
function regionalBriefProxy() {
|
||||
function regionalBriefProxy({ placeProvider = fetchRegionalPlace } = {}) {
|
||||
const _regionalBriefCache = new Map();
|
||||
|
||||
const _regionalBriefInFlight = new Map();
|
||||
|
||||
const _regionalBriefRateLimiter = makeRateLimiter({
|
||||
windowMs: 60_000,
|
||||
max: 30,
|
||||
globalMax: 90,
|
||||
});
|
||||
|
||||
function trimRegionalBriefCache() {
|
||||
while (_regionalBriefCache.size > REGIONAL_BRIEF_MAX_CACHE) {
|
||||
const oldest = _regionalBriefCache.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
_regionalBriefCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh(point, key) {
|
||||
const [placeResult, weatherResult] = await Promise.allSettled([
|
||||
fetchRegionalPlace(point),
|
||||
placeProvider(point),
|
||||
fetchRegionalWeather(point),
|
||||
]);
|
||||
const place = placeResult.status === 'fulfilled' ? placeResult.value : null;
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import { fetchRegionalJson } from './http.js';
|
||||
import { normalizeRegionalPlace } from '../../../src/data/regionalBrief.js';
|
||||
|
||||
let _nominatimQueue = Promise.resolve();
|
||||
/** Construct the serialized Nominatim adapter with a trusted endpoint. */
|
||||
export function createRegionalPlaceProvider({
|
||||
endpoint = 'https://nominatim.openstreetmap.org/reverse',
|
||||
requestJson = fetchRegionalJson,
|
||||
} = {}) {
|
||||
let _nominatimQueue = Promise.resolve();
|
||||
|
||||
let _nominatimLastRequestAt = 0;
|
||||
let _nominatimLastRequestAt = 0;
|
||||
|
||||
function fetchRegionalPlace(point) {
|
||||
const task = _nominatimQueue.then(async () => {
|
||||
const waitMs = Math.max(0, 1100 - (Date.now() - _nominatimLastRequestAt));
|
||||
if (waitMs) await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
_nominatimLastRequestAt = Date.now();
|
||||
const params = new URLSearchParams({
|
||||
format: 'jsonv2',
|
||||
lat: point.latitude.toFixed(5),
|
||||
lon: point.longitude.toFixed(5),
|
||||
zoom: '10',
|
||||
addressdetails: '1',
|
||||
'accept-language': 'en',
|
||||
});
|
||||
const payload = await fetchRegionalJson(
|
||||
`https://nominatim.openstreetmap.org/reverse?${params}`,
|
||||
{
|
||||
function fetchRegionalPlace(point) {
|
||||
const task = _nominatimQueue.then(async () => {
|
||||
const waitMs = Math.max(0, 1100 - (Date.now() - _nominatimLastRequestAt));
|
||||
if (waitMs) await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
_nominatimLastRequestAt = Date.now();
|
||||
const params = new URLSearchParams({
|
||||
format: 'jsonv2',
|
||||
lat: point.latitude.toFixed(5),
|
||||
lon: point.longitude.toFixed(5),
|
||||
zoom: '10',
|
||||
addressdetails: '1',
|
||||
'accept-language': 'en',
|
||||
});
|
||||
const payload = await requestJson(`${endpoint}?${params}`, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'GodsEyeView/0.1 (+https://github.com/bilawalsidhu/gods-eye-view)',
|
||||
Referer: 'https://github.com/bilawalsidhu/gods-eye-view',
|
||||
},
|
||||
},
|
||||
);
|
||||
return normalizeRegionalPlace(payload);
|
||||
});
|
||||
_nominatimQueue = task.catch(() => null);
|
||||
return task;
|
||||
});
|
||||
return normalizeRegionalPlace(payload);
|
||||
});
|
||||
_nominatimQueue = task.catch(() => null);
|
||||
return task;
|
||||
}
|
||||
|
||||
return fetchRegionalPlace;
|
||||
}
|
||||
|
||||
export { fetchRegionalPlace };
|
||||
export const fetchRegionalPlace = createRegionalPlaceProvider();
|
||||
Reference in new issue
Block a user